mirror of
https://github.com/Eugeny/tabby.git
synced 2025-08-14 05:11:55 +00:00
more electron/web separation
This commit is contained in:
@@ -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) {
|
||||||
|
@@ -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
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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',
|
||||||
|
53
terminus-core/src/api/hostApp.ts
Normal file
53
terminus-core/src/api/hostApp.ts
Normal 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
|
||||||
|
}
|
@@ -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 { }
|
||||||
}
|
}
|
||||||
|
@@ -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'
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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 { }
|
||||||
}
|
}
|
||||||
|
@@ -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()
|
||||||
|
@@ -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()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -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'
|
||||||
|
@@ -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')
|
||||||
}
|
}
|
||||||
|
@@ -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:
|
||||||
|
@@ -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:
|
||||||
|
6
terminus-core/src/configDefaults.web.yaml
Normal file
6
terminus-core/src/configDefaults.web.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
pluginBlacklist: ['local']
|
||||||
|
terminal:
|
||||||
|
recoverTabs: false
|
||||||
|
enableAnalytics: false
|
||||||
|
enableWelcomeTab: false
|
||||||
|
enableAutomaticUpdates: false
|
@@ -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'
|
||||||
|
@@ -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',
|
||||||
|
@@ -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> {
|
||||||
|
@@ -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)
|
||||||
|
@@ -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')
|
||||||
|
|
||||||
|
@@ -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[]
|
||||||
}
|
}
|
||||||
|
@@ -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}`)
|
||||||
}
|
}
|
||||||
|
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
26
terminus-electron/src/config.ts
Normal file
26
terminus-electron/src/config.ts
Normal 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 = {}
|
||||||
|
}
|
21
terminus-electron/src/hotkeys.ts
Normal file
21
terminus-electron/src/hotkeys.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@@ -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 }
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
85
terminus-electron/src/services/hostApp.service.ts
Normal file
85
terminus-electron/src/services/hostApp.service.ts
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
@@ -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')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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'],
|
||||||
|
@@ -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' }
|
||||||
|
|
||||||
|
@@ -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[]> {
|
||||||
|
@@ -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') {
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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,
|
||||||
|
@@ -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()
|
||||||
|
@@ -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(),
|
||||||
|
}]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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',
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
|
@@ -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 {
|
||||||
|
33
terminus-web/src/services/hostApp.service.ts
Normal file
33
terminus-web/src/services/hostApp.service.ts
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
@@ -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()
|
||||||
|
Reference in New Issue
Block a user