more electron/web separation

This commit is contained in:
Eugene Pankov 2021-06-19 01:36:25 +02:00
parent fa31ac65ab
commit fad7858f3f
No known key found for this signature in database
GPG Key ID: 5896FCBBDD1CF4F4
50 changed files with 568 additions and 448 deletions

View File

@ -1,6 +1,8 @@
import { app, ipcMain, Menu, Tray, shell, screen, globalShortcut, MenuItemConstructorOptions } from 'electron'
import * as promiseIpc from 'electron-promise-ipc'
import * as remote from '@electron/remote/main'
import * as path from 'path'
import * as fs from 'fs'
import { loadConfig } from './config'
import { Window, WindowOptions } from './window'
@ -17,6 +19,7 @@ export class Application {
private tray?: Tray
private ptyManager = new PTYManager()
private windows: Window[] = []
userPluginsPath: string
constructor () {
remote.initialize()
@ -36,12 +39,12 @@ export class Application {
}
})
;(promiseIpc as any).on('plugin-manager:install', (path, name, version) => {
return pluginManager.install(path, name, version)
;(promiseIpc as any).on('plugin-manager:install', (name, version) => {
return pluginManager.install(this.userPluginsPath, name, version)
})
;(promiseIpc as any).on('plugin-manager:uninstall', (path, name) => {
return pluginManager.uninstall(path, name)
;(promiseIpc as any).on('plugin-manager:uninstall', (name) => {
return pluginManager.uninstall(this.userPluginsPath, name)
})
const configData = loadConfig()
@ -53,6 +56,15 @@ export class Application {
}
}
this.userPluginsPath = path.join(
app.getPath('userData'),
'plugins',
)
if (!fs.existsSync(this.userPluginsPath)) {
fs.mkdirSync(this.userPluginsPath)
}
app.commandLine.appendSwitch('disable-http-cache')
app.commandLine.appendSwitch('max-active-webgl-contexts', '9000')
app.commandLine.appendSwitch('lang', 'EN')
@ -70,7 +82,7 @@ export class Application {
}
async newWindow (options?: WindowOptions): Promise<Window> {
const window = new Window(options)
const window = new Window(this, options)
this.windows.push(window)
window.visible$.subscribe(visible => {
if (visible) {

View File

@ -9,6 +9,7 @@ import * as path from 'path'
import macOSRelease from 'macos-release'
import * as compareVersions from 'compare-versions'
import type { Application } from './app'
import { parseArgs } from './cli'
import { loadConfig } from './config'
@ -43,7 +44,7 @@ export class Window {
get visible$ (): Observable<boolean> { return this.visible }
get closed$ (): Observable<void> { return this.closed }
constructor (options?: WindowOptions) {
constructor (private application: Application, options?: WindowOptions) {
this.configStore = loadConfig()
options = options ?? {}
@ -299,16 +300,10 @@ export class Window {
executable: app.getPath('exe'),
windowID: this.window.id,
isFirstWindow: this.window.id === 1,
userPluginsPath: this.application.userPluginsPath,
})
})
ipcMain.on('window-focus', event => {
if (!this.window || event.sender !== this.window.webContents) {
return
}
this.window.focus()
})
ipcMain.on('window-toggle-maximize', event => {
if (!this.window || event.sender !== this.window.webContents) {
return

View File

@ -11,7 +11,7 @@ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
import { ipcRenderer } from 'electron'
import { getRootModule } from './app.module'
import { findPlugins, loadPlugins, PluginInfo } from './plugins'
import { findPlugins, initModuleLookup, loadPlugins } from './plugins'
import { BootstrapData, BOOTSTRAP_DATA } from '../../terminus-core/src/api/mainProcess'
// Always land on the start view
@ -29,12 +29,12 @@ if (process.env.TERMINUS_DEV && !process.env.TERMINUS_FORCE_ANGULAR_PROD) {
enableProdMode()
}
async function bootstrap (plugins: PluginInfo[], bootstrapData: BootstrapData, safeMode = false): Promise<NgModuleRef<any>> {
async function bootstrap (bootstrapData: BootstrapData, safeMode = false): Promise<NgModuleRef<any>> {
if (safeMode) {
plugins = plugins.filter(x => x.isBuiltin)
bootstrapData.installedPlugins = bootstrapData.installedPlugins.filter(x => x.isBuiltin)
}
const pluginModules = await loadPlugins(plugins, (current, total) => {
const pluginModules = await loadPlugins(bootstrapData.installedPlugins, (current, total) => {
(document.querySelector('.progress .bar') as HTMLElement).style.width = `${100 * current / total}%` // eslint-disable-line
})
const module = getRootModule(pluginModules)
@ -53,20 +53,24 @@ async function bootstrap (plugins: PluginInfo[], bootstrapData: BootstrapData, s
ipcRenderer.once('start', async (_$event, bootstrapData: BootstrapData) => {
console.log('Window bootstrap data:', bootstrapData)
initModuleLookup(bootstrapData.userPluginsPath)
let plugins = await findPlugins()
if (bootstrapData.config.pluginBlacklist) {
plugins = plugins.filter(x => !bootstrapData.config.pluginBlacklist.includes(x.name))
}
plugins = plugins.filter(x => x.name !== 'web')
bootstrapData.installedPlugins = plugins
console.log('Starting with plugins:', plugins)
try {
await bootstrap(plugins, bootstrapData)
await bootstrap(bootstrapData)
} catch (error) {
console.error('Angular bootstrapping error:', error)
console.warn('Trying safe mode')
window['safeModeReason'] = error
try {
await bootstrap(plugins, bootstrapData, true)
await bootstrap(bootstrapData, true)
} catch (error2) {
console.error('Bootstrap failed:', error2)
}

View File

@ -1,8 +1,11 @@
import * as fs from 'mz/fs'
import * as path from 'path'
import * as remote from '@electron/remote'
import { PluginInfo } from '../../terminus-core/src/api/mainProcess'
const nodeModule = require('module') // eslint-disable-line @typescript-eslint/no-var-requires
const nodeRequire = (global as any).require
const nodeRequire = global['require']
function normalizePath (p: string): string {
const cygwinPrefix = '/cygdrive/'
@ -13,45 +16,8 @@ function normalizePath (p: string): string {
return p
}
global['module'].paths.map((x: string) => nodeModule.globalPaths.push(normalizePath(x)))
if (process.env.TERMINUS_DEV) {
nodeModule.globalPaths.unshift(path.dirname(remote.app.getAppPath()))
}
const builtinPluginsPath = process.env.TERMINUS_DEV ? path.dirname(remote.app.getAppPath()) : path.join((process as any).resourcesPath, 'builtin-plugins')
const userPluginsPath = path.join(
remote.app.getPath('userData'),
'plugins',
)
if (!fs.existsSync(userPluginsPath)) {
fs.mkdir(userPluginsPath)
}
Object.assign(window, { builtinPluginsPath, userPluginsPath })
nodeModule.globalPaths.unshift(builtinPluginsPath)
nodeModule.globalPaths.unshift(path.join(userPluginsPath, 'node_modules'))
// nodeModule.globalPaths.unshift(path.join((process as any).resourcesPath, 'app.asar', 'node_modules'))
if (process.env.TERMINUS_PLUGINS) {
process.env.TERMINUS_PLUGINS.split(':').map(x => nodeModule.globalPaths.push(normalizePath(x)))
}
export type ProgressCallback = (current: number, total: number) => void // eslint-disable-line @typescript-eslint/no-type-alias
export interface PluginInfo {
name: string
description: string
packageName: string
isBuiltin: boolean
version: string
author: string
homepage?: string
path?: string
info?: any
}
const builtinModules = [
'@angular/animations',
'@angular/common',
@ -71,25 +37,42 @@ const builtinModules = [
'zone.js/dist/zone.js',
]
const cachedBuiltinModules = {}
builtinModules.forEach(m => {
cachedBuiltinModules[m] = nodeRequire(m)
})
export type ProgressCallback = (current: number, total: number) => void // eslint-disable-line @typescript-eslint/no-type-alias
const originalRequire = (global as any).require
;(global as any).require = function (query: string) {
if (cachedBuiltinModules[query]) {
return cachedBuiltinModules[query]
}
return originalRequire.apply(this, [query])
}
export function initModuleLookup (userPluginsPath: string): void {
global['module'].paths.map((x: string) => nodeModule.globalPaths.push(normalizePath(x)))
const originalModuleRequire = nodeModule.prototype.require
nodeModule.prototype.require = function (query: string) {
if (cachedBuiltinModules[query]) {
return cachedBuiltinModules[query]
if (process.env.TERMINUS_DEV) {
nodeModule.globalPaths.unshift(path.dirname(remote.app.getAppPath()))
}
nodeModule.globalPaths.unshift(builtinPluginsPath)
nodeModule.globalPaths.unshift(path.join(userPluginsPath, 'node_modules'))
// nodeModule.globalPaths.unshift(path.join((process as any).resourcesPath, 'app.asar', 'node_modules'))
if (process.env.TERMINUS_PLUGINS) {
process.env.TERMINUS_PLUGINS.split(':').map(x => nodeModule.globalPaths.push(normalizePath(x)))
}
const cachedBuiltinModules = {}
builtinModules.forEach(m => {
cachedBuiltinModules[m] = nodeRequire(m)
})
const originalRequire = (global as any).require
;(global as any).require = function (query: string) {
if (cachedBuiltinModules[query]) {
return cachedBuiltinModules[query]
}
return originalRequire.apply(this, [query])
}
const originalModuleRequire = nodeModule.prototype.require
nodeModule.prototype.require = function (query: string) {
if (cachedBuiltinModules[query]) {
return cachedBuiltinModules[query]
}
return originalModuleRequire.call(this, query)
}
return originalModuleRequire.call(this, query)
}
export async function findPlugins (): Promise<PluginInfo[]> {
@ -167,8 +150,6 @@ export async function findPlugins (): Promise<PluginInfo[]> {
}
foundPlugins.sort((a, b) => a.name > b.name ? 1 : -1)
;(window as any).installedPlugins = foundPlugins
return foundPlugins
}

View File

@ -3,7 +3,6 @@ const fs = require('fs')
const semver = require('semver')
const childProcess = require('child_process')
const appInfo = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../app/package.json')))
const electronInfo = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../node_modules/electron/package.json')))
exports.version = childProcess.execSync('git describe --tags', {encoding:'utf-8'})
@ -18,10 +17,10 @@ exports.builtinPlugins = [
'terminus-core',
'terminus-settings',
'terminus-terminal',
'terminus-electron',
'terminus-local',
'terminus-web',
'terminus-community-color-schemes',
'terminus-electron',
'terminus-plugin-manager',
'terminus-ssh',
'terminus-serial',

View File

@ -0,0 +1,53 @@
import { Observable, Subject } from 'rxjs'
import { Injector } from '@angular/core'
import { Logger, LogService } from '../services/log.service'
export enum Platform {
Linux = 'Linux',
macOS = 'macOS',
Windows = 'Windows',
Web = 'Web',
}
/**
* Provides interaction with the main process
*/
export abstract class HostAppService {
abstract get platform (): Platform
abstract get configPlatform (): Platform
protected settingsUIRequest = new Subject<void>()
protected configChangeBroadcast = new Subject<void>()
protected logger: Logger
/**
* Fired when Preferences is selected in the macOS menu
*/
get settingsUIRequest$ (): Observable<void> { return this.settingsUIRequest }
/**
* Fired when another window modified the config file
*/
get configChangeBroadcast$ (): Observable<void> { return this.configChangeBroadcast }
constructor (
injector: Injector,
) {
this.logger = injector.get(LogService).create('hostApp')
}
abstract newWindow (): void
/**
* Notifies other windows of config file changes
*/
// eslint-disable-next-line @typescript-eslint/no-empty-function
broadcastConfigChange (_configStore: Record<string, any>): void { }
// eslint-disable-next-line @typescript-eslint/no-empty-function
emitReady (): void { }
abstract relaunch (): void
abstract quit (): void
}

View File

@ -1,7 +1,24 @@
import { Observable } from 'rxjs'
import { Observable, Subject } from 'rxjs'
export abstract class HostWindowService {
abstract readonly closeRequest$: Observable<void>
/**
* Fired once the window is visible
*/
get windowShown$ (): Observable<void> { return this.windowShown }
/**
* Fired when the window close button is pressed
*/
get windowCloseRequest$ (): Observable<void> { return this.windowCloseRequest }
get windowMoved$ (): Observable<void> { return this.windowMoved }
get windowFocused$ (): Observable<void> { return this.windowFocused }
protected windowShown = new Subject<void>()
protected windowCloseRequest = new Subject<void>()
protected windowMoved = new Subject<void>()
protected windowFocused = new Subject<void>()
abstract readonly isFullscreen: boolean
abstract reload (): void
abstract setTitle (title?: string): void
@ -9,4 +26,10 @@ export abstract class HostWindowService {
abstract minimize (): void
abstract toggleMaximize (): void
abstract close (): void
// eslint-disable-next-line @typescript-eslint/no-empty-function
openDevTools (): void { }
// eslint-disable-next-line @typescript-eslint/no-empty-function
bringToFront (): void { }
}

View File

@ -12,8 +12,9 @@ export { SelectorOption } from './selector'
export { CLIHandler, CLIEvent } from './cli'
export { PlatformService, ClipboardContent, MessageBoxResult, MessageBoxOptions, FileDownload, FileUpload, FileTransfer, HTMLFileUpload, FileUploadOptions } from './platform'
export { MenuItemOptions } from './menu'
export { BootstrapData, BOOTSTRAP_DATA } from './mainProcess'
export { BootstrapData, PluginInfo, BOOTSTRAP_DATA } from './mainProcess'
export { HostWindowService } from './hostWindow'
export { HostAppService, Platform } from './hostApp'
export { AppService } from '../services/app.service'
export { ConfigService } from '../services/config.service'
@ -22,7 +23,6 @@ export { ElectronService } from '../services/electron.service'
export { Logger, ConsoleLogger, LogService } from '../services/log.service'
export { HomeBaseService } from '../services/homeBase.service'
export { HotkeysService } from '../services/hotkeys.service'
export { HostAppService, Platform, Bounds } from '../services/hostApp.service'
export { NotificationsService } from '../services/notifications.service'
export { ThemesService } from '../services/themes.service'
export { TabsService } from '../services/tabs.service'

View File

@ -1,8 +1,22 @@
export const BOOTSTRAP_DATA = 'BOOTSTRAP_DATA'
export interface PluginInfo {
name: string
description: string
packageName: string
isBuiltin: boolean
version: string
author: string
homepage?: string
path?: string
info?: any
}
export interface BootstrapData {
config: Record<string, any>
executable: string
isFirstWindow: boolean
windowID: number
installedPlugins: PluginInfo[]
userPluginsPath: string
}

View File

@ -77,8 +77,10 @@ export abstract class PlatformService {
supportsWindowControls = false
get fileTransferStarted$ (): Observable<FileTransfer> { return this.fileTransferStarted }
get displayMetricsChanged$ (): Observable<void> { return this.displayMetricsChanged }
protected fileTransferStarted = new Subject<FileTransfer>()
protected displayMetricsChanged = new Subject<void>()
abstract readClipboard (): string
abstract setClipboard (content: ClipboardContent): void
@ -158,6 +160,7 @@ export abstract class PlatformService {
abstract getAppVersion (): string
abstract openExternal (url: string): void
abstract listFonts (): Promise<string[]>
abstract setErrorHandler (handler: (_: any) => void): void
abstract popupContextMenu (menu: MenuItemOptions[], event?: MouseEvent): void
abstract showMessageBox (options: MessageBoxOptions): Promise<MessageBoxResult>
abstract quit (): void
@ -191,6 +194,9 @@ export class HTMLFileUpload extends FileUpload {
return chunk
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
bringToFront (): void { }
// eslint-disable-next-line @typescript-eslint/no-empty-function
close (): void { }
}

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'
import { HostAppService } from './services/hostApp.service'
import { HostAppService } from './api/hostApp'
import { CLIHandler, CLIEvent } from './api/cli'
@Injectable()

View File

@ -3,7 +3,7 @@ import { Component, Inject, Input, HostListener, HostBinding } from '@angular/co
import { trigger, style, animate, transition, state } from '@angular/animations'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { HostAppService, Platform } from '../services/hostApp.service'
import { HostAppService, Platform } from '../api/hostApp'
import { HotkeysService } from '../services/hotkeys.service'
import { Logger, LogService } from '../services/log.service'
import { ConfigService } from '../services/config.service'
@ -115,7 +115,7 @@ export class AppRootComponent {
}
})
this.hostApp.windowCloseRequest$.subscribe(async () => {
this.hostWindow.windowCloseRequest$.subscribe(async () => {
this.app.closeWindow()
})

View File

@ -7,7 +7,7 @@ import { BaseTabComponent } from './baseTab.component'
import { RenameTabModalComponent } from './renameTabModal.component'
import { HotkeysService } from '../services/hotkeys.service'
import { AppService } from '../services/app.service'
import { HostAppService, Platform } from '../services/hostApp.service'
import { HostAppService, Platform } from '../api/hostApp'
import { ConfigService } from '../services/config.service'
import { BaseComponent } from './base.component'
import { MenuItemOptions } from '../api/menu'

View File

@ -1,5 +1,5 @@
import { ConfigProvider } from './api/configProvider'
import { Platform } from './services/hostApp.service'
import { Platform } from './api/hostApp'
/** @hidden */
export class CoreConfigProvider extends ConfigProvider {
@ -7,7 +7,7 @@ export class CoreConfigProvider extends ConfigProvider {
[Platform.macOS]: require('./configDefaults.macos.yaml'),
[Platform.Windows]: require('./configDefaults.windows.yaml'),
[Platform.Linux]: require('./configDefaults.linux.yaml'),
[Platform.Web]: require('./configDefaults.windows.yaml'),
[Platform.Web]: require('./configDefaults.web.yaml'),
}
defaults = require('./configDefaults.yaml')
}

View File

@ -1,8 +1,4 @@
hotkeys:
new-window:
- 'Ctrl-Shift-N'
toggle-window:
- 'Ctrl+Space'
toggle-fullscreen:
- 'F11'
close-tab:

View File

@ -1,8 +1,4 @@
hotkeys:
new-window:
- '⌘-N'
toggle-window:
- 'Ctrl+Space'
toggle-fullscreen:
- 'Ctrl+⌘+F'
close-tab:

View File

@ -0,0 +1,6 @@
pluginBlacklist: ['local']
terminal:
recoverTabs: false
enableAnalytics: false
enableWelcomeTab: false
enableAutomaticUpdates: false

View File

@ -1,8 +1,4 @@
hotkeys:
new-window:
- 'Ctrl-Shift-N'
toggle-window:
- 'Ctrl+Space'
toggle-fullscreen:
- 'F11'
- 'Alt-Enter'

View File

@ -5,14 +5,6 @@ import { HotkeyDescription, HotkeyProvider } from './api/hotkeyProvider'
@Injectable()
export class AppHotkeyProvider extends HotkeyProvider {
hotkeys: HotkeyDescription[] = [
{
id: 'new-window',
name: 'New window',
},
{
id: 'toggle-window',
name: 'Toggle terminal window',
},
{
id: 'toggle-fullscreen',
name: 'Toggle fullscreen mode',

View File

@ -27,7 +27,7 @@ import { AutofocusDirective } from './directives/autofocus.directive'
import { FastHtmlBindDirective } from './directives/fastHtmlBind.directive'
import { DropZoneDirective } from './directives/dropZone.directive'
import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider } from './api'
import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider, PlatformService } from './api'
import { AppService } from './services/app.service'
import { ConfigService } from './services/config.service'
@ -102,12 +102,16 @@ const PROVIDERS = [
],
})
export default class AppModule { // eslint-disable-line @typescript-eslint/no-extraneous-class
constructor (app: AppService, config: ConfigService) {
constructor (app: AppService, config: ConfigService, platform: PlatformService) {
app.ready$.subscribe(() => {
if (config.store.enableWelcomeTab) {
app.openNewTabRaw(WelcomeTabComponent)
}
})
platform.setErrorHandler(err => {
console.error('Unhandled exception:', err)
})
}
static forRoot (): ModuleWithProviders<AppModule> {

View File

@ -11,9 +11,9 @@ import { SelectorOption } from '../api/selector'
import { RecoveryToken } from '../api/tabRecovery'
import { BootstrapData, BOOTSTRAP_DATA } from '../api/mainProcess'
import { HostWindowService } from '../api/hostWindow'
import { HostAppService } from '../api/hostApp'
import { ConfigService } from './config.service'
import { HostAppService } from './hostApp.service'
import { TabRecoveryService } from './tabRecovery.service'
import { TabsService, TabComponentType } from './tabs.service'
@ -100,7 +100,7 @@ export class AppService {
}
})
hostApp.windowFocused$.subscribe(() => this._activeTab?.emitFocused())
hostWindow.windowFocused$.subscribe(() => this._activeTab?.emitFocused())
this.tabClosed$.subscribe(async tab => {
const token = await tabRecovery.getFullRecoveryToken(tab)

View File

@ -3,7 +3,7 @@ import * as yaml from 'js-yaml'
import { Injectable, Inject } from '@angular/core'
import { ConfigProvider } from '../api/configProvider'
import { PlatformService } from '../api/platform'
import { HostAppService } from './hostApp.service'
import { HostAppService } from '../api/hostApp'
import { Vault, VaultService } from './vault.service'
const deepmerge = require('deepmerge')

View File

@ -1,9 +1,14 @@
import { Observable, Subject } from 'rxjs'
export abstract class Screen {
id: number
name?: string
}
export abstract class DockingService {
get screensChanged$ (): Observable<void> { return this.screensChanged }
protected screensChanged = new Subject<void>()
abstract dock (): void
abstract getScreens (): Screen[]
}

View File

@ -1,8 +1,8 @@
import { Injectable } from '@angular/core'
import { Injectable, Inject } from '@angular/core'
import * as mixpanel from 'mixpanel'
import { v4 as uuidv4 } from 'uuid'
import { ConfigService } from './config.service'
import { PlatformService } from '../api'
import { PlatformService, BOOTSTRAP_DATA, BootstrapData } from '../api'
@Injectable({ providedIn: 'root' })
export class HomeBaseService {
@ -13,6 +13,7 @@ export class HomeBaseService {
private constructor (
private config: ConfigService,
private platform: PlatformService,
@Inject(BOOTSTRAP_DATA) private bootstrapData: BootstrapData,
) {
this.appVersion = platform.getAppVersion()
@ -38,7 +39,7 @@ export class HomeBaseService {
sunos: 'OS: Solaris',
win32: 'OS: Windows',
}[process.platform]
const plugins = (window as any).installedPlugins.filter(x => !x.isBuiltin).map(x => x.name)
const plugins = this.bootstrapData.installedPlugins.filter(x => !x.isBuiltin).map(x => x.name)
body += `Plugins: ${plugins.join(', ') || 'none'}\n\n`
this.platform.openExternal(`https://github.com/eugeny/terminus/issues/new?body=${encodeURIComponent(body)}&labels=${label}`)
}

View File

@ -1,209 +0,0 @@
import type { BrowserWindow, TouchBar } from 'electron'
import { Observable, Subject } from 'rxjs'
import { Injectable, NgZone, EventEmitter, Injector, Inject } from '@angular/core'
import { ElectronService } from './electron.service'
import { Logger, LogService } from './log.service'
import { CLIHandler } from '../api/cli'
import { BootstrapData, BOOTSTRAP_DATA } from '../api/mainProcess'
import { isWindowsBuild, WIN_BUILD_FLUENT_BG_SUPPORTED } from '../utils'
export enum Platform {
Linux = 'Linux',
macOS = 'macOS',
Windows = 'Windows',
Web = 'Web',
}
export interface Bounds {
x: number
y: number
width: number
height: number
}
/**
* Provides interaction with the main process
*/
@Injectable({ providedIn: 'root' })
export class HostAppService {
platform: Platform
configPlatform: Platform
/**
* Fired once the window is visible
*/
shown = new EventEmitter<any>()
isPortable = !!process.env.PORTABLE_EXECUTABLE_FILE
private preferencesMenu = new Subject<void>()
private configChangeBroadcast = new Subject<void>()
private windowCloseRequest = new Subject<void>()
private windowMoved = new Subject<void>()
private windowFocused = new Subject<void>()
private displayMetricsChanged = new Subject<void>()
private displaysChanged = new Subject<void>()
private logger: Logger
/**
* Fired when Preferences is selected in the macOS menu
*/
get preferencesMenu$ (): Observable<void> { return this.preferencesMenu }
/**
* Fired when another window modified the config file
*/
get configChangeBroadcast$ (): Observable<void> { return this.configChangeBroadcast }
/**
* Fired when the window close button is pressed
*/
get windowCloseRequest$ (): Observable<void> { return this.windowCloseRequest }
get windowMoved$ (): Observable<void> { return this.windowMoved }
get windowFocused$ (): Observable<void> { return this.windowFocused }
get displayMetricsChanged$ (): Observable<void> { return this.displayMetricsChanged }
get displaysChanged$ (): Observable<void> { return this.displaysChanged }
private constructor (
private zone: NgZone,
private electron: ElectronService,
@Inject(BOOTSTRAP_DATA) private bootstrapData: BootstrapData,
injector: Injector,
log: LogService,
) {
this.logger = log.create('hostApp')
this.configPlatform = this.platform = {
win32: Platform.Windows,
darwin: Platform.macOS,
linux: Platform.Linux,
}[process.platform]
if (process.env.XWEB) {
this.platform = Platform.Web
}
electron.ipcRenderer.on('host:preferences-menu', () => this.zone.run(() => this.preferencesMenu.next()))
electron.ipcRenderer.on('uncaughtException', (_$event, err) => {
this.logger.error('Unhandled exception:', err)
})
electron.ipcRenderer.on('host:window-shown', () => {
this.zone.run(() => this.shown.emit())
})
electron.ipcRenderer.on('host:window-close-request', () => {
this.zone.run(() => this.windowCloseRequest.next())
})
electron.ipcRenderer.on('host:window-moved', () => {
this.zone.run(() => this.windowMoved.next())
})
electron.ipcRenderer.on('host:window-focused', () => {
this.zone.run(() => this.windowFocused.next())
})
electron.ipcRenderer.on('host:display-metrics-changed', () => {
this.zone.run(() => this.displayMetricsChanged.next())
})
electron.ipcRenderer.on('host:displays-changed', () => {
this.zone.run(() => this.displaysChanged.next())
})
electron.ipcRenderer.on('cli', (_$event, argv: any, cwd: string, secondInstance: boolean) => this.zone.run(async () => {
const event = { argv, cwd, secondInstance }
this.logger.info('CLI arguments received:', event)
const cliHandlers = injector.get(CLIHandler) as unknown as CLIHandler[]
cliHandlers.sort((a, b) => b.priority - a.priority)
let handled = false
for (const handler of cliHandlers) {
if (handled && handler.firstMatchOnly) {
continue
}
if (await handler.handle(event)) {
this.logger.info('CLI handler matched:', handler.constructor.name)
handled = true
}
}
}))
electron.ipcRenderer.on('host:config-change', () => this.zone.run(() => {
this.configChangeBroadcast.next()
}))
if (isWindowsBuild(WIN_BUILD_FLUENT_BG_SUPPORTED)) {
electron.ipcRenderer.send('window-set-disable-vibrancy-while-dragging', true)
}
}
/**
* Returns the current remote [[BrowserWindow]]
*/
getWindow (): BrowserWindow {
return this.electron.BrowserWindow.fromId(this.bootstrapData.windowID)!
}
newWindow (): void {
this.electron.ipcRenderer.send('app:new-window')
}
openDevTools (): void {
this.getWindow().webContents.openDevTools({ mode: 'undocked' })
}
focusWindow (): void {
this.electron.ipcRenderer.send('window-focus')
}
setBounds (bounds: Bounds): void {
this.electron.ipcRenderer.send('window-set-bounds', bounds)
}
setAlwaysOnTop (flag: boolean): void {
this.electron.ipcRenderer.send('window-set-always-on-top', flag)
}
setTouchBar (touchBar: TouchBar): void {
this.getWindow().setTouchBar(touchBar)
}
/**
* Notifies other windows of config file changes
*/
broadcastConfigChange (configStore: Record<string, any>): void {
this.electron.ipcRenderer.send('app:config-change', configStore)
}
emitReady (): void {
this.electron.ipcRenderer.send('app:ready')
}
bringToFront (): void {
this.electron.ipcRenderer.send('window-bring-to-front')
}
registerGlobalHotkey (specs: string[]): void {
this.electron.ipcRenderer.send('app:register-global-hotkey', specs)
}
relaunch (): void {
if (this.isPortable) {
this.electron.app.relaunch({ execPath: process.env.PORTABLE_EXECUTABLE_FILE })
} else {
this.electron.app.relaunch()
}
this.electron.app.exit()
}
quit (): void {
this.logger.info('Quitting')
this.electron.app.quit()
}
}

View File

@ -3,7 +3,6 @@ import { Observable, Subject } from 'rxjs'
import { HotkeyDescription, HotkeyProvider } from '../api/hotkeyProvider'
import { stringifyKeySequence, EventData } from './hotkeys.util'
import { ConfigService } from './config.service'
import { HostAppService } from './hostApp.service'
export interface PartialHotkeyMatch {
id: string
@ -33,7 +32,6 @@ export class HotkeysService {
private constructor (
private zone: NgZone,
private hostApp: HostAppService,
private config: ConfigService,
@Inject(HotkeyProvider) private hotkeyProviders: HotkeyProvider[],
) {
@ -47,11 +45,7 @@ export class HotkeysService {
}
})
})
this.config.changed$.subscribe(() => {
this.registerGlobalHotkey()
})
this.config.ready$.toPromise().then(() => {
this.registerGlobalHotkey()
this.getHotkeyDescriptions().then(hotkeys => {
this.hotkeyDescriptions = hotkeys
})
@ -182,30 +176,6 @@ export class HotkeysService {
).reduce((a, b) => a.concat(b))
}
private registerGlobalHotkey () {
let value = this.config.store.hotkeys['toggle-window'] || []
if (typeof value === 'string') {
value = [value]
}
const specs: string[] = []
value.forEach((item: string | string[]) => {
item = typeof item === 'string' ? [item] : item
try {
let electronKeySpec = item[0]
electronKeySpec = electronKeySpec.replace('Meta', 'Super')
electronKeySpec = electronKeySpec.replace('⌘', 'Command')
electronKeySpec = electronKeySpec.replace('⌥', 'Alt')
electronKeySpec = electronKeySpec.replace(/-/g, '+')
specs.push(electronKeySpec)
} catch (err) {
console.error('Could not register the global hotkey:', err)
}
})
this.hostApp.registerGlobalHotkey(specs)
}
private getHotkeysConfig () {
return this.getHotkeysConfigRecursive(this.config.store.hotkeys)
}

View File

@ -0,0 +1,26 @@
import { ConfigProvider, Platform } from 'terminus-core'
/** @hidden */
export class ElectronConfigProvider extends ConfigProvider {
platformDefaults = {
[Platform.macOS]: {
hotkeys: {
'toggle-window': ['Ctrl-Space'],
'new-window': ['⌘-N'],
},
},
[Platform.Windows]: {
hotkeys: {
'toggle-window': ['Ctrl-Space'],
'new-window': ['Ctrl-Shift-N'],
},
},
[Platform.Linux]: {
hotkeys: {
'toggle-window': ['Ctrl-Space'],
'new-window': ['Ctrl-Shift-N'],
},
},
}
defaults = {}
}

View File

@ -0,0 +1,21 @@
import { Injectable } from '@angular/core'
import { HotkeyDescription, HotkeyProvider } from 'terminus-core'
/** @hidden */
@Injectable()
export class ElectronHotkeyProvider extends HotkeyProvider {
hotkeys: HotkeyDescription[] = [
{
id: 'new-window',
name: 'New window',
},
{
id: 'toggle-window',
name: 'Toggle terminal window',
},
]
async provide (): Promise<HotkeyDescription[]> {
return this.hotkeys
}
}

View File

@ -1,5 +1,5 @@
import { NgModule } from '@angular/core'
import { PlatformService, LogService, UpdaterService, DockingService, HostAppService, ThemesService, Platform, AppService, ConfigService, ElectronService, WIN_BUILD_FLUENT_BG_SUPPORTED, isWindowsBuild, HostWindowService } from 'terminus-core'
import { PlatformService, LogService, UpdaterService, DockingService, HostAppService, ThemesService, Platform, AppService, ConfigService, ElectronService, WIN_BUILD_FLUENT_BG_SUPPORTED, isWindowsBuild, HostWindowService, HotkeyProvider, ConfigProvider } from 'terminus-core'
import { TerminalColorSchemeProvider } from 'terminus-terminal'
import { HyperColorSchemes } from './colorSchemes'
@ -9,39 +9,51 @@ import { ElectronUpdaterService } from './services/updater.service'
import { TouchbarService } from './services/touchbar.service'
import { ElectronDockingService } from './services/docking.service'
import { ElectronHostWindow } from './services/hostWindow.service'
import { ElectronHostAppService } from './services/hostApp.service'
import { ElectronHotkeyProvider } from './hotkeys'
import { ElectronConfigProvider } from './config'
@NgModule({
providers: [
{ provide: TerminalColorSchemeProvider, useClass: HyperColorSchemes, multi: true },
{ provide: PlatformService, useClass: ElectronPlatformService },
{ provide: HostWindowService, useClass: ElectronHostWindow },
{ provide: HostAppService, useClass: ElectronHostAppService },
{ provide: LogService, useClass: ElectronLogService },
{ provide: UpdaterService, useClass: ElectronUpdaterService },
{ provide: DockingService, useClass: ElectronDockingService },
{ provide: HotkeyProvider, useClass: ElectronHotkeyProvider, multi: true },
{ provide: ConfigProvider, useClass: ElectronConfigProvider, multi: true },
],
})
export default class ElectronModule {
constructor (
private config: ConfigService,
private hostApp: HostAppService,
private hostApp: ElectronHostAppService,
private electron: ElectronService,
private hostWindow: ElectronHostWindow,
touchbar: TouchbarService,
docking: DockingService,
themeService: ThemesService,
app: AppService
app: AppService,
) {
config.ready$.toPromise().then(() => {
touchbar.update()
docking.dock()
hostApp.shown.subscribe(() => {
hostWindow.windowShown$.subscribe(() => {
docking.dock()
})
this.registerGlobalHotkey()
this.updateVibrancy()
})
config.changed$.subscribe(() => {
this.registerGlobalHotkey()
})
themeService.themeChanged$.subscribe(theme => {
if (hostApp.platform === Platform.macOS) {
hostApp.getWindow().setTrafficLightPosition({
hostWindow.getWindow().setTrafficLightPosition({
x: theme.macOSWindowButtonsInsetX ?? 14,
y: theme.macOSWindowButtonsInsetY ?? 11,
})
@ -55,9 +67,9 @@ export default class ElectronModule {
return
}
if (progress !== null) {
hostApp.getWindow().setProgressBar(progress / 100.0, { mode: 'normal' })
hostWindow.getWindow().setProgressBar(progress / 100.0, { mode: 'normal' })
} else {
hostApp.getWindow().setProgressBar(-1, { mode: 'none' })
hostWindow.getWindow().setProgressBar(-1, { mode: 'none' })
}
lastProgress = progress
})
@ -66,6 +78,30 @@ export default class ElectronModule {
config.changed$.subscribe(() => this.updateVibrancy())
}
private registerGlobalHotkey () {
let value = this.config.store.hotkeys['toggle-window'] || []
if (typeof value === 'string') {
value = [value]
}
const specs: string[] = []
value.forEach((item: string | string[]) => {
item = typeof item === 'string' ? [item] : item
try {
let electronKeySpec = item[0]
electronKeySpec = electronKeySpec.replace('Meta', 'Super')
electronKeySpec = electronKeySpec.replace('⌘', 'Command')
electronKeySpec = electronKeySpec.replace('⌥', 'Alt')
electronKeySpec = electronKeySpec.replace(/-/g, '+')
specs.push(electronKeySpec)
} catch (err) {
console.error('Could not register the global hotkey:', err)
}
})
this.electron.ipcRenderer.send('app:register-global-hotkey', specs)
}
private updateVibrancy () {
let vibrancyType = this.config.store.appearance.vibrancyType
if (this.hostApp.platform === Platform.Windows && !isWindowsBuild(WIN_BUILD_FLUENT_BG_SUPPORTED)) {
@ -74,6 +110,8 @@ export default class ElectronModule {
document.body.classList.toggle('vibrant', this.config.store.appearance.vibrancy)
this.electron.ipcRenderer.send('window-set-vibrancy', this.config.store.appearance.vibrancy, vibrancyType)
this.hostApp.getWindow().setOpacity(this.config.store.appearance.opacity)
this.hostWindow.getWindow().setOpacity(this.config.store.appearance.opacity)
}
}
export { ElectronHostWindow, ElectronHostAppService }

View File

@ -1,24 +1,31 @@
import { Injectable } from '@angular/core'
import { Injectable, NgZone } from '@angular/core'
import type { Display } from 'electron'
import { ConfigService, ElectronService, HostAppService, Bounds, DockingService, Screen } from 'terminus-core'
import { ConfigService, ElectronService, DockingService, Screen, PlatformService } from 'terminus-core'
import { ElectronHostWindow, Bounds } from './hostWindow.service'
@Injectable()
export class ElectronDockingService extends DockingService {
constructor (
private electron: ElectronService,
private config: ConfigService,
private hostApp: HostAppService,
private zone: NgZone,
private hostWindow: ElectronHostWindow,
platform: PlatformService,
) {
super()
hostApp.displaysChanged$.subscribe(() => this.repositionWindow())
hostApp.displayMetricsChanged$.subscribe(() => this.repositionWindow())
this.screensChanged$.subscribe(() => this.repositionWindow())
platform.displayMetricsChanged$.subscribe(() => this.repositionWindow())
electron.ipcRenderer.on('host:displays-changed', () => {
this.zone.run(() => this.screensChanged.next())
})
}
dock (): void {
const dockSide = this.config.store.appearance.dock
if (dockSide === 'off') {
this.hostApp.setAlwaysOnTop(false)
this.hostWindow.setAlwaysOnTop(false)
return
}
@ -33,7 +40,7 @@ export class ElectronDockingService extends DockingService {
const fill = this.config.store.appearance.dockFill <= 1 ? this.config.store.appearance.dockFill : 1
const space = this.config.store.appearance.dockSpace <= 1 ? this.config.store.appearance.dockSpace : 1
const [minWidth, minHeight] = this.hostApp.getWindow().getMinimumSize()
const [minWidth, minHeight] = this.hostWindow.getWindow().getMinimumSize()
if (dockSide === 'left' || dockSide === 'right') {
newBounds.width = Math.max(minWidth, Math.round(fill * display.bounds.width))
@ -60,9 +67,9 @@ export class ElectronDockingService extends DockingService {
const alwaysOnTop = this.config.store.appearance.dockAlwaysOnTop
this.hostApp.setAlwaysOnTop(alwaysOnTop)
this.hostWindow.setAlwaysOnTop(alwaysOnTop)
setImmediate(() => {
this.hostApp.setBounds(newBounds)
this.hostWindow.setBounds(newBounds)
})
}
@ -84,7 +91,7 @@ export class ElectronDockingService extends DockingService {
}
private repositionWindow () {
const [x, y] = this.hostApp.getWindow().getPosition()
const [x, y] = this.hostWindow.getWindow().getPosition()
for (const screen of this.electron.screen.getAllDisplays()) {
const bounds = screen.bounds
if (x >= bounds.x && x <= bounds.x + bounds.width && y >= bounds.y && y <= bounds.y + bounds.height) {
@ -92,6 +99,6 @@ export class ElectronDockingService extends DockingService {
}
}
const screen = this.electron.screen.getPrimaryDisplay()
this.hostApp.getWindow().setPosition(screen.bounds.x, screen.bounds.y)
this.hostWindow.getWindow().setPosition(screen.bounds.x, screen.bounds.y)
}
}

View File

@ -0,0 +1,85 @@
import { Injectable, NgZone, Injector } from '@angular/core'
import { ElectronService, isWindowsBuild, WIN_BUILD_FLUENT_BG_SUPPORTED, HostAppService, Platform, CLIHandler } from 'terminus-core'
@Injectable({ providedIn: 'root' })
export class ElectronHostAppService extends HostAppService {
get platform (): Platform {
return this.configPlatform
}
get configPlatform (): Platform {
return {
win32: Platform.Windows,
darwin: Platform.macOS,
linux: Platform.Linux,
}[process.platform]
}
constructor (
private zone: NgZone,
private electron: ElectronService,
injector: Injector,
) {
super(injector)
electron.ipcRenderer.on('host:preferences-menu', () => this.zone.run(() => this.settingsUIRequest.next()))
electron.ipcRenderer.on('cli', (_$event, argv: any, cwd: string, secondInstance: boolean) => this.zone.run(async () => {
const event = { argv, cwd, secondInstance }
this.logger.info('CLI arguments received:', event)
const cliHandlers = injector.get(CLIHandler) as unknown as CLIHandler[]
cliHandlers.sort((a, b) => b.priority - a.priority)
let handled = false
for (const handler of cliHandlers) {
if (handled && handler.firstMatchOnly) {
continue
}
if (await handler.handle(event)) {
this.logger.info('CLI handler matched:', handler.constructor.name)
handled = true
}
}
}))
electron.ipcRenderer.on('host:config-change', () => this.zone.run(() => {
this.configChangeBroadcast.next()
}))
if (isWindowsBuild(WIN_BUILD_FLUENT_BG_SUPPORTED)) {
electron.ipcRenderer.send('window-set-disable-vibrancy-while-dragging', true)
}
}
newWindow (): void {
this.electron.ipcRenderer.send('app:new-window')
}
/**
* Notifies other windows of config file changes
*/
broadcastConfigChange (configStore: Record<string, any>): void {
this.electron.ipcRenderer.send('app:config-change', configStore)
}
emitReady (): void {
this.electron.ipcRenderer.send('app:ready')
}
relaunch (): void {
const isPortable = !!process.env.PORTABLE_EXECUTABLE_FILE
if (isPortable) {
this.electron.app.relaunch({ execPath: process.env.PORTABLE_EXECUTABLE_FILE })
} else {
this.electron.app.relaunch()
}
this.electron.app.exit()
}
quit (): void {
this.logger.info('Quitting')
this.electron.app.quit()
}
}

View File

@ -1,19 +1,24 @@
import { Injectable, NgZone } from '@angular/core'
import { Observable, Subject } from 'rxjs'
import { ElectronService, HostAppService, HostWindowService } from 'terminus-core'
import type { BrowserWindow, TouchBar } from 'electron'
import { Injectable, Inject, NgZone } from '@angular/core'
import { BootstrapData, BOOTSTRAP_DATA, ElectronService, HostWindowService } from 'terminus-core'
export interface Bounds {
x: number
y: number
width: number
height: number
}
@Injectable({ providedIn: 'root' })
export class ElectronHostWindow extends HostWindowService {
get closeRequest$ (): Observable<void> { return this.closeRequest }
get isFullscreen (): boolean { return this._isFullScreen}
private closeRequest = new Subject<void>()
private _isFullScreen = false
constructor (
private electron: ElectronService,
private hostApp: HostAppService,
zone: NgZone,
private electron: ElectronService,
@Inject(BOOTSTRAP_DATA) private bootstrapData: BootstrapData,
) {
super()
electron.ipcRenderer.on('host:window-enter-full-screen', () => zone.run(() => {
@ -23,10 +28,34 @@ export class ElectronHostWindow extends HostWindowService {
electron.ipcRenderer.on('host:window-leave-full-screen', () => zone.run(() => {
this._isFullScreen = false
}))
electron.ipcRenderer.on('host:window-shown', () => {
zone.run(() => this.windowShown.next())
})
electron.ipcRenderer.on('host:window-close-request', () => {
zone.run(() => this.windowCloseRequest.next())
})
electron.ipcRenderer.on('host:window-moved', () => {
zone.run(() => this.windowMoved.next())
})
electron.ipcRenderer.on('host:window-focused', () => {
zone.run(() => this.windowFocused.next())
})
}
getWindow (): BrowserWindow {
return this.electron.BrowserWindow.fromId(this.bootstrapData.windowID)!
}
openDevTools (): void {
this.getWindow().webContents.openDevTools({ mode: 'undocked' })
}
reload (): void {
this.hostApp.getWindow().reload()
this.getWindow().reload()
}
setTitle (title?: string): void {
@ -34,7 +63,7 @@ export class ElectronHostWindow extends HostWindowService {
}
toggleFullscreen (): void {
this.hostApp.getWindow().setFullScreen(!this._isFullScreen)
this.getWindow().setFullScreen(!this._isFullScreen)
}
minimize (): void {
@ -48,4 +77,20 @@ export class ElectronHostWindow extends HostWindowService {
close (): void {
this.electron.ipcRenderer.send('window-close')
}
setBounds (bounds: Bounds): void {
this.electron.ipcRenderer.send('window-set-bounds', bounds)
}
setAlwaysOnTop (flag: boolean): void {
this.electron.ipcRenderer.send('window-set-always-on-top', flag)
}
setTouchBar (touchBar: TouchBar): void {
this.getWindow().setTouchBar(touchBar)
}
bringToFront (): void {
this.electron.ipcRenderer.send('window-bring-to-front')
}
}

View File

@ -6,6 +6,7 @@ import promiseIpc from 'electron-promise-ipc'
import { execFile } from 'mz/child_process'
import { Injectable, NgZone } from '@angular/core'
import { PlatformService, ClipboardContent, HostAppService, Platform, ElectronService, MenuItemOptions, MessageBoxOptions, MessageBoxResult, FileUpload, FileDownload, FileUploadOptions, wrapPromise } from 'terminus-core'
import { ElectronHostWindow } from './hostWindow.service'
const fontManager = require('fontmanager-redux') // eslint-disable-line
/* eslint-disable block-scoped-var */
@ -20,16 +21,20 @@ try {
@Injectable()
export class ElectronPlatformService extends PlatformService {
supportsWindowControls = true
private userPluginsPath: string = (window as any).userPluginsPath
private configPath: string
constructor (
private hostApp: HostAppService,
private hostWindow: ElectronHostWindow,
private electron: ElectronService,
private zone: NgZone,
) {
super()
this.configPath = path.join(electron.app.getPath('userData'), 'config.yaml')
electron.ipcRenderer.on('host:display-metrics-changed', () => {
this.zone.run(() => this.displayMetricsChanged.next())
})
}
readClipboard (): string {
@ -41,11 +46,11 @@ export class ElectronPlatformService extends PlatformService {
}
async installPlugin (name: string, version: string): Promise<void> {
await (promiseIpc as any).send('plugin-manager:install', this.userPluginsPath, name, version)
await (promiseIpc as any).send('plugin-manager:install', name, version)
}
async uninstallPlugin (name: string): Promise<void> {
await (promiseIpc as any).send('plugin-manager:uninstall', this.userPluginsPath, name)
await (promiseIpc as any).send('plugin-manager:uninstall', name)
}
async isProcessRunning (name: string): Promise<boolean> {
@ -163,7 +168,7 @@ export class ElectronPlatformService extends PlatformService {
}
async showMessageBox (options: MessageBoxOptions): Promise<MessageBoxResult> {
return this.electron.dialog.showMessageBox(this.hostApp.getWindow(), options)
return this.electron.dialog.showMessageBox(this.hostWindow.getWindow(), options)
}
quit (): void {
@ -179,7 +184,7 @@ export class ElectronPlatformService extends PlatformService {
}
const result = await this.electron.dialog.showOpenDialog(
this.hostApp.getWindow(),
this.hostWindow.getWindow(),
{
buttonLabel: 'Select',
properties,
@ -199,7 +204,7 @@ export class ElectronPlatformService extends PlatformService {
async startDownload (name: string, size: number): Promise<FileDownload|null> {
const result = await this.electron.dialog.showSaveDialog(
this.hostApp.getWindow(),
this.hostWindow.getWindow(),
{
defaultPath: name,
},
@ -212,6 +217,12 @@ export class ElectronPlatformService extends PlatformService {
this.fileTransferStarted.next(transfer)
return transfer
}
setErrorHandler (handler: (_: any) => void): void {
this.electron.ipcRenderer.on('uncaughtException', (_$event, err) => {
handler(err)
})
}
}
class ElectronFileUpload extends FileUpload {

View File

@ -1,6 +1,7 @@
import { SegmentedControlSegment, TouchBarSegmentedControl } from 'electron'
import { Injectable, NgZone } from '@angular/core'
import { AppService, HostAppService, Platform, ElectronService } from 'terminus-core'
import { ElectronHostWindow } from './hostWindow.service'
/** @hidden */
@Injectable({ providedIn: 'root' })
@ -11,6 +12,7 @@ export class TouchbarService {
private constructor (
private app: AppService,
private hostApp: HostAppService,
private hostWindow: ElectronHostWindow,
private electron: ElectronService,
private zone: NgZone,
) {
@ -68,7 +70,7 @@ export class TouchbarService {
this.tabsSegmentedControl,
],
})
this.hostApp.setTouchBar(touchBar)
this.hostWindow.setTouchBar(touchBar)
}
private shortenTitle (title: string): string {

View File

@ -1,7 +1,7 @@
import * as path from 'path'
import * as fs from 'mz/fs'
import { Injectable } from '@angular/core'
import { CLIHandler, CLIEvent, HostAppService, AppService, ConfigService } from 'terminus-core'
import { CLIHandler, CLIEvent, AppService, ConfigService, HostWindowService } from 'terminus-core'
import { TerminalService } from './services/terminal.service'
@Injectable()
@ -11,7 +11,7 @@ export class TerminalCLIHandler extends CLIHandler {
constructor (
private config: ConfigService,
private hostApp: HostAppService,
private hostWindow: HostWindowService,
private terminal: TerminalService,
) {
super()
@ -40,7 +40,7 @@ export class TerminalCLIHandler extends CLIHandler {
if (await fs.exists(directory)) {
if ((await fs.stat(directory)).isDirectory()) {
this.terminal.openTab(undefined, directory)
this.hostApp.bringToFront()
this.hostWindow.bringToFront()
}
}
}
@ -53,7 +53,7 @@ export class TerminalCLIHandler extends CLIHandler {
args: command.slice(1),
},
}, null, true)
this.hostApp.bringToFront()
this.hostWindow.bringToFront()
}
private handleOpenProfile (profileName: string) {
@ -63,7 +63,7 @@ export class TerminalCLIHandler extends CLIHandler {
return
}
this.terminal.openTabWithOptions(profile.sessionOptions)
this.hostApp.bringToFront()
this.hostWindow.bringToFront()
}
}
@ -75,7 +75,7 @@ export class OpenPathCLIHandler extends CLIHandler {
constructor (
private terminal: TerminalService,
private hostApp: HostAppService,
private hostWindow: HostWindowService,
) {
super()
}
@ -86,7 +86,7 @@ export class OpenPathCLIHandler extends CLIHandler {
if (opAsPath && (await fs.lstat(opAsPath)).isDirectory()) {
this.terminal.openTab(undefined, opAsPath)
this.hostApp.bringToFront()
this.hostWindow.bringToFront()
return true
}

View File

@ -2,6 +2,7 @@ import { Component } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { Subscription } from 'rxjs'
import { ConfigService, ElectronService, HostAppService, Platform, WIN_BUILD_CONPTY_SUPPORTED, WIN_BUILD_CONPTY_STABLE, isWindowsBuild } from 'terminus-core'
import { ElectronHostWindow } from 'terminus-electron'
import { EditProfileModalComponent } from './editProfileModal.component'
import { Shell, Profile } from '../api'
import { TerminalService } from '../services/terminal.service'
@ -21,6 +22,7 @@ export class ShellSettingsTabComponent {
constructor (
public config: ConfigService,
public hostApp: HostAppService,
public hostWindow: ElectronHostWindow,
public terminal: TerminalService,
private electron: ElectronService,
private ngbModal: NgbModal,
@ -54,7 +56,7 @@ export class ShellSettingsTabComponent {
return
}
const paths = (await this.electron.dialog.showOpenDialog(
this.hostApp.getWindow(),
this.hostWindow.getWindow(),
{
defaultPath: shell.fsBase,
properties: ['openDirectory', 'showHiddenFiles'],

View File

@ -4,8 +4,8 @@ import { debounceTime, distinctUntilChanged, first, tap, flatMap, map } from 'rx
import semverGt from 'semver/functions/gt'
import { Component, Input } from '@angular/core'
import { ConfigService, PlatformService } from 'terminus-core'
import { PluginInfo, PluginManagerService } from '../services/pluginManager.service'
import { ConfigService, PlatformService, PluginInfo } from 'terminus-core'
import { PluginManagerService } from '../services/pluginManager.service'
enum BusyState { Installing = 'Installing', Uninstalling = 'Uninstalling' }

View File

@ -1,8 +1,8 @@
import axios from 'axios'
import { Observable, from } from 'rxjs'
import { map } from 'rxjs/operators'
import { Injectable } from '@angular/core'
import { Logger, LogService, PlatformService } from 'terminus-core'
import { Injectable, Inject } from '@angular/core'
import { Logger, LogService, PlatformService, BOOTSTRAP_DATA, BootstrapData, PluginInfo } from 'terminus-core'
const NAME_PREFIX = 'terminus-'
const KEYWORD = 'terminus-plugin'
@ -12,30 +12,20 @@ const BLACKLIST = [
'terminus-shell-selector', // superseded by profiles
]
export interface PluginInfo {
name: string
description: string
packageName: string
isBuiltin: boolean
isOfficial: boolean
version: string
homepage?: string
author: string
path?: string
}
@Injectable({ providedIn: 'root' })
export class PluginManagerService {
logger: Logger
builtinPluginsPath: string = (window as any).builtinPluginsPath
userPluginsPath: string = (window as any).userPluginsPath
installedPlugins: PluginInfo[] = (window as any).installedPlugins
userPluginsPath: string
installedPlugins: PluginInfo[]
private constructor (
log: LogService,
private platform: PlatformService,
@Inject(BOOTSTRAP_DATA) bootstrapData: BootstrapData,
) {
this.logger = log.create('pluginManager')
this.installedPlugins = bootstrapData.installedPlugins
this.userPluginsPath = bootstrapData.userPluginsPath
}
listAvailable (query?: string): Observable<PluginInfo[]> {

View File

@ -12,7 +12,7 @@ export class ButtonProvider extends ToolbarButtonProvider {
private app: AppService,
) {
super()
hostApp.preferencesMenu$.subscribe(() => this.open())
hostApp.settingsUIRequest$.subscribe(() => this.open())
hotkeys.matchedHotkey.subscribe(async (hotkey) => {
if (hotkey === 'settings') {

View File

@ -24,7 +24,7 @@ button.btn.btn-outline-warning.btn-block(*ngIf='config.restartRequested', '(clic
span Report a problem
button.btn.btn-secondary(
*ngIf='!updateAvailable',
*ngIf='!updateAvailable && hostApp.platform !== Platform.Web',
(click)='checkForUpdates()',
[disabled]='checkingForUpdate'
)
@ -46,7 +46,7 @@ button.btn.btn-outline-warning.btn-block(*ngIf='config.restartRequested', '(clic
.description Allows quickly opening a terminal in the selected folder
toggle([ngModel]='isShellIntegrationInstalled', (ngModelChange)='toggleShellIntegration()')
.form-line
.form-line(*ngIf='hostApp.platform !== Platform.Web')
.header
.title Enable analytics
.description We're only tracking your Terminus and OS versions.
@ -55,17 +55,17 @@ button.btn.btn-outline-warning.btn-block(*ngIf='config.restartRequested', '(clic
(ngModelChange)='saveConfiguration(true)',
)
.form-line
.form-line(*ngIf='hostApp.platform !== Platform.Web')
.header
.title Automatic Updates
.description Enable automatic installation of updates when they become available.
toggle([(ngModel)]='config.store.enableAutomaticUpdates', (ngModelChange)='saveConfiguration()')
.form-line
.form-line(*ngIf='hostApp.platform !== Platform.Web')
.header
.title Debugging
button.btn.btn-secondary((click)='hostApp.openDevTools()')
button.btn.btn-secondary((click)='hostWindow.openDevTools()')
i.fas.fa-bug
span Open DevTools

View File

@ -10,6 +10,7 @@ import {
HomeBaseService,
UpdaterService,
PlatformService,
HostWindowService,
} from 'terminus-core'
import { SettingsTabProvider } from '../api'
@ -36,6 +37,7 @@ export class SettingsTabComponent extends BaseTabComponent {
constructor (
public config: ConfigService,
public hostApp: HostAppService,
public hostWindow: HostWindowService,
public homeBase: HomeBaseService,
public platform: PlatformService,
public zone: NgZone,

View File

@ -39,7 +39,7 @@ export class WindowSettingsTabComponent extends BaseComponent {
const dockingService = docking
if (dockingService) {
this.subscribeUntilDestroyed(hostApp.displaysChanged$, () => {
this.subscribeUntilDestroyed(dockingService.screensChanged$, () => {
this.zone.run(() => this.screens = dockingService.getScreens())
})
this.screens = dockingService.getScreens()

View File

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Injectable } from '@angular/core'
import { HotkeysService, ToolbarButtonProvider, ToolbarButton } from 'terminus-core'
import { HotkeysService, ToolbarButtonProvider, ToolbarButton, HostAppService, Platform } from 'terminus-core'
import { SSHService } from './services/ssh.service'
/** @hidden */
@ -8,6 +8,7 @@ import { SSHService } from './services/ssh.service'
export class ButtonProvider extends ToolbarButtonProvider {
constructor (
hotkeys: HotkeysService,
private hostApp: HostAppService,
private ssh: SSHService,
) {
super()
@ -23,14 +24,20 @@ export class ButtonProvider extends ToolbarButtonProvider {
}
provide (): ToolbarButton[] {
return [{
icon: require('./icons/globe.svg'),
weight: 5,
title: 'SSH connections',
touchBarNSImage: 'NSTouchBarOpenInBrowserTemplate',
click: () => {
this.activate()
},
}]
if (this.hostApp.platform === Platform.Web) {
return [{
icon: require('../../terminus-local/src/icons/plus.svg'),
title: 'SSH connections',
click: () => this.activate(),
}]
} else {
return [{
icon: require('./icons/globe.svg'),
weight: 5,
title: 'SSH connections',
touchBarNSImage: 'NSTouchBarOpenInBrowserTemplate',
click: () => this.activate(),
}]
}
}
}

View File

@ -4,7 +4,7 @@ import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { Observable } from 'rxjs'
import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators'
import { ElectronService, HostAppService, ConfigService, PlatformService } from 'terminus-core'
import { ElectronService, ConfigService, PlatformService } from 'terminus-core'
import { PasswordStorageService } from '../services/passwordStorage.service'
import { SSHConnection, LoginScript, ForwardedPortConfig, SSHAlgorithmType, ALGORITHM_BLACKLIST } from '../api'
import { PromptModalComponent } from './promptModal.component'
@ -30,7 +30,6 @@ export class EditConnectionModalComponent {
private modalInstance: NgbActiveModal,
private electron: ElectronService,
private platform: PlatformService,
private hostApp: HostAppService,
private passwordStorage: PasswordStorageService,
private ngbModal: NgbModal,
) {
@ -104,7 +103,6 @@ export class EditConnectionModalComponent {
addPrivateKey () {
this.electron.dialog.showOpenDialog(
this.hostApp.getWindow(),
{
defaultPath: this.connection.privateKeys![0],
title: 'Select private key',

View File

@ -3,7 +3,7 @@ import { first } from 'rxjs/operators'
import colors from 'ansi-colors'
import { NgZone, OnInit, OnDestroy, Injector, ViewChild, HostBinding, Input, ElementRef, InjectFlags } from '@angular/core'
import { trigger, transition, style, animate, AnimationTriggerMetadata } from '@angular/animations'
import { AppService, ConfigService, BaseTabComponent, HostAppService, HotkeysService, NotificationsService, Platform, LogService, Logger, TabContextMenuItemProvider, SplitTabComponent, SubscriptionContainer, MenuItemOptions, PlatformService } from 'terminus-core'
import { AppService, ConfigService, BaseTabComponent, HostAppService, HotkeysService, NotificationsService, Platform, LogService, Logger, TabContextMenuItemProvider, SplitTabComponent, SubscriptionContainer, MenuItemOptions, PlatformService, HostWindowService } from 'terminus-core'
import { BaseSession } from '../session'
import { TerminalFrontendService } from '../services/terminalFrontend.service'
@ -108,6 +108,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
protected log: LogService
protected decorators: TerminalDecorator[] = []
protected contextMenuProviders: TabContextMenuItemProvider[]
protected hostWindow: HostWindowService
// Deps end
protected logger: Logger
@ -160,6 +161,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
this.log = injector.get(LogService)
this.decorators = injector.get<any>(TerminalDecorator, null, InjectFlags.Optional) as TerminalDecorator[]
this.contextMenuProviders = injector.get<any>(TabContextMenuItemProvider, null, InjectFlags.Optional) as TabContextMenuItemProvider[]
this.hostWindow = injector.get(HostWindowService)
this.logger = this.log.create('baseTerminalTab')
this.setTitle('Terminal')
@ -596,8 +598,8 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
})
})
this.termContainerSubscriptions.subscribe(this.hostApp.displayMetricsChanged$, maybeConfigure)
this.termContainerSubscriptions.subscribe(this.hostApp.windowMoved$, maybeConfigure)
this.termContainerSubscriptions.subscribe(this.platform.displayMetricsChanged$, maybeConfigure)
this.termContainerSubscriptions.subscribe(this.hostWindow.windowMoved$, maybeConfigure)
}
setSession (session: BaseSession|null, destroyOnSessionClose = false): void {

View File

@ -1,6 +1,6 @@
import shellEscape from 'shell-escape'
import { Injectable } from '@angular/core'
import { CLIHandler, CLIEvent, HostAppService, AppService } from 'terminus-core'
import { CLIHandler, CLIEvent, AppService, HostWindowService } from 'terminus-core'
import { BaseTerminalTabComponent } from './api/baseTerminalTab.component'
@Injectable()
@ -10,7 +10,7 @@ export class TerminalCLIHandler extends CLIHandler {
constructor (
private app: AppService,
private hostApp: HostAppService,
private hostWindow: HostWindowService,
) {
super()
}
@ -30,11 +30,10 @@ export class TerminalCLIHandler extends CLIHandler {
return false
}
private handlePaste (text: string) {
if (this.app.activeTab instanceof BaseTerminalTabComponent && this.app.activeTab.session) {
this.app.activeTab.sendInput(text)
this.hostApp.bringToFront()
this.hostWindow.bringToFront()
}
}
}

View File

@ -1,11 +1,12 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { HostWindowService, LogService, PlatformService, UpdaterService } from 'terminus-core'
import { HostAppService, HostWindowService, LogService, PlatformService, UpdaterService } from 'terminus-core'
import { WebPlatformService } from './platform'
import { ConsoleLogService } from './services/log.service'
import { NullUpdaterService } from './services/updater.service'
import { WebHostWindow } from './services/hostWindow.service'
import { WebHostApp } from './services/hostApp.service'
import { MessageBoxModalComponent } from './components/messageBoxModal.component'
import './styles.scss'
@ -19,6 +20,7 @@ import './styles.scss'
{ provide: LogService, useClass: ConsoleLogService },
{ provide: UpdaterService, useClass: NullUpdaterService },
{ provide: HostWindowService, useClass: WebHostWindow },
{ provide: HostAppService, useClass: WebHostApp },
],
declarations: [
MessageBoxModalComponent,

View File

@ -61,7 +61,7 @@ export class WebPlatformService extends PlatformService {
}
getAppVersion (): string {
return '1.0-web'
return this.connector.getAppVersion()
}
async listFonts (): Promise<string[]> {
@ -136,6 +136,10 @@ export class WebPlatformService extends PlatformService {
this.fileSelector.click()
})
}
setErrorHandler (handler: (_: any) => void): void {
window.addEventListener('error', handler)
}
}
class HTMLFileDownload extends FileDownload {

View File

@ -0,0 +1,33 @@
import { Injectable, Injector } from '@angular/core'
import { HostAppService, Platform } from 'terminus-core'
@Injectable()
export class WebHostApp extends HostAppService {
get platform (): Platform {
return Platform.Web
}
get configPlatform (): Platform {
return Platform.Windows // TODO
}
// Needed for injector metadata
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
constructor (
injector: Injector,
) {
super(injector)
}
newWindow (): void {
throw new Error('Not implemented')
}
relaunch (): void {
location.reload()
}
quit (): void {
window.close()
}
}

View File

@ -1,13 +1,15 @@
import { Injectable } from '@angular/core'
import { Observable, Subject } from 'rxjs'
import { HostWindowService } from 'terminus-core'
@Injectable({ providedIn: 'root' })
export class WebHostWindow extends HostWindowService {
get closeRequest$ (): Observable<void> { return this.closeRequest }
get isFullscreen (): boolean { return !!document.fullscreenElement }
private closeRequest = new Subject<void>()
constructor () {
super()
this.windowShown.next()
this.windowFocused.next()
}
reload (): void {
location.reload()