mirror of
https://github.com/Eugeny/tabby.git
synced 2025-07-20 02:18:01 +00:00
new profile system
This commit is contained in:
@@ -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": {
|
||||
|
@@ -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 {
|
||||
|
@@ -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(),
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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',
|
||||
)
|
47
tabby-local/src/components/localProfileSettings.component.ts
Normal file
47
tabby-local/src/components/localProfileSettings.component.ts
Normal 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
|
||||
}
|
||||
}
|
94
tabby-local/src/components/profilesSettingsTab.component.pug
Normal file
94
tabby-local/src/components/profilesSettingsTab.component.pug
Normal 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)}}
|
201
tabby-local/src/components/profilesSettingsTab.component.ts
Normal file
201
tabby-local/src/components/profilesSettingsTab.component.ts
Normal 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'
|
||||
}
|
||||
}
|
@@ -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}}
|
||||
|
@@ -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()
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -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': [
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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 |
@@ -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(() => {
|
||||
|
72
tabby-local/src/profiles.ts
Normal file
72
tabby-local/src/profiles.ts
Normal 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
|
||||
}
|
||||
}
|
@@ -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,
|
||||
},
|
||||
|
@@ -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)
|
||||
}),
|
||||
}))
|
||||
))
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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: {},
|
||||
}]
|
||||
}
|
||||
}
|
@@ -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,
|
||||
|
@@ -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: {},
|
||||
|
@@ -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: {},
|
||||
}]
|
||||
|
@@ -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,
|
||||
})
|
||||
},
|
||||
|
@@ -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"
|
||||
|
Reference in New Issue
Block a user