new profile system

This commit is contained in:
Eugene Pankov
2021-07-04 12:23:27 +02:00
parent 38b7e44f64
commit 92b34fbc08
104 changed files with 2029 additions and 2205 deletions

View File

@@ -29,7 +29,6 @@
"ps-node": "^0.1.6",
"runes": "^0.4.2",
"shell-escape": "^0.2.0",
"slugify": "^1.5.3",
"utils-decorators": "^1.8.3"
},
"peerDependencies": {

View File

@@ -1,6 +1,8 @@
import { Profile } from 'tabby-core'
export interface Shell {
id: string
name?: string
name: string
command: string
args?: string[]
env: Record<string, string>
@@ -40,14 +42,8 @@ export interface SessionOptions {
runAsAdministrator?: boolean
}
export interface Profile {
name: string
color?: string
sessionOptions: SessionOptions
shell?: string
isBuiltin?: boolean
icon?: string
disableDynamicTitle?: boolean
export interface LocalProfile extends Profile {
options: SessionOptions
}
export interface ChildProcess {

View File

@@ -1,37 +1,17 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Injectable } from '@angular/core'
import { ToolbarButtonProvider, ToolbarButton, ConfigService, SelectorOption, SelectorService } from 'tabby-core'
import { ElectronService } from 'tabby-electron'
import { ToolbarButtonProvider, ToolbarButton } from 'tabby-core'
import { TerminalService } from './services/terminal.service'
/** @hidden */
@Injectable()
export class ButtonProvider extends ToolbarButtonProvider {
constructor (
electron: ElectronService,
private selector: SelectorService,
private config: ConfigService,
private terminal: TerminalService,
) {
super()
}
async activate () {
const options: SelectorOption<void>[] = []
const profiles = await this.terminal.getProfiles({ skipDefault: !this.config.store.terminal.showDefaultProfiles })
for (const profile of profiles) {
options.push({
icon: profile.icon,
name: profile.name,
callback: () => this.terminal.openTab(profile),
})
}
await this.selector.show('Select profile', options)
}
provide (): ToolbarButton[] {
return [
{
@@ -42,11 +22,6 @@ export class ButtonProvider extends ToolbarButtonProvider {
this.terminal.openTab()
},
},
{
icon: require('./icons/profiles.svg'),
title: 'New terminal with profile',
click: () => this.activate(),
},
]
}
}

View File

@@ -10,7 +10,6 @@ export class TerminalCLIHandler extends CLIHandler {
priority = 0
constructor (
private config: ConfigService,
private hostWindow: HostWindowService,
private terminal: TerminalService,
) {
@@ -24,8 +23,6 @@ export class TerminalCLIHandler extends CLIHandler {
this.handleOpenDirectory(path.resolve(event.cwd, event.argv.directory))
} else if (op === 'run') {
this.handleRunCommand(event.argv.command)
} else if (op === 'profile') {
this.handleOpenProfile(event.argv.profileName)
} else {
return false
}
@@ -47,24 +44,15 @@ export class TerminalCLIHandler extends CLIHandler {
private handleRunCommand (command: string[]) {
this.terminal.openTab({
type: 'local',
name: '',
sessionOptions: {
options: {
command: command[0],
args: command.slice(1),
},
}, null, true)
this.hostWindow.bringToFront()
}
private handleOpenProfile (profileName: string) {
const profile = this.config.store.terminal.profiles.find(x => x.name === profileName)
if (!profile) {
console.error('Requested profile', profileName, 'not found')
return
}
this.terminal.openTabWithOptions(profile.sessionOptions)
this.hostWindow.bringToFront()
}
}

View File

@@ -1,72 +1,64 @@
.modal-header
h3.m-0 {{profile.name}}
.modal-body
.form-group
label Name
input.form-control(
type='text',
autofocus,
[(ngModel)]='profile.name',
)
.row
.col-12.col-lg-4
.form-group
label Name
input.form-control(
type='text',
autofocus,
[(ngModel)]='profile.name',
)
.form-group
label Command
input.form-control(
type='text',
[(ngModel)]='profile.sessionOptions.command',
)
.form-group
label Group
input.form-control(
type='text',
alwaysVisibleTypeahead,
placeholder='Ungrouped',
[(ngModel)]='profile.group',
[ngbTypeahead]='groupTypeahead',
)
.form-group
label Arguments
.input-group(
*ngFor='let arg of profile.sessionOptions.args; index as i; trackBy: trackByIndex',
)
input.form-control(
type='text',
[(ngModel)]='profile.sessionOptions.args[i]',
)
.input-group-append
button.btn.btn-secondary((click)='profile.sessionOptions.args.splice(i, 1)')
i.fas.fa-trash
.form-group
label Icon
.input-group
input.form-control(
type='text',
alwaysVisibleTypeahead,
[(ngModel)]='profile.icon',
[ngbTypeahead]='iconSearch',
[resultTemplate]='rt'
)
.input-group-append
.input-group-text
i([class]='"fa-fw " + profile.icon')
.mt-2
button.btn.btn-secondary((click)='profile.sessionOptions.args.push("")')
i.fas.fa-plus.mr-2
| Add
ng-template(#rt,let-r='result',let-t='term')
i([class]='"fa-fw " + r')
ngb-highlight.ml-2([result]='r', [term]='t')
.form-line(*ngIf='uac.isAvailable')
.header
.title Run as administrator
toggle(
[(ngModel)]='profile.sessionOptions.runAsAdministrator',
)
.form-line
.header
.title Color
input.form-control.w-50(
type='text',
[(ngModel)]='profile.color',
placeholder='#000000'
)
.form-group
label Working directory
input.form-control(
type='text',
[(ngModel)]='profile.sessionOptions.cwd',
)
.form-line
.header
.title Disable dynamic tab title
.description Connection name will be used instead
toggle([(ngModel)]='profile.disableDynamicTitle')
.form-group
label Environment
environment-editor(
type='text',
[(model)]='profile.sessionOptions.env',
)
.mb-4
.form-group
label Tab color
input.form-control(
type='text',
autofocus,
[(ngModel)]='profile.color',
placeholder='#000000'
)
.form-line
.header
.title Disable dynamic tab title
.description Connection name will be used as a title instead
toggle([(ngModel)]='profile.disableDynamicTitle')
.col-12.col-lg-8
ng-template(#placeholder)
.modal-footer
button.btn.btn-outline-primary((click)='save()') Save

View File

@@ -1,36 +1,76 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Component } from '@angular/core'
import { Observable, OperatorFunction } from 'rxjs'
import { debounceTime, map, distinctUntilChanged } from 'rxjs/operators'
import { Component, Input, ViewChild, ViewContainerRef, ComponentFactoryResolver, Injector } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { UACService } from '../services/uac.service'
import { Profile } from '../api'
import { LocalProfile } from '../api'
import { ConfigService, Profile, ProfileProvider, ProfileSettingsComponent } from 'tabby-core'
const iconsData = require('../../../tabby-core/src/icons.json')
const iconsClassList = Object.keys(iconsData).map(
icon => iconsData[icon].map(
style => `fa${style[0]} fa-${icon}`
)
).flat()
/** @hidden */
@Component({
template: require('./editProfileModal.component.pug'),
})
export class EditProfileModalComponent {
profile: Profile
@Input() profile: LocalProfile
@Input() profileProvider: ProfileProvider
@Input() settingsComponent: new () => ProfileSettingsComponent
groupNames: string[]
@ViewChild('placeholder', { read: ViewContainerRef }) placeholder: ViewContainerRef
private settingsComponentInstance: ProfileSettingsComponent
constructor (
public uac: UACService,
private injector: Injector,
private componentFactoryResolver: ComponentFactoryResolver,
config: ConfigService,
private modalInstance: NgbActiveModal,
) {
this.groupNames = [...new Set(
(config.store.profiles as Profile[])
.map(x => x.group)
.filter(x => !!x)
)].sort() as string[]
}
ngOnInit () {
this.profile.sessionOptions.env = this.profile.sessionOptions.env ?? {}
this.profile.sessionOptions.args = this.profile.sessionOptions.args ?? []
ngAfterViewInit () {
setTimeout(() => {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.profileProvider.settingsComponent)
const componentRef = componentFactory.create(this.injector)
this.settingsComponentInstance = componentRef.instance
this.settingsComponentInstance.profile = this.profile
this.placeholder.insert(componentRef.hostView)
})
}
groupTypeahead = (text$: Observable<string>) =>
text$.pipe(
debounceTime(200),
distinctUntilChanged(),
map(q => this.groupNames.filter(x => !q || x.toLowerCase().includes(q.toLowerCase())))
)
iconSearch: OperatorFunction<string, string[]> = (text$: Observable<string>) =>
text$.pipe(
debounceTime(200),
map(term => iconsClassList.filter(v => v.toLowerCase().includes(term.toLowerCase())).slice(0, 10))
)
save () {
this.profile.group ||= undefined
this.settingsComponentInstance.save?.()
this.modalInstance.close(this.profile)
}
cancel () {
this.modalInstance.dismiss()
}
trackByIndex (index) {
return index
}
}

View File

@@ -0,0 +1,51 @@
.form-group
label Command
input.form-control(
type='text',
[(ngModel)]='profile.options.command',
)
.form-group
label Arguments
.input-group(
*ngFor='let arg of profile.options.args; index as i; trackBy: trackByIndex',
)
input.form-control(
type='text',
[(ngModel)]='profile.options.args[i]',
)
.input-group-append
button.btn.btn-secondary((click)='profile.options.args.splice(i, 1)')
i.fas.fa-trash
.mt-2
button.btn.btn-secondary((click)='profile.options.args.push("")')
i.fas.fa-plus.mr-2
| Add
.form-line(*ngIf='uac.isAvailable')
.header
.title Run as administrator
toggle(
[(ngModel)]='profile.options.runAsAdministrator',
)
.form-group
label Working directory
.input-group
input.form-control(
type='text',
placeholder='Home directory',
[(ngModel)]='profile.options.cwd'
)
.input-group-append
button.btn.btn-secondary((click)='pickWorkingDirectory()')
i.fas.fa-folder-open
.form-group
label Environment
environment-editor(
type='text',
[(model)]='profile.options.env',
)

View File

@@ -0,0 +1,47 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Component } from '@angular/core'
import { UACService } from '../services/uac.service'
import { LocalProfile } from '../api'
import { ElectronHostWindow, ElectronService } from 'tabby-electron'
import { ProfileSettingsComponent } from '../../../tabby-core/src/api/profileProvider'
/** @hidden */
@Component({
template: require('./localProfileSettings.component.pug'),
})
export class LocalProfileSettingsComponent implements ProfileSettingsComponent {
profile: LocalProfile
constructor (
public uac: UACService,
private hostWindow: ElectronHostWindow,
private electron: ElectronService,
) { }
ngOnInit () {
this.profile.options.env = this.profile.options.env ?? {}
this.profile.options.args = this.profile.options.args ?? []
}
async pickWorkingDirectory (): Promise<void> {
// const profile = await this.terminal.getProfileByID(this.config.store.terminal.profile)
// const shell = this.shells.find(x => x.id === profile?.shell)
// if (!shell) {
// return
// }
const paths = (await this.electron.dialog.showOpenDialog(
this.hostWindow.getWindow(),
{
// TODO
// defaultPath: shell.fsBase,
properties: ['openDirectory', 'showHiddenFiles'],
}
)).filePaths
this.profile.options.cwd = paths[0]
}
trackByIndex (index) {
return index
}
}

View File

@@ -0,0 +1,94 @@
h3.mb-3 Profiles
.form-line
.header
.title Default profile for new tabs
select.form-control(
[(ngModel)]='config.store.terminal.profile',
(ngModelChange)='config.save()',
)
option(
*ngFor='let profile of profiles',
[ngValue]='profile.id'
) {{profile.name}}
option(
*ngFor='let profile of builtinProfiles',
[ngValue]='profile.id'
) {{profile.name}}
.form-line(*ngIf='config.store.profiles.length > 0')
.header
.title Show built-in profiles in selector
.description If disabled, only custom profiles will show up in the profile selector
toggle(
[(ngModel)]='config.store.terminal.showBuiltinProfiles',
(ngModelChange)='config.save()'
)
.d-flex.mb-3.mt-4
.input-group
.input-group-prepend
.input-group-text
i.fas.fa-fw.fa-search
input.form-control(type='search', placeholder='Filter', [(ngModel)]='filter')
button.btn.btn-primary.flex-shrink-0.ml-3((click)='newProfile()')
i.fas.fa-fw.fa-plus
| New profile
.list-group.list-group-light.mt-3.mb-3
ng-container(*ngFor='let group of profileGroups')
ng-container(*ngIf='isGroupVisible(group)')
.list-group-item.list-group-item-action.d-flex.align-items-center(
(click)='group.collapsed = !group.collapsed'
)
.fa.fa-fw.fa-chevron-right(*ngIf='group.collapsed')
.fa.fa-fw.fa-chevron-down(*ngIf='!group.collapsed')
span.ml-3.mr-auto {{group.name || "Ungrouped"}}
button.btn.btn-sm.btn-link.hover-reveal.ml-2(
*ngIf='group.editable && group.name',
(click)='$event.stopPropagation(); editGroup(group)'
)
i.fas.fa-pencil-alt
button.btn.btn-sm.btn-link.hover-reveal.ml-2(
*ngIf='group.editable && group.name',
(click)='$event.stopPropagation(); deleteGroup(group)'
)
i.fas.fa-trash
ng-container(*ngIf='!group.collapsed')
ng-container(*ngFor='let profile of group.profiles')
.list-group-item.pl-5.d-flex.align-items-center(
*ngIf='isProfileVisible(profile)',
[class.list-group-item-action]='!profile.isBuiltin',
(click)='profile.isBuiltin ? null : editProfile(profile)'
)
i.icon(
class='fa-fw {{profile.icon}}',
[style.color]='profile.color',
*ngIf='!iconIsSVG(profile.icon)'
)
.icon(
[fastHtmlBind]='profile.icon',
*ngIf='iconIsSVG(profile.icon)'
)
div {{profile.name}}
.text-muted.ml-2 {{getDescription(profile)}}
.mr-auto
button.btn.btn-link.hover-reveal.ml-1((click)='$event.stopPropagation(); launchProfile(profile)')
i.fas.fa-play
button.btn.btn-link.hover-reveal.ml-1((click)='$event.stopPropagation(); newProfile(profile)')
i.fas.fa-copy
button.btn.btn-link.text-danger.hover-reveal.ml-1(
*ngIf='!profile.isBuiltin',
(click)='$event.stopPropagation(); deleteProfile(profile)'
)
i.fas.fa-trash
.ml-1(class='badge badge-{{getTypeColorClass(profile)}}') {{getTypeLabel(profile)}}

View File

@@ -0,0 +1,201 @@
import { v4 as uuidv4 } from 'uuid'
import slugify from 'slugify'
import deepClone from 'clone-deep'
import { Component } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { ConfigService, HostAppService, Profile, SelectorService, ProfilesService, PromptModalComponent, PlatformService, BaseComponent } from 'tabby-core'
import { EditProfileModalComponent } from './editProfileModal.component'
interface ProfileGroup {
name?: string
profiles: Profile[]
editable: boolean
collapsed: boolean
}
/** @hidden */
@Component({
template: require('./profilesSettingsTab.component.pug'),
})
export class ProfilesSettingsTabComponent extends BaseComponent {
profiles: Profile[] = []
builtinProfiles: Profile[] = []
templateProfiles: Profile[] = []
profileGroups: ProfileGroup[]
filter = ''
constructor (
public config: ConfigService,
public hostApp: HostAppService,
private profilesService: ProfilesService,
private selector: SelectorService,
private ngbModal: NgbModal,
private platform: PlatformService,
) {
super()
}
async ngOnInit (): Promise<void> {
this.refresh()
this.builtinProfiles = (await this.profilesService.getProfiles()).filter(x => x.isBuiltin)
this.templateProfiles = this.builtinProfiles.filter(x => x.isTemplate)
this.builtinProfiles = this.builtinProfiles.filter(x => !x.isTemplate)
this.refresh()
this.subscribeUntilDestroyed(this.config.changed$, () => this.refresh())
}
launchProfile (profile: Profile): void {
this.profilesService.openNewTabForProfile(profile)
}
async newProfile (base?: Profile): Promise<void> {
if (!base) {
const profiles = [...this.templateProfiles, ...this.builtinProfiles, ...this.profiles]
base = await this.selector.show(
'Select a base profile to use as a template',
profiles.map(p => ({
icon: p.icon,
description: this.profilesService.providerForProfile(p)?.getDescription(p),
name: p.group ? `${p.group} / ${p.name}` : p.name,
result: p,
})),
)
}
const profile = deepClone(base)
profile.id = null
profile.name = ''
profile.isBuiltin = false
profile.isTemplate = false
await this.editProfile(profile)
profile.id = `${profile.type}:custom:${slugify(profile.name)}:${uuidv4()}`
this.config.store.profiles = [profile, ...this.config.store.profiles]
await this.config.save()
}
async editProfile (profile: Profile): Promise<void> {
const modal = this.ngbModal.open(
EditProfileModalComponent,
{ size: 'lg' },
)
modal.componentInstance.profile = Object.assign({}, profile)
modal.componentInstance.profileProvider = this.profilesService.providerForProfile(profile)
const result = await modal.result
Object.assign(profile, result)
await this.config.save()
}
async deleteProfile (profile: Profile): Promise<void> {
if ((await this.platform.showMessageBox(
{
type: 'warning',
message: `Delete "${profile.name}"?`,
buttons: ['Keep', 'Delete'],
defaultId: 0,
}
)).response === 1) {
this.profilesService.providerForProfile(profile)?.deleteProfile(profile)
this.config.store.profiles = this.config.store.profiles.filter(x => x !== profile)
await this.config.save()
}
}
refresh (): void {
this.profiles = this.config.store.profiles
this.profileGroups = []
for (const profile of this.profiles) {
let group = this.profileGroups.find(x => x.name === profile.group)
if (!group) {
group = {
name: profile.group,
profiles: [],
editable: true,
collapsed: false,
}
this.profileGroups.push(group)
}
group.profiles.push(profile)
}
this.profileGroups.sort((a, b) => a.name?.localeCompare(b.name ?? '') ?? -1)
this.profileGroups.push({
name: 'Built-in',
profiles: this.builtinProfiles,
editable: false,
collapsed: false,
})
}
async editGroup (group: ProfileGroup): Promise<void> {
const modal = this.ngbModal.open(PromptModalComponent)
modal.componentInstance.prompt = 'New name'
modal.componentInstance.value = group.name
const result = await modal.result
if (result) {
for (const profile of this.profiles.filter(x => x.group === group.name)) {
profile.group = result.value
}
this.config.store.profiles = this.profiles
await this.config.save()
}
}
async deleteGroup (group: ProfileGroup): Promise<void> {
if ((await this.platform.showMessageBox(
{
type: 'warning',
message: `Delete "${group.name}"?`,
buttons: ['Keep', 'Delete'],
defaultId: 0,
}
)).response === 1) {
if ((await this.platform.showMessageBox(
{
type: 'warning',
message: `Delete the group's profiles?`,
buttons: ['Move to "Ungrouped"', 'Delete'],
defaultId: 0,
}
)).response === 0) {
for (const profile of this.profiles.filter(x => x.group === group.name)) {
delete profile.group
}
} else {
this.config.store.profiles = this.config.store.profiles.filter(x => x.group !== group.name)
}
await this.config.save()
}
}
isGroupVisible (group: ProfileGroup): boolean {
return !this.filter || group.profiles.some(x => this.isProfileVisible(x))
}
isProfileVisible (profile: Profile): boolean {
return !this.filter || profile.name.toLowerCase().includes(this.filter.toLowerCase())
}
iconIsSVG (icon?: string): boolean {
return icon?.startsWith('<') ?? false
}
getDescription (profile: Profile): string|null {
return this.profilesService.providerForProfile(profile)?.getDescription(profile) ?? null
}
getTypeLabel (profile: Profile): string {
const name = this.profilesService.providerForProfile(profile)?.name
if (name === 'Local') {
return ''
}
return name ?? 'Unknown'
}
getTypeColorClass (profile: Profile): string {
return {
ssh: 'secondary',
serial: 'success',
}[this.profilesService.providerForProfile(profile)?.id ?? ''] ?? 'warning'
}
}

View File

@@ -1,20 +1,5 @@
h3.mb-3 Shell
.form-line
.header
.title Profile
.description Default profile for new tabs
select.form-control(
[(ngModel)]='config.store.terminal.profile',
(ngModelChange)='config.save()',
)
option(
*ngFor='let profile of profiles',
[ngValue]='terminal.getProfileID(profile)'
) {{profile.name}}
.form-line(*ngIf='isConPTYAvailable')
.header
.title Use ConPTY
@@ -30,75 +15,3 @@ h3.mb-3 Shell
.alert.alert-info.d-flex.align-items-center(*ngIf='config.store.terminal.profile.startsWith("WSL") && (!config.store.terminal.useConPTY)')
.mr-auto WSL terminal only supports TrueColor with ConPTY
.form-line(*ngIf='config.store.terminal.profile == "custom-shell"')
.header
.title Custom shell
input.form-control(
type='text',
[(ngModel)]='config.store.terminal.customShell',
(ngModelChange)='config.save()',
)
.form-line
.header
.title Working directory
.input-group
input.form-control(
type='text',
placeholder='Home directory',
[(ngModel)]='config.store.terminal.workingDirectory',
(ngModelChange)='config.save()',
)
.input-group-append
button.btn.btn-secondary((click)='pickWorkingDirectory()')
i.fas.fa-folder-open
.form-line
.header
.title Directory for new tabs
select.form-control(
[(ngModel)]='config.store.terminal.alwaysUseWorkingDirectory',
(ngModelChange)='config.save()',
)
option([ngValue]='false') Same as active tab's directory
option([ngValue]='true') The working directory from above
.form-line.align-items-start
.header
.title Environment
.description Inject additional environment variables
environment-editor([(model)]='this.config.store.terminal.environment')
.form-line(*ngIf='config.store.terminal.profiles.length > 0')
.header
.title Show default profiles in the selector
.description If disabled, only custom profiles will show up in the profile selector
toggle(
[(ngModel)]='config.store.terminal.showDefaultProfiles',
(ngModelChange)='config.save()'
)
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 config.store.terminal.profiles',
(click)='editProfile(profile)',
)
.mr-auto
div {{profile.name}}
.text-muted {{profile.sessionOptions.command}}
button.btn.btn-outline-danger.ml-1((click)='$event.stopPropagation(); deleteProfile(profile)')
i.fas.fa-trash
.pb-4(ngbDropdown, placement='top-left')
button.btn.btn-primary(ngbDropdownToggle)
i.fas.fa-fw.fa-plus
| New profile
div(ngbDropdownMenu)
button.dropdown-item(*ngFor='let shell of shells', (click)='newProfile(shell)') {{shell.name}}

View File

@@ -1,93 +1,18 @@
import { Component } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { Subscription } from 'rxjs'
import { ConfigService, HostAppService, Platform, WIN_BUILD_CONPTY_SUPPORTED, WIN_BUILD_CONPTY_STABLE, isWindowsBuild } from 'tabby-core'
import { ElectronService, ElectronHostWindow } from 'tabby-electron'
import { EditProfileModalComponent } from './editProfileModal.component'
import { Shell, Profile } from '../api'
import { TerminalService } from '../services/terminal.service'
import { WIN_BUILD_CONPTY_SUPPORTED, WIN_BUILD_CONPTY_STABLE, isWindowsBuild, ConfigService } from 'tabby-core'
/** @hidden */
@Component({
template: require('./shellSettingsTab.component.pug'),
})
export class ShellSettingsTabComponent {
shells: Shell[] = []
profiles: Profile[] = []
Platform = Platform
isConPTYAvailable: boolean
isConPTYStable: boolean
private configSubscription: Subscription
constructor (
public config: ConfigService,
public hostApp: HostAppService,
public hostWindow: ElectronHostWindow,
public terminal: TerminalService,
private electron: ElectronService,
private ngbModal: NgbModal,
) {
config.store.terminal.environment = config.store.terminal.environment || {}
this.configSubscription = this.config.changed$.subscribe(() => {
this.reload()
})
this.reload()
this.isConPTYAvailable = isWindowsBuild(WIN_BUILD_CONPTY_SUPPORTED)
this.isConPTYStable = isWindowsBuild(WIN_BUILD_CONPTY_STABLE)
}
async ngOnInit (): Promise<void> {
this.shells = (await this.terminal.shells$.toPromise())!
}
ngOnDestroy (): void {
this.configSubscription.unsubscribe()
}
async reload (): Promise<void> {
this.profiles = await this.terminal.getProfiles({ includeHidden: true })
}
async pickWorkingDirectory (): Promise<void> {
const profile = await this.terminal.getProfileByID(this.config.store.terminal.profile)
const shell = this.shells.find(x => x.id === profile?.shell)
if (!shell) {
return
}
const paths = (await this.electron.dialog.showOpenDialog(
this.hostWindow.getWindow(),
{
defaultPath: shell.fsBase,
properties: ['openDirectory', 'showHiddenFiles'],
}
)).filePaths
this.config.store.terminal.workingDirectory = paths[0]
}
newProfile (shell: Shell): void {
const profile: Profile = {
name: shell.name ?? '',
shell: shell.id,
sessionOptions: this.terminal.optionsFromShell(shell),
}
this.config.store.terminal.profiles = [profile, ...this.config.store.terminal.profiles]
this.config.save()
this.reload()
}
editProfile (profile: Profile): void {
const modal = this.ngbModal.open(EditProfileModalComponent)
modal.componentInstance.profile = Object.assign({}, profile)
modal.result.then(result => {
Object.assign(profile, result)
this.config.save()
})
}
deleteProfile (profile: Profile): void {
this.config.store.terminal.profiles = this.config.store.terminal.profiles.filter(x => x !== profile)
this.config.save()
this.reload()
}
}

View File

@@ -3,6 +3,7 @@ import { BaseTabProcess, WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild } from 'tabb
import { BaseTerminalTabComponent } from 'tabby-terminal'
import { SessionOptions } from '../api'
import { Session } from '../session'
import { UACService } from '../services/uac.service'
/** @hidden */
@Component({
@@ -18,6 +19,7 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
constructor (
injector: Injector,
private uac: UACService,
) {
super(injector)
}
@@ -52,6 +54,10 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
}
initializeSession (columns: number, rows: number): void {
if (this.sessionOptions.runAsAdministrator && this.uac.isAvailable) {
this.sessionOptions = this.uac.patchSessionOptionsForUAC(this.sessionOptions)
}
this.session!.start({
...this.sessionOptions,
width: columns,

View File

@@ -14,11 +14,8 @@ export class TerminalConfigProvider extends ConfigProvider {
},
terminal: {
autoOpen: false,
customShell: '',
workingDirectory: '',
alwaysUseWorkingDirectory: false,
useConPTY: true,
showDefaultProfiles: true,
showBuiltinProfiles: true,
environment: {},
profiles: [],
},
@@ -28,7 +25,7 @@ export class TerminalConfigProvider extends ConfigProvider {
[Platform.macOS]: {
terminal: {
shell: 'default',
profile: 'user-default',
profile: 'local:user-default',
},
hotkeys: {
'new-tab': [
@@ -39,7 +36,7 @@ export class TerminalConfigProvider extends ConfigProvider {
[Platform.Windows]: {
terminal: {
shell: 'clink',
profile: 'cmd-clink',
profile: 'local:cmd-clink',
},
hotkeys: {
'new-tab': [
@@ -50,7 +47,7 @@ export class TerminalConfigProvider extends ConfigProvider {
[Platform.Linux]: {
terminal: {
shell: 'default',
profile: 'user-default',
profile: 'local:user-default',
},
hotkeys: {
'new-tab': [

View File

@@ -1,6 +1,5 @@
import { Injectable } from '@angular/core'
import { HotkeyDescription, HotkeyProvider } from 'tabby-core'
import { TerminalService } from './services/terminal.service'
/** @hidden */
@Injectable()
@@ -12,18 +11,7 @@ export class LocalTerminalHotkeyProvider extends HotkeyProvider {
},
]
constructor (
private terminal: TerminalService,
) { super() }
async provide (): Promise<HotkeyDescription[]> {
const profiles = await this.terminal.getProfiles()
return [
...this.hotkeys,
...profiles.map(profile => ({
id: `profile.${this.terminal.getProfileID(profile)}`,
name: `New tab: ${profile.name}`,
})),
]
return this.hotkeys
}
}

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="svg-inline--fa fa-window-restore fa-w-16 fa-3x" data-icon="window-restore" data-prefix="fal" focusable="false" role="img" viewBox="0 0 512 512"><path fill="#fff" stroke="none" stroke-width="1" 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"/></svg>

Before

Width:  |  Height:  |  Size: 665 B

View File

@@ -4,7 +4,7 @@ import { FormsModule } from '@angular/forms'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { ToastrModule } from 'ngx-toastr'
import TabbyCorePlugin, { HostAppService, ToolbarButtonProvider, TabRecoveryProvider, ConfigProvider, HotkeysService, HotkeyProvider, TabContextMenuItemProvider, CLIHandler, ConfigService } from 'tabby-core'
import TabbyCorePlugin, { HostAppService, ToolbarButtonProvider, TabRecoveryProvider, ConfigProvider, HotkeysService, HotkeyProvider, TabContextMenuItemProvider, CLIHandler, ConfigService, ProfileProvider } from 'tabby-core'
import TabbyTerminalModule from 'tabby-terminal'
import TabbyElectronPlugin from 'tabby-electron'
import { SettingsTabProvider } from 'tabby-settings'
@@ -13,6 +13,8 @@ import { TerminalTabComponent } from './components/terminalTab.component'
import { ShellSettingsTabComponent } from './components/shellSettingsTab.component'
import { EditProfileModalComponent } from './components/editProfileModal.component'
import { EnvironmentEditorComponent } from './components/environmentEditor.component'
import { ProfilesSettingsTabComponent } from './components/profilesSettingsTab.component'
import { LocalProfileSettingsComponent } from './components/localProfileSettings.component'
import { TerminalService } from './services/terminal.service'
import { DockMenuService } from './services/dockMenu.service'
@@ -20,13 +22,12 @@ import { DockMenuService } from './services/dockMenu.service'
import { ButtonProvider } from './buttonProvider'
import { RecoveryProvider } from './recoveryProvider'
import { ShellProvider } from './api'
import { ShellSettingsTabProvider } from './settings'
import { ProfilesSettingsTabProvider, ShellSettingsTabProvider } from './settings'
import { TerminalConfigProvider } from './config'
import { LocalTerminalHotkeyProvider } from './hotkeys'
import { NewTabContextMenu, SaveAsProfileContextMenu } from './tabContextMenu'
import { CmderShellProvider } from './shells/cmder'
import { CustomShellProvider } from './shells/custom'
import { Cygwin32ShellProvider } from './shells/cygwin32'
import { Cygwin64ShellProvider } from './shells/cygwin64'
import { GitBashShellProvider } from './shells/gitBash'
@@ -39,6 +40,7 @@ import { WindowsStockShellsProvider } from './shells/windowsStock'
import { WSLShellProvider } from './shells/wsl'
import { AutoOpenTabCLIHandler, OpenPathCLIHandler, TerminalCLIHandler } from './cli'
import { LocalProfilesService } from './profiles'
/** @hidden */
@NgModule({
@@ -53,6 +55,7 @@ import { AutoOpenTabCLIHandler, OpenPathCLIHandler, TerminalCLIHandler } from '.
],
providers: [
{ provide: SettingsTabProvider, useClass: ShellSettingsTabProvider, multi: true },
{ provide: SettingsTabProvider, useClass: ProfilesSettingsTabProvider, multi: true },
{ provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
{ provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true },
@@ -65,13 +68,14 @@ import { AutoOpenTabCLIHandler, OpenPathCLIHandler, TerminalCLIHandler } from '.
{ provide: ShellProvider, useClass: WindowsStockShellsProvider, multi: true },
{ provide: ShellProvider, useClass: PowerShellCoreShellProvider, multi: true },
{ provide: ShellProvider, useClass: CmderShellProvider, multi: true },
{ provide: ShellProvider, useClass: CustomShellProvider, multi: true },
{ provide: ShellProvider, useClass: Cygwin32ShellProvider, multi: true },
{ provide: ShellProvider, useClass: Cygwin64ShellProvider, multi: true },
{ provide: ShellProvider, useClass: GitBashShellProvider, multi: true },
{ provide: ShellProvider, useClass: POSIXShellsProvider, multi: true },
{ provide: ShellProvider, useClass: WSLShellProvider, multi: true },
{ provide: ProfileProvider, useClass: LocalProfilesService, multi: true },
{ provide: TabContextMenuItemProvider, useClass: NewTabContextMenu, multi: true },
{ provide: TabContextMenuItemProvider, useClass: SaveAsProfileContextMenu, multi: true },
@@ -86,14 +90,18 @@ import { AutoOpenTabCLIHandler, OpenPathCLIHandler, TerminalCLIHandler } from '.
],
entryComponents: [
TerminalTabComponent,
ProfilesSettingsTabComponent,
ShellSettingsTabComponent,
EditProfileModalComponent,
LocalProfileSettingsComponent,
] as any[],
declarations: [
TerminalTabComponent,
ProfilesSettingsTabComponent,
ShellSettingsTabComponent,
EditProfileModalComponent,
EnvironmentEditorComponent,
LocalProfileSettingsComponent,
] as any[],
exports: [
TerminalTabComponent,
@@ -115,12 +123,6 @@ export default class LocalTerminalModule { // eslint-disable-line @typescript-es
if (hotkey === 'new-window') {
hostApp.newWindow()
}
if (hotkey.startsWith('profile.')) {
const profile = await terminal.getProfileByID(hotkey.split('.')[1])
if (profile) {
terminal.openTabWithOptions(profile.sessionOptions)
}
}
})
config.ready$.toPromise().then(() => {

View File

@@ -0,0 +1,72 @@
import { Injectable, Inject } from '@angular/core'
import { ProfileProvider, Profile, NewTabParameters, ConfigService, SplitTabComponent, AppService } from 'tabby-core'
import { TerminalTabComponent } from './components/terminalTab.component'
import { LocalProfileSettingsComponent } from './components/localProfileSettings.component'
import { ShellProvider, Shell, SessionOptions } from './api'
@Injectable({ providedIn: 'root' })
export class LocalProfilesService extends ProfileProvider {
id = 'local'
name = 'Local'
settingsComponent = LocalProfileSettingsComponent
constructor (
private app: AppService,
private config: ConfigService,
@Inject(ShellProvider) private shellProviders: ShellProvider[],
) {
super()
}
async getBuiltinProfiles (): Promise<Profile[]> {
return (await this.getShells()).map(shell => ({
id: `local:${shell.id}`,
type: 'local',
name: shell.name,
icon: shell.icon,
options: this.optionsFromShell(shell),
isBuiltin: true,
}))
}
async getNewTabParameters (profile: Profile): Promise<NewTabParameters<TerminalTabComponent>> {
const options = { ...profile.options }
if (!options.cwd) {
if (this.app.activeTab instanceof TerminalTabComponent && this.app.activeTab.session) {
options.cwd = await this.app.activeTab.session.getWorkingDirectory()
}
if (this.app.activeTab instanceof SplitTabComponent) {
const focusedTab = this.app.activeTab.getFocusedTab()
if (focusedTab instanceof TerminalTabComponent && focusedTab.session) {
options.cwd = await focusedTab.session.getWorkingDirectory()
}
}
}
return {
type: TerminalTabComponent,
inputs: {
sessionOptions: options,
},
}
}
async getShells (): Promise<Shell[]> {
const shellLists = await Promise.all(this.config.enabledServices(this.shellProviders).map(x => x.provide()))
return shellLists.reduce((a, b) => a.concat(b), [])
}
optionsFromShell (shell: Shell): SessionOptions {
return {
command: shell.command,
args: shell.args ?? [],
env: shell.env,
}
}
getDescription (profile: Profile): string {
return profile.options?.command
}
}

View File

@@ -1,19 +1,19 @@
import { Injectable } from '@angular/core'
import { TabRecoveryProvider, RecoveredTab, RecoveryToken } from 'tabby-core'
import { TabRecoveryProvider, NewTabParameters, RecoveryToken } from 'tabby-core'
import { TerminalTabComponent } from './components/terminalTab.component'
/** @hidden */
@Injectable()
export class RecoveryProvider extends TabRecoveryProvider {
export class RecoveryProvider extends TabRecoveryProvider<TerminalTabComponent> {
async applicableTo (recoveryToken: RecoveryToken): Promise<boolean> {
return recoveryToken.type === 'app:terminal-tab'
}
async recover (recoveryToken: RecoveryToken): Promise<RecoveredTab> {
async recover (recoveryToken: RecoveryToken): Promise<NewTabParameters<TerminalTabComponent>> {
return {
type: TerminalTabComponent,
options: {
inputs: {
sessionOptions: recoveryToken.sessionOptions,
savedState: recoveryToken.savedState,
},

View File

@@ -1,7 +1,6 @@
import { NgZone, Injectable } from '@angular/core'
import { ConfigService, HostAppService, Platform } from 'tabby-core'
import { ConfigService, HostAppService, Platform, ProfilesService } from 'tabby-core'
import { ElectronService } from 'tabby-electron'
import { TerminalService } from './terminal.service'
/** @hidden */
@Injectable({ providedIn: 'root' })
@@ -13,17 +12,17 @@ export class DockMenuService {
private config: ConfigService,
private hostApp: HostAppService,
private zone: NgZone,
private terminalService: TerminalService,
private profilesService: ProfilesService,
) {
config.changed$.subscribe(() => this.update())
}
update (): void {
if (this.hostApp.platform === Platform.Windows) {
this.electron.app.setJumpList(this.config.store.terminal.profiles.length ? [{
this.electron.app.setJumpList(this.config.store.profiles.length ? [{
type: 'custom',
name: 'Profiles',
items: this.config.store.terminal.profiles.map(profile => ({
items: this.config.store.profiles.map(profile => ({
type: 'task',
program: process.execPath,
args: `profile "${profile.name}"`,
@@ -35,10 +34,10 @@ export class DockMenuService {
}
if (this.hostApp.platform === Platform.macOS) {
this.electron.app.dock.setMenu(this.electron.Menu.buildFromTemplate(
this.config.store.terminal.profiles.map(profile => ({
this.config.store.profiles.map(profile => ({
label: profile.name,
click: () => this.zone.run(() => {
this.terminalService.openTabWithOptions(profile.sessionOptions)
click: () => this.zone.run(async () => {
this.profilesService.openNewTabForProfile(profile)
}),
}))
))

View File

@@ -1,150 +1,69 @@
import * as fs from 'mz/fs'
import slugify from 'slugify'
import { Observable, AsyncSubject } from 'rxjs'
import { Injectable, Inject } from '@angular/core'
import { AppService, Logger, LogService, ConfigService, SplitTabComponent } from 'tabby-core'
import { Injectable } from '@angular/core'
import { Logger, LogService, ConfigService, AppService, ProfilesService } from 'tabby-core'
import { TerminalTabComponent } from '../components/terminalTab.component'
import { ShellProvider, Shell, SessionOptions, Profile } from '../api'
import { UACService } from './uac.service'
import { SessionOptions, LocalProfile } from '../api'
@Injectable({ providedIn: 'root' })
export class TerminalService {
private shells = new AsyncSubject<Shell[]>()
private logger: Logger
/**
* A fresh list of all available shells
*/
get shells$ (): Observable<Shell[]> { return this.shells }
/** @hidden */
private constructor (
private app: AppService,
private profilesService: ProfilesService,
private config: ConfigService,
private uac: UACService,
@Inject(ShellProvider) private shellProviders: ShellProvider[],
log: LogService,
) {
this.logger = log.create('terminal')
config.ready$.toPromise().then(() => {
this.reloadShells()
config.changed$.subscribe(() => {
this.reloadShells()
})
})
}
async getProfiles ({ includeHidden, skipDefault }: { includeHidden?: boolean, skipDefault?: boolean } = {}): Promise<Profile[]> {
const shells = (await this.shells$.toPromise())!
return [
...this.config.store.terminal.profiles,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
...skipDefault ? [] : shells.filter(x => includeHidden || !x.hidden).map(shell => ({
name: shell.name,
shell: shell.id,
icon: shell.icon,
sessionOptions: this.optionsFromShell(shell),
isBuiltin: true,
})),
]
}
getProfileID (profile: Profile): string {
return slugify(profile.name, { remove: /[:.]/g }).toLowerCase()
}
async getProfileByID (id: string): Promise<Profile|null> {
const profiles = await this.getProfiles({ includeHidden: true })
return profiles.find(x => this.getProfileID(x) === id) ?? null
async getDefaultProfile (): Promise<LocalProfile> {
const profiles = await this.profilesService.getProfiles()
let profile = profiles.find(x => x.id === this.config.store.terminal.profile)
if (!profile) {
profile = profiles.filter(x => x.type === 'local' && x.isBuiltin)[0]
}
return profile as LocalProfile
}
/**
* Launches a new terminal with a specific shell and CWD
* @param pause Wait for a keypress when the shell exits
*/
async openTab (profile?: Profile|null, cwd?: string|null, pause?: boolean): Promise<TerminalTabComponent> {
async openTab (profile?: LocalProfile|null, cwd?: string|null, pause?: boolean): Promise<TerminalTabComponent> {
if (!profile) {
profile = await this.getProfileByID(this.config.store.terminal.profile)
if (!profile) {
profile = (await this.getProfiles({ includeHidden: true }))[0]
}
profile = await this.getDefaultProfile()
}
cwd = cwd ?? profile.sessionOptions.cwd
cwd = cwd ?? profile.options.cwd
if (cwd && !fs.existsSync(cwd)) {
console.warn('Ignoring non-existent CWD:', cwd)
cwd = null
}
if (!cwd) {
if (!this.config.store.terminal.alwaysUseWorkingDirectory) {
if (this.app.activeTab instanceof TerminalTabComponent && this.app.activeTab.session) {
cwd = await this.app.activeTab.session.getWorkingDirectory()
}
if (this.app.activeTab instanceof SplitTabComponent) {
const focusedTab = this.app.activeTab.getFocusedTab()
if (focusedTab instanceof TerminalTabComponent && focusedTab.session) {
cwd = await focusedTab.session.getWorkingDirectory()
}
}
}
cwd = cwd ?? this.config.store.terminal.workingDirectory
}
this.logger.info(`Starting profile ${profile.name}`, profile)
const sessionOptions = {
...profile.sessionOptions,
const options = {
...profile.options,
pauseAfterExit: pause,
cwd: cwd ?? undefined,
}
const tab = this.openTabWithOptions(sessionOptions)
if (profile.color) {
(this.app.getParentTab(tab) ?? tab).color = profile.color
}
if (profile.disableDynamicTitle) {
tab.enableDynamicTitle = false
tab.setTitle(profile.name)
}
return tab
}
optionsFromShell (shell: Shell): SessionOptions {
return {
command: shell.command,
args: shell.args ?? [],
env: shell.env,
}
return (await this.profilesService.openNewTabForProfile({
...profile,
options,
})) as TerminalTabComponent
}
/**
* Open a terminal with custom session options
*/
openTabWithOptions (sessionOptions: SessionOptions): TerminalTabComponent {
if (sessionOptions.runAsAdministrator && this.uac.isAvailable) {
sessionOptions = this.uac.patchSessionOptionsForUAC(sessionOptions)
}
this.logger.info('Using session options:', sessionOptions)
return this.app.openNewTab(
TerminalTabComponent,
{ sessionOptions }
) as TerminalTabComponent
}
private async getShells (): Promise<Shell[]> {
const shellLists = await Promise.all(this.config.enabledServices(this.shellProviders).map(x => x.provide()))
return shellLists.reduce((a, b) => a.concat(b), [])
}
private async reloadShells () {
this.shells = new AsyncSubject<Shell[]>()
const shells = await this.getShells()
this.logger.debug('Shells list:', shells)
this.shells.next(shells)
this.shells.complete()
return this.app.openNewTab({
type: TerminalTabComponent,
inputs: { sessionOptions },
}) as TerminalTabComponent
}
}

View File

@@ -1,6 +1,8 @@
import { Injectable } from '@angular/core'
import { HostAppService, Platform } from 'tabby-core'
import { SettingsTabProvider } from 'tabby-settings'
import { ProfilesSettingsTabComponent } from './components/profilesSettingsTab.component'
import { ShellSettingsTabComponent } from './components/shellSettingsTab.component'
/** @hidden */
@@ -10,7 +12,25 @@ export class ShellSettingsTabProvider extends SettingsTabProvider {
icon = 'list-ul'
title = 'Shell'
constructor (private hostApp: HostAppService) {
super()
}
getComponentType (): any {
return ShellSettingsTabComponent
if (this.hostApp.platform === Platform.Windows) {
return ShellSettingsTabComponent
}
}
}
/** @hidden */
@Injectable()
export class ProfilesSettingsTabProvider extends SettingsTabProvider {
id = 'profiles'
icon = 'window-restore'
title = 'Profiles'
getComponentType (): any {
return ProfilesSettingsTabComponent
}
}

View File

@@ -1,25 +0,0 @@
import { Injectable } from '@angular/core'
import { ConfigService } from 'tabby-core'
import { ShellProvider, Shell } from '../api'
/** @hidden */
@Injectable()
export class CustomShellProvider extends ShellProvider {
constructor (
private config: ConfigService,
) {
super()
}
async provide (): Promise<Shell[]> {
const args = this.config.store.terminal.customShell.split(' ')
return [{
id: 'custom',
name: 'Custom shell',
command: args[0],
args: args.slice(1),
env: {},
}]
}
}

View File

@@ -21,7 +21,7 @@ export class MacOSDefaultShellProvider extends ShellProvider {
}
return [{
id: 'default',
name: 'User default',
name: 'OS default',
command: await this.getDefaultShellCached(),
args: ['--login'],
hidden: true,

View File

@@ -25,6 +25,7 @@ export class POSIXShellsProvider extends ShellProvider {
.map(x => ({
id: slugify(x),
name: x.split('/')[2],
icon: 'fas fa-terminal',
command: x,
args: ['-l'],
env: {},

View File

@@ -39,7 +39,7 @@ export class WindowsDefaultShellProvider extends ShellProvider {
return [{
...shell,
id: 'default',
name: `Default (${shell.name})`,
name: `OS default (${shell.name})`,
hidden: true,
env: {},
}]

View File

@@ -1,8 +1,9 @@
import { Injectable } from '@angular/core'
import { ConfigService, BaseTabComponent, TabContextMenuItemProvider, TabHeaderComponent, SplitTabComponent, NotificationsService, MenuItemOptions } from 'tabby-core'
import { ConfigService, BaseTabComponent, TabContextMenuItemProvider, TabHeaderComponent, SplitTabComponent, NotificationsService, MenuItemOptions, ProfilesService } from 'tabby-core'
import { TerminalTabComponent } from './components/terminalTab.component'
import { UACService } from './services/uac.service'
import { TerminalService } from './services/terminal.service'
import { LocalProfile } from './api'
/** @hidden */
@Injectable()
@@ -23,14 +24,15 @@ export class SaveAsProfileContextMenu extends TabContextMenuItemProvider {
label: 'Save as profile',
click: async () => {
const profile = {
sessionOptions: {
options: {
...tab.sessionOptions,
cwd: await tab.session?.getWorkingDirectory() ?? tab.sessionOptions.cwd,
},
name: tab.sessionOptions.command,
type: 'local',
}
this.config.store.terminal.profiles = [
...this.config.store.terminal.profiles,
this.config.store.profiles = [
...this.config.store.profiles,
profile,
]
this.config.save()
@@ -50,6 +52,7 @@ export class NewTabContextMenu extends TabContextMenuItemProvider {
constructor (
public config: ConfigService,
private profilesService: ProfilesService,
private terminalService: TerminalService,
private uac: UACService,
) {
@@ -57,7 +60,7 @@ export class NewTabContextMenu extends TabContextMenuItemProvider {
}
async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<MenuItemOptions[]> {
const profiles = await this.terminalService.getProfiles()
const profiles = (await this.profilesService.getProfiles()).filter(x => x.type === 'local') as LocalProfile[]
const items: MenuItemOptions[] = [
{
@@ -71,9 +74,9 @@ export class NewTabContextMenu extends TabContextMenuItemProvider {
submenu: profiles.map(profile => ({
label: profile.name,
click: async () => {
let workingDirectory = this.config.store.terminal.workingDirectory
if (this.config.store.terminal.alwaysUseWorkingDirectory !== true && tab instanceof TerminalTabComponent) {
workingDirectory = await tab.session?.getWorkingDirectory()
let workingDirectory = profile.options.cwd
if (!workingDirectory && tab instanceof TerminalTabComponent) {
workingDirectory = await tab.session?.getWorkingDirectory() ?? undefined
}
await this.terminalService.openTab(profile, workingDirectory)
},
@@ -88,7 +91,7 @@ export class NewTabContextMenu extends TabContextMenuItemProvider {
label: profile.name,
click: () => {
this.terminalService.openTabWithOptions({
...profile.sessionOptions,
...profile.options,
runAsAdministrator: true,
})
},

View File

@@ -371,11 +371,6 @@ side-channel@^1.0.3:
get-intrinsic "^1.0.2"
object-inspect "^1.9.0"
slugify@^1.5.3:
version "1.5.3"
resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.5.3.tgz#36e009864f5476bfd5db681222643d92339c890d"
integrity sha512-/HkjRdwPY3yHJReXu38NiusZw2+LLE2SrhkWJtmlPDB1fqFSvioYj62NkPcrKiNCgRLeGcGK7QBvr1iQwybeXw==
string.prototype.codepointat@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz#004ad44c8afc727527b108cd462b4d971cd469bc"