mirror of
https://github.com/Eugeny/tabby.git
synced 2025-10-03 21:44:54 +00:00
465 lines
15 KiB
TypeScript
465 lines
15 KiB
TypeScript
import * as path from 'path'
|
|
import * as fs from 'fs/promises'
|
|
import * as fsSync from 'fs'
|
|
import * as os from 'os'
|
|
import promiseIpc, { RendererProcessType } from 'electron-promise-ipc'
|
|
import { execFile } from 'mz/child_process'
|
|
import { Injectable, NgZone } from '@angular/core'
|
|
import { PlatformService, ClipboardContent, Platform, MenuItemOptions, MessageBoxOptions, MessageBoxResult, DirectoryUpload, FileUpload, FileDownload, DirectoryDownload, FileUploadOptions, wrapPromise, TranslateService, FileTransfer, PlatformTheme } from 'tabby-core'
|
|
import { ElectronService } from '../services/electron.service'
|
|
import { ElectronHostWindow } from './hostWindow.service'
|
|
import { ShellIntegrationService } from './shellIntegration.service'
|
|
import { ElectronHostAppService } from './hostApp.service'
|
|
import { configPath } from '../../../app/lib/config'
|
|
const fontManager = require('fontmanager-redux') // eslint-disable-line
|
|
|
|
/* eslint-disable block-scoped-var */
|
|
|
|
try {
|
|
// eslint-disable-next-line no-var
|
|
var windowsProcessTreeNative = require('@tabby-gang/windows-process-tree/build/Release/windows_process_tree.node')
|
|
// eslint-disable-next-line no-var
|
|
var wnr = require('windows-native-registry')
|
|
} catch { }
|
|
|
|
@Injectable({ providedIn: 'root' })
|
|
export class ElectronPlatformService extends PlatformService {
|
|
supportsWindowControls = true
|
|
private configPath: string
|
|
|
|
constructor (
|
|
private hostApp: ElectronHostAppService,
|
|
private hostWindow: ElectronHostWindow,
|
|
private electron: ElectronService,
|
|
private zone: NgZone,
|
|
private shellIntegration: ShellIntegrationService,
|
|
private translate: TranslateService,
|
|
) {
|
|
super()
|
|
this.configPath = configPath
|
|
|
|
electron.ipcRenderer.on('host:display-metrics-changed', () => {
|
|
this.zone.run(() => this.displayMetricsChanged.next())
|
|
})
|
|
|
|
electron.nativeTheme.on('updated', () => {
|
|
this.zone.run(() => this.themeChanged.next(this.getTheme()))
|
|
})
|
|
}
|
|
|
|
async getAllFiles (dir: string, root: DirectoryUpload): Promise<DirectoryUpload> {
|
|
const items = await fs.readdir(dir, { withFileTypes: true })
|
|
for (const item of items) {
|
|
if (item.isDirectory()) {
|
|
root.pushChildren(await this.getAllFiles(path.join(dir, item.name), new DirectoryUpload(item.name)))
|
|
} else {
|
|
const file = new ElectronFileUpload(path.join(dir, item.name), this.electron)
|
|
root.pushChildren(file)
|
|
await wrapPromise(this.zone, file.open())
|
|
this.fileTransferStarted.next(file)
|
|
}
|
|
}
|
|
return root
|
|
}
|
|
|
|
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 RendererProcessType).send('plugin-manager:install', name, version)
|
|
}
|
|
|
|
async uninstallPlugin (name: string): Promise<void> {
|
|
await (promiseIpc as RendererProcessType).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
|
|
}
|
|
|
|
async exec (app: string, argv: string[]): Promise<void> {
|
|
await 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 this.hostApp.saveConfig(content)
|
|
}
|
|
|
|
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.getAvailableFonts(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, paths?: string[]): Promise<FileUpload[]> {
|
|
options ??= { multiple: false }
|
|
|
|
const properties: any[] = ['openFile', 'treatPackageAsDirectory']
|
|
if (options.multiple) {
|
|
properties.push('multiSelections')
|
|
}
|
|
|
|
if (!paths) {
|
|
const result = await this.electron.dialog.showOpenDialog(
|
|
this.hostWindow.getWindow(),
|
|
{
|
|
buttonLabel: this.translate.instant('Select'),
|
|
properties,
|
|
},
|
|
)
|
|
if (result.canceled) {
|
|
return []
|
|
}
|
|
paths = result.filePaths
|
|
}
|
|
|
|
return Promise.all(paths.map(async p => {
|
|
const transfer = new ElectronFileUpload(p, this.electron)
|
|
await wrapPromise(this.zone, transfer.open())
|
|
this.fileTransferStarted.next(transfer)
|
|
return transfer
|
|
}))
|
|
}
|
|
|
|
async startUploadDirectory (paths?: string[]): Promise<DirectoryUpload> {
|
|
const properties: any[] = ['openFile', 'treatPackageAsDirectory', 'openDirectory']
|
|
|
|
if (!paths) {
|
|
const result = await this.electron.dialog.showOpenDialog(
|
|
this.hostWindow.getWindow(),
|
|
{
|
|
buttonLabel: this.translate.instant('Select'),
|
|
properties,
|
|
},
|
|
)
|
|
if (result.canceled) {
|
|
return new DirectoryUpload()
|
|
}
|
|
paths = result.filePaths
|
|
}
|
|
|
|
const root = new DirectoryUpload()
|
|
root.pushChildren(await this.getAllFiles(paths[0].split(path.sep).join(path.posix.sep), new DirectoryUpload(path.basename(paths[0]))))
|
|
return root
|
|
}
|
|
|
|
async startDownload (name: string, mode: number, size: number, filePath?: string): Promise<FileDownload|null> {
|
|
if (!filePath) {
|
|
const result = await this.electron.dialog.showSaveDialog(
|
|
this.hostWindow.getWindow(),
|
|
{
|
|
defaultPath: name,
|
|
},
|
|
)
|
|
if (!result.filePath) {
|
|
return null
|
|
}
|
|
filePath = result.filePath
|
|
}
|
|
const transfer = new ElectronFileDownload(filePath, mode, size, this.electron)
|
|
await wrapPromise(this.zone, transfer.open())
|
|
this.fileTransferStarted.next(transfer)
|
|
return transfer
|
|
}
|
|
|
|
async startDownloadDirectory (name: string, estimatedSize?: number): Promise<DirectoryDownload|null> {
|
|
const selectedFolder = await this.pickDirectory(this.translate.instant('Select destination folder for {name}', { name }), this.translate.instant('Download here'))
|
|
if (!selectedFolder) {
|
|
return null
|
|
}
|
|
|
|
let downloadPath = path.join(selectedFolder, name)
|
|
let counter = 1
|
|
while (fsSync.existsSync(downloadPath)) {
|
|
downloadPath = path.join(selectedFolder, `${name} (${counter})`)
|
|
counter++
|
|
}
|
|
|
|
const transfer = new ElectronDirectoryDownload(downloadPath, name, estimatedSize ?? 0, this.electron, this.zone)
|
|
await wrapPromise(this.zone, transfer.open())
|
|
this.fileTransferStarted.next(transfer)
|
|
return transfer
|
|
}
|
|
|
|
_registerFileTransfer (transfer: FileTransfer): void {
|
|
this.fileTransferStarted.next(transfer)
|
|
}
|
|
|
|
setErrorHandler (handler: (_: any) => void): void {
|
|
this.electron.ipcRenderer.on('uncaughtException', (_$event, err) => {
|
|
handler(err)
|
|
})
|
|
}
|
|
|
|
async pickDirectory (title?: string, buttonLabel?: string): Promise<string | null> {
|
|
const result = await this.electron.dialog.showOpenDialog(
|
|
this.hostWindow.getWindow(),
|
|
{
|
|
title,
|
|
buttonLabel,
|
|
properties: ['openDirectory', 'showHiddenFiles'],
|
|
},
|
|
)
|
|
if (result.canceled || !result.filePaths.length) {
|
|
return null
|
|
}
|
|
return result.filePaths[0]
|
|
}
|
|
|
|
getTheme (): PlatformTheme {
|
|
if (this.electron.nativeTheme.shouldUseDarkColors) {
|
|
return 'dark'
|
|
} else {
|
|
return 'light'
|
|
}
|
|
}
|
|
}
|
|
|
|
class ElectronFileUpload extends FileUpload {
|
|
private size: number
|
|
private mode: number
|
|
private file: fs.FileHandle
|
|
private buffer: Uint8Array
|
|
private powerSaveBlocker = 0
|
|
|
|
constructor (private filePath: string, private electron: ElectronService) {
|
|
super()
|
|
this.buffer = new Uint8Array(256 * 1024)
|
|
this.powerSaveBlocker = electron.powerSaveBlocker.start('prevent-app-suspension')
|
|
}
|
|
|
|
async open (): Promise<void> {
|
|
const stat = await fs.stat(this.filePath)
|
|
this.size = stat.size
|
|
this.mode = stat.mode
|
|
this.setTotalSize(this.size)
|
|
this.file = await fs.open(this.filePath, 'r')
|
|
}
|
|
|
|
getName (): string {
|
|
return path.basename(this.filePath)
|
|
}
|
|
|
|
getMode (): number {
|
|
return this.mode
|
|
}
|
|
|
|
getSize (): number {
|
|
return this.size
|
|
}
|
|
|
|
async read (): Promise<Uint8Array> {
|
|
const result = await this.file.read(this.buffer, 0, this.buffer.length, null)
|
|
this.increaseProgress(result.bytesRead)
|
|
if (this.getCompletedBytes() >= this.getSize()) {
|
|
this.setCompleted(true)
|
|
}
|
|
return this.buffer.slice(0, result.bytesRead)
|
|
}
|
|
|
|
close (): void {
|
|
this.electron.powerSaveBlocker.stop(this.powerSaveBlocker)
|
|
this.file.close()
|
|
}
|
|
}
|
|
|
|
class ElectronFileDownload extends FileDownload {
|
|
private file: fs.FileHandle
|
|
private powerSaveBlocker = 0
|
|
|
|
constructor (
|
|
private filePath: string,
|
|
private mode: number,
|
|
private size: number,
|
|
private electron: ElectronService,
|
|
) {
|
|
super()
|
|
this.powerSaveBlocker = electron.powerSaveBlocker.start('prevent-app-suspension')
|
|
this.setTotalSize(size)
|
|
}
|
|
|
|
async open (): Promise<void> {
|
|
this.file = await fs.open(this.filePath, 'w', this.mode)
|
|
}
|
|
|
|
getName (): string {
|
|
return path.basename(this.filePath)
|
|
}
|
|
|
|
getSize (): number {
|
|
return this.size
|
|
}
|
|
|
|
async write (buffer: Uint8Array): 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
|
|
}
|
|
if (this.getCompletedBytes() >= this.getSize()) {
|
|
this.setCompleted(true)
|
|
}
|
|
}
|
|
|
|
close (): void {
|
|
this.electron.powerSaveBlocker.stop(this.powerSaveBlocker)
|
|
this.file.close()
|
|
}
|
|
}
|
|
|
|
class ElectronDirectoryDownload extends DirectoryDownload {
|
|
private powerSaveBlocker = 0
|
|
|
|
constructor (
|
|
private basePath: string,
|
|
private name: string,
|
|
estimatedSize: number,
|
|
private electron: ElectronService,
|
|
private zone: NgZone,
|
|
) {
|
|
super()
|
|
this.powerSaveBlocker = electron.powerSaveBlocker.start('prevent-app-suspension')
|
|
this.setTotalSize(estimatedSize)
|
|
}
|
|
|
|
async open (): Promise<void> {
|
|
await fs.mkdir(this.basePath, { recursive: true })
|
|
}
|
|
|
|
getName (): string {
|
|
return this.name
|
|
}
|
|
|
|
getSize (): number {
|
|
return this.getTotalSize()
|
|
}
|
|
|
|
async createDirectory (relativePath: string): Promise<void> {
|
|
const fullPath = path.join(this.basePath, relativePath)
|
|
await fs.mkdir(fullPath, { recursive: true })
|
|
}
|
|
|
|
async createFile (relativePath: string, mode: number, size: number): Promise<FileDownload> {
|
|
const fullPath = path.join(this.basePath, relativePath)
|
|
await fs.mkdir(path.dirname(fullPath), { recursive: true })
|
|
|
|
const fileDownload = new ElectronFileDownload(fullPath, mode, size, this.electron)
|
|
await wrapPromise(this.zone, fileDownload.open())
|
|
return fileDownload
|
|
}
|
|
|
|
close (): void {
|
|
this.electron.powerSaveBlocker.stop(this.powerSaveBlocker)
|
|
}
|
|
}
|