use profiles for everything

This commit is contained in:
Eugene Pankov 2019-05-11 21:28:04 +02:00
parent bc71547d92
commit 48ff7d7d5a
26 changed files with 159 additions and 101 deletions

View file

@ -23,7 +23,12 @@ export interface IToolbarButton {
weight?: number
click: () => void
click?: () => void
submenu?: () => Promise<IToolbarButton[]>
/** @hidden */
submenuItems?: IToolbarButton[]
}
/**

View file

@ -32,22 +32,44 @@ title-bar(
)
.btn-group.background
button.btn.btn-secondary.btn-tab-bar(
*ngFor='let button of leftToolbarButtons',
[title]='button.title',
(click)='button.click()',
[innerHTML]='button.icon',
.d-flex(
*ngFor='let button of leftToolbarButtons',
ngbDropdown,
(openChange)='generateButtonSubmenu(button)',
)
button.btn.btn-secondary.btn-tab-bar(
[title]='button.title',
(click)='button.click && button.click()',
[innerHTML]='button.icon',
ngbDropdownToggle,
)
div(*ngIf='button.submenu', ngbDropdownMenu)
button.dropdown-item(
*ngFor='let item of button.submenuItems',
(click)='item.click()',
ngbDropdownItem,
) {{item.title}}
.drag-space.background([class.persistent]='config.store.appearance.frame == "thin" && hostApp.platform != Platform.macOS')
.btn-group.background
button.btn.btn-secondary.btn-tab-bar(
*ngFor='let button of rightToolbarButtons',
[title]='button.title',
(click)='button.click()',
[innerHTML]='button.icon',
.d-flex(
*ngFor='let button of rightToolbarButtons',
ngbDropdown,
(openChange)='generateButtonSubmenu(button)',
)
button.btn.btn-secondary.btn-tab-bar(
[title]='button.title',
(click)='button.click && button.click()',
[innerHTML]='button.icon',
ngbDropdownToggle,
)
div(*ngIf='button.submenu', ngbDropdownMenu)
button.dropdown-item(
*ngFor='let item of button.submenuItems',
(click)='item.click()',
ngbDropdownItem,
) {{item.title}}
button.btn.btn-secondary.btn-tab-bar.btn-update(
*ngIf='updatesAvailable',

View file

@ -233,6 +233,12 @@ export class AppRootComponent {
})
}
async generateButtonSubmenu (button: IToolbarButton) {
if (button.submenu) {
button.submenuItems = await button.submenu()
}
}
private getToolbarButtons (aboveZero: boolean): IToolbarButton[] {
let buttons: IToolbarButton[] = []
this.config.enabledServices(this.toolbarButtonProviders).forEach(provider => {

View file

@ -23,6 +23,7 @@ export class StartPageComponent {
return this.config.enabledServices(this.toolbarButtonProviders)
.map(provider => provider.provide())
.reduce((a, b) => a.concat(b))
.filter(x => !!x.click)
.sort((a: IToolbarButton, b: IToolbarButton) => (a.weight || 0) - (b.weight || 0))
}
}

View file

@ -65,6 +65,7 @@ export class TouchbarService {
this.config.enabledServices(this.toolbarButtonProviders).forEach(provider => {
buttons = buttons.concat(provider.provide())
})
buttons = buttons.filter(x => !!x.touchBarNSImage)
buttons.sort((a, b) => (a.weight || 0) - (b.weight || 0))
this.tabSegments = this.app.tabs.map(tab => ({
label: this.shortenTitle(tab.title),

View file

@ -136,6 +136,10 @@ app-root {
background: transparent;
&:hover { background: rgba(0, 0, 0, .25) !important; }
&:active { background: rgba(0, 0, 0, .5) !important; }
&::after {
display: none;
}
}
&>.tabs {

View file

@ -52,6 +52,5 @@
"macos-native-processlist": "^1.0.1",
"windows-native-registry": "^1.0.14",
"@terminus-term/windows-process-tree": "^0.2.4"
},
"false": {}
}
}

View file

@ -35,6 +35,7 @@ export interface SessionOptions {
export interface Profile {
name: string,
sessionOptions: SessionOptions,
isBuiltin?: boolean
}
export interface ITerminalColorScheme {
@ -66,7 +67,7 @@ export interface IShell {
name?: string
command: string
args?: string[]
env?: {[id: string]: string}
env: {[id: string]: string}
/**
* Base path to which shell's internal FS is relative

View file

@ -31,13 +31,27 @@ export class ButtonProvider extends ToolbarButtonProvider {
}
provide (): IToolbarButton[] {
return [{
icon: this.domSanitizer.bypassSecurityTrustHtml(require('./icons/plus.svg')),
title: 'New terminal',
touchBarNSImage: 'NSTouchBarAddDetailTemplate',
click: async () => {
this.terminal.openTab()
}
}]
return [
{
icon: this.domSanitizer.bypassSecurityTrustHtml(require('./icons/plus.svg')),
title: 'New terminal',
touchBarNSImage: 'NSTouchBarAddDetailTemplate',
click: async () => {
this.terminal.openTab()
}
},
{
icon: this.domSanitizer.bypassSecurityTrustHtml(require('./icons/profiles.svg')),
title: 'New terminal with profile',
submenu: async () => {
let profiles = await this.terminal.getProfiles()
return profiles.map(profile => ({
icon: null,
title: profile.name,
click: () => this.terminal.openTab(profile),
}))
}
},
]
}
}

View file

@ -2,17 +2,17 @@ h3.mb-3 Shell
.form-line
.header
.title Shell
.description Default shell for new tabs
.title Profile
.description Default profile for new tabs
select.form-control(
[(ngModel)]='config.store.terminal.shell',
[(ngModel)]='config.store.terminal.profile',
(ngModelChange)='config.save()',
)
option(
*ngFor='let shell of shells',
[ngValue]='shell.id'
) {{shell.name}}
*ngFor='let profile of profiles',
[ngValue]='slug(profile.name)'
) {{profile.name}}
.form-line(*ngIf='isConPTYAvailable')
@ -28,10 +28,10 @@ h3.mb-3 Shell
.alert.alert-info.d-flex.align-items-center(*ngIf='config.store.terminal.useConPTY && isConPTYAvailable && !isConPTYStable')
.mr-auto Windows 10 build 18309 or above is recommended for ConPTY
.alert.alert-info.d-flex.align-items-center(*ngIf='config.store.terminal.shell.startsWith("wsl") && (config.store.terminal.frontend != "hterm" || !config.store.terminal.useConPTY)')
.alert.alert-info.d-flex.align-items-center(*ngIf='config.store.terminal.profile.startsWith("WSL") && (config.store.terminal.frontend != "hterm" || !config.store.terminal.useConPTY)')
.mr-auto WSL terminal only supports TrueColor with ConPTY and the hterm frontend
.form-line(*ngIf='config.store.terminal.shell == "custom"')
.form-line(*ngIf='config.store.terminal.profile == "Custom shell"')
.header
.title Custom shell
@ -66,7 +66,7 @@ h3.mt-3 Saved Profiles
.list-group.list-group-flush.mt-3.mb-3
.list-group-item.list-group-item-action.d-flex.align-items-center(
*ngFor='let profile of profiles',
*ngFor='let profile of config.store.terminal.profiles',
(click)='editProfile(profile)',
)
.mr-auto

View file

@ -1,3 +1,4 @@
import slug from 'slug'
import { Component } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { Subscription } from 'rxjs'
@ -17,6 +18,7 @@ export class ShellSettingsTabComponent {
Platform = Platform
isConPTYAvailable: boolean
isConPTYStable: boolean
slug = slug
private configSubscription: Subscription
constructor (
@ -44,13 +46,12 @@ export class ShellSettingsTabComponent {
this.configSubscription.unsubscribe()
}
reload () {
this.profiles = this.config.store.terminal.profiles
async reload () {
this.profiles = await this.terminalService.getProfiles()
}
pickWorkingDirectory () {
let shell = this.shells.find(x => x.id === this.config.store.terminal.shell)
console.log(shell)
let paths = this.electron.dialog.showOpenDialog(
this.hostApp.getWindow(),
{
@ -68,9 +69,9 @@ export class ShellSettingsTabComponent {
name: shell.name,
sessionOptions: this.terminalService.optionsFromShell(shell),
}
this.profiles.push(profile)
this.config.store.terminal.profiles = this.profiles
this.config.store.terminal.profiles = [profile, ...this.config.store.terminal.profiles]
this.config.save()
this.reload()
}
editProfile (profile: Profile) {
@ -83,8 +84,8 @@ export class ShellSettingsTabComponent {
}
deleteProfile (profile: Profile) {
this.profiles = this.profiles.filter(x => x !== profile)
this.config.store.terminal.profiles = this.profiles
this.config.store.terminal.profiles = this.config.store.terminal.profiles.filter(x => x !== profile)
this.config.save()
this.reload()
}
}

View file

@ -16,9 +16,11 @@ export class TerminalSettingsTabComponent {
openWSLVolumeMixer () {
this.electron.shell.openItem('sndvol.exe')
this.terminal.openTab({
id: '',
command: 'wsl.exe',
args: ['tput', 'bel'],
name: null,
sessionOptions: {
command: 'wsl.exe',
args: ['tput', 'bel'],
},
}, null, true)
}
}

View file

@ -65,6 +65,7 @@ export class TerminalConfigProvider extends ConfigProvider {
terminal: {
font: 'Menlo',
shell: 'default',
profile: 'user-default',
},
hotkeys: {
'ctrl-c': ['Ctrl-C'],
@ -103,6 +104,7 @@ export class TerminalConfigProvider extends ConfigProvider {
terminal: {
font: 'Consolas',
shell: 'clink',
profile: 'cmd-clink',
rightClick: 'paste',
copyOnSelect: true,
},
@ -143,6 +145,7 @@ export class TerminalConfigProvider extends ConfigProvider {
terminal: {
font: 'Liberation Mono',
shell: 'default',
profile: 'user-default',
},
hotkeys: {
'ctrl-c': ['Ctrl-C'],

View file

@ -21,7 +21,7 @@ export class NewTabContextMenu extends TerminalContextMenuItemProvider {
}
async getItems (tab: BaseTerminalTabComponent): Promise<Electron.MenuItemConstructorOptions[]> {
let shells = await this.terminalService.shells$.toPromise()
let profiles = await this.terminalService.getProfiles()
let items: Electron.MenuItemConstructorOptions[] = [
{
@ -31,45 +31,31 @@ export class NewTabContextMenu extends TerminalContextMenuItemProvider {
})
},
{
label: 'New with shell',
submenu: shells.map(shell => ({
label: shell.name,
label: 'New with profile',
submenu: profiles.map(profile => ({
label: profile.name,
click: () => this.zone.run(async () => {
this.terminalService.openTab(shell, await tab.session.getWorkingDirectory())
this.terminalService.openTab(profile, await tab.session.getWorkingDirectory())
}),
})),
}))
},
]
if (this.uac.isAvailable) {
items.push({
label: 'New as admin',
submenu: shells.map(shell => ({
label: shell.name,
label: 'New admin tab',
submenu: profiles.map(profile => ({
label: profile.name,
click: () => this.zone.run(async () => {
let options = this.terminalService.optionsFromShell(shell)
options.runAsAdministrator = true
this.terminalService.openTabWithOptions(options)
this.terminalService.openTabWithOptions({
...profile.sessionOptions,
runAsAdministrator: true
})
}),
})),
})
}
items = items.concat([
{
label: 'New with profile',
submenu: this.config.store.terminal.profiles.length ? this.config.store.terminal.profiles.map(profile => ({
label: profile.name,
click: () => this.zone.run(() => {
this.terminalService.openTabWithOptions(profile.sessionOptions)
}),
})) : [{
label: 'No profiles saved',
enabled: false,
}],
},
])
return items
}
}

View file

@ -1,6 +1,6 @@
import slug from 'slug'
import { Injectable } from '@angular/core'
import { IHotkeyDescription, HotkeyProvider, ConfigService } from 'terminus-core'
import { IHotkeyDescription, HotkeyProvider } from 'terminus-core'
import { TerminalService } from './services/terminal.service'
/** @hidden */
@ -66,19 +66,14 @@ export class TerminalHotkeyProvider extends HotkeyProvider {
]
constructor (
private config: ConfigService,
private terminal: TerminalService,
) { super() }
async provide (): Promise<IHotkeyDescription[]> {
let shells = await this.terminal.shells$.toPromise()
let profiles = await this.terminal.getProfiles()
return [
...this.hotkeys,
...shells.map(shell => ({
id: `shell.${shell.id}`,
name: `New tab: ${shell.name}`
})),
...this.config.store.terminal.profiles.map(profile => ({
...profiles.map(profile => ({
id: `profile.${slug(profile.name)}`,
name: `New tab: ${profile.name}`
})),

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M368 224H224V80c0-8.84-7.16-16-16-16h-32c-8.84 0-16 7.16-16 16v144H16c-8.84 0-16 7.16-16 16v32c0 8.84 7.16 16 16 16h144v144c0 8.84 7.16 16 16 16h32c8.84 0 16-7.16 16-16V288h144c8.84 0 16-7.16 16-16v-32c0-8.84-7.16-16-16-16z"/></svg>
<svg aria-hidden="true" focusable="false" data-prefix="fal" data-icon="plus" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" class="svg-inline--fa fa-plus fa-w-12 fa-3x"><path fill="#ffffff" d="M376 232H216V72c0-4.42-3.58-8-8-8h-32c-4.42 0-8 3.58-8 8v160H8c-4.42 0-8 3.58-8 8v32c0 4.42 3.58 8 8 8h160v160c0 4.42 3.58 8 8 8h32c4.42 0 8-3.58 8-8V280h160c4.42 0 8-3.58 8-8v-32c0-4.42-3.58-8-8-8z" class="" stroke="none" stroke-width="1px"></path></svg>

Before

Width:  |  Height:  |  Size: 303 B

After

Width:  |  Height:  |  Size: 469 B

View file

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fal" data-icon="window-restore" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="svg-inline--fa fa-window-restore fa-w-16 fa-3x"><path fill="#ffffff" d="M464 0H144c-26.5 0-48 21.5-48 48v48H48c-26.5 0-48 21.5-48 48v320c0 26.5 21.5 48 48 48h320c26.5 0 48-21.5 48-48v-48h48c26.5 0 48-21.5 48-48V48c0-26.5-21.5-48-48-48zM32 144c0-8.8 7.2-16 16-16h320c8.8 0 16 7.2 16 16v80H32v-80zm352 320c0 8.8-7.2 16-16 16H48c-8.8 0-16-7.2-16-16V256h352v208zm96-96c0 8.8-7.2 16-16 16h-48V144c0-26.5-21.5-48-48-48H128V48c0-8.8 7.2-16 16-16h320c8.8 0 16 7.2 16 16v320z" class="" stroke="none" stroke-width="1px"></path></svg>

After

Width:  |  Height:  |  Size: 685 B

View file

@ -161,15 +161,8 @@ export default class TerminalModule {
if (hotkey === 'new-window') {
hostApp.newWindow()
}
if (hotkey.startsWith('shell.')) {
let shells = await terminal.shells$.toPromise()
let shell = shells.find(x => x.id === hotkey.split('.')[1])
if (shell) {
terminal.openTab(shell)
}
}
if (hotkey.startsWith('profile.')) {
let profiles = config.store.terminal.profiles
let profiles = await config.store.terminal.getProfiles()
let profile = profiles.find(x => slug(x.name) === hotkey.split('.')[1])
if (profile) {
terminal.openTabWithOptions(profile.sessionOptions)
@ -188,9 +181,11 @@ export default class TerminalModule {
hostApp.cliRunCommand$.subscribe(async command => {
terminal.openTab({
id: '',
command: command[0],
args: command.slice(1),
name: '',
sessionOptions: {
command: command[0],
args: command.slice(1),
},
}, null, true)
hostApp.bringToFront()
})

View file

@ -1,8 +1,9 @@
import * as fs from 'mz/fs'
import slug from 'slug'
import { Observable, AsyncSubject } from 'rxjs'
import { Injectable, Inject } from '@angular/core'
import { AppService, Logger, LogService, ConfigService, SplitTabComponent } from 'terminus-core'
import { IShell, ShellProvider, SessionOptions } from '../api'
import { IShell, ShellProvider, SessionOptions, Profile } from '../api'
import { TerminalTabComponent } from '../components/terminalTab.component'
import { UACService } from './uac.service'
@ -37,6 +38,18 @@ export class TerminalService {
return shellLists.reduce((a, b) => a.concat(b), [])
}
async getProfiles (): Promise<Profile[]> {
let shells = await this.shells$.toPromise()
return [
...this.config.store.terminal.profiles,
...shells.map(shell => ({
name: shell.name,
sessionOptions: this.optionsFromShell(shell),
isBuiltin: true
}))
]
}
private async reloadShells () {
this.shells = new AsyncSubject<IShell[]>()
let shells = await this.getShells()
@ -49,11 +62,16 @@ export class TerminalService {
* Launches a new terminal with a specific shell and CWD
* @param pause Wait for a keypress when the shell exits
*/
async openTab (shell?: IShell, cwd?: string, pause?: boolean): Promise<TerminalTabComponent> {
async openTab (profile?: Profile, cwd?: string, pause?: boolean): Promise<TerminalTabComponent> {
cwd = cwd || profile.sessionOptions.cwd
if (cwd && !fs.existsSync(cwd)) {
console.warn('Ignoring non-existent CWD:', cwd)
cwd = null
}
if (!profile) {
let profiles = await this.getProfiles()
profile = profiles.find(x => slug(x.name) === this.config.store.terminal.profile) || profiles[0]
}
if (!cwd) {
if (this.app.activeTab instanceof TerminalTabComponent && this.app.activeTab.session) {
cwd = await this.app.activeTab.session.getWorkingDirectory()
@ -68,14 +86,10 @@ export class TerminalService {
cwd = cwd || this.config.store.terminal.workingDirectory
cwd = cwd || null
}
if (!shell) {
let shells = await this.shells$.toPromise()
shell = shells.find(x => x.id === this.config.store.terminal.shell) || shells[0]
}
this.logger.log(`Starting shell ${shell.name}`, shell)
this.logger.log(`Starting profile ${profile.name}`, profile)
let sessionOptions = {
...this.optionsFromShell(shell),
...profile.sessionOptions,
pauseAfterExit: pause,
cwd,
}

View file

@ -47,7 +47,8 @@ export class CmderShellProvider extends ShellProvider {
'-noexit',
'-command',
`Invoke-Expression '. ''${path.join(process.env.CMDER_ROOT, 'vendor', 'profile.ps1')}'''`
]
],
env: {},
},
]
}

View file

@ -19,6 +19,7 @@ export class CustomShellProvider extends ShellProvider {
name: 'Custom shell',
command: args[0],
args: args.slice(1),
env: {},
}]
}
}

View file

@ -28,7 +28,8 @@ export class LinuxDefaultShellProvider extends ShellProvider {
return [{
id: 'default',
name: 'User default',
command: '/bin/sh'
command: '/bin/sh',
env: {},
}]
} else {
return [{
@ -36,6 +37,7 @@ export class LinuxDefaultShellProvider extends ShellProvider {
name: 'User default',
command: line.split(':')[6],
args: ['--login'],
env: {},
}]
}
}

View file

@ -23,6 +23,7 @@ export class MacOSDefaultShellProvider extends ShellProvider {
name: 'User default',
command: shellEntry.split(' ')[1].trim(),
args: ['--login'],
env: {},
}]
}
}

View file

@ -24,9 +24,10 @@ export class POSIXShellsProvider extends ShellProvider {
.filter(x => x && !x.startsWith('#'))
.map(x => ({
id: slug(x),
name: x,
name: x.split('/')[2],
command: x,
args: ['-l'],
env: {},
}))
}
}

View file

@ -40,6 +40,7 @@ export class WindowsDefaultShellProvider extends ShellProvider {
...shell,
id: 'default',
name: 'User default',
env: {},
}]
}
}

View file

@ -33,9 +33,10 @@ export class WindowsStockShellsProvider extends ShellProvider {
`clink_${process.arch}.exe`,
),
'inject',
]
],
env: {},
},
{ id: 'cmd', name: 'CMD (stock)', command: 'cmd.exe' },
{ id: 'cmd', name: 'CMD (stock)', command: 'cmd.exe', env: {} },
{
id: 'powershell',
name: 'PowerShell',