diff --git a/terminus-core/src/api/toolbarButtonProvider.ts b/terminus-core/src/api/toolbarButtonProvider.ts index 9e598ebe..12e8b019 100644 --- a/terminus-core/src/api/toolbarButtonProvider.ts +++ b/terminus-core/src/api/toolbarButtonProvider.ts @@ -23,7 +23,12 @@ export interface IToolbarButton { weight?: number - click: () => void + click?: () => void + + submenu?: () => Promise + + /** @hidden */ + submenuItems?: IToolbarButton[] } /** diff --git a/terminus-core/src/components/appRoot.component.pug b/terminus-core/src/components/appRoot.component.pug index 474e01bb..9cdb3eeb 100644 --- a/terminus-core/src/components/appRoot.component.pug +++ b/terminus-core/src/components/appRoot.component.pug @@ -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', diff --git a/terminus-core/src/components/appRoot.component.ts b/terminus-core/src/components/appRoot.component.ts index 3eabb8f0..99997343 100644 --- a/terminus-core/src/components/appRoot.component.ts +++ b/terminus-core/src/components/appRoot.component.ts @@ -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 => { diff --git a/terminus-core/src/components/startPage.component.ts b/terminus-core/src/components/startPage.component.ts index 124a19db..53442f24 100644 --- a/terminus-core/src/components/startPage.component.ts +++ b/terminus-core/src/components/startPage.component.ts @@ -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)) } } diff --git a/terminus-core/src/services/touchbar.service.ts b/terminus-core/src/services/touchbar.service.ts index 1eda0d7c..33d56fcf 100644 --- a/terminus-core/src/services/touchbar.service.ts +++ b/terminus-core/src/services/touchbar.service.ts @@ -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), diff --git a/terminus-core/src/theme.scss b/terminus-core/src/theme.scss index 326aa4a4..1cf4f421 100644 --- a/terminus-core/src/theme.scss +++ b/terminus-core/src/theme.scss @@ -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 { diff --git a/terminus-terminal/package.json b/terminus-terminal/package.json index fe82322c..23c2c5bc 100644 --- a/terminus-terminal/package.json +++ b/terminus-terminal/package.json @@ -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": {} + } } diff --git a/terminus-terminal/src/api.ts b/terminus-terminal/src/api.ts index 3f705337..0b81a208 100644 --- a/terminus-terminal/src/api.ts +++ b/terminus-terminal/src/api.ts @@ -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 diff --git a/terminus-terminal/src/buttonProvider.ts b/terminus-terminal/src/buttonProvider.ts index 90b877c8..ba72033a 100644 --- a/terminus-terminal/src/buttonProvider.ts +++ b/terminus-terminal/src/buttonProvider.ts @@ -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), + })) + } + }, + ] } } diff --git a/terminus-terminal/src/components/shellSettingsTab.component.pug b/terminus-terminal/src/components/shellSettingsTab.component.pug index 68c0c1b1..c8d8df93 100644 --- a/terminus-terminal/src/components/shellSettingsTab.component.pug +++ b/terminus-terminal/src/components/shellSettingsTab.component.pug @@ -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 diff --git a/terminus-terminal/src/components/shellSettingsTab.component.ts b/terminus-terminal/src/components/shellSettingsTab.component.ts index 66ce3d3d..904ee839 100644 --- a/terminus-terminal/src/components/shellSettingsTab.component.ts +++ b/terminus-terminal/src/components/shellSettingsTab.component.ts @@ -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() } } diff --git a/terminus-terminal/src/components/terminalSettingsTab.component.ts b/terminus-terminal/src/components/terminalSettingsTab.component.ts index 69e4248d..85454a25 100644 --- a/terminus-terminal/src/components/terminalSettingsTab.component.ts +++ b/terminus-terminal/src/components/terminalSettingsTab.component.ts @@ -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) } } diff --git a/terminus-terminal/src/config.ts b/terminus-terminal/src/config.ts index b0660b52..57b2384c 100644 --- a/terminus-terminal/src/config.ts +++ b/terminus-terminal/src/config.ts @@ -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'], diff --git a/terminus-terminal/src/contextMenu.ts b/terminus-terminal/src/contextMenu.ts index 70617ad0..645c19a4 100644 --- a/terminus-terminal/src/contextMenu.ts +++ b/terminus-terminal/src/contextMenu.ts @@ -21,7 +21,7 @@ export class NewTabContextMenu extends TerminalContextMenuItemProvider { } async getItems (tab: BaseTerminalTabComponent): Promise { - 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 } } diff --git a/terminus-terminal/src/hotkeys.ts b/terminus-terminal/src/hotkeys.ts index 74a9b0ef..f79f4b6b 100644 --- a/terminus-terminal/src/hotkeys.ts +++ b/terminus-terminal/src/hotkeys.ts @@ -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 { - 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}` })), diff --git a/terminus-terminal/src/icons/plus.svg b/terminus-terminal/src/icons/plus.svg index e4774d87..8abefac7 100644 --- a/terminus-terminal/src/icons/plus.svg +++ b/terminus-terminal/src/icons/plus.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/terminus-terminal/src/icons/profiles.svg b/terminus-terminal/src/icons/profiles.svg new file mode 100644 index 00000000..1f9b929e --- /dev/null +++ b/terminus-terminal/src/icons/profiles.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/terminus-terminal/src/index.ts b/terminus-terminal/src/index.ts index 7fed255e..7f691dd7 100644 --- a/terminus-terminal/src/index.ts +++ b/terminus-terminal/src/index.ts @@ -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() }) diff --git a/terminus-terminal/src/services/terminal.service.ts b/terminus-terminal/src/services/terminal.service.ts index 77a16d1b..7e52b083 100644 --- a/terminus-terminal/src/services/terminal.service.ts +++ b/terminus-terminal/src/services/terminal.service.ts @@ -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 { + 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() 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 { + async openTab (profile?: Profile, cwd?: string, pause?: boolean): Promise { + 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, } diff --git a/terminus-terminal/src/shells/cmder.ts b/terminus-terminal/src/shells/cmder.ts index afb91f6e..dc38c590 100644 --- a/terminus-terminal/src/shells/cmder.ts +++ b/terminus-terminal/src/shells/cmder.ts @@ -47,7 +47,8 @@ export class CmderShellProvider extends ShellProvider { '-noexit', '-command', `Invoke-Expression '. ''${path.join(process.env.CMDER_ROOT, 'vendor', 'profile.ps1')}'''` - ] + ], + env: {}, }, ] } diff --git a/terminus-terminal/src/shells/custom.ts b/terminus-terminal/src/shells/custom.ts index 487b8483..a17a0637 100644 --- a/terminus-terminal/src/shells/custom.ts +++ b/terminus-terminal/src/shells/custom.ts @@ -19,6 +19,7 @@ export class CustomShellProvider extends ShellProvider { name: 'Custom shell', command: args[0], args: args.slice(1), + env: {}, }] } } diff --git a/terminus-terminal/src/shells/linuxDefault.ts b/terminus-terminal/src/shells/linuxDefault.ts index e9015dd8..e6aef68b 100644 --- a/terminus-terminal/src/shells/linuxDefault.ts +++ b/terminus-terminal/src/shells/linuxDefault.ts @@ -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: {}, }] } } diff --git a/terminus-terminal/src/shells/macDefault.ts b/terminus-terminal/src/shells/macDefault.ts index 04df0acd..ee110b51 100644 --- a/terminus-terminal/src/shells/macDefault.ts +++ b/terminus-terminal/src/shells/macDefault.ts @@ -23,6 +23,7 @@ export class MacOSDefaultShellProvider extends ShellProvider { name: 'User default', command: shellEntry.split(' ')[1].trim(), args: ['--login'], + env: {}, }] } } diff --git a/terminus-terminal/src/shells/posix.ts b/terminus-terminal/src/shells/posix.ts index 4ca141d9..2d2f2e54 100644 --- a/terminus-terminal/src/shells/posix.ts +++ b/terminus-terminal/src/shells/posix.ts @@ -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: {}, })) } } diff --git a/terminus-terminal/src/shells/winDefault.ts b/terminus-terminal/src/shells/winDefault.ts index ab03295b..3f08e986 100644 --- a/terminus-terminal/src/shells/winDefault.ts +++ b/terminus-terminal/src/shells/winDefault.ts @@ -40,6 +40,7 @@ export class WindowsDefaultShellProvider extends ShellProvider { ...shell, id: 'default', name: 'User default', + env: {}, }] } } diff --git a/terminus-terminal/src/shells/windowsStock.ts b/terminus-terminal/src/shells/windowsStock.ts index 3e95db98..5691ec6a 100644 --- a/terminus-terminal/src/shells/windowsStock.ts +++ b/terminus-terminal/src/shells/windowsStock.ts @@ -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',