more electron/web separation

This commit is contained in:
Eugene Pankov
2021-06-19 01:36:25 +02:00
parent fa31ac65ab
commit fad7858f3f
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 { app, ipcMain, Menu, Tray, shell, screen, globalShortcut, MenuItemConstructorOptions } from 'electron'
import * as promiseIpc from 'electron-promise-ipc' import * as promiseIpc from 'electron-promise-ipc'
import * as remote from '@electron/remote/main' import * as remote from '@electron/remote/main'
import * as path from 'path'
import * as fs from 'fs'
import { loadConfig } from './config' import { loadConfig } from './config'
import { Window, WindowOptions } from './window' import { Window, WindowOptions } from './window'
@@ -17,6 +19,7 @@ export class Application {
private tray?: Tray private tray?: Tray
private ptyManager = new PTYManager() private ptyManager = new PTYManager()
private windows: Window[] = [] private windows: Window[] = []
userPluginsPath: string
constructor () { constructor () {
remote.initialize() remote.initialize()
@@ -36,12 +39,12 @@ export class Application {
} }
}) })
;(promiseIpc as any).on('plugin-manager:install', (path, name, version) => { ;(promiseIpc as any).on('plugin-manager:install', (name, version) => {
return pluginManager.install(path, name, version) return pluginManager.install(this.userPluginsPath, name, version)
}) })
;(promiseIpc as any).on('plugin-manager:uninstall', (path, name) => { ;(promiseIpc as any).on('plugin-manager:uninstall', (name) => {
return pluginManager.uninstall(path, name) return pluginManager.uninstall(this.userPluginsPath, name)
}) })
const configData = loadConfig() 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('disable-http-cache')
app.commandLine.appendSwitch('max-active-webgl-contexts', '9000') app.commandLine.appendSwitch('max-active-webgl-contexts', '9000')
app.commandLine.appendSwitch('lang', 'EN') app.commandLine.appendSwitch('lang', 'EN')
@@ -70,7 +82,7 @@ export class Application {
} }
async newWindow (options?: WindowOptions): Promise<Window> { async newWindow (options?: WindowOptions): Promise<Window> {
const window = new Window(options) const window = new Window(this, options)
this.windows.push(window) this.windows.push(window)
window.visible$.subscribe(visible => { window.visible$.subscribe(visible => {
if (visible) { if (visible) {

View File

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

View File

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

View File

@@ -1,8 +1,11 @@
import * as fs from 'mz/fs' import * as fs from 'mz/fs'
import * as path from 'path' import * as path from 'path'
import * as remote from '@electron/remote' 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 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 { function normalizePath (p: string): string {
const cygwinPrefix = '/cygdrive/' const cygwinPrefix = '/cygdrive/'
@@ -13,45 +16,8 @@ function normalizePath (p: string): string {
return p 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 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 = [ const builtinModules = [
'@angular/animations', '@angular/animations',
'@angular/common', '@angular/common',
@@ -71,25 +37,42 @@ const builtinModules = [
'zone.js/dist/zone.js', 'zone.js/dist/zone.js',
] ]
const cachedBuiltinModules = {} export type ProgressCallback = (current: number, total: number) => void // eslint-disable-line @typescript-eslint/no-type-alias
builtinModules.forEach(m => {
cachedBuiltinModules[m] = nodeRequire(m)
})
const originalRequire = (global as any).require export function initModuleLookup (userPluginsPath: string): void {
;(global as any).require = function (query: string) { global['module'].paths.map((x: string) => nodeModule.globalPaths.push(normalizePath(x)))
if (cachedBuiltinModules[query]) {
return cachedBuiltinModules[query]
}
return originalRequire.apply(this, [query])
}
const originalModuleRequire = nodeModule.prototype.require if (process.env.TERMINUS_DEV) {
nodeModule.prototype.require = function (query: string) { nodeModule.globalPaths.unshift(path.dirname(remote.app.getAppPath()))
if (cachedBuiltinModules[query]) { }
return cachedBuiltinModules[query]
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[]> { 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) foundPlugins.sort((a, b) => a.name > b.name ? 1 : -1)
;(window as any).installedPlugins = foundPlugins
return foundPlugins return foundPlugins
} }

View File

@@ -3,7 +3,6 @@ const fs = require('fs')
const semver = require('semver') const semver = require('semver')
const childProcess = require('child_process') 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'))) const electronInfo = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../node_modules/electron/package.json')))
exports.version = childProcess.execSync('git describe --tags', {encoding:'utf-8'}) exports.version = childProcess.execSync('git describe --tags', {encoding:'utf-8'})
@@ -18,10 +17,10 @@ exports.builtinPlugins = [
'terminus-core', 'terminus-core',
'terminus-settings', 'terminus-settings',
'terminus-terminal', 'terminus-terminal',
'terminus-electron',
'terminus-local', 'terminus-local',
'terminus-web', 'terminus-web',
'terminus-community-color-schemes', 'terminus-community-color-schemes',
'terminus-electron',
'terminus-plugin-manager', 'terminus-plugin-manager',
'terminus-ssh', 'terminus-ssh',
'terminus-serial', '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 { 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 readonly isFullscreen: boolean
abstract reload (): void abstract reload (): void
abstract setTitle (title?: string): void abstract setTitle (title?: string): void
@@ -9,4 +26,10 @@ export abstract class HostWindowService {
abstract minimize (): void abstract minimize (): void
abstract toggleMaximize (): void abstract toggleMaximize (): void
abstract close (): 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 { CLIHandler, CLIEvent } from './cli'
export { PlatformService, ClipboardContent, MessageBoxResult, MessageBoxOptions, FileDownload, FileUpload, FileTransfer, HTMLFileUpload, FileUploadOptions } from './platform' export { PlatformService, ClipboardContent, MessageBoxResult, MessageBoxOptions, FileDownload, FileUpload, FileTransfer, HTMLFileUpload, FileUploadOptions } from './platform'
export { MenuItemOptions } from './menu' export { MenuItemOptions } from './menu'
export { BootstrapData, BOOTSTRAP_DATA } from './mainProcess' export { BootstrapData, PluginInfo, BOOTSTRAP_DATA } from './mainProcess'
export { HostWindowService } from './hostWindow' export { HostWindowService } from './hostWindow'
export { HostAppService, Platform } from './hostApp'
export { AppService } from '../services/app.service' export { AppService } from '../services/app.service'
export { ConfigService } from '../services/config.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 { Logger, ConsoleLogger, LogService } from '../services/log.service'
export { HomeBaseService } from '../services/homeBase.service' export { HomeBaseService } from '../services/homeBase.service'
export { HotkeysService } from '../services/hotkeys.service' export { HotkeysService } from '../services/hotkeys.service'
export { HostAppService, Platform, Bounds } from '../services/hostApp.service'
export { NotificationsService } from '../services/notifications.service' export { NotificationsService } from '../services/notifications.service'
export { ThemesService } from '../services/themes.service' export { ThemesService } from '../services/themes.service'
export { TabsService } from '../services/tabs.service' export { TabsService } from '../services/tabs.service'

View File

@@ -1,8 +1,22 @@
export const BOOTSTRAP_DATA = 'BOOTSTRAP_DATA' 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 { export interface BootstrapData {
config: Record<string, any> config: Record<string, any>
executable: string executable: string
isFirstWindow: boolean isFirstWindow: boolean
windowID: number windowID: number
installedPlugins: PluginInfo[]
userPluginsPath: string
} }

View File

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

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { HostAppService } from './services/hostApp.service' import { HostAppService } from './api/hostApp'
import { CLIHandler, CLIEvent } from './api/cli' import { CLIHandler, CLIEvent } from './api/cli'
@Injectable() @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 { trigger, style, animate, transition, state } from '@angular/animations'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 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 { HotkeysService } from '../services/hotkeys.service'
import { Logger, LogService } from '../services/log.service' import { Logger, LogService } from '../services/log.service'
import { ConfigService } from '../services/config.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() this.app.closeWindow()
}) })

View File

@@ -7,7 +7,7 @@ import { BaseTabComponent } from './baseTab.component'
import { RenameTabModalComponent } from './renameTabModal.component' import { RenameTabModalComponent } from './renameTabModal.component'
import { HotkeysService } from '../services/hotkeys.service' import { HotkeysService } from '../services/hotkeys.service'
import { AppService } from '../services/app.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 { ConfigService } from '../services/config.service'
import { BaseComponent } from './base.component' import { BaseComponent } from './base.component'
import { MenuItemOptions } from '../api/menu' import { MenuItemOptions } from '../api/menu'

View File

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

View File

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

View File

@@ -1,8 +1,4 @@
hotkeys: hotkeys:
new-window:
- '⌘-N'
toggle-window:
- 'Ctrl+Space'
toggle-fullscreen: toggle-fullscreen:
- 'Ctrl+⌘+F' - 'Ctrl+⌘+F'
close-tab: 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: hotkeys:
new-window:
- 'Ctrl-Shift-N'
toggle-window:
- 'Ctrl+Space'
toggle-fullscreen: toggle-fullscreen:
- 'F11' - 'F11'
- 'Alt-Enter' - 'Alt-Enter'

View File

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

View File

@@ -27,7 +27,7 @@ import { AutofocusDirective } from './directives/autofocus.directive'
import { FastHtmlBindDirective } from './directives/fastHtmlBind.directive' import { FastHtmlBindDirective } from './directives/fastHtmlBind.directive'
import { DropZoneDirective } from './directives/dropZone.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 { AppService } from './services/app.service'
import { ConfigService } from './services/config.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 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(() => { app.ready$.subscribe(() => {
if (config.store.enableWelcomeTab) { if (config.store.enableWelcomeTab) {
app.openNewTabRaw(WelcomeTabComponent) app.openNewTabRaw(WelcomeTabComponent)
} }
}) })
platform.setErrorHandler(err => {
console.error('Unhandled exception:', err)
})
} }
static forRoot (): ModuleWithProviders<AppModule> { static forRoot (): ModuleWithProviders<AppModule> {

View File

@@ -11,9 +11,9 @@ import { SelectorOption } from '../api/selector'
import { RecoveryToken } from '../api/tabRecovery' import { RecoveryToken } from '../api/tabRecovery'
import { BootstrapData, BOOTSTRAP_DATA } from '../api/mainProcess' import { BootstrapData, BOOTSTRAP_DATA } from '../api/mainProcess'
import { HostWindowService } from '../api/hostWindow' import { HostWindowService } from '../api/hostWindow'
import { HostAppService } from '../api/hostApp'
import { ConfigService } from './config.service' import { ConfigService } from './config.service'
import { HostAppService } from './hostApp.service'
import { TabRecoveryService } from './tabRecovery.service' import { TabRecoveryService } from './tabRecovery.service'
import { TabsService, TabComponentType } from './tabs.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 => { this.tabClosed$.subscribe(async tab => {
const token = await tabRecovery.getFullRecoveryToken(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 { Injectable, Inject } from '@angular/core'
import { ConfigProvider } from '../api/configProvider' import { ConfigProvider } from '../api/configProvider'
import { PlatformService } from '../api/platform' import { PlatformService } from '../api/platform'
import { HostAppService } from './hostApp.service' import { HostAppService } from '../api/hostApp'
import { Vault, VaultService } from './vault.service' import { Vault, VaultService } from './vault.service'
const deepmerge = require('deepmerge') const deepmerge = require('deepmerge')

View File

@@ -1,9 +1,14 @@
import { Observable, Subject } from 'rxjs'
export abstract class Screen { export abstract class Screen {
id: number id: number
name?: string name?: string
} }
export abstract class DockingService { export abstract class DockingService {
get screensChanged$ (): Observable<void> { return this.screensChanged }
protected screensChanged = new Subject<void>()
abstract dock (): void abstract dock (): void
abstract getScreens (): Screen[] 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 * as mixpanel from 'mixpanel'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { ConfigService } from './config.service' import { ConfigService } from './config.service'
import { PlatformService } from '../api' import { PlatformService, BOOTSTRAP_DATA, BootstrapData } from '../api'
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class HomeBaseService { export class HomeBaseService {
@@ -13,6 +13,7 @@ export class HomeBaseService {
private constructor ( private constructor (
private config: ConfigService, private config: ConfigService,
private platform: PlatformService, private platform: PlatformService,
@Inject(BOOTSTRAP_DATA) private bootstrapData: BootstrapData,
) { ) {
this.appVersion = platform.getAppVersion() this.appVersion = platform.getAppVersion()
@@ -38,7 +39,7 @@ export class HomeBaseService {
sunos: 'OS: Solaris', sunos: 'OS: Solaris',
win32: 'OS: Windows', win32: 'OS: Windows',
}[process.platform] }[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` body += `Plugins: ${plugins.join(', ') || 'none'}\n\n`
this.platform.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}`)
} }

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 { HotkeyDescription, HotkeyProvider } from '../api/hotkeyProvider'
import { stringifyKeySequence, EventData } from './hotkeys.util' import { stringifyKeySequence, EventData } from './hotkeys.util'
import { ConfigService } from './config.service' import { ConfigService } from './config.service'
import { HostAppService } from './hostApp.service'
export interface PartialHotkeyMatch { export interface PartialHotkeyMatch {
id: string id: string
@@ -33,7 +32,6 @@ export class HotkeysService {
private constructor ( private constructor (
private zone: NgZone, private zone: NgZone,
private hostApp: HostAppService,
private config: ConfigService, private config: ConfigService,
@Inject(HotkeyProvider) private hotkeyProviders: HotkeyProvider[], @Inject(HotkeyProvider) private hotkeyProviders: HotkeyProvider[],
) { ) {
@@ -47,11 +45,7 @@ export class HotkeysService {
} }
}) })
}) })
this.config.changed$.subscribe(() => {
this.registerGlobalHotkey()
})
this.config.ready$.toPromise().then(() => { this.config.ready$.toPromise().then(() => {
this.registerGlobalHotkey()
this.getHotkeyDescriptions().then(hotkeys => { this.getHotkeyDescriptions().then(hotkeys => {
this.hotkeyDescriptions = hotkeys this.hotkeyDescriptions = hotkeys
}) })
@@ -182,30 +176,6 @@ export class HotkeysService {
).reduce((a, b) => a.concat(b)) ).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 () { private getHotkeysConfig () {
return this.getHotkeysConfigRecursive(this.config.store.hotkeys) 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 { 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 { TerminalColorSchemeProvider } from 'terminus-terminal'
import { HyperColorSchemes } from './colorSchemes' import { HyperColorSchemes } from './colorSchemes'
@@ -9,39 +9,51 @@ import { ElectronUpdaterService } from './services/updater.service'
import { TouchbarService } from './services/touchbar.service' import { TouchbarService } from './services/touchbar.service'
import { ElectronDockingService } from './services/docking.service' import { ElectronDockingService } from './services/docking.service'
import { ElectronHostWindow } from './services/hostWindow.service' import { ElectronHostWindow } from './services/hostWindow.service'
import { ElectronHostAppService } from './services/hostApp.service'
import { ElectronHotkeyProvider } from './hotkeys'
import { ElectronConfigProvider } from './config'
@NgModule({ @NgModule({
providers: [ providers: [
{ provide: TerminalColorSchemeProvider, useClass: HyperColorSchemes, multi: true }, { provide: TerminalColorSchemeProvider, useClass: HyperColorSchemes, multi: true },
{ provide: PlatformService, useClass: ElectronPlatformService }, { provide: PlatformService, useClass: ElectronPlatformService },
{ provide: HostWindowService, useClass: ElectronHostWindow }, { provide: HostWindowService, useClass: ElectronHostWindow },
{ provide: HostAppService, useClass: ElectronHostAppService },
{ provide: LogService, useClass: ElectronLogService }, { provide: LogService, useClass: ElectronLogService },
{ provide: UpdaterService, useClass: ElectronUpdaterService }, { provide: UpdaterService, useClass: ElectronUpdaterService },
{ provide: DockingService, useClass: ElectronDockingService }, { provide: DockingService, useClass: ElectronDockingService },
{ provide: HotkeyProvider, useClass: ElectronHotkeyProvider, multi: true },
{ provide: ConfigProvider, useClass: ElectronConfigProvider, multi: true },
], ],
}) })
export default class ElectronModule { export default class ElectronModule {
constructor ( constructor (
private config: ConfigService, private config: ConfigService,
private hostApp: HostAppService, private hostApp: ElectronHostAppService,
private electron: ElectronService, private electron: ElectronService,
private hostWindow: ElectronHostWindow,
touchbar: TouchbarService, touchbar: TouchbarService,
docking: DockingService, docking: DockingService,
themeService: ThemesService, themeService: ThemesService,
app: AppService app: AppService,
) { ) {
config.ready$.toPromise().then(() => { config.ready$.toPromise().then(() => {
touchbar.update() touchbar.update()
docking.dock() docking.dock()
hostApp.shown.subscribe(() => { hostWindow.windowShown$.subscribe(() => {
docking.dock() docking.dock()
}) })
this.registerGlobalHotkey()
this.updateVibrancy() this.updateVibrancy()
}) })
config.changed$.subscribe(() => {
this.registerGlobalHotkey()
})
themeService.themeChanged$.subscribe(theme => { themeService.themeChanged$.subscribe(theme => {
if (hostApp.platform === Platform.macOS) { if (hostApp.platform === Platform.macOS) {
hostApp.getWindow().setTrafficLightPosition({ hostWindow.getWindow().setTrafficLightPosition({
x: theme.macOSWindowButtonsInsetX ?? 14, x: theme.macOSWindowButtonsInsetX ?? 14,
y: theme.macOSWindowButtonsInsetY ?? 11, y: theme.macOSWindowButtonsInsetY ?? 11,
}) })
@@ -55,9 +67,9 @@ export default class ElectronModule {
return return
} }
if (progress !== null) { if (progress !== null) {
hostApp.getWindow().setProgressBar(progress / 100.0, { mode: 'normal' }) hostWindow.getWindow().setProgressBar(progress / 100.0, { mode: 'normal' })
} else { } else {
hostApp.getWindow().setProgressBar(-1, { mode: 'none' }) hostWindow.getWindow().setProgressBar(-1, { mode: 'none' })
} }
lastProgress = progress lastProgress = progress
}) })
@@ -66,6 +78,30 @@ export default class ElectronModule {
config.changed$.subscribe(() => this.updateVibrancy()) 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 () { private updateVibrancy () {
let vibrancyType = this.config.store.appearance.vibrancyType let vibrancyType = this.config.store.appearance.vibrancyType
if (this.hostApp.platform === Platform.Windows && !isWindowsBuild(WIN_BUILD_FLUENT_BG_SUPPORTED)) { 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) document.body.classList.toggle('vibrant', this.config.store.appearance.vibrancy)
this.electron.ipcRenderer.send('window-set-vibrancy', this.config.store.appearance.vibrancy, vibrancyType) 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 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() @Injectable()
export class ElectronDockingService extends DockingService { export class ElectronDockingService extends DockingService {
constructor ( constructor (
private electron: ElectronService, private electron: ElectronService,
private config: ConfigService, private config: ConfigService,
private hostApp: HostAppService, private zone: NgZone,
private hostWindow: ElectronHostWindow,
platform: PlatformService,
) { ) {
super() super()
hostApp.displaysChanged$.subscribe(() => this.repositionWindow()) this.screensChanged$.subscribe(() => this.repositionWindow())
hostApp.displayMetricsChanged$.subscribe(() => this.repositionWindow()) platform.displayMetricsChanged$.subscribe(() => this.repositionWindow())
electron.ipcRenderer.on('host:displays-changed', () => {
this.zone.run(() => this.screensChanged.next())
})
} }
dock (): void { dock (): void {
const dockSide = this.config.store.appearance.dock const dockSide = this.config.store.appearance.dock
if (dockSide === 'off') { if (dockSide === 'off') {
this.hostApp.setAlwaysOnTop(false) this.hostWindow.setAlwaysOnTop(false)
return 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 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 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') { if (dockSide === 'left' || dockSide === 'right') {
newBounds.width = Math.max(minWidth, Math.round(fill * display.bounds.width)) 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 const alwaysOnTop = this.config.store.appearance.dockAlwaysOnTop
this.hostApp.setAlwaysOnTop(alwaysOnTop) this.hostWindow.setAlwaysOnTop(alwaysOnTop)
setImmediate(() => { setImmediate(() => {
this.hostApp.setBounds(newBounds) this.hostWindow.setBounds(newBounds)
}) })
} }
@@ -84,7 +91,7 @@ export class ElectronDockingService extends DockingService {
} }
private repositionWindow () { private repositionWindow () {
const [x, y] = this.hostApp.getWindow().getPosition() const [x, y] = this.hostWindow.getWindow().getPosition()
for (const screen of this.electron.screen.getAllDisplays()) { for (const screen of this.electron.screen.getAllDisplays()) {
const bounds = screen.bounds const bounds = screen.bounds
if (x >= bounds.x && x <= bounds.x + bounds.width && y >= bounds.y && y <= bounds.y + bounds.height) { 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() 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 type { BrowserWindow, TouchBar } from 'electron'
import { Observable, Subject } from 'rxjs' import { Injectable, Inject, NgZone } from '@angular/core'
import { ElectronService, HostAppService, HostWindowService } from 'terminus-core' import { BootstrapData, BOOTSTRAP_DATA, ElectronService, HostWindowService } from 'terminus-core'
export interface Bounds {
x: number
y: number
width: number
height: number
}
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class ElectronHostWindow extends HostWindowService { export class ElectronHostWindow extends HostWindowService {
get closeRequest$ (): Observable<void> { return this.closeRequest }
get isFullscreen (): boolean { return this._isFullScreen} get isFullscreen (): boolean { return this._isFullScreen}
private closeRequest = new Subject<void>()
private _isFullScreen = false private _isFullScreen = false
constructor ( constructor (
private electron: ElectronService,
private hostApp: HostAppService,
zone: NgZone, zone: NgZone,
private electron: ElectronService,
@Inject(BOOTSTRAP_DATA) private bootstrapData: BootstrapData,
) { ) {
super() super()
electron.ipcRenderer.on('host:window-enter-full-screen', () => zone.run(() => { 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(() => { electron.ipcRenderer.on('host:window-leave-full-screen', () => zone.run(() => {
this._isFullScreen = false 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 { reload (): void {
this.hostApp.getWindow().reload() this.getWindow().reload()
} }
setTitle (title?: string): void { setTitle (title?: string): void {
@@ -34,7 +63,7 @@ export class ElectronHostWindow extends HostWindowService {
} }
toggleFullscreen (): void { toggleFullscreen (): void {
this.hostApp.getWindow().setFullScreen(!this._isFullScreen) this.getWindow().setFullScreen(!this._isFullScreen)
} }
minimize (): void { minimize (): void {
@@ -48,4 +77,20 @@ export class ElectronHostWindow extends HostWindowService {
close (): void { close (): void {
this.electron.ipcRenderer.send('window-close') 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 { execFile } from 'mz/child_process'
import { Injectable, NgZone } from '@angular/core' import { Injectable, NgZone } from '@angular/core'
import { PlatformService, ClipboardContent, HostAppService, Platform, ElectronService, MenuItemOptions, MessageBoxOptions, MessageBoxResult, FileUpload, FileDownload, FileUploadOptions, wrapPromise } from 'terminus-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 const fontManager = require('fontmanager-redux') // eslint-disable-line
/* eslint-disable block-scoped-var */ /* eslint-disable block-scoped-var */
@@ -20,16 +21,20 @@ try {
@Injectable() @Injectable()
export class ElectronPlatformService extends PlatformService { export class ElectronPlatformService extends PlatformService {
supportsWindowControls = true supportsWindowControls = true
private userPluginsPath: string = (window as any).userPluginsPath
private configPath: string private configPath: string
constructor ( constructor (
private hostApp: HostAppService, private hostApp: HostAppService,
private hostWindow: ElectronHostWindow,
private electron: ElectronService, private electron: ElectronService,
private zone: NgZone, private zone: NgZone,
) { ) {
super() super()
this.configPath = path.join(electron.app.getPath('userData'), 'config.yaml') 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 { readClipboard (): string {
@@ -41,11 +46,11 @@ export class ElectronPlatformService extends PlatformService {
} }
async installPlugin (name: string, version: string): Promise<void> { 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> { 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> { async isProcessRunning (name: string): Promise<boolean> {
@@ -163,7 +168,7 @@ export class ElectronPlatformService extends PlatformService {
} }
async showMessageBox (options: MessageBoxOptions): Promise<MessageBoxResult> { 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 { quit (): void {
@@ -179,7 +184,7 @@ export class ElectronPlatformService extends PlatformService {
} }
const result = await this.electron.dialog.showOpenDialog( const result = await this.electron.dialog.showOpenDialog(
this.hostApp.getWindow(), this.hostWindow.getWindow(),
{ {
buttonLabel: 'Select', buttonLabel: 'Select',
properties, properties,
@@ -199,7 +204,7 @@ export class ElectronPlatformService extends PlatformService {
async startDownload (name: string, size: number): Promise<FileDownload|null> { async startDownload (name: string, size: number): Promise<FileDownload|null> {
const result = await this.electron.dialog.showSaveDialog( const result = await this.electron.dialog.showSaveDialog(
this.hostApp.getWindow(), this.hostWindow.getWindow(),
{ {
defaultPath: name, defaultPath: name,
}, },
@@ -212,6 +217,12 @@ export class ElectronPlatformService extends PlatformService {
this.fileTransferStarted.next(transfer) this.fileTransferStarted.next(transfer)
return transfer return transfer
} }
setErrorHandler (handler: (_: any) => void): void {
this.electron.ipcRenderer.on('uncaughtException', (_$event, err) => {
handler(err)
})
}
} }
class ElectronFileUpload extends FileUpload { class ElectronFileUpload extends FileUpload {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Injectable } from '@angular/core' 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' import { SSHService } from './services/ssh.service'
/** @hidden */ /** @hidden */
@@ -8,6 +8,7 @@ import { SSHService } from './services/ssh.service'
export class ButtonProvider extends ToolbarButtonProvider { export class ButtonProvider extends ToolbarButtonProvider {
constructor ( constructor (
hotkeys: HotkeysService, hotkeys: HotkeysService,
private hostApp: HostAppService,
private ssh: SSHService, private ssh: SSHService,
) { ) {
super() super()
@@ -23,14 +24,20 @@ export class ButtonProvider extends ToolbarButtonProvider {
} }
provide (): ToolbarButton[] { provide (): ToolbarButton[] {
return [{ if (this.hostApp.platform === Platform.Web) {
icon: require('./icons/globe.svg'), return [{
weight: 5, icon: require('../../terminus-local/src/icons/plus.svg'),
title: 'SSH connections', title: 'SSH connections',
touchBarNSImage: 'NSTouchBarOpenInBrowserTemplate', click: () => this.activate(),
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 { Observable } from 'rxjs'
import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators' 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 { PasswordStorageService } from '../services/passwordStorage.service'
import { SSHConnection, LoginScript, ForwardedPortConfig, SSHAlgorithmType, ALGORITHM_BLACKLIST } from '../api' import { SSHConnection, LoginScript, ForwardedPortConfig, SSHAlgorithmType, ALGORITHM_BLACKLIST } from '../api'
import { PromptModalComponent } from './promptModal.component' import { PromptModalComponent } from './promptModal.component'
@@ -30,7 +30,6 @@ export class EditConnectionModalComponent {
private modalInstance: NgbActiveModal, private modalInstance: NgbActiveModal,
private electron: ElectronService, private electron: ElectronService,
private platform: PlatformService, private platform: PlatformService,
private hostApp: HostAppService,
private passwordStorage: PasswordStorageService, private passwordStorage: PasswordStorageService,
private ngbModal: NgbModal, private ngbModal: NgbModal,
) { ) {
@@ -104,7 +103,6 @@ export class EditConnectionModalComponent {
addPrivateKey () { addPrivateKey () {
this.electron.dialog.showOpenDialog( this.electron.dialog.showOpenDialog(
this.hostApp.getWindow(),
{ {
defaultPath: this.connection.privateKeys![0], defaultPath: this.connection.privateKeys![0],
title: 'Select private key', title: 'Select private key',

View File

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

View File

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

View File

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

View File

@@ -61,7 +61,7 @@ export class WebPlatformService extends PlatformService {
} }
getAppVersion (): string { getAppVersion (): string {
return '1.0-web' return this.connector.getAppVersion()
} }
async listFonts (): Promise<string[]> { async listFonts (): Promise<string[]> {
@@ -136,6 +136,10 @@ export class WebPlatformService extends PlatformService {
this.fileSelector.click() this.fileSelector.click()
}) })
} }
setErrorHandler (handler: (_: any) => void): void {
window.addEventListener('error', handler)
}
} }
class HTMLFileDownload extends FileDownload { 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 { Injectable } from '@angular/core'
import { Observable, Subject } from 'rxjs'
import { HostWindowService } from 'terminus-core' import { HostWindowService } from 'terminus-core'
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class WebHostWindow extends HostWindowService { export class WebHostWindow extends HostWindowService {
get closeRequest$ (): Observable<void> { return this.closeRequest }
get isFullscreen (): boolean { return !!document.fullscreenElement } get isFullscreen (): boolean { return !!document.fullscreenElement }
private closeRequest = new Subject<void>() constructor () {
super()
this.windowShown.next()
this.windowFocused.next()
}
reload (): void { reload (): void {
location.reload() location.reload()