project rename

This commit is contained in:
Eugene Pankov
2021-06-29 23:57:04 +02:00
parent c61be3d52b
commit 43cd3318da
609 changed files with 510 additions and 530 deletions

View File

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

View File

@@ -0,0 +1,47 @@
import { Injectable } from '@angular/core'
import { App, IpcRenderer, Shell, Dialog, Clipboard, GlobalShortcut, Screen, Remote, AutoUpdater, TouchBar, BrowserWindow, Menu, MenuItem, NativeImage } from 'electron'
import * as remote from '@electron/remote'
export interface MessageBoxResponse {
response: number
checkboxChecked?: boolean
}
@Injectable({ providedIn: 'root' })
export class ElectronService {
app: App
ipcRenderer: IpcRenderer
shell: Shell
dialog: Dialog
clipboard: Clipboard
globalShortcut: GlobalShortcut
nativeImage: typeof NativeImage
screen: Screen
remote: Remote
process: any
autoUpdater: AutoUpdater
TouchBar: typeof TouchBar
BrowserWindow: typeof BrowserWindow
Menu: typeof Menu
MenuItem: typeof MenuItem
/** @hidden */
private constructor () {
const electron = require('electron')
this.shell = electron.shell
this.clipboard = electron.clipboard
this.ipcRenderer = electron.ipcRenderer
this.process = remote.getGlobal('process')
this.app = remote.app
this.screen = remote.screen
this.dialog = remote.dialog
this.globalShortcut = remote.globalShortcut
this.nativeImage = remote.nativeImage
this.autoUpdater = remote.autoUpdater
this.TouchBar = remote.TouchBar
this.BrowserWindow = remote.BrowserWindow
this.Menu = remote.Menu
this.MenuItem = remote.MenuItem
}
}

View File

@@ -0,0 +1,41 @@
import { promises as fs } from 'fs'
import { Injectable } from '@angular/core'
import { FileProvider } from 'tabby-core'
import { ElectronService } from '../services/electron.service'
import { ElectronHostWindow } from './hostWindow.service'
@Injectable()
export class ElectronFileProvider extends FileProvider {
name = 'Filesystem'
constructor (
private electron: ElectronService,
private hostWindow: ElectronHostWindow,
) {
super()
}
async selectAndStoreFile (description: string): Promise<string> {
const result = await this.electron.dialog.showOpenDialog(
this.hostWindow.getWindow(),
{
buttonLabel: `Select ${description}`,
properties: ['openFile', 'treatPackageAsDirectory'],
},
)
if (result.canceled || !result.filePaths.length) {
throw new Error('canceled')
}
return `file://${result.filePaths[0]}`
}
async retrieveFile (key: string): Promise<Buffer> {
if (key.startsWith('file://')) {
key = key.substring('file://'.length)
} else if (key.includes('://')) {
throw new Error('Incorrect type')
}
return fs.readFile(key, { encoding: null })
}
}

View File

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

View File

@@ -0,0 +1,105 @@
import type { BrowserWindow, TouchBar } from 'electron'
import { Injectable, Inject, NgZone } from '@angular/core'
import { BootstrapData, BOOTSTRAP_DATA, HostWindowService } from 'tabby-core'
import { ElectronService } from '../services/electron.service'
export interface Bounds {
x: number
y: number
width: number
height: number
}
@Injectable({ providedIn: 'root' })
export class ElectronHostWindow extends HostWindowService {
get isFullscreen (): boolean { return this._isFullScreen}
private _isFullScreen = false
constructor (
zone: NgZone,
private electron: ElectronService,
@Inject(BOOTSTRAP_DATA) private bootstrapData: BootstrapData,
) {
super()
electron.ipcRenderer.on('host:window-enter-full-screen', () => zone.run(() => {
this._isFullScreen = true
}))
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.getWindow().reload()
}
setTitle (title?: string): void {
this.electron.ipcRenderer.send('window-set-title', title ?? 'Tabby')
}
toggleFullscreen (): void {
this.getWindow().setFullScreen(!this._isFullScreen)
}
minimize (): void {
this.electron.ipcRenderer.send('window-minimize')
}
isMaximized (): boolean {
return this.getWindow().isMaximized()
}
toggleMaximize (): void {
if (this.getWindow().isMaximized()) {
this.getWindow().unmaximize()
} else {
this.getWindow().maximize()
}
}
close (): void {
this.electron.ipcRenderer.send('window-close')
}
setBounds (bounds: Bounds): void {
this.electron.ipcRenderer.send('window-set-bounds', bounds)
}
setAlwaysOnTop (flag: boolean): void {
this.electron.ipcRenderer.send('window-set-always-on-top', flag)
}
setTouchBar (touchBar: TouchBar): void {
this.getWindow().setTouchBar(touchBar)
}
bringToFront (): void {
this.electron.ipcRenderer.send('window-bring-to-front')
}
}

View File

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

View File

@@ -0,0 +1,299 @@
import * as path from 'path'
import * as fs from 'fs/promises'
import * as fsSync from 'fs'
import * as os from 'os'
import promiseIpc from 'electron-promise-ipc'
import { execFile } from 'mz/child_process'
import { Injectable, NgZone } from '@angular/core'
import { PlatformService, ClipboardContent, HostAppService, Platform, MenuItemOptions, MessageBoxOptions, MessageBoxResult, FileUpload, FileDownload, FileUploadOptions, wrapPromise } from 'tabby-core'
import { ElectronService } from '../services/electron.service'
import { ElectronHostWindow } from './hostWindow.service'
import { ShellIntegrationService } from './shellIntegration.service'
const fontManager = require('fontmanager-redux') // eslint-disable-line
/* eslint-disable block-scoped-var */
try {
// eslint-disable-next-line no-var
var windowsProcessTreeNative = require('windows-process-tree/build/Release/windows_process_tree.node')
// eslint-disable-next-line no-var
var wnr = require('windows-native-registry')
} catch { }
@Injectable()
export class ElectronPlatformService extends PlatformService {
supportsWindowControls = true
private configPath: string
constructor (
private hostApp: HostAppService,
private hostWindow: ElectronHostWindow,
private electron: ElectronService,
private zone: NgZone,
private shellIntegration: ShellIntegrationService,
) {
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 {
return this.electron.clipboard.readText()
}
setClipboard (content: ClipboardContent): void {
require('@electron/remote').clipboard.write(content)
}
async installPlugin (name: string, version: string): Promise<void> {
await (promiseIpc as any).send('plugin-manager:install', name, version)
}
async uninstallPlugin (name: string): Promise<void> {
await (promiseIpc as any).send('plugin-manager:uninstall', name)
}
async isProcessRunning (name: string): Promise<boolean> {
if (this.hostApp.platform === Platform.Windows) {
return new Promise<boolean>(resolve => {
windowsProcessTreeNative.getProcessList(list => { // eslint-disable-line block-scoped-var
resolve(list.some(x => x.name === name))
}, 0)
})
} else {
throw new Error('Not supported')
}
}
getWinSCPPath (): string|null {
const key = wnr.getRegistryKey(wnr.HK.CR, 'WinSCP.Url\\DefaultIcon')
if (key?.['']) {
let detectedPath = key[''].value?.split(',')[0]
detectedPath = detectedPath?.substring(1, detectedPath.length - 1)
return detectedPath
}
return null
}
exec (app: string, argv: string[]): void {
execFile(app, argv)
}
isShellIntegrationSupported (): boolean {
return this.hostApp.platform !== Platform.Linux
}
async isShellIntegrationInstalled (): Promise<boolean> {
return this.shellIntegration.isInstalled()
}
async installShellIntegration (): Promise<void> {
await this.shellIntegration.install()
}
async uninstallShellIntegration (): Promise<void> {
await this.shellIntegration.remove()
}
async loadConfig (): Promise<string> {
if (fsSync.existsSync(this.configPath)) {
return fs.readFile(this.configPath, 'utf8')
} else {
return ''
}
}
async saveConfig (content: string): Promise<void> {
await fs.writeFile(this.configPath, content, 'utf8')
}
getConfigPath (): string|null {
return this.configPath
}
showItemInFolder (p: string): void {
this.electron.shell.showItemInFolder(p)
}
openExternal (url: string): void {
this.electron.shell.openExternal(url)
}
openPath (p: string): void {
this.electron.shell.openPath(p)
}
getOSRelease (): string {
return os.release()
}
getAppVersion (): string {
return this.electron.app.getVersion()
}
async listFonts (): Promise<string[]> {
if (this.hostApp.platform === Platform.Windows || this.hostApp.platform === Platform.macOS) {
let fonts = await new Promise<any[]>((resolve) => fontManager.findFonts({ monospace: true }, resolve))
fonts = fonts.map(x => x.family.trim())
return fonts
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (this.hostApp.platform === Platform.Linux) {
const stdout = (await execFile('fc-list', [':spacing=mono']))[0]
const fonts = stdout.toString()
.split('\n')
.filter(x => !!x)
.map(x => x.split(':')[1].trim())
.map(x => x.split(',')[0].trim())
fonts.sort()
return fonts
}
return []
}
popupContextMenu (menu: MenuItemOptions[], _event?: MouseEvent): void {
this.electron.Menu.buildFromTemplate(menu.map(item => this.rewrapMenuItemOptions(item))).popup({})
}
rewrapMenuItemOptions (menu: MenuItemOptions): MenuItemOptions {
return {
...menu,
click: () => {
this.zone.run(() => {
menu.click?.()
})
},
submenu: menu.submenu ? menu.submenu.map(x => this.rewrapMenuItemOptions(x)) : undefined,
}
}
async showMessageBox (options: MessageBoxOptions): Promise<MessageBoxResult> {
return this.electron.dialog.showMessageBox(this.hostWindow.getWindow(), options)
}
quit (): void {
this.electron.app.exit(0)
}
async startUpload (options?: FileUploadOptions): Promise<FileUpload[]> {
options ??= { multiple: false }
const properties: any[] = ['openFile', 'treatPackageAsDirectory']
if (options.multiple) {
properties.push('multiSelections')
}
const result = await this.electron.dialog.showOpenDialog(
this.hostWindow.getWindow(),
{
buttonLabel: 'Select',
properties,
},
)
if (result.canceled) {
return []
}
return Promise.all(result.filePaths.map(async p => {
const transfer = new ElectronFileUpload(p)
await wrapPromise(this.zone, transfer.open())
this.fileTransferStarted.next(transfer)
return transfer
}))
}
async startDownload (name: string, size: number): Promise<FileDownload|null> {
const result = await this.electron.dialog.showSaveDialog(
this.hostWindow.getWindow(),
{
defaultPath: name,
},
)
if (!result.filePath) {
return null
}
const transfer = new ElectronFileDownload(result.filePath, size)
await wrapPromise(this.zone, transfer.open())
this.fileTransferStarted.next(transfer)
return transfer
}
setErrorHandler (handler: (_: any) => void): void {
this.electron.ipcRenderer.on('uncaughtException', (_$event, err) => {
handler(err)
})
}
}
class ElectronFileUpload extends FileUpload {
private size: number
private file: fs.FileHandle
private buffer: Buffer
constructor (private filePath: string) {
super()
this.buffer = Buffer.alloc(256 * 1024)
}
async open (): Promise<void> {
this.size = (await fs.stat(this.filePath)).size
this.file = await fs.open(this.filePath, 'r')
}
getName (): string {
return path.basename(this.filePath)
}
getSize (): number {
return this.size
}
async read (): Promise<Buffer> {
const result = await this.file.read(this.buffer, 0, this.buffer.length, null)
this.increaseProgress(result.bytesRead)
return this.buffer.slice(0, result.bytesRead)
}
close (): void {
this.file.close()
}
}
class ElectronFileDownload extends FileDownload {
private file: fs.FileHandle
constructor (
private filePath: string,
private size: number,
) {
super()
}
async open (): Promise<void> {
this.file = await fs.open(this.filePath, 'w')
}
getName (): string {
return path.basename(this.filePath)
}
getSize (): number {
return this.size
}
async write (buffer: Buffer): Promise<void> {
let pos = 0
while (pos < buffer.length) {
const result = await this.file.write(buffer, pos, buffer.length - pos, null)
this.increaseProgress(result.bytesWritten)
pos += result.bytesWritten
}
}
close (): void {
this.file.close()
}
}

View File

@@ -0,0 +1,105 @@
import * as path from 'path'
import * as fs from 'mz/fs'
import { exec } from 'mz/child_process'
import { Injectable } from '@angular/core'
import { HostAppService, Platform } from 'tabby-core'
import { ElectronService } from '../services/electron.service'
/* eslint-disable block-scoped-var */
try {
var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
} catch (_) { }
@Injectable({ providedIn: 'root' })
export class ShellIntegrationService {
private automatorWorkflows = ['Open Tabby here.workflow', 'Paste path into Tabby.workflow']
private automatorWorkflowsLocation: string
private automatorWorkflowsDestination: string
private registryKeys = [
{
path: 'Software\\Classes\\Directory\\Background\\shell\\Tabby',
value: 'Open Tabby here',
command: 'open "%V"',
},
{
path: 'SOFTWARE\\Classes\\Directory\\shell\\Tabby',
value: 'Open Tabby here',
command: 'open "%V"',
},
{
path: 'Software\\Classes\\*\\shell\\Tabby',
value: 'Paste path into Tabby',
command: 'paste "%V"',
},
]
private constructor (
private electron: ElectronService,
private hostApp: HostAppService,
) {
if (this.hostApp.platform === Platform.macOS) {
this.automatorWorkflowsLocation = path.join(
path.dirname(path.dirname(this.electron.app.getPath('exe'))),
'Resources',
'extras',
'automator-workflows',
)
this.automatorWorkflowsDestination = path.join(process.env.HOME!, 'Library', 'Services')
}
this.updatePaths()
}
async isInstalled (): Promise<boolean> {
if (this.hostApp.platform === Platform.macOS) {
return fs.exists(path.join(this.automatorWorkflowsDestination, this.automatorWorkflows[0]))
} else if (this.hostApp.platform === Platform.Windows) {
return !!wnr.getRegistryKey(wnr.HK.CU, this.registryKeys[0].path)
}
return true
}
async install (): Promise<void> {
const exe: string = process.env.PORTABLE_EXECUTABLE_FILE ?? this.electron.app.getPath('exe')
if (this.hostApp.platform === Platform.macOS) {
for (const wf of this.automatorWorkflows) {
await exec(`cp -r "${this.automatorWorkflowsLocation}/${wf}" "${this.automatorWorkflowsDestination}"`)
}
} else if (this.hostApp.platform === Platform.Windows) {
for (const registryKey of this.registryKeys) {
wnr.createRegistryKey(wnr.HK.CU, registryKey.path)
wnr.createRegistryKey(wnr.HK.CU, registryKey.path + '\\command')
wnr.setRegistryValue(wnr.HK.CU, registryKey.path, '', wnr.REG.SZ, registryKey.value)
wnr.setRegistryValue(wnr.HK.CU, registryKey.path, 'Icon', wnr.REG.SZ, exe)
wnr.setRegistryValue(wnr.HK.CU, registryKey.path + '\\command', '', wnr.REG.SZ, exe + ' ' + registryKey.command)
}
if (wnr.getRegistryKey(wnr.HK.CU, 'Software\\Classes\\Directory\\Background\\shell\\Open Tabby here')) {
wnr.deleteRegistryKey(wnr.HK.CU, 'Software\\Classes\\Directory\\Background\\shell\\Open Tabby here')
}
if (wnr.getRegistryKey(wnr.HK.CU, 'Software\\Classes\\*\\shell\\Paste path into Tabby')) {
wnr.deleteRegistryKey(wnr.HK.CU, 'Software\\Classes\\*\\shell\\Paste path into Tabby')
}
}
}
async remove (): Promise<void> {
if (this.hostApp.platform === Platform.macOS) {
for (const wf of this.automatorWorkflows) {
await exec(`rm -rf "${this.automatorWorkflowsDestination}/${wf}"`)
}
} else if (this.hostApp.platform === Platform.Windows) {
for (const registryKey of this.registryKeys) {
wnr.deleteRegistryKey(wnr.HK.CU, registryKey.path)
}
}
}
private async updatePaths (): Promise<void> {
// Update paths in case of an update
if (this.hostApp.platform === Platform.Windows) {
if (await this.isInstalled()) {
await this.install()
}
}
}
}

View File

@@ -0,0 +1,83 @@
import { SegmentedControlSegment, TouchBarSegmentedControl } from 'electron'
import { Injectable, NgZone } from '@angular/core'
import { AppService, HostAppService, Platform } from 'tabby-core'
import { ElectronService } from '../services/electron.service'
import { ElectronHostWindow } from './hostWindow.service'
/** @hidden */
@Injectable({ providedIn: 'root' })
export class TouchbarService {
private tabsSegmentedControl: TouchBarSegmentedControl
private tabSegments: SegmentedControlSegment[] = []
private constructor (
private app: AppService,
private hostApp: HostAppService,
private hostWindow: ElectronHostWindow,
private electron: ElectronService,
private zone: NgZone,
) {
if (this.hostApp.platform !== Platform.macOS) {
return
}
app.tabsChanged$.subscribe(() => this.updateTabs())
app.activeTabChange$.subscribe(() => this.updateTabs())
const activityIconPath = `${electron.app.getAppPath()}/assets/activity.png`
const activityIcon = this.electron.nativeImage.createFromPath(activityIconPath)
app.tabOpened$.subscribe(tab => {
tab.titleChange$.subscribe(title => {
const segment = this.tabSegments[app.tabs.indexOf(tab)]
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (segment) {
segment.label = this.shortenTitle(title)
this.tabsSegmentedControl.segments = this.tabSegments
}
})
tab.activity$.subscribe(hasActivity => {
const showIcon = this.app.activeTab !== tab && hasActivity
const segment = this.tabSegments[app.tabs.indexOf(tab)]
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (segment) {
segment.icon = showIcon ? activityIcon : undefined
}
})
})
}
updateTabs (): void {
this.tabSegments = this.app.tabs.map(tab => ({
label: this.shortenTitle(tab.title),
}))
this.tabsSegmentedControl.segments = this.tabSegments
this.tabsSegmentedControl.selectedIndex = this.app.activeTab ? this.app.tabs.indexOf(this.app.activeTab) : 0
}
update (): void {
if (this.hostApp.platform !== Platform.macOS) {
return
}
this.tabsSegmentedControl = new this.electron.TouchBar.TouchBarSegmentedControl({
segments: this.tabSegments,
selectedIndex: this.app.activeTab ? this.app.tabs.indexOf(this.app.activeTab) : undefined,
change: (selectedIndex) => this.zone.run(() => {
this.app.selectTab(this.app.tabs[selectedIndex])
}),
})
const touchBar = new this.electron.TouchBar({
items: [
this.tabsSegmentedControl,
],
})
this.hostWindow.setTouchBar(touchBar)
}
private shortenTitle (title: string): string {
if (title.length > 15) {
title = title.substring(0, 15) + '...'
}
return title
}
}

View File

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