Compare commits

...

20 Commits

Author SHA1 Message Date
Eugene Pankov
d3a192da58 offer using Alt key as Meta key (fixes #316) 2018-04-24 16:41:34 +02:00
Eugene Pankov
4b30dfef58 settings layout fixes 2018-04-24 16:07:42 +02:00
Eugene Pankov
8432e3ef66 remove selection after copying using smart Ctrl-C 2018-04-24 16:01:05 +02:00
Eugene Pankov
cdfd84a7f8 auto-show window when cmd-tabbing into Terminus 2018-04-24 15:56:54 +02:00
Eugene Pankov
128fe24003 fixed NPM detection in cases when node is not on PATH 2018-04-03 13:31:56 +02:00
Eugene Pankov
30f221d05e convert CRLF to LF on paste (fixes #293) 2018-04-01 20:05:30 +02:00
Eugene Pankov
5087224017 refreshed settings UI (fixes #314) 2018-04-01 19:51:04 +02:00
Eugene Pankov
9a8bad4851 touchbar improvements 2018-04-01 19:50:43 +02:00
Eugene Pankov
c3c983daf6 updated to the new NPM API 2018-03-31 13:23:32 +02:00
Eugene Pankov
dce8647f55 smart ctrl-c behaviour (fixes #307) 2018-03-30 23:42:50 +02:00
Eugene Pankov
f947fe3f0f paste as a configurable hotkey (fixes #260) 2018-03-30 23:33:46 +02:00
Eugene Pankov
b5f96a59f8 copy notification 2018-03-30 23:24:34 +02:00
Eugene Pankov
c90a5678cf don't repatch node-pty on window reload 2018-03-29 14:23:33 +02:00
Eugene Pankov
663da34e6d performance improv for flowing output 2018-03-29 00:25:57 +02:00
Eugene Pankov
049f08b8f9 namespacing fix 2018-03-24 23:45:40 +01:00
Eugene Pankov
3c3b14bf09 Smarted spawn hotkey behaviour on macOS to give the focus to the previous app on hide 2018-03-24 23:40:45 +01:00
Eugene Pankov
5e07dd5442 macOS touchbar support 2018-03-24 23:19:47 +01:00
Eugene Pankov
8f2d2cbe30 don't attempt to resize beyond min size when docking (fixes #308) 2018-03-23 17:53:20 +01:00
Eugene Pankov
bebde4799d updated tab design 2018-03-23 17:45:11 +01:00
Eugene Pankov
9cedeb3efb build fixes 2018-03-23 17:15:11 +01:00
38 changed files with 741 additions and 382 deletions

50
app/bufferizedPTY.js Normal file
View File

@@ -0,0 +1,50 @@
module.exports = function patchPTYModule (path) {
const mod = require(path)
const oldSpawn = mod.spawn
if (mod.patched) {
return mod
}
mod.patched = true
mod.spawn = (file, args, opt) => {
let terminal = oldSpawn(file, args, opt)
let timeout = null
let buffer = ''
let lastFlush = 0
let nextTimeout = 0
const maxWindow = 250
const minWindow = 50
function flush () {
if (buffer) {
terminal.emit('data-buffered', buffer)
}
lastFlush = Date.now()
buffer = ''
}
function reschedule () {
if (timeout) {
clearTimeout(timeout)
}
nextTimeout = Date.now() + minWindow
timeout = setTimeout(() => {
timeout = null
flush()
}, minWindow)
}
terminal.on('data', data => {
buffer += data
if (Date.now() - lastFlush > maxWindow) {
flush()
} else {
if (Date.now() > nextTimeout - (minWindow / 10)) {
reschedule()
}
}
})
return terminal
}
return mod
}

View File

@@ -1,11 +1,9 @@
if (process.platform == 'win32' && require('electron-squirrel-startup')) process.exit(0)
const electron = require('electron')
if (process.argv.indexOf('--debug') !== -1) {
require('electron-debug')({enabled: true, showDevTools: 'undocked'})
}
let app = electron.app
let secondInstance = app.makeSingleInstance((argv, cwd) => {
@@ -44,6 +42,9 @@ setupWindowManagement = () => {
}
})
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', (e) => {
windowConfig.set('windowBoundaries', app.window.getBounds())
})
@@ -56,14 +57,6 @@ setupWindowManagement = () => {
app.window.focus()
})
electron.ipcMain.on('window-toggle-focus', () => {
if (app.window.isFocused()) {
app.window.minimize()
} else {
app.window.focus()
}
})
electron.ipcMain.on('window-maximize', () => {
app.window.maximize()
})
@@ -183,7 +176,6 @@ setupMenu = () => {
{
role: 'window',
submenu: [
{role: 'close'},
{role: 'minimize'},
{role: 'zoom'},
{type: 'separator'},

View File

@@ -2,6 +2,7 @@ import 'zone.js'
import 'core-js/es7/reflect'
import 'core-js/core/delay'
import 'rxjs'
import './toastr.scss'
// Always land on the start view
location.hash = ''

16
app/src/toastr.scss Normal file
View File

@@ -0,0 +1,16 @@
#toast-container {
display: flex;
flex-direction: column;
align-items: center;
.toast {
box-shadow: 0 1px 0 rgba(0,0,0,.25);
padding: 10px;
background-image: none;
width: auto;
&.toast-info {
background-color: #555;
}
}
}

View File

@@ -20,3 +20,14 @@ vars.builtinPlugins.forEach(plugin => {
sh.exec(`${npx} yarn install`)
sh.cd('..')
})
if (['darwin', 'linux'].includes(process.platform)) {
sh.cd('node_modules')
for (let x of vars.builtinPlugins) {
sh.ln('-fs', '../' + x, x)
}
for (let x of vars.bundledModules) {
sh.ln('-fs', '../app/node_modules/' + x, x)
}
sh.cd('..')
}

View File

@@ -16,5 +16,9 @@ exports.builtinPlugins = [
'terminus-plugin-manager',
'terminus-ssh',
]
exports.bundledModules = [
'@angular',
'@ng-bootstrap',
]
exports.nativeModules = ['node-pty-tmp', 'font-manager', 'xkeychain']
exports.electronVersion = pkgInfo.devDependencies.electron

View File

@@ -1,6 +1,7 @@
export interface IToolbarButton {
icon: string
title: string
touchBarTitle?: string
weight?: number
click: () => void
}

View File

@@ -1,5 +1,5 @@
title-bar(
*ngIf='!hostApp.getWindow().isFullScreen() && config.store.appearance.frame == "full" && config.store.appearance.dock == "off"',
*ngIf='!hostApp.isFullScreen && config.store.appearance.frame == "full" && config.store.appearance.dock == "off"',
[class.inset]='hostApp.platform == Platform.macOS'
)
@@ -7,7 +7,7 @@ title-bar(
[class.tabs-on-top]='config.store.appearance.tabsLocation == "top"'
)
.tab-bar(
*ngIf='!hostApp.getWindow().isFullScreen()',
*ngIf='!hostApp.isFullScreen',
[class.inset]='hostApp.platform == Platform.macOS && config.store.appearance.frame == "thin" && config.store.appearance.tabsLocation == "top"'
)
.tabs

View File

@@ -11,6 +11,7 @@ import { DockingService } from '../services/docking.service'
import { TabRecoveryService } from '../services/tabRecovery.service'
import { ThemesService } from '../services/themes.service'
import { UpdaterService, Update } from '../services/updater.service'
import { TouchbarService } from '../services/touchbar.service'
import { SafeModeModalComponent } from './safeModeModal.component'
import { AppService, IToolbarButton, ToolbarButtonProvider } from '../api'
@@ -62,6 +63,7 @@ export class AppRootComponent {
private tabRecovery: TabRecoveryService,
private hotkeys: HotkeysService,
private updater: UpdaterService,
private touchbar: TouchbarService,
public hostApp: HostAppService,
public config: ConfigService,
public app: AppService,
@@ -121,16 +123,22 @@ export class AppRootComponent {
this.updater.check().then(update => {
this.appUpdate = update
})
this.touchbar.update()
}
onGlobalHotkey () {
if (this.electron.app.window.isFocused()) {
// focused
this.electron.app.window.hide()
this.electron.loseFocus()
if (this.hostApp.platform !== Platform.macOS) {
this.electron.app.window.hide()
}
} else {
if (!this.electron.app.window.isVisible()) {
// unfocused, invisible
this.electron.app.window.show()
this.electron.app.window.focus()
} else {
if (this.config.store.appearance.dock === 'off') {
// not docked, visible

View File

@@ -5,6 +5,7 @@ export abstract class BaseTabComponent {
private static lastTabID = 0
id: number
title: string
titleChange$ = new Subject<string>()
customTitle: string
scrollable: boolean
hasActivity = false
@@ -23,6 +24,13 @@ export abstract class BaseTabComponent {
})
}
setTitle (title: string) {
this.title = title
if (!this.customTitle) {
this.titleChange$.next(title)
}
}
displayActivity (): void {
this.hasActivity = true
}

View File

@@ -0,0 +1,4 @@
.icon((click)='click()', tabindex='0', [class.active]='model', (keyup.space)='click()')
i.fa.fa-square-o.off
i.fa.fa-check-square.on
.text((click)='click()') {{text}}

View File

@@ -0,0 +1,51 @@
:host {
cursor: pointer;
margin: 5px 0;
&:focus {
background: rgba(255,255,255,.05);
border-radius: 5px;
}
&:active {
background: rgba(255,255,255,.1);
border-radius: 3px;
}
&[disabled] {
opacity: 0.5;
}
display: flex;
flex-direction: row;
align-items: center;
.icon {
position: relative;
flex: none;
width: 14px;
height: 14px;
i {
position: absolute;
left: 0;
top: -2px;
transition: 0.25s opacity;
display: block;
font-size: 18px;
}
i.on, &.active i.off {
opacity: 0;
}
i.off, &.active i.on {
opacity: 1;
}
}
.text {
flex: auto;
margin-left: 8px;
}
}

View File

@@ -0,0 +1,45 @@
import { NgZone, Component, Input } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
@Component({
selector: 'checkbox',
template: require('./checkbox.component.pug'),
styles: [require('./checkbox.component.scss')],
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: CheckboxComponent, multi: true },
]
})
export class CheckboxComponent implements ControlValueAccessor {
@Input() model: boolean
@Input() disabled: boolean
@Input() text: string
private changed = new Array<(val: boolean) => void>()
click () {
NgZone.assertInAngularZone()
if (this.disabled) {
return
}
this.model = !this.model
for (let fx of this.changed) {
fx(this.model)
}
}
writeValue (obj: any) {
this.model = obj
}
registerOnChange (fn: any): void {
this.changed.push(fn)
}
registerOnTouched (fn: any): void {
this.changed.push(fn)
}
setDisabledState (isDisabled: boolean) {
this.disabled = isDisabled
}
}

View File

@@ -14,8 +14,6 @@ $tabs-height: 36px;
transition: 0.125s ease-out all;
border-top: 1px solid transparent;
.index {
flex: none;
font-weight: bold;

View File

@@ -69,6 +69,7 @@ export class TabHeaderComponent {
let modal = this.ngbModal.open(RenameTabModalComponent)
modal.componentInstance.value = this.tab.customTitle || this.tab.title
modal.result.then(result => {
this.tab.setTitle(result)
this.tab.customTitle = result
}).catch(() => null)
}

View File

@@ -14,9 +14,11 @@ import { HotkeysService, AppHotkeyProvider } from './services/hotkeys.service'
import { DockingService } from './services/docking.service'
import { TabRecoveryService } from './services/tabRecovery.service'
import { ThemesService } from './services/themes.service'
import { TouchbarService } from './services/touchbar.service'
import { UpdaterService } from './services/updater.service'
import { AppRootComponent } from './components/appRoot.component'
import { CheckboxComponent } from './components/checkbox.component'
import { TabBodyComponent } from './components/tabBody.component'
import { SafeModeModalComponent } from './components/safeModeModal.component'
import { StartPageComponent } from './components/startPage.component'
@@ -44,6 +46,7 @@ const PROVIDERS = [
LogService,
TabRecoveryService,
ThemesService,
TouchbarService,
UpdaterService,
{ provide: HotkeyProvider, useClass: AppHotkeyProvider, multi: true },
{ provide: Theme, useClass: StandardTheme, multi: true },
@@ -63,6 +66,7 @@ const PROVIDERS = [
],
declarations: [
AppRootComponent,
CheckboxComponent,
StartPageComponent,
TabBodyComponent,
TabHeaderComponent,
@@ -74,6 +78,9 @@ const PROVIDERS = [
entryComponents: [
RenameTabModalComponent,
SafeModeModalComponent,
],
exports: [
CheckboxComponent
]
})
export default class AppModule {

View File

@@ -2,8 +2,8 @@ import { Subject, AsyncSubject } from 'rxjs'
import { Injectable, ComponentFactoryResolver, Injector, Optional } from '@angular/core'
import { DefaultTabProvider } from '../api/defaultTabProvider'
import { BaseTabComponent } from '../components/baseTab.component'
import { Logger, LogService } from '../services/log.service'
import { ConfigService } from '../services/config.service'
import { Logger, LogService } from './log.service'
import { ConfigService } from './config.service'
export declare type TabComponentType = new (...args: any[]) => BaseTabComponent
@@ -11,9 +11,12 @@ export declare type TabComponentType = new (...args: any[]) => BaseTabComponent
export class AppService {
tabs: BaseTabComponent[] = []
activeTab: BaseTabComponent
activeTabChange$ = new Subject<BaseTabComponent>()
lastTabIndex = 0
logger: Logger
tabsChanged$ = new Subject<void>()
tabOpened$ = new Subject<BaseTabComponent>()
tabClosed$ = new Subject<BaseTabComponent>()
ready$ = new AsyncSubject<void>()
constructor (
@@ -35,6 +38,7 @@ export class AppService {
this.tabs.push(componentRef.instance)
this.selectTab(componentRef.instance)
this.tabsChanged$.next()
this.tabOpened$.next(componentRef.instance)
return componentRef.instance
}
@@ -59,6 +63,7 @@ export class AppService {
this.activeTab.blurred$.next()
}
this.activeTab = tab
this.activeTabChange$.next(tab)
if (this.activeTab) {
this.activeTab.focused$.next()
}
@@ -107,6 +112,7 @@ export class AppService {
this.selectTab(this.tabs[newIndex])
}
this.tabsChanged$.next()
this.tabClosed$.next(tab)
}
emitReady () {

View File

@@ -29,18 +29,19 @@ export class DockingService {
let dockSide = this.config.store.appearance.dock
let newBounds: Bounds = { x: 0, y: 0, width: 0, height: 0 }
let fill = this.config.store.appearance.dockFill
let [minWidth, minHeight] = this.hostApp.getWindow().getMinimumSize()
if (dockSide === 'off') {
this.hostApp.setAlwaysOnTop(false)
return
}
if (dockSide === 'left' || dockSide === 'right') {
newBounds.width = Math.round(fill * display.bounds.width)
newBounds.width = Math.max(minWidth, Math.round(fill * display.bounds.width))
newBounds.height = display.bounds.height
}
if (dockSide === 'top' || dockSide === 'bottom') {
newBounds.width = display.bounds.width
newBounds.height = Math.round(fill * display.bounds.height)
newBounds.height = Math.max(minHeight, Math.round(fill * display.bounds.height))
}
if (dockSide === 'right') {
newBounds.x = display.bounds.x + display.bounds.width - newBounds.width

View File

@@ -1,4 +1,5 @@
import { Injectable } from '@angular/core'
import { TouchBar } from 'electron'
@Injectable()
export class ElectronService {
@@ -10,6 +11,7 @@ export class ElectronService {
globalShortcut: any
screen: any
remote: any
TouchBar: typeof TouchBar
private electron: any
constructor () {
@@ -22,6 +24,7 @@ export class ElectronService {
this.clipboard = this.electron.clipboard
this.ipcRenderer = this.electron.ipcRenderer
this.globalShortcut = this.remote.globalShortcut
this.TouchBar = this.remote.TouchBar
}
remoteRequire (name: string): any {
@@ -29,6 +32,16 @@ export class ElectronService {
}
remoteRequirePluginModule (plugin: string, module: string, globals: any): any {
return this.remoteRequire(globals.require.resolve(`${plugin}/node_modules/${module}`))
return this.remoteRequire(this.remoteResolvePluginModule(plugin, module, globals))
}
remoteResolvePluginModule (plugin: string, module: string, globals: any): any {
return globals.require.resolve(`${plugin}/node_modules/${module}`)
}
loseFocus () {
if (process.platform === 'darwin') {
this.remote.Menu.sendActionToFirstResponder('hide:')
}
}
}

View File

@@ -22,7 +22,7 @@ export class HostAppService {
ready = new EventEmitter<any>()
shown = new EventEmitter<any>()
secondInstance$ = new Subject<{ argv: string[], cwd: string }>()
isFullScreen = false
private logger: Logger
constructor (
@@ -44,6 +44,14 @@ export class HostAppService {
this.logger.error('Unhandled exception:', err)
})
electron.ipcRenderer.on('host:window-enter-full-screen', () => this.zone.run(() => {
this.isFullScreen = true
}))
electron.ipcRenderer.on('host:window-leave-full-screen', () => this.zone.run(() => {
this.isFullScreen = false
}))
electron.ipcRenderer.on('host:window-shown', () => {
this.zone.run(() => this.shown.emit())
})
@@ -86,10 +94,6 @@ export class HostAppService {
this.electron.ipcRenderer.send('window-focus')
}
toggleWindow () {
this.electron.ipcRenderer.send('window-toggle-focus')
}
minimize () {
this.electron.ipcRenderer.send('window-minimize')
}

View File

@@ -0,0 +1,76 @@
import { Injectable, Inject, NgZone } from '@angular/core'
import { TouchBarSegmentedControl, SegmentedControlSegment } from 'electron'
import { Subject, Subscription } from 'rxjs'
import { AppService } from './app.service'
import { ConfigService } from './config.service'
import { ElectronService } from './electron.service'
import { BaseTabComponent } from '../components/baseTab.component'
import { IToolbarButton, ToolbarButtonProvider } from '../api'
@Injectable()
export class TouchbarService {
tabSelected$ = new Subject<number>()
private titleSubscriptions = new Map<BaseTabComponent, Subscription>()
private tabsSegmentedControl: TouchBarSegmentedControl
private tabSegments: SegmentedControlSegment[] = []
constructor (
private app: AppService,
@Inject(ToolbarButtonProvider) private toolbarButtonProviders: ToolbarButtonProvider[],
private config: ConfigService,
private electron: ElectronService,
private zone: NgZone,
) {
app.tabsChanged$.subscribe(() => this.update())
app.activeTabChange$.subscribe(() => this.update())
app.tabOpened$.subscribe(tab => {
let sub = tab.titleChange$.subscribe(title => {
this.tabSegments[app.tabs.indexOf(tab)].label = this.shortenTitle(title)
this.tabsSegmentedControl.segments = this.tabSegments
})
this.titleSubscriptions.set(tab, sub)
})
app.tabClosed$.subscribe(tab => {
this.titleSubscriptions.get(tab).unsubscribe()
this.titleSubscriptions.delete(tab)
})
}
update () {
let buttons: IToolbarButton[] = []
this.config.enabledServices(this.toolbarButtonProviders).forEach(provider => {
buttons = buttons.concat(provider.provide())
})
buttons.sort((a, b) => (a.weight || 0) - (b.weight || 0))
this.tabSegments = this.app.tabs.map(tab => ({
label: this.shortenTitle(tab.title),
}))
this.tabsSegmentedControl = new this.electron.TouchBar.TouchBarSegmentedControl({
segments: this.tabSegments,
selectedIndex: this.app.tabs.indexOf(this.app.activeTab),
change: (selectedIndex) => this.zone.run(() => {
this.app.selectTab(this.app.tabs[selectedIndex])
})
})
let touchBar = new this.electron.TouchBar({
items: [
this.tabsSegmentedControl,
new this.electron.TouchBar.TouchBarSpacer({size: 'flexible'}),
new this.electron.TouchBar.TouchBarSpacer({size: 'small'}),
...buttons.map(button => new this.electron.TouchBar.TouchBarButton({
label: this.shortenTitle(button.touchBarTitle || button.title),
// backgroundColor: '#0022cc',
click: () => this.zone.run(() => button.click()),
}))
]
})
this.electron.app.window.setTouchBar(touchBar)
}
private shortenTitle (title: string): string {
if (title.length > 15) {
title = title.substring(0, 15) + '...'
}
return title
}
}

View File

@@ -47,6 +47,7 @@ $input-color-placeholder: #333;
$input-border-color: #344;
//$input-box-shadow: inset 0 1px 1px rgba($black,.075);
$input-border-radius: 0;
$custom-select-border-radius: 0;
$input-bg-focus: $input-bg;
//$input-border-focus: lighten($brand-primary, 25%);
//$input-box-shadow-focus: $input-box-shadow, rgba($input-border-focus, .6);
@@ -83,6 +84,8 @@ $alert-danger-bg: $body-bg2;
$alert-danger-text: $red;
$alert-danger-border: $red;
$headings-font-weight: lighter;
$headings-color: #eee;
@import '~bootstrap/scss/bootstrap.scss';
@@ -105,7 +108,7 @@ window-controls {
}
}
$border-color: #141414;
$border-color: #111;
app-root {
&> .content {
@@ -131,7 +134,7 @@ app-root {
background: $body-bg2;
border-left: 1px solid transparent;
border-right: 1px solid transparent;
border-top: 1px solid transparent;
transition: 0.25s all;
.index {
color: #555;
@@ -147,6 +150,7 @@ app-root {
}
&.active {
color: white;
background: $body-bg;
border-left: 1px solid $border-color;
border-right: 1px solid $border-color;
@@ -159,17 +163,15 @@ app-root {
border-bottom: 1px solid $border-color;
tab-header {
border-top: 1px solid transparent;
border-bottom: 1px solid $border-color;
margin-bottom: -1px;
&.active {
border-top: 1px solid $teal;
border-bottom-color: transparent;
}
&.has-activity:not(.active) {
border-top: 1px solid $green;
background: linear-gradient(to bottom, rgba(208, 0, 0, 0) 95%, #1aa99c 100%);
}
}
}
@@ -178,17 +180,15 @@ app-root {
border-top: 1px solid $border-color;
tab-header {
border-bottom: 1px solid transparent;
border-top: 1px solid $border-color;
margin-top: -1px;
&.active {
border-bottom: 1px solid $teal;
margin-top: -1px;
}
&.has-activity:not(.active) {
border-bottom: 1px solid $green;
background: linear-gradient(to top, rgba(208, 0, 0, 0) 95%, #1aa99c 100%);
}
}
}
@@ -332,3 +332,15 @@ ngb-tabset .tab-content {
margin-left: 10px;
}
}
select.form-control {
-webkit-appearance: none;
background-image: url("data:image/svg+xml;utf8,<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='24' height='24' viewBox='0 0 24 24'><path fill='#444' d='M7.406 7.828l4.594 4.594 4.594-4.594 1.406 1.406-6 6-6-6z'></path></svg>");
background-position: 100% 50%;
background-repeat: no-repeat;
padding-right: 30px;
}
checkbox i.on {
color: $blue;
}

View File

@@ -29,6 +29,7 @@ export class PluginManagerService {
userPluginsPath: string = (window as any).userPluginsPath
installedPlugins: IPluginInfo[] = (window as any).installedPlugins
npmPath: string
private envPath: string
constructor (
log: LogService,
@@ -41,11 +42,13 @@ export class PluginManagerService {
async detectPath () {
this.npmPath = this.config.store.npm
this.envPath = process.env.PATH
if (await fs.exists(this.npmPath)) {
return
}
if (this.hostApp.platform !== Platform.Windows) {
let searchPaths = (await exec('$SHELL -c -i \'echo $PATH\''))[0].toString().trim().split(':')
this.envPath = (await exec('$SHELL -c -i \'echo $PATH\''))[0].toString().trim()
let searchPaths = this.envPath.split(':')
for (let searchPath of searchPaths) {
if (await fs.exists(path.join(searchPath, 'npm'))) {
this.logger.debug('Found npm in', searchPath)
@@ -59,7 +62,7 @@ export class PluginManagerService {
async isNPMInstalled (): Promise<boolean> {
await this.detectPath()
try {
await exec(`${this.npmPath} -v`)
await exec(`${this.npmPath} -v`, { env: this.getEnv() })
return true
} catch (_) {
return false
@@ -69,7 +72,11 @@ export class PluginManagerService {
listAvailable (query?: string): Observable<IPluginInfo[]> {
return Observable
.fromPromise(
axios.get(`https://www.npmjs.com/-/search?text=keywords:${KEYWORD}+${encodeURIComponent(query || '')}&from=0&size=1000`)
axios.get(`https://www.npmjs.com/search?q=keywords%3A${KEYWORD}+${encodeURIComponent(query || '')}&from=0&size=1000`, {
headers: {
'x-spiferack': '1',
}
})
)
.map(response => response.data.objects.map(item => ({
name: item.package.name.substring(NAME_PREFIX.length),
@@ -84,13 +91,17 @@ export class PluginManagerService {
}
async installPlugin (plugin: IPluginInfo) {
await exec(`${this.npmPath} --prefix "${this.userPluginsPath}" install ${plugin.packageName}@${plugin.version}`)
await exec(`${this.npmPath} --prefix "${this.userPluginsPath}" install ${plugin.packageName}@${plugin.version}`, { env: this.getEnv() })
this.installedPlugins = this.installedPlugins.filter(x => x.packageName !== plugin.packageName)
this.installedPlugins.push(plugin)
}
async uninstallPlugin (plugin: IPluginInfo) {
await exec(`${this.npmPath} --prefix "${this.userPluginsPath}" remove ${plugin.packageName}`)
await exec(`${this.npmPath} --prefix "${this.userPluginsPath}" remove ${plugin.packageName}`, { env: this.getEnv() })
this.installedPlugins = this.installedPlugins.filter(x => x.packageName !== plugin.packageName)
}
private getEnv (): any {
return Object.assign(process.env, { PATH: this.envPath })
}
}

View File

@@ -17,6 +17,7 @@ export class ButtonProvider extends ToolbarButtonProvider {
return [{
icon: 'sliders',
title: 'Settings',
touchBarTitle: '⚙️',
weight: 10,
click: () => this.open(),
}]

View File

@@ -1,5 +1,10 @@
:host {
display: flex;
flex-wrap: nowrap;
&:hover .add {
display: initial;
}
}
.item {
@@ -22,4 +27,5 @@
.add {
flex: auto;
display: none;
}

View File

@@ -5,6 +5,7 @@ ngb-tabset.vertical(type='tabs', [activeId]='activeTab')
ng-template(ngbTabTitle)
| Application
ng-template(ngbTabContent)
h3.mb-3 Application
.row
.col.col-lg-6
.form-group
@@ -168,6 +169,7 @@ ngb-tabset.vertical(type='tabs', [activeId]='activeTab')
ng-template(ngbTabTitle)
| Hotkeys
ng-template(ngbTabContent)
h3.mb-3 Hotkeys
input.form-control(type='search', placeholder='Search hotkeys', [(ngModel)]='hotkeyFilter')
.form-group
table.hotkeys-table

View File

@@ -28,7 +28,7 @@ export class SettingsTabComponent extends BaseTabComponent {
) {
super()
this.hotkeyDescriptions = config.enabledServices(hotkeyProviders).map(x => x.hotkeys).reduce((a, b) => a.concat(b))
this.title = 'Settings'
this.setTitle('Settings')
this.scrollable = true
this.screens = this.docking.getScreens()
this.settingsProviders = config.enabledServices(this.settingsProviders)

View File

@@ -18,10 +18,7 @@ export class ButtonProvider extends ToolbarButtonProvider {
}
activate () {
let modal = this.ngbModal.open(SSHModalComponent)
modal.result.then(() => {
//this.terminal.openTab(shell)
})
this.ngbModal.open(SSHModalComponent)
}
provide (): IToolbarButton[] {
@@ -29,6 +26,7 @@ export class ButtonProvider extends ToolbarButtonProvider {
icon: 'globe',
weight: 5,
title: 'SSH connections',
touchBarTitle: 'SSH',
click: async () => {
this.activate()
}

View File

@@ -56,6 +56,7 @@ export class ButtonProvider extends ToolbarButtonProvider {
return [{
icon: 'plus',
title: 'New terminal',
touchBarTitle: 'New',
click: async () => {
this.openNewTab()
}

View File

@@ -1,5 +1,158 @@
h3.mb-2 Appearance
h3.mb-3 Appearance
.row
.col-md-6
.form-group
label Font
.row
.col-8
input.form-control(
type='text',
[ngbTypeahead]='fontAutocomplete',
[(ngModel)]='config.store.terminal.font',
(ngModelChange)='config.save()',
)
.col-4
input.form-control(
type='number',
[(ngModel)]='config.store.terminal.fontSize',
(ngModelChange)='config.save()',
)
div
checkbox(
text='Enable font ligatures',
[(ngModel)]='config.store.terminal.ligatures',
(ngModelChange)='config.save()',
)
.form-group(*ngIf='!editingColorScheme')
label Color scheme
.input-group
select.form-control(
[compareWith]='equalComparator',
[(ngModel)]='config.store.terminal.colorScheme',
(ngModelChange)='config.save()',
)
option(*ngFor='let scheme of config.store.terminal.customColorSchemes', [ngValue]='scheme') Custom: {{scheme.name}}
option(*ngFor='let scheme of colorSchemes', [ngValue]='scheme') {{scheme.name}}
.input-group-btn
button.btn.btn-secondary((click)='editScheme(config.store.terminal.colorScheme)') Edit
.input-group-btn
button.btn.btn-outline-danger(
(click)='deleteScheme(config.store.terminal.colorScheme)',
*ngIf='isCustomScheme(config.store.terminal.colorScheme)'
)
i.fa.fa-trash-o
.form-group(*ngIf='editingColorScheme')
label Editing
.input-group
input.form-control(type='text', [(ngModel)]='editingColorScheme.name')
.input-group-btn
button.btn.btn-secondary((click)='saveScheme()') Save
.input-group-btn
button.btn.btn-secondary((click)='cancelEditing()') Cancel
.form-group(*ngIf='editingColorScheme')
color-picker(
'[(model)]'='editingColorScheme.foreground',
(modelChange)='config.save(); schemeChanged = true',
title='FG',
)
color-picker(
'[(model)]'='editingColorScheme.background',
(modelChange)='config.save(); schemeChanged = true',
title='BG',
)
color-picker(
'[(model)]'='editingColorScheme.cursor',
(modelChange)='config.save(); schemeChanged = true',
title='CU',
)
color-picker(
*ngFor='let _ of editingColorScheme.colors; let idx = index; trackBy: colorsTrackBy',
'[(model)]'='editingColorScheme.colors[idx]',
(modelChange)='config.save(); schemeChanged = true',
[title]='idx',
)
.form-group
label Terminal background
br
.btn-group(
[(ngModel)]='config.store.terminal.background',
(ngModelChange)='config.save()',
ngbRadioGroup
)
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='"theme"'
)
| From theme
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='"colorScheme"'
)
| From colors
.d-flex
.form-group.mr-3
label Cursor shape
br
.btn-group(
[(ngModel)]='config.store.terminal.cursor',
(ngModelChange)='config.save()',
ngbRadioGroup
)
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='"block"'
)
| █
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='"beam"'
)
| |
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='"underline"'
)
| ▁
.form-group
label Blink cursor
br
.btn-group(
[(ngModel)]='config.store.terminal.cursorBlink',
(ngModelChange)='config.save()',
ngbRadioGroup
)
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='false'
)
| Off
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='true'
)
| On
.col-md-6
.form-group
.appearance-preview(
@@ -7,6 +160,8 @@ h3.mb-2 Appearance
[style.font-size]='config.store.terminal.fontSize + "px"',
[style.background-color]='(config.store.terminal.background == "theme") ? null : config.store.terminal.colorScheme.background',
[style.color]='config.store.terminal.colorScheme.foreground',
[style.font-feature-settings]='\'"liga" \' + config.store.terminal.ligatures ? 1 : 0',
[style.font-variant-ligatures]='config.store.terminal.ligatures ? "initial" : "none"',
)
div
span([style.background-color]='config.store.terminal.colorScheme.colors[0]') &nbsp;
@@ -85,298 +240,127 @@ h3.mb-2 Appearance
span rm -rf /
span([style.background-color]='config.store.terminal.colorScheme.cursor') &nbsp;
h3.mt-3.mb-3 Shell
.col-md-6
.form-group
label Font
.row
.col-8
input.form-control(
type='text',
[ngbTypeahead]='fontAutocomplete',
'[(ngModel)]'='config.store.terminal.font',
(ngModelChange)='config.save()',
)
.col-4
input.form-control(
type='number',
'[(ngModel)]'='config.store.terminal.fontSize',
(ngModelChange)='config.save()',
)
small.form-text.text-muted Font to be used in the terminal
.d-flex
.form-group.mr-3
label Shell
select.form-control(
[(ngModel)]='config.store.terminal.shell',
(ngModelChange)='config.save()',
)
option(
*ngFor='let shell of shells',
[ngValue]='shell.id'
) {{shell.name}}
.form-group(*ngIf='!editingColorScheme')
label Color scheme
.input-group
select.form-control(
[compareWith]='equalComparator',
'[(ngModel)]'='config.store.terminal.colorScheme',
(ngModelChange)='config.save()',
)
option(*ngFor='let scheme of config.store.terminal.customColorSchemes', [ngValue]='scheme') Custom: {{scheme.name}}
option(*ngFor='let scheme of colorSchemes', [ngValue]='scheme') {{scheme.name}}
.input-group-btn
button.btn.btn-secondary((click)='editScheme(config.store.terminal.colorScheme)') Edit
.input-group-btn
button.btn.btn-outline-danger(
(click)='deleteScheme(config.store.terminal.colorScheme)',
*ngIf='isCustomScheme(config.store.terminal.colorScheme)'
)
i.fa.fa-trash-o
.form-group(*ngIf='editingColorScheme')
label Editing
.input-group
input.form-control(type='text', '[(ngModel)]'='editingColorScheme.name')
.input-group-btn
button.btn.btn-secondary((click)='saveScheme()') Save
.input-group-btn
button.btn.btn-secondary((click)='cancelEditing()') Cancel
.form-group(*ngIf='editingColorScheme')
color-picker(
'[(model)]'='editingColorScheme.foreground',
(modelChange)='config.save(); schemeChanged = true',
title='FG',
)
color-picker(
'[(model)]'='editingColorScheme.background',
(modelChange)='config.save(); schemeChanged = true',
title='BG',
)
color-picker(
'[(model)]'='editingColorScheme.cursor',
(modelChange)='config.save(); schemeChanged = true',
title='CU',
)
color-picker(
*ngFor='let _ of editingColorScheme.colors; let idx = index; trackBy: colorsTrackBy',
'[(model)]'='editingColorScheme.colors[idx]',
(modelChange)='config.save(); schemeChanged = true',
[title]='idx',
)
.d-flex
.form-group.mr-3
label Terminal background
br
.btn-group(
'[(ngModel)]'='config.store.terminal.background',
(ngModelChange)='config.save()',
ngbRadioGroup
)
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='"theme"'
)
| From theme
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='"colorScheme"'
)
| From colors
.form-group
label Cursor shape
br
.btn-group(
[(ngModel)]='config.store.terminal.cursor',
(ngModelChange)='config.save()',
ngbRadioGroup
)
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='"block"'
)
| █
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='"beam"'
)
| |
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='"underline"'
)
| ▁
h3.mt-2.mb-2 Behaviour
.row
.col-md-6
.d-flex
.form-group.mr-3
label Shell
select.form-control(
'[(ngModel)]'='config.store.terminal.shell',
(ngModelChange)='config.save()',
)
option(
*ngFor='let shell of shells',
[ngValue]='shell.id'
) {{shell.name}}
.form-group
label Session persistence
select.form-control(
'[(ngModel)]'='config.store.terminal.persistence',
(ngModelChange)='config.save()',
)
option([ngValue]='null') Off
option(
*ngFor='let provider of persistenceProviders',
[ngValue]='provider.id'
) {{provider.displayName}}
.form-group(*ngIf='config.store.terminal.shell == "custom"')
label Custom shell
input.form-control(
type='text',
'[(ngModel)]'='config.store.terminal.customShell',
(ngModelChange)='config.save()',
)
.form-group
label Working directory
input.form-control(
type='text',
placeholder='Home directory',
'[(ngModel)]'='config.store.terminal.workingDirectory',
(ngModelChange)='config.save()',
)
.form-group
label Auto-open a terminal on app start
br
.btn-group(
'[(ngModel)]'='config.store.terminal.autoOpen',
(ngModelChange)='config.save()',
ngbRadioGroup
)
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='false'
)
| Off
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='true'
)
| On
.form-group.mr-3(*ngIf='persistenceProviders.length > 0')
label Session persistence
select.form-control(
[(ngModel)]='config.store.terminal.persistence',
(ngModelChange)='config.save()',
)
option([ngValue]='null') Off
option(
*ngFor='let provider of persistenceProviders',
[ngValue]='provider.id'
) {{provider.displayName}}
.col-md-6
.d-flex
.form-group.mr-3
label Terminal bell
br
.btn-group(
'[(ngModel)]'='config.store.terminal.bell',
(ngModelChange)='config.save()',
ngbRadioGroup
)
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='"off"'
)
| Off
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='"visual"'
)
| Visual
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='"audible"'
)
| Audible
.form-group
label Working directory
input.form-control(
type='text',
placeholder='Home directory',
[(ngModel)]='config.store.terminal.workingDirectory',
(ngModelChange)='config.save()',
)
.form-group(*ngIf='config.store.terminal.shell == "custom"')
label Custom shell
input.form-control(
type='text',
[(ngModel)]='config.store.terminal.customShell',
(ngModelChange)='config.save()',
)
.form-group
label Blink cursor
br
.btn-group(
'[(ngModel)]'='config.store.terminal.cursorBlink',
(ngModelChange)='config.save()',
ngbRadioGroup
)
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='false'
)
| Off
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='true'
)
| On
h3.mt-3.mb-3 Behaviour
.d-flex
.form-group.mr-3
label Copy on select
br
.btn-group(
'[(ngModel)]'='config.store.terminal.copyOnSelect',
(ngModelChange)='config.save()',
ngbRadioGroup
)
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='false'
)
| Off
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='true'
)
| On
.form-group
label Right click behaviour
br
.btn-group(
'[(ngModel)]'='config.store.terminal.rightClick',
(ngModelChange)='config.save()',
ngbRadioGroup
)
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
value='menu'
)
| Menu
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
value='paste'
)
| Paste
.form-group
label Terminal bell
br
.btn-group(
[(ngModel)]='config.store.terminal.bell',
(ngModelChange)='config.save()',
ngbRadioGroup
)
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='"off"'
)
| Off
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='"visual"'
)
| Visual
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='"audible"'
)
| Audible
.form-group
label Right click
br
.btn-group(
[(ngModel)]='config.store.terminal.rightClick',
(ngModelChange)='config.save()',
ngbRadioGroup
)
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
value='menu'
)
| Context menu
label.btn.btn-secondary(ngbButtonLabel)
input(
type='radio',
ngbButton,
value='paste'
)
| Paste
.form-group
checkbox(
[(ngModel)]='config.store.terminal.autoOpen',
(ngModelChange)='config.save()',
text='Auto-open a terminal on app start',
)
checkbox(
[(ngModel)]='config.store.terminal.bracketedPaste',
(ngModelChange)='config.save()',
text='Bracketed paste (requires shell support)',
)
checkbox(
[(ngModel)]='config.store.terminal.copyOnSelect',
(ngModelChange)='config.save()',
text='Copy on select',
)
checkbox(
[(ngModel)]='config.store.terminal.altIsMeta',
(ngModelChange)='config.save()',
text='Use Alt key as the Meta key',
)

View File

@@ -1,5 +1,6 @@
.appearance-preview {
padding: 10px 20px;
padding: 10px 0;
margin-left: 30px;
margin: 0 0 10px;
overflow: hidden;
span {

View File

@@ -1,5 +1,5 @@
import { BehaviorSubject, Subject, Subscription } from 'rxjs'
import 'rxjs/add/operator/bufferTime'
import { ToastrService } from 'ngx-toastr'
import { Component, NgZone, Inject, Optional, ViewChild, HostBinding, Input } from '@angular/core'
import { AppService, ConfigService, BaseTabComponent, ElectronService, ThemesService, HostAppService, HotkeysService, Platform } from 'terminus-core'
@@ -54,11 +54,12 @@ export class TerminalTabComponent extends BaseTabComponent {
private electron: ElectronService,
private terminalService: TerminalService,
public config: ConfigService,
private toastr: ToastrService,
@Optional() @Inject(TerminalDecorator) private decorators: TerminalDecorator[],
) {
super()
this.decorators = this.decorators || []
this.title = 'Terminal'
this.setTitle('Terminal')
this.resize$.first().subscribe(async (resizeEvent) => {
if (!this.session) {
this.session = this.sessions.addSession(
@@ -89,39 +90,50 @@ export class TerminalTabComponent extends BaseTabComponent {
return
}
switch (hotkey) {
case 'copy':
case 'ctrl-c':
if (this.hterm.getSelectionText()) {
this.hterm.copySelectionToClipboard()
break
case 'clear':
this.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.hterm.getDocument().getSelection().removeAllRanges()
} else {
this.sendInput('\x03')
}
break
case 'copy':
this.hterm.copySelectionToClipboard()
break
case 'paste':
this.paste()
break
case 'clear':
this.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')
@@ -211,11 +223,7 @@ export class TerminalTabComponent extends BaseTabComponent {
}
attachHTermHandlers (hterm: any) {
hterm.setWindowTitle = (title) => {
this.zone.run(() => {
this.title = title
})
}
hterm.setWindowTitle = title => this.zone.run(() => this.setTitle(title))
const _setAlternateMode = hterm.setAlternateMode.bind(hterm)
hterm.setAlternateMode = (state) => {
@@ -223,15 +231,18 @@ export class TerminalTabComponent extends BaseTabComponent {
this.alternateScreenActive$.next(state)
}
const _copySelectionToClipboard = hterm.copySelectionToClipboard.bind(hterm)
hterm.copySelectionToClipboard = () => {
_copySelectionToClipboard()
this.toastr.info('Copied')
}
hterm.primaryScreen_.syncSelectionCaret = () => null
hterm.alternateScreen_.syncSelectionCaret = () => null
hterm.primaryScreen_.terminal = hterm
hterm.alternateScreen_.terminal = hterm
const _onPaste = hterm.scrollPort_.onPaste_.bind(hterm.scrollPort_)
hterm.scrollPort_.onPaste_ = (event) => {
hterm.scrollPort_.pasteTarget_.value = event.clipboardData.getData('text/plain').trim()
_onPaste()
event.preventDefault()
}
@@ -333,7 +344,13 @@ export class TerminalTabComponent extends BaseTabComponent {
}
paste () {
this.sendInput(this.electron.clipboard.readText())
let data = this.electron.clipboard.readText()
data = this.hterm.keyboard.encode(data)
if (this.hterm.options_.bracketedPaste) {
data = '\x1b[200~' + data + '\x1b[201~'
}
data = data.replace(/\r\n/g, '\n')
this.sendInput(data)
}
clear () {
@@ -354,10 +371,12 @@ export class TerminalTabComponent extends BaseTabComponent {
preferenceManager.set('ctrl-plus-minus-zero-zoom', false)
preferenceManager.set('scrollbar-visible', this.hostApp.platform === Platform.macOS)
preferenceManager.set('copy-on-select', config.terminal.copyOnSelect)
preferenceManager.set('alt-is-meta', config.terminal.altIsMeta)
preferenceManager.set('alt-sends-what', 'browser-key')
preferenceManager.set('alt-gr-mode', 'ctrl-alt')
preferenceManager.set('pass-alt-number', true)
preferenceManager.set('cursor-blink', config.terminal.cursorBlink)
preferenceManager.set('clear-selection-after-copy', true)
if (config.terminal.colorScheme.foreground) {
preferenceManager.set('foreground-color', config.terminal.colorScheme.foreground)

View File

@@ -16,6 +16,7 @@ export class TerminalConfigProvider extends ConfigProvider {
rightClick: 'menu',
copyOnSelect: false,
workingDirectory: '',
altIsMeta: false,
colorScheme: {
__nonStructural: true,
name: 'Material',
@@ -53,9 +54,13 @@ export class TerminalConfigProvider extends ConfigProvider {
persistence: 'screen',
},
hotkeys: {
'ctrl-c': ['Ctrl-C'],
'copy': [
'⌘-C',
],
'paste': [
'⌘-V',
],
'clear': [
'⌘-K',
],
@@ -93,9 +98,13 @@ export class TerminalConfigProvider extends ConfigProvider {
copyOnSelect: true,
},
hotkeys: {
'ctrl-c': ['Ctrl-C'],
'copy': [
'Ctrl-Shift-C',
],
'paste': [
'Ctrl-Shift-V',
],
'clear': [
'Ctrl-L',
],
@@ -130,9 +139,13 @@ export class TerminalConfigProvider extends ConfigProvider {
persistence: 'tmux',
},
hotkeys: {
'ctrl-c': ['Ctrl-C'],
'copy': [
'Ctrl-Shift-C',
],
'paste': [
'Ctrl-Shift-V',
],
'clear': [
'Ctrl-L',
],

View File

@@ -8,6 +8,10 @@ export class TerminalHotkeyProvider extends HotkeyProvider {
id: 'copy',
name: 'Copy to clipboard',
},
{
id: 'paste',
name: 'Paste from clipboard',
},
{
id: 'home',
name: 'Beginning of the line',
@@ -52,5 +56,9 @@ export class TerminalHotkeyProvider extends HotkeyProvider {
id: 'new-tab',
name: 'New tab',
},
{
id: 'ctrl-c',
name: 'Intelligent Ctrl-C (copy/abort)',
},
]
}

View File

@@ -2,6 +2,8 @@ import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { FormsModule } from '@angular/forms'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { ToastrModule } from 'ngx-toastr'
import TerminusCorePlugin from 'terminus-core'
import { ToolbarButtonProvider, TabRecoveryProvider, ConfigProvider, HotkeysService, HotkeyProvider, AppService, ConfigService } from 'terminus-core'
import { SettingsTabProvider } from 'terminus-settings'
@@ -41,6 +43,8 @@ import { hterm } from './hterm'
BrowserModule,
FormsModule,
NgbModule,
ToastrModule,
TerminusCorePlugin,
],
providers: [
SessionsService,

View File

@@ -100,7 +100,7 @@ export class Session extends BaseSession {
this.open = true
this.pty.on('data', data => {
this.pty.on('data-buffered', data => {
this.emitOutput(data)
})
@@ -200,7 +200,8 @@ export class SessionsService {
electron: ElectronService,
log: LogService,
) {
nodePTY = electron.remoteRequirePluginModule('terminus-terminal', 'node-pty-tmp', global as any)
const nodePTYPath = electron.remoteResolvePluginModule('terminus-terminal', 'node-pty-tmp', global as any)
nodePTY = electron.remoteRequire('./bufferizedPTY')(nodePTYPath)
this.logger = log.create('sessions')
this.persistenceProviders = this.config.enabledServices(this.persistenceProviders).filter(x => x.isAvailable())
}

View File

@@ -55,6 +55,7 @@ module.exports = {
/^rxjs/,
/^@angular/,
/^@ng-bootstrap/,
'ngx-toastr',
/^terminus-/,
],
plugins: [

View File

@@ -1459,7 +1459,7 @@ electron-to-chromium@^1.2.7:
version "1.3.31"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.31.tgz#00d832cba9fe2358652b0c48a8816c8e3a037e9f"
electron@^1.8.4:
electron@1.8.4:
version "1.8.4"
resolved "https://registry.yarnpkg.com/electron/-/electron-1.8.4.tgz#cca8d0e6889f238f55b414ad224f03e03b226a38"
dependencies: