diff --git a/tabby-core/src/configDefaults.yaml b/tabby-core/src/configDefaults.yaml index 875b9f3f..bc2ccdcf 100644 --- a/tabby-core/src/configDefaults.yaml +++ b/tabby-core/src/configDefaults.yaml @@ -21,6 +21,8 @@ hotkeys: profile: __nonStructural: true profiles: [] +profileDefaults: + __nonStructural: true recentProfiles: [] recoverTabs: true enableAnalytics: true diff --git a/tabby-core/src/services/profiles.service.ts b/tabby-core/src/services/profiles.service.ts index 7d2ada78..5f3fe45e 100644 --- a/tabby-core/src/services/profiles.service.ts +++ b/tabby-core/src/services/profiles.service.ts @@ -179,9 +179,13 @@ export class ProfilesService { return null } - getConfigProxyForProfile (profile: PartialProfile): T { + getConfigProxyForProfile (profile: PartialProfile, skipUserDefaults = false): T { const provider = this.providerForProfile(profile) - const defaults = configMerge(this.profileDefaults, provider?.configDefaults ?? {}) + const defaults = [ + this.profileDefaults, + provider?.configDefaults ?? {}, + !provider || skipUserDefaults ? {} : this.config.store.profileDefaults[provider.id] ?? {}, + ].reduce(configMerge, {}) return new ConfigProxy(profile, defaults) as unknown as T } } diff --git a/tabby-settings/src/components/editProfileModal.component.pug b/tabby-settings/src/components/editProfileModal.component.pug index d1b7b951..eef3168f 100644 --- a/tabby-settings/src/components/editProfileModal.component.pug +++ b/tabby-settings/src/components/editProfileModal.component.pug @@ -1,10 +1,13 @@ -.modal-header +.modal-header(*ngIf='!defaultsMode') h3.m-0 {{profile.name}} +.modal-header(*ngIf='defaultsMode') + h3.m-0 Defaults for {{profileProvider.name}} + .modal-body .row .col-12.col-lg-4 - .form-group + .form-group(*ngIf='!defaultsMode') label Name input.form-control( type='text', @@ -12,7 +15,7 @@ [(ngModel)]='profile.name', ) - .form-group + .form-group(*ngIf='!defaultsMode') label Group input.form-control( type='text', @@ -22,7 +25,7 @@ [ngbTypeahead]='groupTypeahead', ) - .form-group + .form-group(*ngIf='!defaultsMode') label Icon .input-group input.form-control( @@ -47,7 +50,6 @@ type='text', [(ngModel)]='profile.color', placeholder='#000000', - alwaysVisibleTypeahead, [ngbTypeahead]='colorsAutocomplete', [resultFormatter]='colorsFormatter' ) diff --git a/tabby-settings/src/components/editProfileModal.component.ts b/tabby-settings/src/components/editProfileModal.component.ts index 7b1515e3..97e1aadf 100644 --- a/tabby-settings/src/components/editProfileModal.component.ts +++ b/tabby-settings/src/components/editProfileModal.component.ts @@ -19,6 +19,7 @@ export class EditProfileModalComponent

{ @Input() profile: P & ConfigProxy @Input() profileProvider: ProfileProvider

@Input() settingsComponent: new () => ProfileSettingsComponent

+ @Input() defaultsMode = false groupNames: string[] @ViewChild('placeholder', { read: ViewContainerRef }) placeholder: ViewContainerRef @@ -55,7 +56,7 @@ export class EditProfileModalComponent

{ ngOnInit () { this._profile = this.profile - this.profile = this.profilesService.getConfigProxyForProfile(this.profile) + this.profile = this.profilesService.getConfigProxyForProfile(this.profile, this.defaultsMode) } ngAfterViewInit () { diff --git a/tabby-settings/src/components/profilesSettingsTab.component.pug b/tabby-settings/src/components/profilesSettingsTab.component.pug index 09fd384b..c18df516 100644 --- a/tabby-settings/src/components/profilesSettingsTab.component.pug +++ b/tabby-settings/src/components/profilesSettingsTab.component.pug @@ -1,108 +1,127 @@ h3.mb-3 Profiles -.form-line - .header - .title Default profile for new tabs +ul.nav-tabs(ngbNav, #nav='ngbNav') + li(ngbNavItem) + a(ngbNavLink) Profiles + ng-template(ngbNavContent) + .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 recent profiles in selector - .description Set to 0 to disable recent profiles - - input.form-control( - type='number', - min='0', - step='1', - [(ngModel)]='config.store.terminal.showRecentProfiles', - (ngModelChange)='config.save()' - ) - -.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)' + select.form-control( + [(ngModel)]='config.store.terminal.profile', + (ngModelChange)='config.save()', ) - 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)' + option( + *ngFor='let profile of profiles', + [ngValue]='profile.id' + ) {{profile.name}} + option( + *ngFor='let profile of builtinProfiles', + [ngValue]='profile.id' + ) {{profile.name}} + + .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-alt + 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.hover-reveal.ml-1( + *ngIf='!profile.isBuiltin', + (click)='$event.stopPropagation(); deleteProfile(profile)' + ) + i.fas.fa-trash-alt + + .ml-1(class='badge badge-{{getTypeColorClass(profile)}}') {{getTypeLabel(profile)}} + + li(ngbNavItem) + a(ngbNavLink) Advanced + ng-template(ngbNavContent) + .form-line(*ngIf='config.store.profiles.length > 0') + .header + .title Show recent profiles in selector + .description Set to 0 to disable recent profiles + + input.form-control( + type='number', + min='0', + step='1', + [(ngModel)]='config.store.terminal.showRecentProfiles', + (ngModelChange)='config.save()' ) - i.fas.fa-trash-alt - 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)}} + .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 - .mr-auto + toggle( + [(ngModel)]='config.store.terminal.showBuiltinProfiles', + (ngModelChange)='config.save()' + ) - button.btn.btn-link.hover-reveal.ml-1((click)='$event.stopPropagation(); launchProfile(profile)') - i.fas.fa-play + .form-line + .header + .title Default profile settings + .description These apply to all profiles of a given type - button.btn.btn-link.hover-reveal.ml-1((click)='$event.stopPropagation(); newProfile(profile)') - i.fas.fa-copy + .list-group.list-group-light.mt-3.mb-3 + a.list-group-item.list-group-item-action( + (click)='editDefaults(provider)', + *ngFor='let provider of profileProviders' + ) {{provider.name}} - button.btn.btn-link.hover-reveal.ml-1( - *ngIf='!profile.isBuiltin', - (click)='$event.stopPropagation(); deleteProfile(profile)' - ) - i.fas.fa-trash-alt - - .ml-1(class='badge badge-{{getTypeColorClass(profile)}}') {{getTypeLabel(profile)}} +div([ngbNavOutlet]='nav') diff --git a/tabby-settings/src/components/profilesSettingsTab.component.ts b/tabby-settings/src/components/profilesSettingsTab.component.ts index e6c74b18..c1abf6f8 100644 --- a/tabby-settings/src/components/profilesSettingsTab.component.ts +++ b/tabby-settings/src/components/profilesSettingsTab.component.ts @@ -1,9 +1,9 @@ import { v4 as uuidv4 } from 'uuid' import slugify from 'slugify' import deepClone from 'clone-deep' -import { Component } from '@angular/core' +import { Component, Inject } from '@angular/core' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' -import { ConfigService, HostAppService, Profile, SelectorService, ProfilesService, PromptModalComponent, PlatformService, BaseComponent, PartialProfile } from 'tabby-core' +import { ConfigService, HostAppService, Profile, SelectorService, ProfilesService, PromptModalComponent, PlatformService, BaseComponent, PartialProfile, ProfileProvider } from 'tabby-core' import { EditProfileModalComponent } from './editProfileModal.component' interface ProfileGroup { @@ -28,12 +28,14 @@ export class ProfilesSettingsTabComponent extends BaseComponent { constructor ( public config: ConfigService, public hostApp: HostAppService, + @Inject(ProfileProvider) public profileProviders: ProfileProvider[], private profilesService: ProfilesService, private selector: SelectorService, private ngbModal: NgbModal, private platform: PlatformService, ) { super() + this.profileProviders.sort((a, b) => a.name.localeCompare(b.name)) } async ngOnInit (): Promise { @@ -102,6 +104,8 @@ export class ProfilesSettingsTabComponent extends BaseComponent { delete profile[k] } Object.assign(profile, result) + + profile.type = modal.componentInstance.profileProvider.id } async deleteProfile (profile: PartialProfile): Promise { @@ -224,4 +228,26 @@ export class ProfilesSettingsTabComponent extends BaseComponent { 'split-layout': 'primary', }[this.profilesService.providerForProfile(profile)?.id ?? ''] ?? 'warning' } + + async editDefaults (provider: ProfileProvider): Promise { + 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() + } }