mirror of
https://github.com/Eugeny/tabby.git
synced 2026-08-24 10:14:31 -05:00
277 lines
10 KiB
TypeScript
277 lines
10 KiB
TypeScript
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'
|
|
import { v4 as uuidv4 } from 'uuid'
|
|
import slugify from 'slugify'
|
|
import deepClone from 'clone-deep'
|
|
import { Component, HostBinding, Inject } from '@angular/core'
|
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
|
import { ConfigService, HostAppService, Profile, SelectorService, ProfilesService, PromptModalComponent, PlatformService, BaseComponent, PartialProfile, ProfileProvider, TranslateService } from 'tabby-core'
|
|
import { EditProfileModalComponent } from './editProfileModal.component'
|
|
|
|
interface ProfileGroup {
|
|
name?: string
|
|
profiles: PartialProfile<Profile>[]
|
|
editable: boolean
|
|
collapsed: boolean
|
|
}
|
|
|
|
_('Ungrouped')
|
|
|
|
/** @hidden */
|
|
@Component({
|
|
template: require('./profilesSettingsTab.component.pug'),
|
|
styles: [require('./profilesSettingsTab.component.scss')],
|
|
})
|
|
export class ProfilesSettingsTabComponent extends BaseComponent {
|
|
profiles: PartialProfile<Profile>[] = []
|
|
builtinProfiles: PartialProfile<Profile>[] = []
|
|
templateProfiles: PartialProfile<Profile>[] = []
|
|
profileGroups: ProfileGroup[]
|
|
filter = ''
|
|
|
|
@HostBinding('class.content-box') true
|
|
|
|
constructor (
|
|
public config: ConfigService,
|
|
public hostApp: HostAppService,
|
|
@Inject(ProfileProvider) public profileProviders: ProfileProvider<Profile>[],
|
|
private profilesService: ProfilesService,
|
|
private selector: SelectorService,
|
|
private ngbModal: NgbModal,
|
|
private platform: PlatformService,
|
|
private translate: TranslateService,
|
|
) {
|
|
super()
|
|
this.profileProviders.sort((a, b) => a.name.localeCompare(b.name))
|
|
}
|
|
|
|
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: PartialProfile<Profile>): void {
|
|
this.profilesService.openNewTabForProfile(profile)
|
|
}
|
|
|
|
async newProfile (base?: PartialProfile<Profile>): Promise<void> {
|
|
if (!base) {
|
|
const profiles = [...this.templateProfiles, ...this.builtinProfiles, ...this.profiles]
|
|
profiles.sort((a, b) => (a.weight ?? 0) - (b.weight ?? 0))
|
|
base = await this.selector.show(
|
|
this.translate.instant('Select a base profile to use as a template'),
|
|
profiles.map(p => ({
|
|
icon: p.icon,
|
|
description: this.profilesService.getDescription(p) ?? undefined,
|
|
name: p.group ? `${p.group} / ${p.name}` : p.name,
|
|
result: p,
|
|
})),
|
|
)
|
|
}
|
|
const profile: PartialProfile<Profile> = deepClone(base)
|
|
delete profile.id
|
|
if (base.isTemplate) {
|
|
profile.name = ''
|
|
} else if (!base.isBuiltin) {
|
|
profile.name = this.translate.instant('{name} copy', base)
|
|
}
|
|
profile.isBuiltin = false
|
|
profile.isTemplate = false
|
|
await this.showProfileEditModal(profile)
|
|
if (!profile.name) {
|
|
const cfgProxy = this.profilesService.getConfigProxyForProfile(profile)
|
|
profile.name = this.profilesService.providerForProfile(profile)?.getSuggestedName(cfgProxy) ?? this.translate.instant('{name} copy', base)
|
|
}
|
|
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: PartialProfile<Profile>): Promise<void> {
|
|
await this.showProfileEditModal(profile)
|
|
await this.config.save()
|
|
}
|
|
|
|
async showProfileEditModal (profile: PartialProfile<Profile>): Promise<void> {
|
|
const modal = this.ngbModal.open(
|
|
EditProfileModalComponent,
|
|
{ size: 'lg' },
|
|
)
|
|
const provider = this.profilesService.providerForProfile(profile)
|
|
if (!provider) {
|
|
throw new Error('Cannot edit a profile without a provider')
|
|
}
|
|
modal.componentInstance.profile = deepClone(profile)
|
|
modal.componentInstance.profileProvider = provider
|
|
|
|
const result = await modal.result.catch(() => null)
|
|
if (!result) {
|
|
return
|
|
}
|
|
|
|
// Fully replace the config
|
|
for (const k in profile) {
|
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
delete profile[k]
|
|
}
|
|
Object.assign(profile, result)
|
|
|
|
profile.type = provider.id
|
|
}
|
|
|
|
async deleteProfile (profile: PartialProfile<Profile>): Promise<void> {
|
|
if ((await this.platform.showMessageBox(
|
|
{
|
|
type: 'warning',
|
|
message: this.translate.instant('Delete "{name}"?', profile),
|
|
buttons: [
|
|
this.translate.instant('Delete'),
|
|
this.translate.instant('Keep'),
|
|
],
|
|
defaultId: 1,
|
|
cancelId: 1,
|
|
}
|
|
)).response === 0) {
|
|
this.profilesService.providerForProfile(profile)?.deleteProfile(
|
|
this.profilesService.getConfigProxyForProfile(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: this.translate.instant('Built-in'),
|
|
profiles: this.builtinProfiles,
|
|
editable: false,
|
|
collapsed: false,
|
|
})
|
|
}
|
|
|
|
async editGroup (group: ProfileGroup): Promise<void> {
|
|
const modal = this.ngbModal.open(PromptModalComponent)
|
|
modal.componentInstance.prompt = this.translate.instant('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: this.translate.instant('Delete "{name}"?', group),
|
|
buttons: [
|
|
this.translate.instant('Delete'),
|
|
this.translate.instant('Keep'),
|
|
],
|
|
defaultId: 1,
|
|
cancelId: 1,
|
|
}
|
|
)).response === 0) {
|
|
if ((await this.platform.showMessageBox(
|
|
{
|
|
type: 'warning',
|
|
message: this.translate.instant('Delete the group\'s profiles?'),
|
|
buttons: [
|
|
this.translate.instant('Move to "Ungrouped"'),
|
|
this.translate.instant('Delete'),
|
|
],
|
|
defaultId: 0,
|
|
cancelId: 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: PartialProfile<Profile>): boolean {
|
|
return !this.filter || (profile.name + '$' + (this.getDescription(profile) ?? '')).toLowerCase().includes(this.filter.toLowerCase())
|
|
}
|
|
|
|
iconIsSVG (icon?: string): boolean {
|
|
return icon?.startsWith('<') ?? false
|
|
}
|
|
|
|
getDescription (profile: PartialProfile<Profile>): string|null {
|
|
return this.profilesService.getDescription(profile)
|
|
}
|
|
|
|
getTypeLabel (profile: PartialProfile<Profile>): string {
|
|
const name = this.profilesService.providerForProfile(profile)?.name
|
|
if (name === this.translate.instant('Local terminal')) {
|
|
return ''
|
|
}
|
|
return name ?? this.translate.instant('Unknown')
|
|
}
|
|
|
|
getTypeColorClass (profile: PartialProfile<Profile>): string {
|
|
return {
|
|
ssh: 'secondary',
|
|
serial: 'success',
|
|
telnet: 'info',
|
|
'split-layout': 'primary',
|
|
}[this.profilesService.providerForProfile(profile)?.id ?? ''] ?? 'warning'
|
|
}
|
|
|
|
async editDefaults (provider: ProfileProvider<Profile>): Promise<void> {
|
|
const modal = this.ngbModal.open(
|
|
EditProfileModalComponent,
|
|
{ size: 'lg' },
|
|
)
|
|
const model = this.config.store.profileDefaults[provider.id] ?? {}
|
|
model.type = provider.id
|
|
modal.componentInstance.profile = Object.assign({}, model)
|
|
modal.componentInstance.profileProvider = provider
|
|
modal.componentInstance.defaultsMode = true
|
|
const result = await modal.result
|
|
|
|
// Fully replace the config
|
|
for (const k in model) {
|
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
delete model[k]
|
|
}
|
|
Object.assign(model, result)
|
|
this.config.store.profileDefaults[provider.id] = model
|
|
await this.config.save()
|
|
}
|
|
}
|