mirror of
https://github.com/Eugeny/tabby.git
synced 2025-06-25 05:50:01 +00:00
parent
0749096d9f
commit
4b7b692ace
171
app/lib/app.ts
Normal file
171
app/lib/app.ts
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import { app, ipcMain, Menu, Tray, shell } from 'electron'
|
||||||
|
import { Window } from './window'
|
||||||
|
|
||||||
|
export class Application {
|
||||||
|
private tray: Tray
|
||||||
|
private windows: Window[] = []
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
ipcMain.on('app:config-change', () => {
|
||||||
|
this.broadcast('host:config-change')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async newWindow (): Promise<Window> {
|
||||||
|
let window = new Window()
|
||||||
|
this.windows.push(window)
|
||||||
|
window.visible$.subscribe(visible => {
|
||||||
|
if (visible) {
|
||||||
|
this.disableTray()
|
||||||
|
} else {
|
||||||
|
this.enableTray()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.setupMenu()
|
||||||
|
await window.ready
|
||||||
|
return window
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcast (event, ...args) {
|
||||||
|
for (let window of this.windows) {
|
||||||
|
window.send(event, ...args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async send (event, ...args) {
|
||||||
|
if (!this.hasWindows()) {
|
||||||
|
await this.newWindow()
|
||||||
|
}
|
||||||
|
this.windows[0].send(event, ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
enableTray () {
|
||||||
|
if (this.tray) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
this.tray = new Tray(`${app.getAppPath()}/assets/tray-darwinTemplate.png`)
|
||||||
|
this.tray.setPressedImage(`${app.getAppPath()}/assets/tray-darwinHighlightTemplate.png`)
|
||||||
|
} else {
|
||||||
|
this.tray = new Tray(`${app.getAppPath()}/assets/tray.png`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tray.on('click', () => this.focus())
|
||||||
|
|
||||||
|
const contextMenu = Menu.buildFromTemplate([{
|
||||||
|
label: 'Show',
|
||||||
|
click: () => this.focus(),
|
||||||
|
}])
|
||||||
|
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
this.tray.setContextMenu(contextMenu)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tray.setToolTip(`Terminus ${app.getVersion()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
disableTray () {
|
||||||
|
if (this.tray) {
|
||||||
|
this.tray.destroy()
|
||||||
|
this.tray = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasWindows () {
|
||||||
|
return !!this.windows.length
|
||||||
|
}
|
||||||
|
|
||||||
|
focus () {
|
||||||
|
for (let window of this.windows) {
|
||||||
|
window.show()
|
||||||
|
window.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupMenu () {
|
||||||
|
let template: Electron.MenuItemConstructorOptions[] = [
|
||||||
|
{
|
||||||
|
label: 'Application',
|
||||||
|
submenu: [
|
||||||
|
{ role: 'about', label: 'About Terminus' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Preferences',
|
||||||
|
accelerator: 'Cmd+,',
|
||||||
|
async click () {
|
||||||
|
if (!this.hasWindows()) {
|
||||||
|
await this.newWindow()
|
||||||
|
}
|
||||||
|
this.windows[0].send('host:preferences-menu')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'services', submenu: [] },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'hide' },
|
||||||
|
{ role: 'hideothers' },
|
||||||
|
{ role: 'unhide' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Quit',
|
||||||
|
accelerator: 'Cmd+Q',
|
||||||
|
click () {
|
||||||
|
app.quit()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
submenu: [
|
||||||
|
{ role: 'undo' },
|
||||||
|
{ role: 'redo' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'cut' },
|
||||||
|
{ role: 'copy' },
|
||||||
|
{ role: 'paste' },
|
||||||
|
{ role: 'pasteandmatchstyle' },
|
||||||
|
{ role: 'delete' },
|
||||||
|
{ role: 'selectall' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'View',
|
||||||
|
submenu: [
|
||||||
|
{ role: 'reload' },
|
||||||
|
{ role: 'forcereload' },
|
||||||
|
{ role: 'toggledevtools' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'resetzoom' },
|
||||||
|
{ role: 'zoomin' },
|
||||||
|
{ role: 'zoomout' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'togglefullscreen' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'window',
|
||||||
|
submenu: [
|
||||||
|
{ role: 'minimize' },
|
||||||
|
{ role: 'close' },
|
||||||
|
{ role: 'zoom' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'front' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'help',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Website',
|
||||||
|
click () {
|
||||||
|
shell.openExternal('https://eugeny.github.io/terminus')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
|
||||||
|
}
|
||||||
|
}
|
@ -1,23 +0,0 @@
|
|||||||
import { app } from 'electron'
|
|
||||||
|
|
||||||
export function parseArgs (argv, cwd) {
|
|
||||||
if (argv[0].includes('node')) {
|
|
||||||
argv = argv.slice(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return require('yargs')
|
|
||||||
.usage('terminus [command] [arguments]')
|
|
||||||
.command('open [directory]', 'open a shell in a directory', {
|
|
||||||
directory: { type: 'string', 'default': cwd },
|
|
||||||
})
|
|
||||||
.command('run [command...]', 'run a command in the terminal', {
|
|
||||||
command: { type: 'string' },
|
|
||||||
})
|
|
||||||
.version('v', 'Show version and exit', app.getVersion())
|
|
||||||
.alias('d', 'debug')
|
|
||||||
.describe('d', 'Show DevTools on start')
|
|
||||||
.alias('h', 'help')
|
|
||||||
.help('h')
|
|
||||||
.strict()
|
|
||||||
.parse(argv.slice(1))
|
|
||||||
}
|
|
23
app/lib/cli.ts
Normal file
23
app/lib/cli.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { app } from 'electron'
|
||||||
|
|
||||||
|
export function parseArgs (argv, cwd) {
|
||||||
|
if (argv[0].includes('node')) {
|
||||||
|
argv = argv.slice(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return require('yargs')
|
||||||
|
.usage('terminus [command] [arguments]')
|
||||||
|
.command('open [directory]', 'open a shell in a directory', {
|
||||||
|
directory: { type: 'string', 'default': cwd },
|
||||||
|
})
|
||||||
|
.command('run [command...]', 'run a command in the terminal', {
|
||||||
|
command: { type: 'string' },
|
||||||
|
})
|
||||||
|
.version('v', 'Show version and exit', app.getVersion())
|
||||||
|
.alias('d', 'debug')
|
||||||
|
.describe('d', 'Show DevTools on start')
|
||||||
|
.alias('h', 'help')
|
||||||
|
.help('h')
|
||||||
|
.strict()
|
||||||
|
.parse(argv.slice(1))
|
||||||
|
}
|
304
app/lib/index.js
304
app/lib/index.js
@ -1,304 +0,0 @@
|
|||||||
import { app, ipcMain, BrowserWindow, Menu, Tray, shell } from 'electron'
|
|
||||||
import * as path from 'path'
|
|
||||||
import electronDebug from 'electron-debug'
|
|
||||||
import * as fs from 'fs'
|
|
||||||
import * as yaml from 'js-yaml'
|
|
||||||
import './lru'
|
|
||||||
import { parseArgs } from './cli'
|
|
||||||
import ElectronConfig from 'electron-config'
|
|
||||||
if (process.platform === 'win32' && require('electron-squirrel-startup')) process.exit(0)
|
|
||||||
|
|
||||||
let electronVibrancy
|
|
||||||
if (process.platform !== 'linux') {
|
|
||||||
electronVibrancy = require('electron-vibrancy')
|
|
||||||
}
|
|
||||||
|
|
||||||
let windowConfig = new ElectronConfig({ name: 'window' })
|
|
||||||
|
|
||||||
if (!process.env.TERMINUS_PLUGINS) {
|
|
||||||
process.env.TERMINUS_PLUGINS = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const setWindowVibrancy = (enabled) => {
|
|
||||||
if (enabled && !app.window.vibrancyViewID) {
|
|
||||||
app.window.vibrancyViewID = electronVibrancy.SetVibrancy(app.window, 0)
|
|
||||||
} else if (!enabled && app.window.vibrancyViewID) {
|
|
||||||
electronVibrancy.RemoveView(app.window, app.window.vibrancyViewID)
|
|
||||||
app.window.vibrancyViewID = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const setupTray = () => {
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
app.tray = new Tray(`${app.getAppPath()}/assets/tray-darwinTemplate.png`)
|
|
||||||
app.tray.setPressedImage(`${app.getAppPath()}/assets/tray-darwinHighlightTemplate.png`)
|
|
||||||
} else {
|
|
||||||
app.tray = new Tray(`${app.getAppPath()}/assets/tray.png`)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.tray.on('click', () => {
|
|
||||||
app.window.show()
|
|
||||||
app.window.focus()
|
|
||||||
})
|
|
||||||
|
|
||||||
const contextMenu = Menu.buildFromTemplate([{
|
|
||||||
label: 'Show',
|
|
||||||
click () {
|
|
||||||
app.window.show()
|
|
||||||
app.window.focus()
|
|
||||||
},
|
|
||||||
}])
|
|
||||||
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
app.tray.setContextMenu(contextMenu)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.tray.setToolTip(`Terminus ${app.getVersion()}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const setupWindowManagement = () => {
|
|
||||||
app.window.on('show', () => {
|
|
||||||
app.window.webContents.send('host:window-shown')
|
|
||||||
if (app.tray) {
|
|
||||||
app.tray.destroy()
|
|
||||||
app.tray = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.window.on('hide', () => {
|
|
||||||
if (!app.tray) {
|
|
||||||
setupTray()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.window.on('enter-full-screen', () => app.window.webContents.send('host:window-enter-full-screen'))
|
|
||||||
app.window.on('leave-full-screen', () => app.window.webContents.send('host:window-leave-full-screen'))
|
|
||||||
|
|
||||||
app.window.on('close', () => {
|
|
||||||
windowConfig.set('windowBoundaries', app.window.getBounds())
|
|
||||||
})
|
|
||||||
|
|
||||||
app.window.on('closed', () => {
|
|
||||||
app.window = null
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.on('window-focus', () => {
|
|
||||||
app.window.focus()
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.on('window-maximize', () => {
|
|
||||||
app.window.maximize()
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.on('window-unmaximize', () => {
|
|
||||||
app.window.unmaximize()
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.on('window-toggle-maximize', () => {
|
|
||||||
if (app.window.isMaximized()) {
|
|
||||||
app.window.unmaximize()
|
|
||||||
} else {
|
|
||||||
app.window.maximize()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.on('window-minimize', () => {
|
|
||||||
app.window.minimize()
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.on('window-set-bounds', (event, bounds) => {
|
|
||||||
app.window.setBounds(bounds)
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.on('window-set-always-on-top', (event, flag) => {
|
|
||||||
app.window.setAlwaysOnTop(flag)
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.on('window-set-vibrancy', (event, enabled) => {
|
|
||||||
setWindowVibrancy(enabled)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const setupMenu = () => {
|
|
||||||
let template = [{
|
|
||||||
label: 'Application',
|
|
||||||
submenu: [
|
|
||||||
{ role: 'about', label: 'About Terminus' },
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
label: 'Preferences',
|
|
||||||
accelerator: 'Cmd+,',
|
|
||||||
click () {
|
|
||||||
app.window.webContents.send('host:preferences-menu')
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
{ role: 'services', submenu: [] },
|
|
||||||
{ type: 'separator' },
|
|
||||||
{ role: 'hide' },
|
|
||||||
{ role: 'hideothers' },
|
|
||||||
{ role: 'unhide' },
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
label: 'Quit',
|
|
||||||
accelerator: 'Cmd+Q',
|
|
||||||
click () {
|
|
||||||
app.quit()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Edit',
|
|
||||||
submenu: [
|
|
||||||
{ role: 'undo' },
|
|
||||||
{ role: 'redo' },
|
|
||||||
{ type: 'separator' },
|
|
||||||
{ role: 'cut' },
|
|
||||||
{ role: 'copy' },
|
|
||||||
{ role: 'paste' },
|
|
||||||
{ role: 'pasteandmatchstyle' },
|
|
||||||
{ role: 'delete' },
|
|
||||||
{ role: 'selectall' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'View',
|
|
||||||
submenu: [
|
|
||||||
{ role: 'reload' },
|
|
||||||
{ role: 'forcereload' },
|
|
||||||
{ role: 'toggledevtools' },
|
|
||||||
{ type: 'separator' },
|
|
||||||
{ role: 'resetzoom' },
|
|
||||||
{ role: 'zoomin' },
|
|
||||||
{ role: 'zoomout' },
|
|
||||||
{ type: 'separator' },
|
|
||||||
{ role: 'togglefullscreen' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'window',
|
|
||||||
submenu: [
|
|
||||||
{ role: 'minimize' },
|
|
||||||
{ role: 'zoom' },
|
|
||||||
{ type: 'separator' },
|
|
||||||
{ role: 'front' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'help',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: 'Website',
|
|
||||||
click () {
|
|
||||||
shell.openExternal('https://eugeny.github.io/terminus')
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
|
|
||||||
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = () => {
|
|
||||||
let t0 = Date.now()
|
|
||||||
|
|
||||||
let configPath = path.join(app.getPath('userData'), 'config.yaml')
|
|
||||||
let configData
|
|
||||||
if (fs.existsSync(configPath)) {
|
|
||||||
configData = yaml.safeLoad(fs.readFileSync(configPath, 'utf8'))
|
|
||||||
} else {
|
|
||||||
configData = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
let options = {
|
|
||||||
width: 800,
|
|
||||||
height: 600,
|
|
||||||
title: 'Terminus',
|
|
||||||
minWidth: 400,
|
|
||||||
minHeight: 300,
|
|
||||||
webPreferences: { webSecurity: false },
|
|
||||||
frame: false,
|
|
||||||
show: false,
|
|
||||||
}
|
|
||||||
Object.assign(options, windowConfig.get('windowBoundaries'))
|
|
||||||
|
|
||||||
if ((configData.appearance || {}).frame === 'native') {
|
|
||||||
options.frame = true
|
|
||||||
} else {
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
options.titleBarStyle = 'hiddenInset'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.platform === 'win32' && (configData.appearance || {}).vibrancy) {
|
|
||||||
options.transparent = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.platform === 'linux') {
|
|
||||||
options.backgroundColor = '#131d27'
|
|
||||||
}
|
|
||||||
|
|
||||||
app.commandLine.appendSwitch('disable-http-cache')
|
|
||||||
|
|
||||||
app.window = new BrowserWindow(options)
|
|
||||||
app.window.once('ready-to-show', () => {
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
app.window.setVibrancy('dark')
|
|
||||||
} else if (process.platform === 'win32' && (configData.appearance || {}).vibrancy) {
|
|
||||||
setWindowVibrancy(true)
|
|
||||||
}
|
|
||||||
app.window.show()
|
|
||||||
app.window.focus()
|
|
||||||
})
|
|
||||||
app.window.loadURL(`file://${app.getAppPath()}/dist/index.html`, { extraHeaders: 'pragma: no-cache\n' })
|
|
||||||
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
app.window.setMenu(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
setupWindowManagement()
|
|
||||||
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
setupMenu()
|
|
||||||
} else {
|
|
||||||
app.window.setMenu(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.info(`Host startup: ${Date.now() - t0}ms`)
|
|
||||||
t0 = Date.now()
|
|
||||||
ipcMain.on('app:ready', () => {
|
|
||||||
console.info(`App startup: ${Date.now() - t0}ms`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
app.on('activate', () => {
|
|
||||||
if (!app.window) {
|
|
||||||
start()
|
|
||||||
} else {
|
|
||||||
app.window.show()
|
|
||||||
app.window.focus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
process.on('uncaughtException', function (err) {
|
|
||||||
console.log(err)
|
|
||||||
app.window.webContents.send('uncaughtException', err)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on('second-instance', (event, argv, cwd) => {
|
|
||||||
app.window.webContents.send('host:second-instance', parseArgs(argv, cwd))
|
|
||||||
})
|
|
||||||
|
|
||||||
const argv = parseArgs(process.argv, process.cwd())
|
|
||||||
|
|
||||||
if (!app.requestSingleInstanceLock()) {
|
|
||||||
app.quit()
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (argv.d) {
|
|
||||||
electronDebug({ enabled: true, showDevTools: 'undocked' })
|
|
||||||
}
|
|
||||||
|
|
||||||
app.on('ready', start)
|
|
59
app/lib/index.ts
Normal file
59
app/lib/index.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import './lru'
|
||||||
|
import { app, ipcMain, Menu } from 'electron'
|
||||||
|
import electronDebug = require('electron-debug')
|
||||||
|
import { parseArgs } from './cli'
|
||||||
|
import { Application } from './app'
|
||||||
|
if (process.platform === 'win32' && require('electron-squirrel-startup')) process.exit(0)
|
||||||
|
|
||||||
|
if (!process.env.TERMINUS_PLUGINS) {
|
||||||
|
process.env.TERMINUS_PLUGINS = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const application = new Application()
|
||||||
|
|
||||||
|
app.commandLine.appendSwitch('disable-http-cache')
|
||||||
|
|
||||||
|
ipcMain.on('app:new-window', () => {
|
||||||
|
console.log('new-window')
|
||||||
|
application.newWindow()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('activate', () => {
|
||||||
|
if (!application.hasWindows()) {
|
||||||
|
application.newWindow()
|
||||||
|
} else {
|
||||||
|
application.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
process.on('uncaughtException' as any, err => {
|
||||||
|
console.log(err)
|
||||||
|
application.broadcast('uncaughtException', err)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('second-instance', (_event, argv, cwd) => {
|
||||||
|
application.send('host:second-instance', parseArgs(argv, cwd))
|
||||||
|
})
|
||||||
|
|
||||||
|
const argv = parseArgs(process.argv, process.cwd())
|
||||||
|
|
||||||
|
if (!app.requestSingleInstanceLock()) {
|
||||||
|
app.quit()
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argv.d) {
|
||||||
|
electronDebug({ enabled: true, showDevTools: 'undocked' })
|
||||||
|
}
|
||||||
|
|
||||||
|
app.on('ready', () => {
|
||||||
|
app.dock.setMenu(Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: 'New window',
|
||||||
|
click () {
|
||||||
|
this.app.newWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]))
|
||||||
|
application.newWindow()
|
||||||
|
})
|
@ -6,10 +6,10 @@ let origLstat = fs.realpathSync.bind(fs)
|
|||||||
// NB: The biggest offender of thrashing realpathSync is the node module system
|
// NB: The biggest offender of thrashing realpathSync is the node module system
|
||||||
// itself, which we can't get into via any sane means.
|
// itself, which we can't get into via any sane means.
|
||||||
require('fs').realpathSync = function (p) {
|
require('fs').realpathSync = function (p) {
|
||||||
let r = lru.get(p)
|
let r = lru.get(p)
|
||||||
if (r) return r
|
if (r) return r
|
||||||
|
|
||||||
r = origLstat(p)
|
r = origLstat(p)
|
||||||
lru.set(p, r)
|
lru.set(p, r)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
173
app/lib/window.ts
Normal file
173
app/lib/window.ts
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
import { Subject, Observable } from 'rxjs'
|
||||||
|
import { BrowserWindow, app, ipcMain } from 'electron'
|
||||||
|
import ElectronConfig = require('electron-config')
|
||||||
|
import * as yaml from 'js-yaml'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as path from 'path'
|
||||||
|
|
||||||
|
let electronVibrancy: any
|
||||||
|
if (process.platform !== 'linux') {
|
||||||
|
electronVibrancy = require('electron-vibrancy')
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Window {
|
||||||
|
ready: Promise<void>
|
||||||
|
private visible = new Subject<boolean>()
|
||||||
|
private window: BrowserWindow
|
||||||
|
private vibrancyViewID: number
|
||||||
|
private windowConfig: ElectronConfig
|
||||||
|
|
||||||
|
get visible$ (): Observable<boolean> { return this.visible }
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
let configPath = path.join(app.getPath('userData'), 'config.yaml')
|
||||||
|
let configData
|
||||||
|
if (fs.existsSync(configPath)) {
|
||||||
|
configData = yaml.safeLoad(fs.readFileSync(configPath, 'utf8'))
|
||||||
|
} else {
|
||||||
|
configData = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.windowConfig = new ElectronConfig({ name: 'window' })
|
||||||
|
|
||||||
|
let options: Electron.BrowserWindowConstructorOptions = {
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
title: 'Terminus',
|
||||||
|
minWidth: 400,
|
||||||
|
minHeight: 300,
|
||||||
|
webPreferences: { webSecurity: false },
|
||||||
|
frame: false,
|
||||||
|
show: false,
|
||||||
|
}
|
||||||
|
Object.assign(options, this.windowConfig.get('windowBoundaries'))
|
||||||
|
|
||||||
|
if ((configData.appearance || {}).frame === 'native') {
|
||||||
|
options.frame = true
|
||||||
|
} else {
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
options.titleBarStyle = 'hiddenInset'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'win32' && (configData.appearance || {}).vibrancy) {
|
||||||
|
options.transparent = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
options.backgroundColor = '#131d27'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.window = new BrowserWindow(options)
|
||||||
|
this.window.once('ready-to-show', () => {
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
this.window.setVibrancy('dark')
|
||||||
|
} else if (process.platform === 'win32' && (configData.appearance || {}).vibrancy) {
|
||||||
|
this.setVibrancy(true)
|
||||||
|
}
|
||||||
|
this.window.show()
|
||||||
|
this.window.focus()
|
||||||
|
})
|
||||||
|
this.window.loadURL(`file://${app.getAppPath()}/dist/index.html?${this.window.id}`, { extraHeaders: 'pragma: no-cache\n' })
|
||||||
|
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
this.window.setMenu(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setupWindowManagement()
|
||||||
|
|
||||||
|
this.ready = new Promise(resolve => {
|
||||||
|
const listener = event => {
|
||||||
|
if (event.sender === this.window.webContents) {
|
||||||
|
ipcMain.removeListener('app:ready', listener)
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ipcMain.on('app:ready', listener)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setVibrancy (enabled: boolean) {
|
||||||
|
if (enabled && !this.vibrancyViewID) {
|
||||||
|
this.vibrancyViewID = electronVibrancy.SetVibrancy(this.window, 0)
|
||||||
|
} else if (!enabled && this.vibrancyViewID) {
|
||||||
|
electronVibrancy.RemoveView(this.window, this.vibrancyViewID)
|
||||||
|
this.vibrancyViewID = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
show () {
|
||||||
|
this.window.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
focus () {
|
||||||
|
this.window.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
send (event, ...args) {
|
||||||
|
this.window.webContents.send(event, ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupWindowManagement () {
|
||||||
|
this.window.on('show', () => {
|
||||||
|
this.visible.next(true)
|
||||||
|
this.window.webContents.send('host:window-shown')
|
||||||
|
})
|
||||||
|
|
||||||
|
this.window.on('hide', () => {
|
||||||
|
this.visible.next(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.window.on('enter-full-screen', () => this.window.webContents.send('host:window-enter-full-screen'))
|
||||||
|
this.window.on('leave-full-screen', () => this.window.webContents.send('host:window-leave-full-screen'))
|
||||||
|
|
||||||
|
this.window.on('close', () => {
|
||||||
|
this.windowConfig.set('windowBoundaries', this.window.getBounds())
|
||||||
|
})
|
||||||
|
|
||||||
|
this.window.on('closed', () => {
|
||||||
|
this.destroy()
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.on('window-focus', () => {
|
||||||
|
this.window.focus()
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.on('window-maximize', () => {
|
||||||
|
this.window.maximize()
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.on('window-unmaximize', () => {
|
||||||
|
this.window.unmaximize()
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.on('window-toggle-maximize', () => {
|
||||||
|
if (this.window.isMaximized()) {
|
||||||
|
this.window.unmaximize()
|
||||||
|
} else {
|
||||||
|
this.window.maximize()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.on('window-minimize', () => {
|
||||||
|
this.window.minimize()
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.on('window-set-bounds', (_event, bounds) => {
|
||||||
|
this.window.setBounds(bounds)
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.on('window-set-always-on-top', (_event, flag) => {
|
||||||
|
this.window.setAlwaysOnTop(flag)
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.on('window-set-vibrancy', (_event, enabled) => {
|
||||||
|
this.setVibrancy(enabled)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private destroy () {
|
||||||
|
this.window = null
|
||||||
|
this.visible.complete()
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import '../lib/lru.js'
|
import '../lib/lru'
|
||||||
import 'source-sans-pro'
|
import 'source-sans-pro'
|
||||||
import 'font-awesome/css/font-awesome.css'
|
import 'font-awesome/css/font-awesome.css'
|
||||||
import 'ngx-toastr/toastr.css'
|
import 'ngx-toastr/toastr.css'
|
||||||
@ -29,7 +29,7 @@ Raven.config(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
process.on('uncaughtException', (err) => {
|
process.on('uncaughtException' as any, (err) => {
|
||||||
Raven.captureException(err)
|
Raven.captureException(err)
|
||||||
console.error(err)
|
console.error(err)
|
||||||
})
|
})
|
||||||
|
31
app/tsconfig.main.json
Normal file
31
app/tsconfig.main.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./lib",
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es2017",
|
||||||
|
"declaration": false,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"removeComments": false,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es2015",
|
||||||
|
"es2015.iterable",
|
||||||
|
"es2017",
|
||||||
|
"es7"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"compileOnSave": false,
|
||||||
|
"exclude": [
|
||||||
|
"dist",
|
||||||
|
"node_modules",
|
||||||
|
"*/node_modules"
|
||||||
|
]
|
||||||
|
}
|
@ -6,8 +6,8 @@ module.exports = {
|
|||||||
target: 'node',
|
target: 'node',
|
||||||
entry: {
|
entry: {
|
||||||
'index.ignore': 'file-loader?name=index.html!val-loader!pug-html-loader!' + path.resolve(__dirname, './index.pug'),
|
'index.ignore': 'file-loader?name=index.html!val-loader!pug-html-loader!' + path.resolve(__dirname, './index.pug'),
|
||||||
'preload': path.resolve(__dirname, 'src/entry.preload.ts'),
|
preload: path.resolve(__dirname, 'src/entry.preload.ts'),
|
||||||
'bundle': path.resolve(__dirname, 'src/entry.ts'),
|
bundle: path.resolve(__dirname, 'src/entry.ts'),
|
||||||
},
|
},
|
||||||
mode: process.env.DEV ? 'development' : 'production',
|
mode: process.env.DEV ? 'development' : 'production',
|
||||||
context: __dirname,
|
context: __dirname,
|
||||||
@ -15,7 +15,7 @@ module.exports = {
|
|||||||
output: {
|
output: {
|
||||||
path: path.join(__dirname, 'dist'),
|
path: path.join(__dirname, 'dist'),
|
||||||
pathinfo: true,
|
pathinfo: true,
|
||||||
filename: '[name].js'
|
filename: '[name].js',
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
modules: ['src/', 'node_modules', '../node_modules', 'assets/'].map(x => path.join(__dirname, x)),
|
modules: ['src/', 'node_modules', '../node_modules', 'assets/'].map(x => path.join(__dirname, x)),
|
||||||
@ -29,8 +29,8 @@ module.exports = {
|
|||||||
loader: 'awesome-typescript-loader',
|
loader: 'awesome-typescript-loader',
|
||||||
options: {
|
options: {
|
||||||
configFileName: path.resolve(__dirname, 'tsconfig.json'),
|
configFileName: path.resolve(__dirname, 'tsconfig.json'),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{ test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
|
{ test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
|
||||||
{ test: /\.css$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
|
{ test: /\.css$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
|
||||||
@ -39,20 +39,20 @@ module.exports = {
|
|||||||
use: {
|
use: {
|
||||||
loader: 'file-loader',
|
loader: 'file-loader',
|
||||||
options: {
|
options: {
|
||||||
name: 'images/[name].[ext]'
|
name: 'images/[name].[ext]',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(ttf|eot|otf|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
test: /\.(ttf|eot|otf|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||||
use: {
|
use: {
|
||||||
loader: 'file-loader',
|
loader: 'file-loader',
|
||||||
options: {
|
options: {
|
||||||
name: 'fonts/[name].[ext]'
|
name: 'fonts/[name].[ext]',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
externals: {
|
externals: {
|
||||||
'@angular/core': 'commonjs @angular/core',
|
'@angular/core': 'commonjs @angular/core',
|
||||||
@ -62,15 +62,15 @@ module.exports = {
|
|||||||
'@angular/forms': 'commonjs @angular/forms',
|
'@angular/forms': 'commonjs @angular/forms',
|
||||||
'@angular/common': 'commonjs @angular/common',
|
'@angular/common': 'commonjs @angular/common',
|
||||||
'@ng-bootstrap/ng-bootstrap': 'commonjs @ng-bootstrap/ng-bootstrap',
|
'@ng-bootstrap/ng-bootstrap': 'commonjs @ng-bootstrap/ng-bootstrap',
|
||||||
'child_process': 'commonjs child_process',
|
child_process: 'commonjs child_process',
|
||||||
'electron': 'commonjs electron',
|
electron: 'commonjs electron',
|
||||||
'electron-is-dev': 'commonjs electron-is-dev',
|
'electron-is-dev': 'commonjs electron-is-dev',
|
||||||
'fs': 'commonjs fs',
|
fs: 'commonjs fs',
|
||||||
'ngx-toastr': 'commonjs ngx-toastr',
|
'ngx-toastr': 'commonjs ngx-toastr',
|
||||||
'module': 'commonjs module',
|
module: 'commonjs module',
|
||||||
'mz': 'commonjs mz',
|
mz: 'commonjs mz',
|
||||||
'path': 'commonjs path',
|
path: 'commonjs path',
|
||||||
'rxjs': 'commonjs rxjs',
|
rxjs: 'commonjs rxjs',
|
||||||
'zone.js': 'commonjs zone.js/dist/zone.js',
|
'zone.js': 'commonjs zone.js/dist/zone.js',
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
@ -5,7 +5,7 @@ module.exports = {
|
|||||||
name: 'terminus-main',
|
name: 'terminus-main',
|
||||||
target: 'node',
|
target: 'node',
|
||||||
entry: {
|
entry: {
|
||||||
main: path.resolve(__dirname, 'lib/index.js'),
|
main: path.resolve(__dirname, 'lib/index.ts'),
|
||||||
},
|
},
|
||||||
mode: process.env.DEV ? 'development' : 'production',
|
mode: process.env.DEV ? 'development' : 'production',
|
||||||
context: __dirname,
|
context: __dirname,
|
||||||
@ -22,12 +22,11 @@ module.exports = {
|
|||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /lib[\\/].*\.js$/,
|
test: /\.ts$/,
|
||||||
exclude: /node_modules/,
|
|
||||||
use: {
|
use: {
|
||||||
loader: 'babel-loader',
|
loader: 'awesome-typescript-loader',
|
||||||
options: {
|
options: {
|
||||||
presets: ['babel-preset-es2015'],
|
configFileName: path.resolve(__dirname, 'tsconfig.main.json'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
"@types/electron-config": "^0.2.1",
|
"@types/electron-config": "^0.2.1",
|
||||||
"@types/electron-debug": "^1.1.0",
|
"@types/electron-debug": "^1.1.0",
|
||||||
"@types/fs-promise": "1.0.1",
|
"@types/fs-promise": "1.0.1",
|
||||||
|
"@types/js-yaml": "^3.11.2",
|
||||||
"@types/node": "7.0.5",
|
"@types/node": "7.0.5",
|
||||||
"@types/webpack-env": "1.13.0",
|
"@types/webpack-env": "1.13.0",
|
||||||
"apply-loader": "0.1.0",
|
"apply-loader": "0.1.0",
|
||||||
|
@ -142,9 +142,9 @@ export class AppRootComponent {
|
|||||||
this.unsortedTabs.push(tab)
|
this.unsortedTabs.push(tab)
|
||||||
tab.progress$.subscribe(progress => {
|
tab.progress$.subscribe(progress => {
|
||||||
if (progress !== null) {
|
if (progress !== null) {
|
||||||
this.hostApp.getWindow().setProgressBar(progress / 100.0, 'normal')
|
this.hostApp.getWindow().setProgressBar(progress / 100.0, { mode: 'normal' })
|
||||||
} else {
|
} else {
|
||||||
this.hostApp.getWindow().setProgressBar(-1, 'none')
|
this.hostApp.getWindow().setProgressBar(-1, { mode: 'none' })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -154,26 +154,26 @@ export class AppRootComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onGlobalHotkey () {
|
onGlobalHotkey () {
|
||||||
if (this.electron.app.window.isFocused()) {
|
if (this.hostApp.getWindow().isFocused()) {
|
||||||
// focused
|
// focused
|
||||||
this.electron.loseFocus()
|
this.electron.loseFocus()
|
||||||
if (this.hostApp.platform !== Platform.macOS) {
|
if (this.hostApp.platform !== Platform.macOS) {
|
||||||
this.electron.app.window.hide()
|
this.hostApp.getWindow().hide()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!this.electron.app.window.isVisible()) {
|
if (!this.hostApp.getWindow().isVisible()) {
|
||||||
// unfocused, invisible
|
// unfocused, invisible
|
||||||
this.electron.app.window.show()
|
this.hostApp.getWindow().show()
|
||||||
this.electron.app.window.focus()
|
this.hostApp.getWindow().focus()
|
||||||
} else {
|
} else {
|
||||||
if (this.config.store.appearance.dock === 'off') {
|
if (this.config.store.appearance.dock === 'off') {
|
||||||
// not docked, visible
|
// not docked, visible
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.electron.app.window.focus()
|
this.hostApp.getWindow().focus()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// docked, visible
|
// docked, visible
|
||||||
this.electron.app.window.hide()
|
this.hostApp.getWindow().hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -223,7 +223,7 @@ export class AppRootComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateVibrancy () {
|
private updateVibrancy () {
|
||||||
this.hostApp.setVibrancy(this.config.store.appearance.vibrancy)
|
this.hostApp.setVibrancy(this.config.store.appearance.vibrancy)
|
||||||
this.hostApp.getWindow().setOpacity(this.config.store.appearance.opacity)
|
this.hostApp.getWindow().setOpacity(this.config.store.appearance.opacity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
hotkeys:
|
hotkeys:
|
||||||
|
new-window:
|
||||||
|
- 'Ctrl-Shift-N'
|
||||||
toggle-window:
|
toggle-window:
|
||||||
- 'Ctrl+Space'
|
- 'Ctrl+Space'
|
||||||
toggle-fullscreen:
|
toggle-fullscreen:
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
hotkeys:
|
hotkeys:
|
||||||
|
new-window:
|
||||||
|
- '⌘-N'
|
||||||
toggle-window:
|
toggle-window:
|
||||||
- 'Ctrl+Space'
|
- 'Ctrl+Space'
|
||||||
toggle-fullscreen:
|
toggle-fullscreen:
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
hotkeys:
|
hotkeys:
|
||||||
|
new-window:
|
||||||
|
- 'Ctrl-Shift-N'
|
||||||
toggle-window:
|
toggle-window:
|
||||||
- 'Ctrl+Space'
|
- 'Ctrl+Space'
|
||||||
toggle-fullscreen:
|
toggle-fullscreen:
|
||||||
|
@ -3,6 +3,7 @@ import { Injectable, ComponentFactoryResolver, Injector } from '@angular/core'
|
|||||||
import { BaseTabComponent } from '../components/baseTab.component'
|
import { BaseTabComponent } from '../components/baseTab.component'
|
||||||
import { Logger, LogService } from './log.service'
|
import { Logger, LogService } from './log.service'
|
||||||
import { ConfigService } from './config.service'
|
import { ConfigService } from './config.service'
|
||||||
|
import { HostAppService } from './hostApp.service'
|
||||||
|
|
||||||
export declare type TabComponentType = new (...args: any[]) => BaseTabComponent
|
export declare type TabComponentType = new (...args: any[]) => BaseTabComponent
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ export class AppService {
|
|||||||
constructor (
|
constructor (
|
||||||
private componentFactoryResolver: ComponentFactoryResolver,
|
private componentFactoryResolver: ComponentFactoryResolver,
|
||||||
private config: ConfigService,
|
private config: ConfigService,
|
||||||
|
private hostApp: HostAppService,
|
||||||
private injector: Injector,
|
private injector: Injector,
|
||||||
log: LogService,
|
log: LogService,
|
||||||
) {
|
) {
|
||||||
@ -37,15 +39,21 @@ export class AppService {
|
|||||||
openNewTab (type: TabComponentType, inputs?: any): BaseTabComponent {
|
openNewTab (type: TabComponentType, inputs?: any): BaseTabComponent {
|
||||||
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(type)
|
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(type)
|
||||||
let componentRef = componentFactory.create(this.injector)
|
let componentRef = componentFactory.create(this.injector)
|
||||||
componentRef.instance.hostView = componentRef.hostView
|
let tab = componentRef.instance
|
||||||
Object.assign(componentRef.instance, inputs || {})
|
tab.hostView = componentRef.hostView
|
||||||
|
Object.assign(tab, inputs || {})
|
||||||
|
|
||||||
this.tabs.push(componentRef.instance)
|
this.tabs.push(tab)
|
||||||
this.selectTab(componentRef.instance)
|
this.selectTab(tab)
|
||||||
this.tabsChanged.next()
|
this.tabsChanged.next()
|
||||||
this.tabOpened.next(componentRef.instance)
|
this.tabOpened.next(tab)
|
||||||
|
|
||||||
return componentRef.instance
|
tab.titleChange$.subscribe(title => {
|
||||||
|
if (tab === this.activeTab) {
|
||||||
|
this.hostApp.getWindow().setTitle(title)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return tab
|
||||||
}
|
}
|
||||||
|
|
||||||
selectTab (tab: BaseTabComponent) {
|
selectTab (tab: BaseTabComponent) {
|
||||||
@ -67,6 +75,7 @@ export class AppService {
|
|||||||
if (this.activeTab) {
|
if (this.activeTab) {
|
||||||
this.activeTab.emitFocused()
|
this.activeTab.emitFocused()
|
||||||
}
|
}
|
||||||
|
this.hostApp.getWindow().setTitle(this.activeTab.title)
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleLastTab () {
|
toggleLastTab () {
|
||||||
@ -122,5 +131,6 @@ export class AppService {
|
|||||||
emitReady () {
|
emitReady () {
|
||||||
this.ready.next(null)
|
this.ready.next(null)
|
||||||
this.ready.complete()
|
this.ready.complete()
|
||||||
|
this.hostApp.emitReady()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,7 @@ export class ConfigService {
|
|||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
electron: ElectronService,
|
electron: ElectronService,
|
||||||
hostApp: HostAppService,
|
private hostApp: HostAppService,
|
||||||
@Inject(ConfigProvider) configProviders: ConfigProvider[],
|
@Inject(ConfigProvider) configProviders: ConfigProvider[],
|
||||||
) {
|
) {
|
||||||
this.path = path.join(electron.app.getPath('userData'), 'config.yaml')
|
this.path = path.join(electron.app.getPath('userData'), 'config.yaml')
|
||||||
@ -78,6 +78,11 @@ export class ConfigService {
|
|||||||
return defaults
|
return defaults
|
||||||
}).reduce(configMerge)
|
}).reduce(configMerge)
|
||||||
this.load()
|
this.load()
|
||||||
|
|
||||||
|
hostApp.configChangeBroadcast$.subscribe(() => {
|
||||||
|
this.load()
|
||||||
|
this.emitChange()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getDefaults () {
|
getDefaults () {
|
||||||
@ -96,6 +101,7 @@ export class ConfigService {
|
|||||||
save (): void {
|
save (): void {
|
||||||
fs.writeFileSync(this.path, yaml.safeDump(this._store), 'utf8')
|
fs.writeFileSync(this.path, yaml.safeDump(this._store), 'utf8')
|
||||||
this.emitChange()
|
this.emitChange()
|
||||||
|
this.hostApp.broadcastConfigChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
readRaw (): string {
|
readRaw (): string {
|
||||||
|
@ -76,12 +76,8 @@ export class DockingService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getWindow () {
|
|
||||||
return this.electron.app.window
|
|
||||||
}
|
|
||||||
|
|
||||||
repositionWindow () {
|
repositionWindow () {
|
||||||
let [x, y] = this.getWindow().getPosition()
|
let [x, y] = this.hostApp.getWindow().getPosition()
|
||||||
for (let screen of this.electron.screen.getAllDisplays()) {
|
for (let screen of this.electron.screen.getAllDisplays()) {
|
||||||
let bounds = screen.bounds
|
let bounds = screen.bounds
|
||||||
if (x >= bounds.x && x <= bounds.x + bounds.width && y >= bounds.y && y <= bounds.y + bounds.height) {
|
if (x >= bounds.x && x <= bounds.x + bounds.width && y >= bounds.y && y <= bounds.y + bounds.height) {
|
||||||
@ -89,6 +85,6 @@ export class DockingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let screen = this.electron.screen.getPrimaryDisplay()
|
let screen = this.electron.screen.getPrimaryDisplay()
|
||||||
this.getWindow().setPosition(screen.bounds.x, screen.bounds.y)
|
this.hostApp.getWindow().setPosition(screen.bounds.x, screen.bounds.y)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { TouchBar } from 'electron'
|
import { TouchBar, BrowserWindow } from 'electron'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ElectronService {
|
export class ElectronService {
|
||||||
@ -13,6 +13,7 @@ export class ElectronService {
|
|||||||
screen: any
|
screen: any
|
||||||
remote: any
|
remote: any
|
||||||
TouchBar: typeof TouchBar
|
TouchBar: typeof TouchBar
|
||||||
|
BrowserWindow: typeof BrowserWindow
|
||||||
private electron: any
|
private electron: any
|
||||||
|
|
||||||
constructor () {
|
constructor () {
|
||||||
@ -27,6 +28,7 @@ export class ElectronService {
|
|||||||
this.globalShortcut = this.remote.globalShortcut
|
this.globalShortcut = this.remote.globalShortcut
|
||||||
this.nativeImage = this.remote.nativeImage
|
this.nativeImage = this.remote.nativeImage
|
||||||
this.TouchBar = this.remote.TouchBar
|
this.TouchBar = this.remote.TouchBar
|
||||||
|
this.BrowserWindow = this.remote.BrowserWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
remoteRequire (name: string): any {
|
remoteRequire (name: string): any {
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import { Observable, Subject } from 'rxjs'
|
import { Observable, Subject } from 'rxjs'
|
||||||
import { Injectable, NgZone, EventEmitter } from '@angular/core'
|
import { Injectable, NgZone, EventEmitter } from '@angular/core'
|
||||||
import { ElectronService } from '../services/electron.service'
|
import { ElectronService } from './electron.service'
|
||||||
import { Logger, LogService } from '../services/log.service'
|
import { Logger, LogService } from './log.service'
|
||||||
|
|
||||||
export enum Platform {
|
export enum Platform {
|
||||||
Linux, macOS, Windows,
|
Linux, macOS, Windows,
|
||||||
@ -19,19 +19,21 @@ export interface Bounds {
|
|||||||
export class HostAppService {
|
export class HostAppService {
|
||||||
platform: Platform
|
platform: Platform
|
||||||
nodePlatform: string
|
nodePlatform: string
|
||||||
ready = new EventEmitter<any>()
|
|
||||||
shown = new EventEmitter<any>()
|
shown = new EventEmitter<any>()
|
||||||
isFullScreen = false
|
isFullScreen = false
|
||||||
private preferencesMenu = new Subject<void>()
|
private preferencesMenu = new Subject<void>()
|
||||||
private secondInstance = new Subject<void>()
|
private secondInstance = new Subject<void>()
|
||||||
private cliOpenDirectory = new Subject<string>()
|
private cliOpenDirectory = new Subject<string>()
|
||||||
private cliRunCommand = new Subject<string[]>()
|
private cliRunCommand = new Subject<string[]>()
|
||||||
|
private configChangeBroadcast = new Subject<void>()
|
||||||
private logger: Logger
|
private logger: Logger
|
||||||
|
private windowId: number
|
||||||
|
|
||||||
get preferencesMenu$ (): Observable<void> { return this.preferencesMenu }
|
get preferencesMenu$ (): Observable<void> { return this.preferencesMenu }
|
||||||
get secondInstance$ (): Observable<void> { return this.secondInstance }
|
get secondInstance$ (): Observable<void> { return this.secondInstance }
|
||||||
get cliOpenDirectory$ (): Observable<string> { return this.cliOpenDirectory }
|
get cliOpenDirectory$ (): Observable<string> { return this.cliOpenDirectory }
|
||||||
get cliRunCommand$ (): Observable<string[]> { return this.cliRunCommand }
|
get cliRunCommand$ (): Observable<string[]> { return this.cliRunCommand }
|
||||||
|
get configChangeBroadcast$ (): Observable<void> { return this.configChangeBroadcast }
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
@ -46,9 +48,12 @@ export class HostAppService {
|
|||||||
linux: Platform.Linux
|
linux: Platform.Linux
|
||||||
}[this.nodePlatform]
|
}[this.nodePlatform]
|
||||||
|
|
||||||
|
this.windowId = parseInt(location.search.substring(1))
|
||||||
|
this.logger.info('Window ID:', this.windowId)
|
||||||
|
|
||||||
electron.ipcRenderer.on('host:preferences-menu', () => this.zone.run(() => this.preferencesMenu.next()))
|
electron.ipcRenderer.on('host:preferences-menu', () => this.zone.run(() => this.preferencesMenu.next()))
|
||||||
|
|
||||||
electron.ipcRenderer.on('uncaughtException', ($event, err) => {
|
electron.ipcRenderer.on('uncaughtException', (_$event, err) => {
|
||||||
this.logger.error('Unhandled exception:', err)
|
this.logger.error('Unhandled exception:', err)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -64,7 +69,7 @@ export class HostAppService {
|
|||||||
this.zone.run(() => this.shown.emit())
|
this.zone.run(() => this.shown.emit())
|
||||||
})
|
})
|
||||||
|
|
||||||
electron.ipcRenderer.on('host:second-instance', ($event, argv: any, cwd: string) => this.zone.run(() => {
|
electron.ipcRenderer.on('host:second-instance', (_$event, argv: any, cwd: string) => this.zone.run(() => {
|
||||||
this.logger.info('Second instance', argv)
|
this.logger.info('Second instance', argv)
|
||||||
const op = argv._[0]
|
const op = argv._[0]
|
||||||
if (op === 'open') {
|
if (op === 'open') {
|
||||||
@ -74,13 +79,17 @@ export class HostAppService {
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
this.ready.subscribe(() => {
|
electron.ipcRenderer.on('host:config-change', () => this.zone.run(() => {
|
||||||
electron.ipcRenderer.send('app:ready')
|
this.configChangeBroadcast.next()
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
getWindow () {
|
getWindow () {
|
||||||
return this.electron.app.window
|
return this.electron.BrowserWindow.fromId(this.windowId)
|
||||||
|
}
|
||||||
|
|
||||||
|
newWindow () {
|
||||||
|
this.electron.ipcRenderer.send('app:new-window')
|
||||||
}
|
}
|
||||||
|
|
||||||
getShell () {
|
getShell () {
|
||||||
@ -142,6 +151,14 @@ export class HostAppService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
broadcastConfigChange () {
|
||||||
|
this.electron.ipcRenderer.send('app:config-change')
|
||||||
|
}
|
||||||
|
|
||||||
|
emitReady () {
|
||||||
|
this.electron.ipcRenderer.send('app:ready')
|
||||||
|
}
|
||||||
|
|
||||||
quit () {
|
quit () {
|
||||||
this.logger.info('Quitting')
|
this.logger.info('Quitting')
|
||||||
this.electron.app.quit()
|
this.electron.app.quit()
|
||||||
|
@ -174,6 +174,10 @@ export class HotkeysService {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class AppHotkeyProvider extends HotkeyProvider {
|
export class AppHotkeyProvider extends HotkeyProvider {
|
||||||
hotkeys: IHotkeyDescription[] = [
|
hotkeys: IHotkeyDescription[] = [
|
||||||
|
{
|
||||||
|
id: 'new-window',
|
||||||
|
name: 'New window',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'toggle-window',
|
id: 'toggle-window',
|
||||||
name: 'Toggle terminal window',
|
name: 'Toggle terminal window',
|
||||||
|
@ -3,6 +3,7 @@ import { TouchBarSegmentedControl, SegmentedControlSegment } from 'electron'
|
|||||||
import { AppService } from './app.service'
|
import { AppService } from './app.service'
|
||||||
import { ConfigService } from './config.service'
|
import { ConfigService } from './config.service'
|
||||||
import { ElectronService } from './electron.service'
|
import { ElectronService } from './electron.service'
|
||||||
|
import { HostAppService } from './hostApp.service'
|
||||||
import { IToolbarButton, ToolbarButtonProvider } from '../api'
|
import { IToolbarButton, ToolbarButtonProvider } from '../api'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -12,6 +13,7 @@ export class TouchbarService {
|
|||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private app: AppService,
|
private app: AppService,
|
||||||
|
private hostApp: HostAppService,
|
||||||
@Inject(ToolbarButtonProvider) private toolbarButtonProviders: ToolbarButtonProvider[],
|
@Inject(ToolbarButtonProvider) private toolbarButtonProviders: ToolbarButtonProvider[],
|
||||||
private config: ConfigService,
|
private config: ConfigService,
|
||||||
private electron: ElectronService,
|
private electron: ElectronService,
|
||||||
@ -51,7 +53,7 @@ export class TouchbarService {
|
|||||||
...buttons.map(button => this.getButton(button))
|
...buttons.map(button => this.getButton(button))
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
this.electron.app.window.setTouchBar(touchBar)
|
this.hostApp.getWindow().setTouchBar(touchBar)
|
||||||
}
|
}
|
||||||
|
|
||||||
private getButton (button: IToolbarButton): Electron.TouchBarButton {
|
private getButton (button: IToolbarButton): Electron.TouchBarButton {
|
||||||
|
@ -17,7 +17,12 @@ export class ButtonProvider extends ToolbarButtonProvider {
|
|||||||
super()
|
super()
|
||||||
hotkeys.matchedHotkey.subscribe(async (hotkey) => {
|
hotkeys.matchedHotkey.subscribe(async (hotkey) => {
|
||||||
if (hotkey === 'new-tab') {
|
if (hotkey === 'new-tab') {
|
||||||
this.terminal.openTab()
|
terminal.openTab()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
hotkeys.matchedHotkey.subscribe(async (hotkey) => {
|
||||||
|
if (hotkey === 'new-window') {
|
||||||
|
hostApp.newWindow()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
hostApp.cliOpenDirectory$.subscribe(async directory => {
|
hostApp.cliOpenDirectory$.subscribe(async directory => {
|
||||||
|
@ -148,6 +148,10 @@
|
|||||||
"@types/mz" "*"
|
"@types/mz" "*"
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/js-yaml@^3.11.2":
|
||||||
|
version "3.11.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.11.2.tgz#699ad86054cc20043c30d66a6fcde30bbf5d3d5e"
|
||||||
|
|
||||||
"@types/mz@*":
|
"@types/mz@*":
|
||||||
version "0.0.32"
|
version "0.0.32"
|
||||||
resolved "https://registry.yarnpkg.com/@types/mz/-/mz-0.0.32.tgz#e8248b4e41424c052edc1725dd33650c313a3659"
|
resolved "https://registry.yarnpkg.com/@types/mz/-/mz-0.0.32.tgz#e8248b4e41424c052edc1725dd33650c313a3659"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user