mirror of
https://github.com/Eugeny/tabby.git
synced 2025-09-05 16:11:49 +00:00
Compare commits
24 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
78f8f4005e | ||
![]() |
38cfb3f036 | ||
![]() |
4e4d8a0e91 | ||
![]() |
21cfd14f1c | ||
![]() |
a64bbe145c | ||
![]() |
6a5dc79c5d | ||
![]() |
b799128427 | ||
![]() |
8b64a819e7 | ||
![]() |
5b78a5c1ed | ||
![]() |
91b318853f | ||
![]() |
ce3610c2da | ||
![]() |
d03430fb2e | ||
![]() |
caacc01aea | ||
![]() |
bcb6963c35 | ||
![]() |
deb99b0865 | ||
![]() |
2101c18657 | ||
![]() |
1a258f32b0 | ||
![]() |
3aaf490f57 | ||
![]() |
9faa346699 | ||
![]() |
d5b6a686f8 | ||
![]() |
492d006f64 | ||
![]() |
d999320c24 | ||
![]() |
5142d12e7e | ||
![]() |
453c613571 |
@@ -15,6 +15,8 @@ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
|
||||
import { getRootModule } from './app.module'
|
||||
import { findPlugins, loadPlugins, IPluginInfo } from './plugins'
|
||||
|
||||
;(process as any).enablePromiseAPI = true
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
process.env.HOME = process.env.HOMEDRIVE + process.env.HOMEPATH
|
||||
}
|
||||
|
BIN
build/windows/squirrel.gif
Normal file
BIN
build/windows/squirrel.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
@@ -77,6 +77,7 @@
|
||||
},
|
||||
"squirrelWindows": {
|
||||
"iconUrl": "https://github.com/Eugeny/terminus/raw/master/build/windows/icon.ico",
|
||||
"loadingGif": "./build/windows/squirrel.gif",
|
||||
"artifactName": "terminus-${version}-setup.exe"
|
||||
},
|
||||
"portable": {
|
||||
@@ -85,6 +86,7 @@
|
||||
"mac": {
|
||||
"category": "public.app-category.video",
|
||||
"icon": "./build/mac/icon.icns",
|
||||
"artifactName": "terminus-${version}-macos.${ext}",
|
||||
"publish": [
|
||||
"github"
|
||||
],
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "terminus-community-color-schemes",
|
||||
"version": "1.0.0-alpha.55",
|
||||
"version": "1.0.68-c17-g8b64a81",
|
||||
"description": "Community color schemes for Terminus",
|
||||
"keywords": [
|
||||
"terminus-builtin-plugin"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "terminus-core",
|
||||
"version": "1.0.0-alpha.55",
|
||||
"version": "1.0.68-c17-g8b64a81",
|
||||
"description": "Terminus core",
|
||||
"keywords": [
|
||||
"terminus-builtin-plugin"
|
||||
|
@@ -4,6 +4,7 @@ export { ToolbarButtonProvider, IToolbarButton } from './toolbarButtonProvider'
|
||||
export { ConfigProvider } from './configProvider'
|
||||
export { HotkeyProvider, IHotkeyDescription } from './hotkeyProvider'
|
||||
export { Theme } from './theme'
|
||||
export { TabContextMenuItemProvider } from './tabContextMenuProvider'
|
||||
|
||||
export { AppService } from '../services/app.service'
|
||||
export { ConfigService } from '../services/config.service'
|
||||
|
8
terminus-core/src/api/tabContextMenuProvider.ts
Normal file
8
terminus-core/src/api/tabContextMenuProvider.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
import { TabHeaderComponent } from '../components/tabHeader.component'
|
||||
|
||||
export abstract class TabContextMenuItemProvider {
|
||||
weight = 0
|
||||
|
||||
abstract async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<Electron.MenuItemConstructorOptions[]>
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
import { Component, Input, HostBinding, HostListener, NgZone, ViewChild, ElementRef } from '@angular/core'
|
||||
import { Component, Input, Optional, Inject, HostBinding, HostListener, ViewChild, ElementRef } from '@angular/core'
|
||||
import { SortableComponent } from 'ng2-dnd'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { TabContextMenuItemProvider } from '../api/tabContextMenuProvider'
|
||||
import { BaseTabComponent } from './baseTab.component'
|
||||
import { RenameTabModalComponent } from './renameTabModal.component'
|
||||
import { HotkeysService } from '../services/hotkeys.service'
|
||||
@@ -8,16 +9,6 @@ import { ElectronService } from '../services/electron.service'
|
||||
import { AppService } from '../services/app.service'
|
||||
import { HostAppService, Platform } from '../services/hostApp.service'
|
||||
|
||||
const COLORS = [
|
||||
{ name: 'No color', value: null },
|
||||
{ name: 'Blue', value: '#0275d8' },
|
||||
{ name: 'Green', value: '#5cb85c' },
|
||||
{ name: 'Orange', value: '#f0ad4e' },
|
||||
{ name: 'Purple', value: '#613d7c' },
|
||||
{ name: 'Red', value: '#d9534f' },
|
||||
{ name: 'Yellow', value: '#ffd500' },
|
||||
]
|
||||
|
||||
@Component({
|
||||
selector: 'tab-header',
|
||||
template: require('./tabHeader.component.pug'),
|
||||
@@ -31,16 +22,14 @@ export class TabHeaderComponent {
|
||||
@Input() progress: number
|
||||
@ViewChild('handle') handle: ElementRef
|
||||
|
||||
private completionNotificationEnabled = false
|
||||
|
||||
constructor (
|
||||
public app: AppService,
|
||||
private electron: ElectronService,
|
||||
private zone: NgZone,
|
||||
private hostApp: HostAppService,
|
||||
private ngbModal: NgbModal,
|
||||
private hotkeys: HotkeysService,
|
||||
private parentDraggable: SortableComponent,
|
||||
@Optional() @Inject(TabContextMenuItemProvider) protected contextMenuProviders: TabContextMenuItemProvider[],
|
||||
) {
|
||||
this.hotkeys.matchedHotkey.subscribe((hotkey) => {
|
||||
if (this.app.activeTab === this.tab) {
|
||||
@@ -49,6 +38,7 @@ export class TabHeaderComponent {
|
||||
}
|
||||
}
|
||||
})
|
||||
this.contextMenuProviders.sort((a, b) => a.weight - b.weight)
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
@@ -69,6 +59,15 @@ export class TabHeaderComponent {
|
||||
}).catch(() => null)
|
||||
}
|
||||
|
||||
async buildContextMenu (): Promise<Electron.MenuItemConstructorOptions[]> {
|
||||
let items: Electron.MenuItemConstructorOptions[] = []
|
||||
for (let section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(this.tab, this)))) {
|
||||
items.push({ type: 'separator' })
|
||||
items = items.concat(section)
|
||||
}
|
||||
return items.slice(1)
|
||||
}
|
||||
|
||||
@HostListener('dblclick') onDoubleClick (): void {
|
||||
this.showRenameTabModal()
|
||||
}
|
||||
@@ -80,96 +79,7 @@ export class TabHeaderComponent {
|
||||
if ($event.which === 3) {
|
||||
event.preventDefault()
|
||||
|
||||
let contextMenu = this.electron.remote.Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Close',
|
||||
click: () => this.zone.run(() => {
|
||||
this.app.closeTab(this.tab, true)
|
||||
})
|
||||
},
|
||||
{
|
||||
label: 'Close other tabs',
|
||||
click: () => this.zone.run(() => {
|
||||
for (let tab of this.app.tabs.filter(x => x !== this.tab)) {
|
||||
this.app.closeTab(tab, true)
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: 'Close tabs to the right',
|
||||
click: () => this.zone.run(() => {
|
||||
for (let tab of this.app.tabs.slice(this.app.tabs.indexOf(this.tab) + 1)) {
|
||||
this.app.closeTab(tab, true)
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: 'Close tabs to the left',
|
||||
click: () => this.zone.run(() => {
|
||||
for (let tab of this.app.tabs.slice(0, this.app.tabs.indexOf(this.tab))) {
|
||||
this.app.closeTab(tab, true)
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: 'Rename',
|
||||
click: () => this.zone.run(() => this.showRenameTabModal())
|
||||
},
|
||||
{
|
||||
label: 'Color',
|
||||
sublabel: COLORS.find(x => x.value === this.tab.color).name,
|
||||
submenu: COLORS.map(color => ({
|
||||
label: color.name,
|
||||
type: 'radio',
|
||||
checked: this.tab.color === color.value,
|
||||
click: () => this.zone.run(() => {
|
||||
this.tab.color = color.value
|
||||
}),
|
||||
})),
|
||||
}
|
||||
])
|
||||
|
||||
if ((this.tab as any).saveAsProfile) {
|
||||
contextMenu.append(new this.electron.MenuItem({
|
||||
label: 'Save as a profile',
|
||||
click: () => this.zone.run(() => (this.tab as any).saveAsProfile())
|
||||
}))
|
||||
}
|
||||
|
||||
let process = await this.tab.getCurrentProcess()
|
||||
if (process) {
|
||||
contextMenu.append(new this.electron.MenuItem({
|
||||
id: 'sep',
|
||||
type: 'separator',
|
||||
}))
|
||||
contextMenu.append(new this.electron.MenuItem({
|
||||
id: 'process-name',
|
||||
enabled: false,
|
||||
label: 'Current process: ' + process.name,
|
||||
}))
|
||||
contextMenu.append(new this.electron.MenuItem({
|
||||
id: 'completion',
|
||||
label: 'Notify when done',
|
||||
type: 'checkbox',
|
||||
checked: this.completionNotificationEnabled,
|
||||
click: () => this.zone.run(() => {
|
||||
this.completionNotificationEnabled = !this.completionNotificationEnabled
|
||||
|
||||
if (this.completionNotificationEnabled) {
|
||||
this.app.observeTabCompletion(this.tab).subscribe(() => {
|
||||
new Notification('Process completed', {
|
||||
body: process.name,
|
||||
}).addEventListener('click', () => {
|
||||
this.app.selectTab(this.tab)
|
||||
})
|
||||
this.completionNotificationEnabled = false
|
||||
})
|
||||
} else {
|
||||
this.app.stopObservingTabCompletion(this.tab)
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
const contextMenu = this.electron.remote.Menu.buildFromTemplate(await this.buildContextMenu())
|
||||
|
||||
contextMenu.popup({
|
||||
x: $event.pageX,
|
||||
|
@@ -24,9 +24,11 @@ import { AutofocusDirective } from './directives/autofocus.directive'
|
||||
import { HotkeyProvider } from './api/hotkeyProvider'
|
||||
import { ConfigProvider } from './api/configProvider'
|
||||
import { Theme } from './api/theme'
|
||||
import { TabContextMenuItemProvider } from './api/tabContextMenuProvider'
|
||||
|
||||
import { StandardTheme, StandardCompactTheme, PaperTheme } from './theme'
|
||||
import { CoreConfigProvider } from './config'
|
||||
import { TaskCompletionContextMenu, CommonOptionsContextMenu, CloseContextMenu } from './tabContextMenu'
|
||||
|
||||
import 'perfect-scrollbar/css/perfect-scrollbar.css'
|
||||
import 'ng2-dnd/bundles/style.css'
|
||||
@@ -37,6 +39,9 @@ const PROVIDERS = [
|
||||
{ provide: Theme, useClass: StandardCompactTheme, multi: true },
|
||||
{ provide: Theme, useClass: PaperTheme, multi: true },
|
||||
{ provide: ConfigProvider, useClass: CoreConfigProvider, multi: true },
|
||||
{ provide: TabContextMenuItemProvider, useClass: CommonOptionsContextMenu, multi: true },
|
||||
{ provide: TabContextMenuItemProvider, useClass: CloseContextMenu, multi: true },
|
||||
{ provide: TabContextMenuItemProvider, useClass: TaskCompletionContextMenu, multi: true },
|
||||
{ provide: PERFECT_SCROLLBAR_CONFIG, useValue: { suppressScrollX: true } }
|
||||
]
|
||||
|
||||
|
@@ -1,6 +1,11 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { TouchBar, BrowserWindow, Menu, MenuItem } from 'electron'
|
||||
|
||||
export interface MessageBoxResponse {
|
||||
response: number
|
||||
checkboxChecked?: boolean
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ElectronService {
|
||||
app: any
|
||||
@@ -54,4 +59,15 @@ export class ElectronService {
|
||||
this.remote.Menu.sendActionToFirstResponder('hide:')
|
||||
}
|
||||
}
|
||||
|
||||
showMessageBox (
|
||||
browserWindow: Electron.BrowserWindow,
|
||||
options: Electron.MessageBoxOptions
|
||||
): Promise<MessageBoxResponse> {
|
||||
return new Promise(resolve => {
|
||||
this.dialog.showMessageBox(browserWindow, options, (response, checkboxChecked) => {
|
||||
resolve({ response, checkboxChecked })
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
139
terminus-core/src/tabContextMenu.ts
Normal file
139
terminus-core/src/tabContextMenu.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { Injectable, NgZone } from '@angular/core'
|
||||
import { AppService } from './services/app.service'
|
||||
import { BaseTabComponent } from './components/baseTab.component'
|
||||
import { TabHeaderComponent } from './components/tabHeader.component'
|
||||
import { TabContextMenuItemProvider } from './api/tabContextMenuProvider'
|
||||
|
||||
@Injectable()
|
||||
export class CloseContextMenu extends TabContextMenuItemProvider {
|
||||
weight = -5
|
||||
|
||||
constructor (
|
||||
private app: AppService,
|
||||
private zone: NgZone,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async getItems (tab: BaseTabComponent): Promise<Electron.MenuItemConstructorOptions[]> {
|
||||
return [
|
||||
{
|
||||
label: 'Close',
|
||||
click: () => this.zone.run(() => {
|
||||
this.app.closeTab(tab, true)
|
||||
})
|
||||
},
|
||||
{
|
||||
label: 'Close other tabs',
|
||||
click: () => this.zone.run(() => {
|
||||
for (let t of this.app.tabs.filter(x => x !== tab)) {
|
||||
this.app.closeTab(t, true)
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: 'Close tabs to the right',
|
||||
click: () => this.zone.run(() => {
|
||||
for (let t of this.app.tabs.slice(this.app.tabs.indexOf(tab) + 1)) {
|
||||
this.app.closeTab(t, true)
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: 'Close tabs to the left',
|
||||
click: () => this.zone.run(() => {
|
||||
for (let t of this.app.tabs.slice(0, this.app.tabs.indexOf(tab))) {
|
||||
this.app.closeTab(t, true)
|
||||
}
|
||||
})
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const COLORS = [
|
||||
{ name: 'No color', value: null },
|
||||
{ name: 'Blue', value: '#0275d8' },
|
||||
{ name: 'Green', value: '#5cb85c' },
|
||||
{ name: 'Orange', value: '#f0ad4e' },
|
||||
{ name: 'Purple', value: '#613d7c' },
|
||||
{ name: 'Red', value: '#d9534f' },
|
||||
{ name: 'Yellow', value: '#ffd500' },
|
||||
]
|
||||
|
||||
@Injectable()
|
||||
export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
|
||||
weight = -1
|
||||
|
||||
constructor (
|
||||
private zone: NgZone,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<Electron.MenuItemConstructorOptions[]> {
|
||||
return [
|
||||
{
|
||||
label: 'Rename',
|
||||
click: () => this.zone.run(() => tabHeader.showRenameTabModal())
|
||||
},
|
||||
{
|
||||
label: 'Color',
|
||||
sublabel: COLORS.find(x => x.value === tab.color).name,
|
||||
submenu: COLORS.map(color => ({
|
||||
label: color.name,
|
||||
type: 'radio',
|
||||
checked: tab.color === color.value,
|
||||
click: () => this.zone.run(() => {
|
||||
tab.color = color.value
|
||||
}),
|
||||
})) as Electron.MenuItemConstructorOptions[],
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TaskCompletionContextMenu extends TabContextMenuItemProvider {
|
||||
constructor (
|
||||
private app: AppService,
|
||||
private zone: NgZone,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async getItems (tab: BaseTabComponent): Promise<Electron.MenuItemConstructorOptions[]> {
|
||||
let process = await tab.getCurrentProcess()
|
||||
if (process) {
|
||||
return [
|
||||
{
|
||||
id: 'process-name',
|
||||
enabled: false,
|
||||
label: 'Current process: ' + process.name,
|
||||
},
|
||||
{
|
||||
label: 'Notify when done',
|
||||
type: 'checkbox',
|
||||
checked: (tab as any).__completionNotificationEnabled,
|
||||
click: () => this.zone.run(() => {
|
||||
;(tab as any).__completionNotificationEnabled = !(tab as any).__completionNotificationEnabled
|
||||
|
||||
if ((tab as any).__completionNotificationEnabled) {
|
||||
this.app.observeTabCompletion(tab).subscribe(() => {
|
||||
new Notification('Process completed', {
|
||||
body: process.name,
|
||||
}).addEventListener('click', () => {
|
||||
this.app.selectTab(tab)
|
||||
})
|
||||
;(tab as any).__completionNotificationEnabled = false
|
||||
})
|
||||
} else {
|
||||
this.app.stopObservingTabCompletion(tab)
|
||||
}
|
||||
})
|
||||
},
|
||||
]
|
||||
}
|
||||
return []
|
||||
}
|
||||
}
|
@@ -5,19 +5,19 @@ import { Theme } from './api'
|
||||
export class StandardTheme extends Theme {
|
||||
name = 'Standard'
|
||||
css = require('./theme.scss')
|
||||
terminalBackground = '#1D272D'
|
||||
terminalBackground = '#222a33'
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class StandardCompactTheme extends Theme {
|
||||
name = 'Compact'
|
||||
css = require('./theme.compact.scss')
|
||||
terminalBackground = '#1D272D'
|
||||
terminalBackground = '#222a33'
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PaperTheme extends Theme {
|
||||
name = 'Paper'
|
||||
css = require('./theme.paper.scss')
|
||||
terminalBackground = '#1D272D'
|
||||
terminalBackground = '#f7f1e0'
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "terminus-plugin-manager",
|
||||
"version": "1.0.0-alpha.55",
|
||||
"version": "1.0.68-c17-g8b64a81",
|
||||
"description": "Terminus' plugin manager",
|
||||
"keywords": [
|
||||
"terminus-builtin-plugin"
|
||||
|
@@ -48,7 +48,7 @@ export class PluginManagerService {
|
||||
return
|
||||
}
|
||||
if (this.hostApp.platform !== Platform.Windows) {
|
||||
this.envPath = (await exec('$SHELL -c -i \'echo $PATH\''))[0].toString().trim()
|
||||
this.envPath = (await exec('$SHELL -i -c \'echo $PATH\''))[0].toString().trim()
|
||||
let searchPaths = this.envPath.split(':')
|
||||
for (let searchPath of searchPaths) {
|
||||
if (await fs.exists(path.join(searchPath, 'npm'))) {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "terminus-settings",
|
||||
"version": "1.0.0-alpha.55",
|
||||
"version": "1.0.68-c17-g8b64a81",
|
||||
"description": "Terminus terminal settings page",
|
||||
"keywords": [
|
||||
"terminus-builtin-plugin"
|
||||
|
@@ -1,5 +1,6 @@
|
||||
export abstract class SettingsTabProvider {
|
||||
id: string
|
||||
icon: string
|
||||
title: string
|
||||
|
||||
getComponentType (): any {
|
||||
|
@@ -3,6 +3,7 @@ button.btn.btn-outline-warning.btn-block(*ngIf='config.restartRequested', '(clic
|
||||
ngb-tabset.vertical(type='pills', [activeId]='activeTab')
|
||||
ngb-tab(id='application')
|
||||
ng-template(ngbTabTitle)
|
||||
i.fas.fa-fw.fa-window-maximize.mr-2
|
||||
| Application
|
||||
ng-template(ngbTabContent)
|
||||
.d-flex.align-items-center.mb-4
|
||||
@@ -247,6 +248,7 @@ ngb-tabset.vertical(type='pills', [activeId]='activeTab')
|
||||
|
||||
ngb-tab(id='hotkeys')
|
||||
ng-template(ngbTabTitle)
|
||||
i.fas.fa-fw.fa-keyboard.mr-2
|
||||
| Hotkeys
|
||||
ng-template(ngbTabContent)
|
||||
h3.mb-3 Hotkeys
|
||||
@@ -274,6 +276,7 @@ ngb-tabset.vertical(type='pills', [activeId]='activeTab')
|
||||
|
||||
ngb-tab(*ngFor='let provider of settingsProviders', [id]='provider.id')
|
||||
ng-template(ngbTabTitle)
|
||||
i(class='fas fa-fw mr-2 fa-{{provider.icon || "puzzle-piece"}}')
|
||||
| {{provider.title}}
|
||||
ng-template(ngbTabContent)
|
||||
settings-tab-body([provider]='provider')
|
||||
@@ -281,6 +284,7 @@ ngb-tabset.vertical(type='pills', [activeId]='activeTab')
|
||||
|
||||
ngb-tab(id='config-file')
|
||||
ng-template(ngbTabTitle)
|
||||
i.fas.fa-fw.fa-code.mr-2
|
||||
| Config file
|
||||
ng-template.test(ngbTabContent)
|
||||
.d-flex.flex-column.w-100.h-100
|
||||
|
@@ -6,7 +6,7 @@ import { SettingsTabComponent } from './components/settingsTab.component'
|
||||
@Injectable()
|
||||
export class RecoveryProvider extends TabRecoveryProvider {
|
||||
async recover (recoveryToken: any): Promise<RecoveredTab> {
|
||||
if (recoveryToken.type === 'app:settings') {
|
||||
if (recoveryToken && recoveryToken.type === 'app:settings') {
|
||||
return { type: SettingsTabComponent }
|
||||
}
|
||||
return null
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "terminus-ssh",
|
||||
"version": "1.0.0-alpha.55",
|
||||
"version": "1.0.68-c17-g8b64a81",
|
||||
"description": "SSH connection manager for Terminus",
|
||||
"keywords": [
|
||||
"terminus-builtin-plugin"
|
||||
|
@@ -23,10 +23,11 @@ export interface SSHConnection {
|
||||
|
||||
export class SSHSession extends BaseSession {
|
||||
scripts?: LoginScript[]
|
||||
shell: any
|
||||
|
||||
constructor (private shell: any, conn: SSHConnection) {
|
||||
constructor (public connection: SSHConnection) {
|
||||
super()
|
||||
this.scripts = conn.scripts || []
|
||||
this.scripts = connection.scripts || []
|
||||
}
|
||||
|
||||
start () {
|
||||
@@ -87,15 +88,21 @@ export class SSHSession extends BaseSession {
|
||||
}
|
||||
|
||||
resize (columns, rows) {
|
||||
this.shell.setWindow(rows, columns)
|
||||
if (this.shell) {
|
||||
this.shell.setWindow(rows, columns)
|
||||
}
|
||||
}
|
||||
|
||||
write (data) {
|
||||
this.shell.write(data)
|
||||
if (this.shell) {
|
||||
this.shell.write(data)
|
||||
}
|
||||
}
|
||||
|
||||
kill (signal?: string) {
|
||||
this.shell.signal(signal || 'TERM')
|
||||
if (this.shell) {
|
||||
this.shell.signal(signal || 'TERM')
|
||||
}
|
||||
}
|
||||
|
||||
async getChildProcesses (): Promise<any[]> {
|
||||
|
@@ -66,8 +66,17 @@ export class EditConnectionModalComponent {
|
||||
}
|
||||
}
|
||||
|
||||
deleteScript (script: LoginScript) {
|
||||
if (confirm(`Delete?`)) {
|
||||
async deleteScript (script: LoginScript) {
|
||||
if ((await this.electron.showMessageBox(
|
||||
this.hostApp.getWindow(),
|
||||
{
|
||||
type: 'warning',
|
||||
message: 'Delete this script?',
|
||||
detail: script.expect,
|
||||
buttons: ['Keep', 'Delete'],
|
||||
defaultId: 1,
|
||||
}
|
||||
)).response === 1) {
|
||||
this.connection.scripts = this.connection.scripts.filter(x => x !== script)
|
||||
}
|
||||
}
|
||||
|
@@ -61,7 +61,7 @@ export class SSHModalComponent {
|
||||
|
||||
connect (connection: SSHConnection) {
|
||||
this.close()
|
||||
this.ssh.connect(connection).catch(error => {
|
||||
this.ssh.openTab(connection).catch(error => {
|
||||
this.toastr.error(`Could not connect: ${error}`)
|
||||
}).then(() => {
|
||||
setTimeout(() => {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ConfigService } from 'terminus-core'
|
||||
import { ConfigService, ElectronService, HostAppService } from 'terminus-core'
|
||||
import { SSHConnection, ISSHConnectionGroup } from '../api'
|
||||
import { EditConnectionModalComponent } from './editConnectionModal.component'
|
||||
import { PromptModalComponent } from './promptModal.component'
|
||||
@@ -15,6 +15,8 @@ export class SSHSettingsTabComponent {
|
||||
|
||||
constructor (
|
||||
public config: ConfigService,
|
||||
private electron: ElectronService,
|
||||
private hostApp: HostAppService,
|
||||
private ngbModal: NgbModal,
|
||||
) {
|
||||
this.connections = this.config.store.ssh.connections
|
||||
@@ -44,13 +46,22 @@ export class SSHSettingsTabComponent {
|
||||
modal.componentInstance.connection = Object.assign({}, connection)
|
||||
modal.result.then(result => {
|
||||
Object.assign(connection, result)
|
||||
this.config.store.ssh.connections = this.connections
|
||||
this.config.save()
|
||||
this.refresh()
|
||||
})
|
||||
}
|
||||
|
||||
deleteConnection (connection: SSHConnection) {
|
||||
if (confirm(`Delete "${connection.name}"?`)) {
|
||||
async deleteConnection (connection: SSHConnection) {
|
||||
if ((await this.electron.showMessageBox(
|
||||
this.hostApp.getWindow(),
|
||||
{
|
||||
type: 'warning',
|
||||
message: `Delete "${connection.name}"?`,
|
||||
buttons: ['Keep', 'Delete'],
|
||||
defaultId: 1,
|
||||
}
|
||||
)).response === 1) {
|
||||
this.connections = this.connections.filter(x => x !== connection)
|
||||
this.config.store.ssh.connections = this.connections
|
||||
this.config.save()
|
||||
@@ -67,14 +78,23 @@ export class SSHSettingsTabComponent {
|
||||
for (let connection of this.connections.filter(x => x.group === group.name)) {
|
||||
connection.group = result
|
||||
}
|
||||
this.config.store.ssh.connections = this.connections
|
||||
this.config.save()
|
||||
this.refresh()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
deleteGroup (group: ISSHConnectionGroup) {
|
||||
if (confirm(`Delete "${group}"?`)) {
|
||||
async deleteGroup (group: ISSHConnectionGroup) {
|
||||
if ((await this.electron.showMessageBox(
|
||||
this.hostApp.getWindow(),
|
||||
{
|
||||
type: 'warning',
|
||||
message: `Delete "${group}"?`,
|
||||
buttons: ['Keep', 'Delete'],
|
||||
defaultId: 1,
|
||||
}
|
||||
)).response === 1) {
|
||||
for (let connection of this.connections.filter(x => x.group === group.name)) {
|
||||
connection.group = null
|
||||
}
|
||||
@@ -84,6 +104,7 @@ export class SSHSettingsTabComponent {
|
||||
}
|
||||
|
||||
refresh () {
|
||||
this.connections = this.config.store.ssh.connections
|
||||
this.childGroups = []
|
||||
|
||||
for (let connection of this.connections) {
|
||||
|
13
terminus-ssh/src/components/sshTab.component.scss
Normal file
13
terminus-ssh/src/components/sshTab.component.scss
Normal file
@@ -0,0 +1,13 @@
|
||||
:host {
|
||||
flex: auto;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
&> .content {
|
||||
flex: auto;
|
||||
position: relative;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
margin: 15px;
|
||||
}
|
||||
}
|
63
terminus-ssh/src/components/sshTab.component.ts
Normal file
63
terminus-ssh/src/components/sshTab.component.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { first } from 'rxjs/operators'
|
||||
import { BaseTerminalTabComponent } from 'terminus-terminal'
|
||||
import { SSHService } from '../services/ssh.service'
|
||||
import { SSHConnection, SSHSession } from '../api'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div
|
||||
#content
|
||||
class="content"
|
||||
></div>
|
||||
`,
|
||||
styles: [require('./sshTab.component.scss')],
|
||||
})
|
||||
export class SSHTabComponent extends BaseTerminalTabComponent {
|
||||
connection: SSHConnection
|
||||
ssh: SSHService
|
||||
session: SSHSession
|
||||
|
||||
ngOnInit () {
|
||||
this.logger = this.log.create('terminalTab')
|
||||
this.ssh = this.injector.get(SSHService)
|
||||
this.frontendReady$.pipe(first()).subscribe(() => {
|
||||
this.initializeSession()
|
||||
})
|
||||
|
||||
super.ngOnInit()
|
||||
}
|
||||
|
||||
async initializeSession () {
|
||||
if (!this.connection) {
|
||||
this.logger.error('No SSH connection info supplied')
|
||||
return
|
||||
}
|
||||
|
||||
this.session = new SSHSession(this.connection)
|
||||
this.attachSessionHandlers()
|
||||
this.write(`Connecting to ${this.connection.host}`)
|
||||
let interval = setInterval(() => this.write('.'), 500)
|
||||
try {
|
||||
await this.ssh.connectSession(this.session, message => {
|
||||
this.write('\r\n' + message)
|
||||
})
|
||||
} catch (e) {
|
||||
this.write('\r\n')
|
||||
this.write(e.message)
|
||||
return
|
||||
} finally {
|
||||
clearInterval(interval)
|
||||
this.write('\r\n')
|
||||
}
|
||||
this.session.resize(this.size.columns, this.size.rows)
|
||||
this.session.start()
|
||||
}
|
||||
|
||||
async getRecoveryToken (): Promise<any> {
|
||||
return {
|
||||
type: 'app:ssh-tab',
|
||||
connection: this.connection,
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,17 +3,19 @@ import { CommonModule } from '@angular/common'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ToastrModule } from 'ngx-toastr'
|
||||
import TerminusCoreModule, { ToolbarButtonProvider, ConfigProvider } from 'terminus-core'
|
||||
import TerminusCoreModule, { ToolbarButtonProvider, ConfigProvider, TabRecoveryProvider } from 'terminus-core'
|
||||
import { SettingsTabProvider } from 'terminus-settings'
|
||||
|
||||
import { EditConnectionModalComponent } from './components/editConnectionModal.component'
|
||||
import { SSHModalComponent } from './components/sshModal.component'
|
||||
import { PromptModalComponent } from './components/promptModal.component'
|
||||
import { SSHSettingsTabComponent } from './components/sshSettingsTab.component'
|
||||
import { SSHTabComponent } from './components/sshTab.component'
|
||||
|
||||
import { ButtonProvider } from './buttonProvider'
|
||||
import { SSHConfigProvider } from './config'
|
||||
import { SSHSettingsTabProvider } from './settings'
|
||||
import { RecoveryProvider } from './recoveryProvider'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -27,18 +29,21 @@ import { SSHSettingsTabProvider } from './settings'
|
||||
{ provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
|
||||
{ provide: ConfigProvider, useClass: SSHConfigProvider, multi: true },
|
||||
{ provide: SettingsTabProvider, useClass: SSHSettingsTabProvider, multi: true },
|
||||
{ provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true },
|
||||
],
|
||||
entryComponents: [
|
||||
EditConnectionModalComponent,
|
||||
PromptModalComponent,
|
||||
SSHModalComponent,
|
||||
SSHSettingsTabComponent,
|
||||
SSHTabComponent,
|
||||
],
|
||||
declarations: [
|
||||
EditConnectionModalComponent,
|
||||
PromptModalComponent,
|
||||
SSHModalComponent,
|
||||
SSHSettingsTabComponent,
|
||||
SSHTabComponent,
|
||||
],
|
||||
})
|
||||
export default class SSHModule { }
|
||||
|
17
terminus-ssh/src/recoveryProvider.ts
Normal file
17
terminus-ssh/src/recoveryProvider.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { TabRecoveryProvider, RecoveredTab } from 'terminus-core'
|
||||
|
||||
import { SSHTabComponent } from './components/sshTab.component'
|
||||
|
||||
@Injectable()
|
||||
export class RecoveryProvider extends TabRecoveryProvider {
|
||||
async recover (recoveryToken: any): Promise<RecoveredTab> {
|
||||
if (recoveryToken && recoveryToken.type === 'app:ssh-tab') {
|
||||
return {
|
||||
type: SSHTabComponent,
|
||||
options: { connection: recoveryToken.connection },
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
@@ -5,9 +5,9 @@ import * as fs from 'mz/fs'
|
||||
import * as path from 'path'
|
||||
import { ToastrService } from 'ngx-toastr'
|
||||
import { AppService, HostAppService, Platform, Logger, LogService } from 'terminus-core'
|
||||
import { TerminalTabComponent } from 'terminus-terminal'
|
||||
import { SSHConnection, SSHSession } from '../api'
|
||||
import { PromptModalComponent } from '../components/promptModal.component'
|
||||
import { SSHTabComponent } from '../components/sshTab.component'
|
||||
import { PasswordStorageService } from './passwordStorage.service'
|
||||
const { SSH2Stream } = require('ssh2-streams')
|
||||
|
||||
@@ -33,14 +33,31 @@ export class SSHService {
|
||||
this.logger = log.create('ssh')
|
||||
}
|
||||
|
||||
async connect (connection: SSHConnection): Promise<TerminalTabComponent> {
|
||||
async openTab (connection: SSHConnection): Promise<SSHTabComponent> {
|
||||
return this.zone.run(() => this.app.openNewTab(
|
||||
SSHTabComponent,
|
||||
{ connection }
|
||||
) as SSHTabComponent)
|
||||
}
|
||||
|
||||
async connectSession (session: SSHSession, logCallback?: (s: string) => void): Promise<void> {
|
||||
let privateKey: string = null
|
||||
let privateKeyPassphrase: string = null
|
||||
let privateKeyPath = connection.privateKey
|
||||
let privateKeyPath = session.connection.privateKey
|
||||
|
||||
if (!logCallback) {
|
||||
logCallback = (s) => null
|
||||
}
|
||||
|
||||
const log = s => {
|
||||
logCallback(s)
|
||||
this.logger.info(s)
|
||||
}
|
||||
|
||||
if (!privateKeyPath) {
|
||||
let userKeyPath = path.join(process.env.HOME, '.ssh', 'id_rsa')
|
||||
if (await fs.exists(userKeyPath)) {
|
||||
this.logger.info('Using user\'s default private key:', userKeyPath)
|
||||
log(`Using user's default private key: ${userKeyPath}`)
|
||||
privateKeyPath = userKeyPath
|
||||
}
|
||||
}
|
||||
@@ -49,11 +66,12 @@ export class SSHService {
|
||||
try {
|
||||
privateKey = (await fs.readFile(privateKeyPath)).toString()
|
||||
} catch (error) {
|
||||
log('Could not read the private key file')
|
||||
this.toastr.warning('Could not read the private key file')
|
||||
}
|
||||
|
||||
if (privateKey) {
|
||||
this.logger.info('Loaded private key from', privateKeyPath)
|
||||
log(`Loading private key from ${privateKeyPath}`)
|
||||
|
||||
let encrypted = privateKey.includes('ENCRYPTED')
|
||||
if (privateKeyPath.toLowerCase().endsWith('.ppk')) {
|
||||
@@ -61,6 +79,7 @@ export class SSHService {
|
||||
}
|
||||
if (encrypted) {
|
||||
let modal = this.ngbModal.open(PromptModalComponent)
|
||||
log('Key requires passphrase')
|
||||
modal.componentInstance.prompt = 'Private key passphrase'
|
||||
modal.componentInstance.password = true
|
||||
try {
|
||||
@@ -77,12 +96,12 @@ export class SSHService {
|
||||
ssh.on('ready', () => {
|
||||
connected = true
|
||||
if (savedPassword) {
|
||||
this.passwordStorage.savePassword(connection, savedPassword)
|
||||
this.passwordStorage.savePassword(session.connection, savedPassword)
|
||||
}
|
||||
this.zone.run(resolve)
|
||||
})
|
||||
ssh.on('error', error => {
|
||||
this.passwordStorage.deletePassword(connection)
|
||||
this.passwordStorage.deletePassword(session.connection)
|
||||
this.zone.run(() => {
|
||||
if (connected) {
|
||||
this.toastr.error(error.toString())
|
||||
@@ -92,7 +111,8 @@ export class SSHService {
|
||||
})
|
||||
})
|
||||
ssh.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => this.zone.run(async () => {
|
||||
console.log(name, instructions, instructionsLang)
|
||||
log(`Keyboard-interactive auth requested: ${name}`)
|
||||
this.logger.info('Keyboard-interactive auth:', name, instructions, instructionsLang)
|
||||
let results = []
|
||||
for (let prompt of prompts) {
|
||||
let modal = this.ngbModal.open(PromptModalComponent)
|
||||
@@ -103,6 +123,14 @@ export class SSHService {
|
||||
finish(results)
|
||||
}))
|
||||
|
||||
ssh.on('greeting', greeting => {
|
||||
log('Greeting: ' + greeting)
|
||||
})
|
||||
|
||||
ssh.on('banner', banner => {
|
||||
log('Banner: ' + banner)
|
||||
})
|
||||
|
||||
let agent: string = null
|
||||
if (this.hostApp.platform === Platform.Windows) {
|
||||
let pageantRunning = new Promise<boolean>(resolve => {
|
||||
@@ -117,48 +145,62 @@ export class SSHService {
|
||||
agent = process.env.SSH_AUTH_SOCK
|
||||
}
|
||||
|
||||
ssh.connect({
|
||||
host: connection.host,
|
||||
port: connection.port || 22,
|
||||
username: connection.user,
|
||||
password: connection.privateKey ? undefined : '',
|
||||
privateKey,
|
||||
passphrase: privateKeyPassphrase,
|
||||
tryKeyboard: true,
|
||||
agent,
|
||||
agentForward: !!agent,
|
||||
keepaliveInterval: connection.keepaliveInterval,
|
||||
keepaliveCountMax: connection.keepaliveCountMax,
|
||||
readyTimeout: connection.readyTimeout,
|
||||
})
|
||||
try {
|
||||
ssh.connect({
|
||||
host: session.connection.host,
|
||||
port: session.connection.port || 22,
|
||||
username: session.connection.user,
|
||||
password: session.connection.privateKey ? undefined : '',
|
||||
privateKey,
|
||||
passphrase: privateKeyPassphrase,
|
||||
tryKeyboard: true,
|
||||
agent,
|
||||
agentForward: !!agent,
|
||||
keepaliveInterval: session.connection.keepaliveInterval,
|
||||
keepaliveCountMax: session.connection.keepaliveCountMax,
|
||||
readyTimeout: session.connection.readyTimeout,
|
||||
hostVerifier: digest => {
|
||||
log('SHA256 fingerprint: ' + digest)
|
||||
return true
|
||||
},
|
||||
hostHash: 'sha256' as any,
|
||||
})
|
||||
} catch (e) {
|
||||
this.toastr.error(e.message)
|
||||
reject(e)
|
||||
}
|
||||
|
||||
let keychainPasswordUsed = false
|
||||
|
||||
;(ssh as any).config.password = () => this.zone.run(async () => {
|
||||
if (connection.password) {
|
||||
this.logger.info('Using preset password')
|
||||
return connection.password
|
||||
if (session.connection.password) {
|
||||
log('Using preset password')
|
||||
return session.connection.password
|
||||
}
|
||||
|
||||
if (!keychainPasswordUsed) {
|
||||
let password = await this.passwordStorage.loadPassword(connection)
|
||||
let password = await this.passwordStorage.loadPassword(session.connection)
|
||||
if (password) {
|
||||
this.logger.info('Using saved password')
|
||||
log('Trying saved password')
|
||||
keychainPasswordUsed = true
|
||||
return password
|
||||
}
|
||||
}
|
||||
|
||||
let modal = this.ngbModal.open(PromptModalComponent)
|
||||
modal.componentInstance.prompt = `Password for ${connection.user}@${connection.host}`
|
||||
modal.componentInstance.prompt = `Password for ${session.connection.user}@${session.connection.host}`
|
||||
modal.componentInstance.password = true
|
||||
savedPassword = await modal.result
|
||||
try {
|
||||
savedPassword = await modal.result
|
||||
} catch (_) {
|
||||
return ''
|
||||
}
|
||||
return savedPassword
|
||||
})
|
||||
})
|
||||
|
||||
try {
|
||||
let shell = await new Promise((resolve, reject) => {
|
||||
let shell: any = await new Promise<any>((resolve, reject) => {
|
||||
ssh.shell({ term: 'xterm-256color' }, (err, shell) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
@@ -168,14 +210,17 @@ export class SSHService {
|
||||
})
|
||||
})
|
||||
|
||||
let session = new SSHSession(shell, connection)
|
||||
session.shell = shell
|
||||
|
||||
return this.zone.run(() => this.app.openNewTab(
|
||||
TerminalTabComponent,
|
||||
{ session, sessionOptions: {} }
|
||||
) as TerminalTabComponent)
|
||||
shell.on('greeting', greeting => {
|
||||
log('Shell Greeting: ' + greeting)
|
||||
})
|
||||
|
||||
shell.on('banner', banner => {
|
||||
log('Shell Banner: ' + banner)
|
||||
})
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
this.toastr.error(error.message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import { SSHSettingsTabComponent } from './components/sshSettingsTab.component'
|
||||
@Injectable()
|
||||
export class SSHSettingsTabProvider extends SettingsTabProvider {
|
||||
id = 'ssh'
|
||||
icon = 'globe'
|
||||
title = 'SSH'
|
||||
|
||||
getComponentType (): any {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "terminus-terminal",
|
||||
"version": "1.0.0-alpha.55",
|
||||
"version": "1.0.68-c17-g8b64a81",
|
||||
"description": "Terminus' terminal emulation core",
|
||||
"keywords": [
|
||||
"terminus-builtin-plugin"
|
||||
@@ -17,7 +17,6 @@
|
||||
"author": "Eugene Pankov",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@terminus-term/xterm": "3.8.4",
|
||||
"@types/deep-equal": "^1.0.0",
|
||||
"@types/mz": "0.0.31",
|
||||
"@types/node": "7.0.12",
|
||||
@@ -27,6 +26,7 @@
|
||||
"file-loader": "^0.11.2",
|
||||
"rage-edit": "1.2.0",
|
||||
"uuid": "^3.3.2",
|
||||
"xterm": "3.10.1",
|
||||
"xterm-addon-ligatures-tmp": "^0.1.0-beta-1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -40,10 +40,10 @@
|
||||
"terminus-settings": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@terminus-term/node-pty": "0.8.0-1",
|
||||
"font-manager": "0.3.0",
|
||||
"hterm-umdjs": "1.4.1",
|
||||
"mz": "^2.6.0",
|
||||
"node-pty": "^0.8.0",
|
||||
"ps-node": "^0.1.6",
|
||||
"runes": "^0.4.2"
|
||||
},
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { TerminalTabComponent } from './components/terminalTab.component'
|
||||
import { BaseTerminalTabComponent } from './components/baseTerminalTab.component'
|
||||
|
||||
export abstract class TerminalDecorator {
|
||||
// tslint:disable-next-line no-empty
|
||||
attach (_terminal: TerminalTabComponent): void { }
|
||||
attach (_terminal: BaseTerminalTabComponent): void { }
|
||||
// tslint:disable-next-line no-empty
|
||||
detach (_terminal: TerminalTabComponent): void { }
|
||||
detach (_terminal: BaseTerminalTabComponent): void { }
|
||||
}
|
||||
|
||||
export interface ResizeEvent {
|
||||
@@ -44,7 +44,7 @@ export abstract class TerminalColorSchemeProvider {
|
||||
export abstract class TerminalContextMenuItemProvider {
|
||||
weight: number
|
||||
|
||||
abstract async getItems (tab: TerminalTabComponent): Promise<Electron.MenuItemConstructorOptions[]>
|
||||
abstract async getItems (tab: BaseTerminalTabComponent): Promise<Electron.MenuItemConstructorOptions[]>
|
||||
}
|
||||
|
||||
export interface IShell {
|
||||
|
@@ -5,7 +5,7 @@ import deepEqual = require('deep-equal')
|
||||
const fontManager = require('font-manager')
|
||||
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { ConfigService, HostAppService, Platform } from 'terminus-core'
|
||||
import { ConfigService, HostAppService, Platform, ElectronService } from 'terminus-core'
|
||||
import { TerminalColorSchemeProvider, ITerminalColorScheme } from '../api'
|
||||
|
||||
@Component({
|
||||
@@ -22,6 +22,7 @@ export class AppearanceSettingsTabComponent {
|
||||
constructor (
|
||||
@Inject(TerminalColorSchemeProvider) private colorSchemeProviders: TerminalColorSchemeProvider[],
|
||||
private hostApp: HostAppService,
|
||||
private electron: ElectronService,
|
||||
public config: ConfigService,
|
||||
) { }
|
||||
|
||||
@@ -71,8 +72,16 @@ export class AppearanceSettingsTabComponent {
|
||||
this.editingColorScheme = null
|
||||
}
|
||||
|
||||
deleteScheme (scheme: ITerminalColorScheme) {
|
||||
if (confirm(`Delete "${scheme.name}"?`)) {
|
||||
async deleteScheme (scheme: ITerminalColorScheme) {
|
||||
if ((await this.electron.showMessageBox(
|
||||
this.hostApp.getWindow(),
|
||||
{
|
||||
type: 'warning',
|
||||
message: `Delete "${scheme.name}"?`,
|
||||
buttons: ['Keep', 'Delete'],
|
||||
defaultId: 1,
|
||||
}
|
||||
)).response === 1) {
|
||||
let schemes = this.config.store.terminal.customColorSchemes
|
||||
schemes = schemes.filter(x => x !== scheme)
|
||||
this.config.store.terminal.customColorSchemes = schemes
|
||||
|
357
terminus-terminal/src/components/baseTerminalTab.component.ts
Normal file
357
terminus-terminal/src/components/baseTerminalTab.component.ts
Normal file
@@ -0,0 +1,357 @@
|
||||
import { Observable, Subject, Subscription } from 'rxjs'
|
||||
import { first } from 'rxjs/operators'
|
||||
import { ToastrService } from 'ngx-toastr'
|
||||
import { NgZone, OnInit, OnDestroy, Inject, Injector, Optional, ViewChild, HostBinding, Input, ElementRef } from '@angular/core'
|
||||
import { AppService, ConfigService, BaseTabComponent, ElectronService, HostAppService, HotkeysService, Platform, LogService, Logger } from 'terminus-core'
|
||||
|
||||
import { BaseSession, SessionsService } from '../services/sessions.service'
|
||||
import { TerminalFrontendService } from '../services/terminalFrontend.service'
|
||||
|
||||
import { TerminalDecorator, ResizeEvent, TerminalContextMenuItemProvider } from '../api'
|
||||
import { Frontend } from '../frontends/frontend'
|
||||
|
||||
export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit, OnDestroy {
|
||||
static template = `
|
||||
<div
|
||||
#content
|
||||
class="content"
|
||||
[style.opacity]="htermVisible ? 1 : 0"
|
||||
></div>
|
||||
`
|
||||
static styles = [require('./terminalTab.component.scss')]
|
||||
|
||||
session: BaseSession
|
||||
@Input() zoom = 0
|
||||
@ViewChild('content') content
|
||||
@HostBinding('style.background-color') backgroundColor: string
|
||||
frontend: Frontend
|
||||
sessionCloseSubscription: Subscription
|
||||
hotkeysSubscription: Subscription
|
||||
htermVisible = false
|
||||
frontendReady = new Subject<void>()
|
||||
size: ResizeEvent
|
||||
protected logger: Logger
|
||||
protected output = new Subject<string>()
|
||||
private bellPlayer: HTMLAudioElement
|
||||
private termContainerSubscriptions: Subscription[] = []
|
||||
|
||||
get input$ (): Observable<string> { return this.frontend.input$ }
|
||||
get output$ (): Observable<string> { return this.output }
|
||||
get resize$ (): Observable<ResizeEvent> { return this.frontend.resize$ }
|
||||
get alternateScreenActive$ (): Observable<boolean> { return this.frontend.alternateScreenActive$ }
|
||||
get frontendReady$ (): Observable<void> { return this.frontendReady }
|
||||
|
||||
constructor (
|
||||
public config: ConfigService,
|
||||
public element: ElementRef,
|
||||
protected injector: Injector,
|
||||
protected zone: NgZone,
|
||||
protected app: AppService,
|
||||
protected hostApp: HostAppService,
|
||||
protected hotkeys: HotkeysService,
|
||||
protected sessions: SessionsService,
|
||||
protected electron: ElectronService,
|
||||
protected terminalContainersService: TerminalFrontendService,
|
||||
protected toastr: ToastrService,
|
||||
protected log: LogService,
|
||||
@Optional() @Inject(TerminalDecorator) protected decorators: TerminalDecorator[],
|
||||
@Optional() @Inject(TerminalContextMenuItemProvider) protected contextMenuProviders: TerminalContextMenuItemProvider[],
|
||||
) {
|
||||
super()
|
||||
this.logger = log.create('baseTerminalTab')
|
||||
this.decorators = this.decorators || []
|
||||
this.setTitle('Terminal')
|
||||
|
||||
this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
|
||||
if (!this.hasFocus) {
|
||||
return
|
||||
}
|
||||
switch (hotkey) {
|
||||
case 'ctrl-c':
|
||||
if (this.frontend.getSelection()) {
|
||||
this.frontend.copySelection()
|
||||
this.frontend.clearSelection()
|
||||
this.toastr.info('Copied')
|
||||
} else {
|
||||
this.sendInput('\x03')
|
||||
}
|
||||
break
|
||||
case 'copy':
|
||||
this.frontend.copySelection()
|
||||
this.toastr.info('Copied')
|
||||
break
|
||||
case 'paste':
|
||||
this.paste()
|
||||
break
|
||||
case 'clear':
|
||||
this.frontend.clear()
|
||||
break
|
||||
case 'zoom-in':
|
||||
this.zoomIn()
|
||||
break
|
||||
case 'zoom-out':
|
||||
this.zoomOut()
|
||||
break
|
||||
case 'reset-zoom':
|
||||
this.resetZoom()
|
||||
break
|
||||
case 'home':
|
||||
this.sendInput('\x1bOH')
|
||||
break
|
||||
case 'end':
|
||||
this.sendInput('\x1bOF')
|
||||
break
|
||||
case 'previous-word':
|
||||
this.sendInput('\x1bb')
|
||||
break
|
||||
case 'next-word':
|
||||
this.sendInput('\x1bf')
|
||||
break
|
||||
case 'delete-previous-word':
|
||||
this.sendInput('\x1b\x7f')
|
||||
break
|
||||
case 'delete-next-word':
|
||||
this.sendInput('\x1bd')
|
||||
break
|
||||
}
|
||||
})
|
||||
this.bellPlayer = document.createElement('audio')
|
||||
this.bellPlayer.src = require<string>('../bell.ogg')
|
||||
|
||||
this.contextMenuProviders.sort((a, b) => a.weight - b.weight)
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.focused$.subscribe(() => {
|
||||
this.configure()
|
||||
this.frontend.focus()
|
||||
})
|
||||
|
||||
this.frontend = this.terminalContainersService.getFrontend(this.session)
|
||||
|
||||
this.frontend.ready$.subscribe(() => {
|
||||
this.htermVisible = true
|
||||
})
|
||||
|
||||
this.frontend.resize$.pipe(first()).subscribe(async ({ columns, rows }) => {
|
||||
this.size = { columns, rows }
|
||||
this.frontendReady.next()
|
||||
|
||||
setTimeout(() => {
|
||||
this.session.resize(columns, rows)
|
||||
}, 1000)
|
||||
|
||||
this.session.releaseInitialDataBuffer()
|
||||
})
|
||||
|
||||
this.frontend.configure()
|
||||
this.frontend.attach(this.content.nativeElement)
|
||||
this.attachTermContainerHandlers()
|
||||
|
||||
this.configure()
|
||||
|
||||
this.config.enabledServices(this.decorators).forEach((decorator) => {
|
||||
decorator.attach(this)
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
this.output.subscribe(() => {
|
||||
this.displayActivity()
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
this.frontend.bell$.subscribe(() => {
|
||||
if (this.config.store.terminal.bell === 'visual') {
|
||||
this.frontend.visualBell()
|
||||
}
|
||||
if (this.config.store.terminal.bell === 'audible') {
|
||||
this.bellPlayer.play()
|
||||
}
|
||||
})
|
||||
|
||||
this.frontend.focus()
|
||||
}
|
||||
|
||||
async buildContextMenu (): Promise<Electron.MenuItemConstructorOptions[]> {
|
||||
let items: Electron.MenuItemConstructorOptions[] = []
|
||||
for (let section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(this)))) {
|
||||
items = items.concat(section)
|
||||
items.push({ type: 'separator' })
|
||||
}
|
||||
items.splice(items.length - 1, 1)
|
||||
return items
|
||||
}
|
||||
|
||||
detachTermContainerHandlers () {
|
||||
for (let subscription of this.termContainerSubscriptions) {
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
this.termContainerSubscriptions = []
|
||||
}
|
||||
|
||||
attachTermContainerHandlers () {
|
||||
this.detachTermContainerHandlers()
|
||||
this.termContainerSubscriptions = [
|
||||
this.frontend.title$.subscribe(title => this.zone.run(() => this.setTitle(title))),
|
||||
|
||||
this.focused$.subscribe(() => this.frontend.enableResizing = true),
|
||||
this.blurred$.subscribe(() => this.frontend.enableResizing = false),
|
||||
|
||||
this.frontend.mouseEvent$.subscribe(async event => {
|
||||
if (event.type === 'mousedown') {
|
||||
if (event.which === 2) {
|
||||
this.paste()
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
if (event.which === 3) {
|
||||
if (this.config.store.terminal.rightClick === 'menu') {
|
||||
this.hostApp.popupContextMenu(await this.buildContextMenu())
|
||||
} else if (this.config.store.terminal.rightClick === 'paste') {
|
||||
this.paste()
|
||||
}
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
}
|
||||
if (event.type === 'mousewheel') {
|
||||
let wheelDeltaY = 0
|
||||
|
||||
if ('wheelDeltaY' in event) {
|
||||
wheelDeltaY = (event as MouseWheelEvent)['wheelDeltaY']
|
||||
} else {
|
||||
wheelDeltaY = (event as MouseWheelEvent)['deltaY']
|
||||
}
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
|
||||
if (wheelDeltaY > 0) {
|
||||
this.zoomIn()
|
||||
} else {
|
||||
this.zoomOut()
|
||||
}
|
||||
} else if (event.altKey) {
|
||||
event.preventDefault()
|
||||
let delta = Math.round(wheelDeltaY / 50)
|
||||
this.sendInput(((delta > 0) ? '\u001bOA' : '\u001bOB').repeat(Math.abs(delta)))
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
this.frontend.input$.subscribe(data => {
|
||||
this.sendInput(data)
|
||||
}),
|
||||
|
||||
this.frontend.resize$.subscribe(({ columns, rows }) => {
|
||||
this.logger.info(`Resizing to ${columns}x${rows}`)
|
||||
this.size = { columns, rows }
|
||||
this.zone.run(() => {
|
||||
if (this.session && this.session.open) {
|
||||
this.session.resize(columns, rows)
|
||||
}
|
||||
})
|
||||
}),
|
||||
|
||||
this.hostApp.windowMoved$.subscribe(() => setTimeout(() => {
|
||||
this.configure()
|
||||
}, 250)),
|
||||
]
|
||||
}
|
||||
|
||||
sendInput (data: string) {
|
||||
this.session.write(data)
|
||||
if (this.config.store.terminal.scrollOnInput) {
|
||||
this.frontend.scrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
write (data: string) {
|
||||
let percentageMatch = /(^|[^\d])(\d+(\.\d+)?)%([^\d]|$)/.exec(data)
|
||||
if (percentageMatch) {
|
||||
let percentage = percentageMatch[3] ? parseFloat(percentageMatch[2]) : parseInt(percentageMatch[2])
|
||||
if (percentage > 0 && percentage <= 100) {
|
||||
this.setProgress(percentage)
|
||||
this.logger.debug('Detected progress:', percentage)
|
||||
}
|
||||
} else {
|
||||
this.setProgress(null)
|
||||
}
|
||||
this.frontend.write(data)
|
||||
}
|
||||
|
||||
paste () {
|
||||
let data = this.electron.clipboard.readText()
|
||||
if (this.config.store.terminal.bracketedPaste) {
|
||||
data = '\x1b[200~' + data + '\x1b[201~'
|
||||
}
|
||||
if (this.hostApp.platform === Platform.Windows) {
|
||||
data = data.replace(/\r\n/g, '\r')
|
||||
} else {
|
||||
data = data.replace(/\n/g, '\r')
|
||||
}
|
||||
this.sendInput(data)
|
||||
}
|
||||
|
||||
configure (): void {
|
||||
this.frontend.configure()
|
||||
|
||||
if (this.config.store.terminal.background === 'colorScheme') {
|
||||
if (this.config.store.terminal.colorScheme.background) {
|
||||
this.backgroundColor = this.config.store.terminal.colorScheme.background
|
||||
}
|
||||
} else {
|
||||
this.backgroundColor = null
|
||||
}
|
||||
}
|
||||
|
||||
zoomIn () {
|
||||
this.zoom++
|
||||
this.frontend.setZoom(this.zoom)
|
||||
}
|
||||
|
||||
zoomOut () {
|
||||
this.zoom--
|
||||
this.frontend.setZoom(this.zoom)
|
||||
}
|
||||
|
||||
resetZoom () {
|
||||
this.zoom = 0
|
||||
this.frontend.setZoom(this.zoom)
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.frontend.detach(this.content.nativeElement)
|
||||
this.detachTermContainerHandlers()
|
||||
this.config.enabledServices(this.decorators).forEach(decorator => {
|
||||
decorator.detach(this)
|
||||
})
|
||||
this.hotkeysSubscription.unsubscribe()
|
||||
if (this.sessionCloseSubscription) {
|
||||
this.sessionCloseSubscription.unsubscribe()
|
||||
}
|
||||
this.output.complete()
|
||||
}
|
||||
|
||||
async destroy () {
|
||||
super.destroy()
|
||||
if (this.session && this.session.open) {
|
||||
await this.session.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
protected attachSessionHandlers () {
|
||||
// this.session.output$.bufferTime(10).subscribe((datas) => {
|
||||
this.session.output$.subscribe(data => {
|
||||
this.zone.run(() => {
|
||||
this.output.next(data)
|
||||
this.write(data)
|
||||
})
|
||||
})
|
||||
|
||||
this.sessionCloseSubscription = this.session.closed$.subscribe(() => {
|
||||
this.frontend.destroy()
|
||||
this.app.closeTab(this)
|
||||
})
|
||||
}
|
||||
}
|
@@ -48,10 +48,13 @@ export class ShellSettingsTabComponent {
|
||||
pickWorkingDirectory () {
|
||||
let shell = this.shells.find(x => x.id === this.config.store.terminal.shell)
|
||||
console.log(shell)
|
||||
let paths = this.electron.dialog.showOpenDialog({
|
||||
defaultPath: shell.fsBase,
|
||||
properties: ['openDirectory', 'showHiddenFiles'],
|
||||
})
|
||||
let paths = this.electron.dialog.showOpenDialog(
|
||||
this.hostApp.getWindow(),
|
||||
{
|
||||
defaultPath: shell.fsBase,
|
||||
properties: ['openDirectory', 'showHiddenFiles'],
|
||||
}
|
||||
)
|
||||
if (paths) {
|
||||
this.config.store.terminal.workingDirectory = paths[0]
|
||||
}
|
||||
|
@@ -1,121 +1,27 @@
|
||||
import { Observable, Subject, Subscription } from 'rxjs'
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { first } from 'rxjs/operators'
|
||||
import { ToastrService } from 'ngx-toastr'
|
||||
import { Component, NgZone, Inject, Optional, ViewChild, HostBinding, Input } from '@angular/core'
|
||||
import { AppService, ConfigService, BaseTabComponent, BaseTabProcess, ElectronService, HostAppService, HotkeysService, Platform } from 'terminus-core'
|
||||
|
||||
import { Session, SessionsService } from '../services/sessions.service'
|
||||
import { TerminalFrontendService } from '../services/terminalFrontend.service'
|
||||
|
||||
import { TerminalDecorator, ResizeEvent, SessionOptions, TerminalContextMenuItemProvider } from '../api'
|
||||
import { Frontend } from '../frontends/frontend'
|
||||
import { BaseTabProcess } from 'terminus-core'
|
||||
import { BaseTerminalTabComponent } from './baseTerminalTab.component'
|
||||
import { SessionOptions } from '../api'
|
||||
import { Session } from '../services/sessions.service'
|
||||
|
||||
@Component({
|
||||
selector: 'terminalTab',
|
||||
template: `
|
||||
<div
|
||||
#content
|
||||
class="content"
|
||||
[style.opacity]="htermVisible ? 1 : 0"
|
||||
></div>
|
||||
`,
|
||||
styles: [require('./terminalTab.component.scss')],
|
||||
template: BaseTerminalTabComponent.template,
|
||||
styles: BaseTerminalTabComponent.styles,
|
||||
})
|
||||
export class TerminalTabComponent extends BaseTabComponent {
|
||||
session: Session
|
||||
export class TerminalTabComponent extends BaseTerminalTabComponent {
|
||||
@Input() sessionOptions: SessionOptions
|
||||
@Input() zoom = 0
|
||||
@ViewChild('content') content
|
||||
@HostBinding('style.background-color') backgroundColor: string
|
||||
frontend: Frontend
|
||||
sessionCloseSubscription: Subscription
|
||||
hotkeysSubscription: Subscription
|
||||
htermVisible = false
|
||||
private output = new Subject<string>()
|
||||
private bellPlayer: HTMLAudioElement
|
||||
private termContainerSubscriptions: Subscription[] = []
|
||||
|
||||
get input$ (): Observable<string> { return this.frontend.input$ }
|
||||
get output$ (): Observable<string> { return this.output }
|
||||
get resize$ (): Observable<ResizeEvent> { return this.frontend.resize$ }
|
||||
get alternateScreenActive$ (): Observable<boolean> { return this.frontend.alternateScreenActive$ }
|
||||
|
||||
constructor (
|
||||
private zone: NgZone,
|
||||
private app: AppService,
|
||||
private hostApp: HostAppService,
|
||||
private hotkeys: HotkeysService,
|
||||
private sessions: SessionsService,
|
||||
private electron: ElectronService,
|
||||
private terminalContainersService: TerminalFrontendService,
|
||||
public config: ConfigService,
|
||||
private toastr: ToastrService,
|
||||
@Optional() @Inject(TerminalDecorator) private decorators: TerminalDecorator[],
|
||||
@Optional() @Inject(TerminalContextMenuItemProvider) private contextMenuProviders: TerminalContextMenuItemProvider[],
|
||||
) {
|
||||
super()
|
||||
this.decorators = this.decorators || []
|
||||
this.setTitle('Terminal')
|
||||
|
||||
ngOnInit () {
|
||||
this.logger = this.log.create('terminalTab')
|
||||
this.session = new Session(this.config)
|
||||
|
||||
this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
|
||||
if (!this.hasFocus) {
|
||||
return
|
||||
}
|
||||
switch (hotkey) {
|
||||
case 'ctrl-c':
|
||||
if (this.frontend.getSelection()) {
|
||||
this.frontend.copySelection()
|
||||
this.frontend.clearSelection()
|
||||
this.toastr.info('Copied')
|
||||
} else {
|
||||
this.sendInput('\x03')
|
||||
}
|
||||
break
|
||||
case 'copy':
|
||||
this.frontend.copySelection()
|
||||
this.toastr.info('Copied')
|
||||
break
|
||||
case 'paste':
|
||||
this.paste()
|
||||
break
|
||||
case 'clear':
|
||||
this.frontend.clear()
|
||||
break
|
||||
case 'zoom-in':
|
||||
this.zoomIn()
|
||||
break
|
||||
case 'zoom-out':
|
||||
this.zoomOut()
|
||||
break
|
||||
case 'reset-zoom':
|
||||
this.resetZoom()
|
||||
break
|
||||
case 'home':
|
||||
this.sendInput('\x1bOH')
|
||||
break
|
||||
case 'end':
|
||||
this.sendInput('\x1bOF')
|
||||
break
|
||||
case 'previous-word':
|
||||
this.sendInput('\x1bb')
|
||||
break
|
||||
case 'next-word':
|
||||
this.sendInput('\x1bf')
|
||||
break
|
||||
case 'delete-previous-word':
|
||||
this.sendInput('\x1b\x7f')
|
||||
break
|
||||
case 'delete-next-word':
|
||||
this.sendInput('\x1bd')
|
||||
break
|
||||
}
|
||||
this.frontendReady$.pipe(first()).subscribe(() => {
|
||||
this.initializeSession(this.size.columns, this.size.rows)
|
||||
})
|
||||
this.bellPlayer = document.createElement('audio')
|
||||
this.bellPlayer.src = require<string>('../bell.ogg')
|
||||
|
||||
this.contextMenuProviders.sort((a, b) => a.weight - b.weight)
|
||||
super.ngOnInit()
|
||||
}
|
||||
|
||||
initializeSession (columns: number, rows: number) {
|
||||
@@ -127,18 +33,7 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
})
|
||||
)
|
||||
|
||||
// this.session.output$.bufferTime(10).subscribe((datas) => {
|
||||
this.session.output$.subscribe(data => {
|
||||
this.zone.run(() => {
|
||||
this.output.next(data)
|
||||
this.write(data)
|
||||
})
|
||||
})
|
||||
|
||||
this.sessionCloseSubscription = this.session.closed$.subscribe(() => {
|
||||
this.frontend.destroy()
|
||||
this.app.closeTab(this)
|
||||
})
|
||||
this.attachSessionHandlers()
|
||||
}
|
||||
|
||||
async getRecoveryToken (): Promise<any> {
|
||||
@@ -152,199 +47,6 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.focused$.subscribe(() => {
|
||||
this.configure()
|
||||
this.frontend.focus()
|
||||
})
|
||||
|
||||
this.frontend = this.terminalContainersService.getFrontend(this.session)
|
||||
|
||||
this.frontend.ready$.subscribe(() => {
|
||||
this.htermVisible = true
|
||||
})
|
||||
|
||||
this.frontend.resize$.pipe(first()).subscribe(async ({ columns, rows }) => {
|
||||
if (!this.session.open) {
|
||||
this.initializeSession(columns, rows)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.session.resize(columns, rows)
|
||||
}, 1000)
|
||||
|
||||
this.session.releaseInitialDataBuffer()
|
||||
})
|
||||
|
||||
this.frontend.configure(this.config.store)
|
||||
this.frontend.attach(this.content.nativeElement)
|
||||
this.attachTermContainerHandlers()
|
||||
|
||||
this.configure()
|
||||
|
||||
this.config.enabledServices(this.decorators).forEach((decorator) => {
|
||||
decorator.attach(this)
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
this.output.subscribe(() => {
|
||||
this.displayActivity()
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
this.frontend.bell$.subscribe(() => {
|
||||
if (this.config.store.terminal.bell === 'visual') {
|
||||
this.frontend.visualBell()
|
||||
}
|
||||
if (this.config.store.terminal.bell === 'audible') {
|
||||
this.bellPlayer.play()
|
||||
}
|
||||
})
|
||||
|
||||
this.frontend.focus()
|
||||
}
|
||||
|
||||
async buildContextMenu (): Promise<Electron.MenuItemConstructorOptions[]> {
|
||||
let items: Electron.MenuItemConstructorOptions[] = []
|
||||
for (let section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(this)))) {
|
||||
items = items.concat(section)
|
||||
items.push({ type: 'separator' })
|
||||
}
|
||||
items.splice(items.length - 1, 1)
|
||||
return items
|
||||
}
|
||||
|
||||
detachTermContainerHandlers () {
|
||||
for (let subscription of this.termContainerSubscriptions) {
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
this.termContainerSubscriptions = []
|
||||
}
|
||||
|
||||
attachTermContainerHandlers () {
|
||||
this.detachTermContainerHandlers()
|
||||
this.termContainerSubscriptions = [
|
||||
this.frontend.title$.subscribe(title => this.zone.run(() => this.setTitle(title))),
|
||||
|
||||
this.focused$.subscribe(() => this.frontend.enableResizing = true),
|
||||
this.blurred$.subscribe(() => this.frontend.enableResizing = false),
|
||||
|
||||
this.frontend.mouseEvent$.subscribe(async event => {
|
||||
if (event.type === 'mousedown') {
|
||||
if (event.which === 3) {
|
||||
if (this.config.store.terminal.rightClick === 'menu') {
|
||||
this.hostApp.popupContextMenu(await this.buildContextMenu())
|
||||
} else if (this.config.store.terminal.rightClick === 'paste') {
|
||||
this.paste()
|
||||
}
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
}
|
||||
if (event.type === 'mousewheel') {
|
||||
let wheelDeltaY = 0
|
||||
|
||||
if ('wheelDeltaY' in event) {
|
||||
wheelDeltaY = (event as MouseWheelEvent)['wheelDeltaY']
|
||||
} else {
|
||||
wheelDeltaY = (event as MouseWheelEvent)['deltaY']
|
||||
}
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
|
||||
if (wheelDeltaY > 0) {
|
||||
this.zoomIn()
|
||||
} else {
|
||||
this.zoomOut()
|
||||
}
|
||||
} else if (event.altKey) {
|
||||
event.preventDefault()
|
||||
let delta = Math.round(wheelDeltaY / 50)
|
||||
this.sendInput(((delta > 0) ? '\u001bOA' : '\u001bOB').repeat(Math.abs(delta)))
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
this.frontend.input$.subscribe(data => {
|
||||
this.sendInput(data)
|
||||
}),
|
||||
|
||||
this.frontend.resize$.subscribe(({ columns, rows }) => {
|
||||
console.log(`Resizing to ${columns}x${rows}`)
|
||||
this.zone.run(() => {
|
||||
if (this.session.open) {
|
||||
this.session.resize(columns, rows)
|
||||
}
|
||||
})
|
||||
}),
|
||||
|
||||
this.hostApp.windowMoved$.subscribe(() => setTimeout(() => {
|
||||
this.configure()
|
||||
}, 250)),
|
||||
]
|
||||
}
|
||||
|
||||
sendInput (data: string) {
|
||||
this.session.write(data)
|
||||
if (this.config.store.terminal.scrollOnInput) {
|
||||
this.frontend.scrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
write (data: string) {
|
||||
let percentageMatch = /(^|[^\d])(\d+(\.\d+)?)%([^\d]|$)/.exec(data)
|
||||
if (percentageMatch) {
|
||||
let percentage = percentageMatch[3] ? parseFloat(percentageMatch[2]) : parseInt(percentageMatch[2])
|
||||
if (percentage > 0 && percentage <= 100) {
|
||||
this.setProgress(percentage)
|
||||
console.log('Detected progress:', percentage)
|
||||
}
|
||||
} else {
|
||||
this.setProgress(null)
|
||||
}
|
||||
this.frontend.write(data)
|
||||
}
|
||||
|
||||
paste () {
|
||||
let data = this.electron.clipboard.readText()
|
||||
if (this.config.store.terminal.bracketedPaste) {
|
||||
data = '\x1b[200~' + data + '\x1b[201~'
|
||||
}
|
||||
if (this.hostApp.platform === Platform.Windows) {
|
||||
data = data.replace(/\r\n/g, '\r')
|
||||
} else {
|
||||
data = data.replace(/\n/g, '\r')
|
||||
}
|
||||
this.sendInput(data)
|
||||
}
|
||||
|
||||
configure (): void {
|
||||
this.frontend.configure(this.config.store)
|
||||
|
||||
if (this.config.store.terminal.background === 'colorScheme') {
|
||||
if (this.config.store.terminal.colorScheme.background) {
|
||||
this.backgroundColor = this.config.store.terminal.colorScheme.background
|
||||
}
|
||||
} else {
|
||||
this.backgroundColor = null
|
||||
}
|
||||
}
|
||||
|
||||
zoomIn () {
|
||||
this.zoom++
|
||||
this.frontend.setZoom(this.zoom)
|
||||
}
|
||||
|
||||
zoomOut () {
|
||||
this.zoom--
|
||||
this.frontend.setZoom(this.zoom)
|
||||
}
|
||||
|
||||
resetZoom () {
|
||||
this.zoom = 0
|
||||
this.frontend.setZoom(this.zoom)
|
||||
}
|
||||
|
||||
async getCurrentProcess (): Promise<BaseTabProcess> {
|
||||
let children = await this.session.getChildProcesses()
|
||||
if (!children.length) {
|
||||
@@ -355,47 +57,19 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.frontend.detach(this.content.nativeElement)
|
||||
this.detachTermContainerHandlers()
|
||||
this.config.enabledServices(this.decorators).forEach(decorator => {
|
||||
decorator.detach(this)
|
||||
})
|
||||
this.hotkeysSubscription.unsubscribe()
|
||||
if (this.sessionCloseSubscription) {
|
||||
this.sessionCloseSubscription.unsubscribe()
|
||||
}
|
||||
this.output.complete()
|
||||
}
|
||||
|
||||
async destroy () {
|
||||
super.destroy()
|
||||
if (this.session && this.session.open) {
|
||||
await this.session.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
async canClose (): Promise<boolean> {
|
||||
let children = await this.session.getChildProcesses()
|
||||
if (children.length === 0) {
|
||||
return true
|
||||
}
|
||||
return confirm(`"${children[0].command}" is still running. Close?`)
|
||||
}
|
||||
|
||||
async saveAsProfile () {
|
||||
let profile = {
|
||||
sessionOptions: {
|
||||
...this.sessionOptions,
|
||||
cwd: (await this.session.getWorkingDirectory()) || this.sessionOptions.cwd,
|
||||
},
|
||||
name: this.sessionOptions.command,
|
||||
}
|
||||
this.config.store.terminal.profiles = [
|
||||
...this.config.store.terminal.profiles,
|
||||
profile,
|
||||
]
|
||||
this.config.save()
|
||||
this.toastr.info('Saved')
|
||||
return (await this.electron.showMessageBox(
|
||||
this.hostApp.getWindow(),
|
||||
{
|
||||
type: 'warning',
|
||||
message: `"${children[0].command}" is still running. Close?`,
|
||||
buttons: ['Cancel', 'Kill'],
|
||||
defaultId: 1,
|
||||
}
|
||||
)).response === 1
|
||||
}
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ import { ToastrService } from 'ngx-toastr'
|
||||
import { ConfigService } from 'terminus-core'
|
||||
import { UACService } from './services/uac.service'
|
||||
import { TerminalService } from './services/terminal.service'
|
||||
import { TerminalTabComponent } from './components/terminalTab.component'
|
||||
import { BaseTerminalTabComponent } from './components/baseTerminalTab.component'
|
||||
import { TerminalContextMenuItemProvider } from './api'
|
||||
|
||||
@Injectable()
|
||||
@@ -19,14 +19,14 @@ export class NewTabContextMenu extends TerminalContextMenuItemProvider {
|
||||
super()
|
||||
}
|
||||
|
||||
async getItems (tab: TerminalTabComponent): Promise<Electron.MenuItemConstructorOptions[]> {
|
||||
async getItems (tab: BaseTerminalTabComponent): Promise<Electron.MenuItemConstructorOptions[]> {
|
||||
let shells = await this.terminalService.shells$.toPromise()
|
||||
|
||||
let items: Electron.MenuItemConstructorOptions[] = [
|
||||
{
|
||||
label: 'New terminal',
|
||||
click: () => this.zone.run(() => {
|
||||
this.terminalService.openTabWithOptions(tab.sessionOptions)
|
||||
this.terminalService.openTabWithOptions((tab as any).sessionOptions)
|
||||
})
|
||||
},
|
||||
{
|
||||
@@ -84,7 +84,7 @@ export class CopyPasteContextMenu extends TerminalContextMenuItemProvider {
|
||||
super()
|
||||
}
|
||||
|
||||
async getItems (tab: TerminalTabComponent): Promise<Electron.MenuItemConstructorOptions[]> {
|
||||
async getItems (tab: BaseTerminalTabComponent): Promise<Electron.MenuItemConstructorOptions[]> {
|
||||
return [
|
||||
{
|
||||
label: 'Copy',
|
||||
|
@@ -1,7 +1,11 @@
|
||||
import { Observable, Subject, AsyncSubject, ReplaySubject, BehaviorSubject } from 'rxjs'
|
||||
import { ResizeEvent } from '../api'
|
||||
import { ConfigService, ThemesService } from 'terminus-core'
|
||||
|
||||
export abstract class Frontend {
|
||||
configService: ConfigService
|
||||
themesService: ThemesService
|
||||
|
||||
enableResizing = true
|
||||
protected ready = new AsyncSubject<void>()
|
||||
protected title = new ReplaySubject<string>(1)
|
||||
@@ -54,6 +58,6 @@ export abstract class Frontend {
|
||||
abstract visualBell (): void
|
||||
abstract scrollToBottom (): void
|
||||
|
||||
abstract configure (configStore: any): void
|
||||
abstract configure (): void
|
||||
abstract setZoom (zoom: number): void
|
||||
}
|
||||
|
@@ -51,7 +51,9 @@ export class HTermFrontend extends Frontend {
|
||||
this.term.onVTKeystroke('\f')
|
||||
}
|
||||
|
||||
configure (config: any): void {
|
||||
configure (): void {
|
||||
let config = this.configService.store
|
||||
|
||||
this.configuredFontSize = config.terminal.fontSize
|
||||
this.configuredLinePadding = config.terminal.linePadding
|
||||
this.setFontSize()
|
||||
@@ -85,8 +87,7 @@ export class HTermFrontend extends Frontend {
|
||||
preferenceManager.set('background-color', config.terminal.colorScheme.background)
|
||||
}
|
||||
} else {
|
||||
// hterm can't parse "transparent"
|
||||
preferenceManager.set('background-color', 'transparent')
|
||||
preferenceManager.set('background-color', config.appearance.vibrancy ? 'transparent' : this.themesService.findCurrentTheme().terminalBackground)
|
||||
}
|
||||
|
||||
this.configuredBackgroundColor = preferenceManager.get('background-color')
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { Frontend } from './frontend'
|
||||
import { Terminal, ITheme } from '@terminus-term/xterm'
|
||||
import * as fit from '@terminus-term/xterm/src/addons/fit/fit'
|
||||
import { Terminal, ITheme } from 'xterm'
|
||||
import * as fit from 'xterm/src/addons/fit/fit'
|
||||
import * as ligatures from 'xterm-addon-ligatures-tmp'
|
||||
import '@terminus-term/xterm/lib/xterm.css'
|
||||
import 'xterm/lib/xterm.css'
|
||||
import './xterm.css'
|
||||
import deepEqual = require('deep-equal')
|
||||
|
||||
@@ -12,9 +12,10 @@ Terminal.applyAddon(ligatures)
|
||||
export class XTermFrontend extends Frontend {
|
||||
enableResizing = true
|
||||
xterm: Terminal
|
||||
xtermCore: any
|
||||
private configuredFontSize = 0
|
||||
private zoom = 0
|
||||
private resizeHandler: any
|
||||
private resizeHandler: () => void
|
||||
private configuredTheme: ITheme = {}
|
||||
private copyOnSelect = false
|
||||
|
||||
@@ -24,6 +25,7 @@ export class XTermFrontend extends Frontend {
|
||||
allowTransparency: true,
|
||||
enableBold: true,
|
||||
})
|
||||
this.xtermCore = (this.xterm as any)._core
|
||||
|
||||
this.xterm.on('data', data => {
|
||||
this.input.next(data)
|
||||
@@ -39,6 +41,19 @@ export class XTermFrontend extends Frontend {
|
||||
this.copySelection()
|
||||
}
|
||||
})
|
||||
this.xterm.attachCustomKeyEventHandler((event: KeyboardEvent) => {
|
||||
if ((event.getModifierState('Control') || event.getModifierState('Meta')) && event.key.toLowerCase() === 'v') {
|
||||
event.preventDefault()
|
||||
return false
|
||||
}
|
||||
if (event.getModifierState('Meta') && event.key.startsWith('Arrow')) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
this.xtermCore._scrollToBottom = this.xtermCore.scrollToBottom.bind(this.xtermCore)
|
||||
this.xtermCore.scrollToBottom = () => null
|
||||
}
|
||||
|
||||
attach (host: HTMLElement): void {
|
||||
@@ -88,14 +103,21 @@ export class XTermFrontend extends Frontend {
|
||||
}
|
||||
|
||||
visualBell (): void {
|
||||
(this.xterm as any).bell()
|
||||
this.xtermCore.bell()
|
||||
}
|
||||
|
||||
scrollToBottom (): void {
|
||||
this.xterm.scrollToBottom()
|
||||
this.xtermCore._scrollToBottom()
|
||||
}
|
||||
|
||||
configure (config: any): void {
|
||||
configure (): void {
|
||||
let config = this.configService.store
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.xterm.cols && this.xterm.rows) {
|
||||
this.resizeHandler()
|
||||
}
|
||||
})
|
||||
this.xterm.setOption('fontFamily', `"${config.terminal.font}", "monospace-fallback", monospace`)
|
||||
this.xterm.setOption('bellStyle', config.terminal.bell)
|
||||
this.xterm.setOption('cursorStyle', {
|
||||
@@ -103,7 +125,7 @@ export class XTermFrontend extends Frontend {
|
||||
}[config.terminal.cursor] || config.terminal.cursor)
|
||||
this.xterm.setOption('cursorBlink', config.terminal.cursorBlink)
|
||||
this.xterm.setOption('macOptionIsMeta', config.terminal.altIsMeta)
|
||||
// this.xterm.setOption('colors', )
|
||||
this.xterm.setOption('scrollback', 100000)
|
||||
this.configuredFontSize = config.terminal.fontSize
|
||||
this.setFontSize()
|
||||
|
||||
@@ -111,7 +133,7 @@ export class XTermFrontend extends Frontend {
|
||||
|
||||
let theme: ITheme = {
|
||||
foreground: config.terminal.colorScheme.foreground,
|
||||
background: (config.terminal.background === 'colorScheme') ? config.terminal.colorScheme.background : 'transparent',
|
||||
background: (config.terminal.background === 'colorScheme') ? config.terminal.colorScheme.background : (config.appearance.vibrancy ? 'transparent' : this.themesService.findCurrentTheme().terminalBackground),
|
||||
cursor: config.terminal.colorScheme.cursor,
|
||||
}
|
||||
|
||||
|
@@ -6,7 +6,7 @@ import { FormsModule } from '@angular/forms'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ToastrModule } from 'ngx-toastr'
|
||||
|
||||
import TerminusCorePlugin, { HostAppService, ToolbarButtonProvider, TabRecoveryProvider, ConfigProvider, HotkeysService, HotkeyProvider, AppService, ConfigService } from 'terminus-core'
|
||||
import TerminusCorePlugin, { HostAppService, ToolbarButtonProvider, TabRecoveryProvider, ConfigProvider, HotkeysService, HotkeyProvider, AppService, ConfigService, TabContextMenuItemProvider } from 'terminus-core'
|
||||
import { SettingsTabProvider } from 'terminus-settings'
|
||||
|
||||
import { AppearanceSettingsTabComponent } from './components/appearanceSettingsTab.component'
|
||||
@@ -16,6 +16,7 @@ import { TerminalSettingsTabComponent } from './components/terminalSettingsTab.c
|
||||
import { ColorPickerComponent } from './components/colorPicker.component'
|
||||
import { EditProfileModalComponent } from './components/editProfileModal.component'
|
||||
import { EnvironmentEditorComponent } from './components/environmentEditor.component'
|
||||
import { BaseTerminalTabComponent } from './components/baseTerminalTab.component'
|
||||
|
||||
import { BaseSession } from './services/sessions.service'
|
||||
import { TerminalFrontendService } from './services/terminalFrontend.service'
|
||||
@@ -31,6 +32,7 @@ import { TerminalConfigProvider } from './config'
|
||||
import { TerminalHotkeyProvider } from './hotkeys'
|
||||
import { HyperColorSchemes } from './colorSchemes'
|
||||
import { NewTabContextMenu, CopyPasteContextMenu } from './contextMenu'
|
||||
import { SaveAsProfileContextMenu } from './tabContextMenu'
|
||||
|
||||
import { CmderShellProvider } from './shells/cmder'
|
||||
import { CustomShellProvider } from './shells/custom'
|
||||
@@ -83,6 +85,8 @@ import { hterm } from './hterm'
|
||||
{ provide: TerminalContextMenuItemProvider, useClass: NewTabContextMenu, multi: true },
|
||||
{ provide: TerminalContextMenuItemProvider, useClass: CopyPasteContextMenu, multi: true },
|
||||
|
||||
{ provide: TabContextMenuItemProvider, useClass: SaveAsProfileContextMenu, multi: true },
|
||||
|
||||
// For WindowsDefaultShellProvider
|
||||
PowerShellCoreShellProvider,
|
||||
WSLShellProvider,
|
||||
@@ -203,5 +207,5 @@ export default class TerminalModule {
|
||||
}
|
||||
}
|
||||
|
||||
export { TerminalService, BaseSession, TerminalTabComponent, TerminalFrontendService }
|
||||
export { TerminalService, BaseSession, TerminalTabComponent, TerminalFrontendService, BaseTerminalTabComponent }
|
||||
export * from './api'
|
||||
|
@@ -5,14 +5,8 @@ import { TerminalTabComponent } from './components/terminalTab.component'
|
||||
|
||||
@Injectable()
|
||||
export class RecoveryProvider extends TabRecoveryProvider {
|
||||
constructor (
|
||||
// private sessions: SessionsService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async recover (recoveryToken: any): Promise<RecoveredTab> {
|
||||
if (recoveryToken.type === 'app:terminal-tab') {
|
||||
if (recoveryToken && recoveryToken.type === 'app:terminal-tab') {
|
||||
return {
|
||||
type: TerminalTabComponent,
|
||||
options: { sessionOptions: recoveryToken.sessionOptions },
|
||||
|
@@ -264,7 +264,7 @@ export class Session extends BaseSession {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
fs.access(this.guessedCWD)
|
||||
await fs.access(this.guessedCWD)
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
@@ -290,7 +290,7 @@ export class SessionsService {
|
||||
constructor (
|
||||
log: LogService,
|
||||
) {
|
||||
nodePTY = require('@terminus-term/node-pty')
|
||||
nodePTY = require('node-pty')
|
||||
nodePTY = require('../bufferizedPTY')(nodePTY)
|
||||
this.logger = log.create('sessions')
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import * as fs from 'mz/fs'
|
||||
import { Observable, AsyncSubject } from 'rxjs'
|
||||
import { Injectable, Inject } from '@angular/core'
|
||||
import { AppService, Logger, LogService, ConfigService } from 'terminus-core'
|
||||
@@ -41,6 +42,10 @@ export class TerminalService {
|
||||
}
|
||||
|
||||
async openTab (shell?: IShell, cwd?: string, pause?: boolean): Promise<TerminalTabComponent> {
|
||||
if (cwd && !fs.existsSync(cwd)) {
|
||||
console.warn('Ignoring non-existent CWD:', cwd)
|
||||
cwd = null
|
||||
}
|
||||
if (!cwd) {
|
||||
if (this.app.activeTab instanceof TerminalTabComponent && this.app.activeTab.session) {
|
||||
cwd = await this.app.activeTab.session.getWorkingDirectory()
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ConfigService } from 'terminus-core'
|
||||
import { ConfigService, ThemesService } from 'terminus-core'
|
||||
import { Frontend } from '../frontends/frontend'
|
||||
import { HTermFrontend } from '../frontends/htermFrontend'
|
||||
import { XTermFrontend } from '../frontends/xtermFrontend'
|
||||
@@ -9,15 +9,21 @@ import { BaseSession } from '../services/sessions.service'
|
||||
export class TerminalFrontendService {
|
||||
private containers = new WeakMap<BaseSession, Frontend>()
|
||||
|
||||
constructor (private config: ConfigService) { }
|
||||
constructor (private config: ConfigService, private themes: ThemesService) { }
|
||||
|
||||
getFrontend (session: BaseSession): Frontend {
|
||||
getFrontend (session?: BaseSession): Frontend {
|
||||
if (!session) {
|
||||
let frontend: Frontend = (this.config.store.terminal.frontend === 'xterm')
|
||||
? new XTermFrontend()
|
||||
: new HTermFrontend()
|
||||
frontend.configService = this.config
|
||||
frontend.themesService = this.themes
|
||||
return frontend
|
||||
}
|
||||
if (!this.containers.has(session)) {
|
||||
this.containers.set(
|
||||
session,
|
||||
(this.config.store.terminal.frontend === 'xterm')
|
||||
? new XTermFrontend()
|
||||
: new HTermFrontend()
|
||||
this.getFrontend(),
|
||||
)
|
||||
}
|
||||
return this.containers.get(session)
|
||||
|
@@ -8,6 +8,7 @@ import { TerminalSettingsTabComponent } from './components/terminalSettingsTab.c
|
||||
@Injectable()
|
||||
export class AppearanceSettingsTabProvider extends SettingsTabProvider {
|
||||
id = 'terminal-appearance'
|
||||
icon = 'palette'
|
||||
title = 'Appearance'
|
||||
|
||||
getComponentType (): any {
|
||||
@@ -18,6 +19,7 @@ export class AppearanceSettingsTabProvider extends SettingsTabProvider {
|
||||
@Injectable()
|
||||
export class ShellSettingsTabProvider extends SettingsTabProvider {
|
||||
id = 'terminal-shell'
|
||||
icon = 'list-ul'
|
||||
title = 'Shell'
|
||||
|
||||
getComponentType (): any {
|
||||
@@ -28,6 +30,7 @@ export class ShellSettingsTabProvider extends SettingsTabProvider {
|
||||
@Injectable()
|
||||
export class TerminalSettingsTabProvider extends SettingsTabProvider {
|
||||
id = 'terminal'
|
||||
icon = 'terminal'
|
||||
title = 'Terminal'
|
||||
|
||||
getComponentType (): any {
|
||||
|
41
terminus-terminal/src/tabContextMenu.ts
Normal file
41
terminus-terminal/src/tabContextMenu.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Injectable, NgZone } from '@angular/core'
|
||||
import { ToastrService } from 'ngx-toastr'
|
||||
import { ConfigService, BaseTabComponent, TabContextMenuItemProvider } from 'terminus-core'
|
||||
import { TerminalTabComponent } from './components/terminalTab.component'
|
||||
|
||||
@Injectable()
|
||||
export class SaveAsProfileContextMenu extends TabContextMenuItemProvider {
|
||||
constructor (
|
||||
private config: ConfigService,
|
||||
private zone: NgZone,
|
||||
private toastr: ToastrService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async getItems (tab: BaseTabComponent): Promise<Electron.MenuItemConstructorOptions[]> {
|
||||
if (!(tab instanceof TerminalTabComponent)) {
|
||||
return []
|
||||
}
|
||||
return [
|
||||
{
|
||||
label: 'Save as profile',
|
||||
click: () => this.zone.run(async () => {
|
||||
let profile = {
|
||||
sessionOptions: {
|
||||
...tab.sessionOptions,
|
||||
cwd: (await tab.session.getWorkingDirectory()) || tab.sessionOptions.cwd,
|
||||
},
|
||||
name: tab.sessionOptions.command,
|
||||
}
|
||||
this.config.store.terminal.profiles = [
|
||||
...this.config.store.terminal.profiles,
|
||||
profile,
|
||||
]
|
||||
this.config.save()
|
||||
this.toastr.info('Saved')
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@@ -61,7 +61,7 @@ module.exports = {
|
||||
'windows-process-tree',
|
||||
'mz/fs',
|
||||
'mz/child_process',
|
||||
'@terminus-term/node-pty',
|
||||
'node-pty',
|
||||
/^rxjs/,
|
||||
/^@angular/,
|
||||
/^@ng-bootstrap/,
|
||||
|
@@ -2,18 +2,6 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@terminus-term/node-pty@0.8.0-1":
|
||||
version "0.8.0-1"
|
||||
resolved "https://registry.yarnpkg.com/@terminus-term/node-pty/-/node-pty-0.8.0-1.tgz#3cb682f2351179842d195c074acf7d5b54e96ffe"
|
||||
integrity sha512-mEP5zQC/yHtvCbYjdGmwzFkkdTqCe0Jfd1o35yzM9jfGrVpW9qlvo/ZrzyOLSH2tJlYRB5SqfdWlo/LVXrAEYA==
|
||||
dependencies:
|
||||
nan "2.10.0"
|
||||
|
||||
"@terminus-term/xterm@3.8.4":
|
||||
version "3.8.4"
|
||||
resolved "https://registry.yarnpkg.com/@terminus-term/xterm/-/xterm-3.8.4.tgz#c9a9d9e0d46dbd8a94e06384e2d7268d36f5b0c6"
|
||||
integrity sha512-DrxCjnJh9n3ivpldwI098PnuVYwg9e5lFlU8/1qfh/J/wFHbG3dX/bEtB4ynfTi3IXVJozFO2psD96+W2h3yeQ==
|
||||
|
||||
"@types/deep-equal@^1.0.0":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/deep-equal/-/deep-equal-1.0.1.tgz#71cfabb247c22bcc16d536111f50c0ed12476b03"
|
||||
@@ -160,6 +148,13 @@ nan@>=2.10.0, nan@^2.10.0:
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.1.tgz#90e22bccb8ca57ea4cd37cc83d3819b52eea6766"
|
||||
integrity sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==
|
||||
|
||||
node-pty@^0.8.0:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.8.0.tgz#08bccb633f49e2e3f7245eb56ea6b40f37ccd64f"
|
||||
integrity sha512-g5ggk3gN4gLrDmAllee5ScFyX3YzpOC/U8VJafha4pE7do0TIE1voiIxEbHSRUOPD1xYqmY+uHhOKAd3avbxGQ==
|
||||
dependencies:
|
||||
nan "2.10.0"
|
||||
|
||||
object-assign@^4.0.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||
@@ -245,6 +240,11 @@ xterm-addon-ligatures-tmp@^0.1.0-beta-1:
|
||||
font-finder "^1.0.2"
|
||||
font-ligatures "^1.3.1"
|
||||
|
||||
xterm@3.10.1:
|
||||
version "3.10.1"
|
||||
resolved "https://registry.yarnpkg.com/xterm/-/xterm-3.10.1.tgz#14accf92772e5a6728f317a3c209ba714b73c8b5"
|
||||
integrity sha512-RHaUwJ8zwLiICu1QsXoxUHP+R2Pp8Rc8yVoNali/nKw3CVXwmXxT/4mgbk7U22psuNgOqLyI4Sg9nlQfYeTRQw==
|
||||
|
||||
yallist@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
|
||||
|
Reference in New Issue
Block a user