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