mirror of
https://github.com/Eugeny/tabby
synced 2024-12-04 18:40:16 +00:00
Merge pull request #8726 from Clem-Fern/profiles-rework
This commit is contained in:
commit
4684b0d6f5
39 changed files with 786 additions and 274 deletions
|
@ -16,7 +16,7 @@ export { BootstrapData, PluginInfo, BOOTSTRAP_DATA } from './mainProcess'
|
||||||
export { HostWindowService } from './hostWindow'
|
export { HostWindowService } from './hostWindow'
|
||||||
export { HostAppService, Platform } from './hostApp'
|
export { HostAppService, Platform } from './hostApp'
|
||||||
export { FileProvider } from './fileProvider'
|
export { FileProvider } from './fileProvider'
|
||||||
export { ProfileProvider, Profile, PartialProfile, ProfileSettingsComponent } from './profileProvider'
|
export { ProfileProvider, ConnectableProfileProvider, QuickConnectProfileProvider, Profile, ConnectableProfile, PartialProfile, ProfileSettingsComponent, ProfileGroup, PartialProfileGroup } from './profileProvider'
|
||||||
export { PromptModalComponent } from '../components/promptModal.component'
|
export { PromptModalComponent } from '../components/promptModal.component'
|
||||||
export * from './commands'
|
export * from './commands'
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,10 @@ export interface Profile {
|
||||||
isTemplate: boolean
|
isTemplate: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConnectableProfile extends Profile {
|
||||||
|
clearServiceMessagesOnConnect: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export type PartialProfile<T extends Profile> = Omit<Omit<Omit<{
|
export type PartialProfile<T extends Profile> = Omit<Omit<Omit<{
|
||||||
[K in keyof T]?: T[K]
|
[K in keyof T]?: T[K]
|
||||||
}, 'options'>, 'type'>, 'name'> & {
|
}, 'options'>, 'type'>, 'name'> & {
|
||||||
|
@ -31,6 +35,21 @@ export type PartialProfile<T extends Profile> = Omit<Omit<Omit<{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProfileGroup {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
profiles: PartialProfile<Profile>[]
|
||||||
|
defaults: any
|
||||||
|
editable: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PartialProfileGroup<T extends ProfileGroup> = Omit<Omit<{
|
||||||
|
[K in keyof T]?: T[K]
|
||||||
|
}, 'id'>, 'name'> & {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProfileSettingsComponent<P extends Profile> {
|
export interface ProfileSettingsComponent<P extends Profile> {
|
||||||
profile: P
|
profile: P
|
||||||
save?: () => void
|
save?: () => void
|
||||||
|
@ -39,7 +58,6 @@ export interface ProfileSettingsComponent<P extends Profile> {
|
||||||
export abstract class ProfileProvider<P extends Profile> {
|
export abstract class ProfileProvider<P extends Profile> {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
supportsQuickConnect = false
|
|
||||||
settingsComponent?: new (...args: any[]) => ProfileSettingsComponent<P>
|
settingsComponent?: new (...args: any[]) => ProfileSettingsComponent<P>
|
||||||
configDefaults = {}
|
configDefaults = {}
|
||||||
|
|
||||||
|
@ -53,13 +71,15 @@ export abstract class ProfileProvider<P extends Profile> {
|
||||||
|
|
||||||
abstract getDescription (profile: PartialProfile<P>): string
|
abstract getDescription (profile: PartialProfile<P>): string
|
||||||
|
|
||||||
quickConnect (query: string): PartialProfile<P>|null {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
intoQuickConnectString (profile: P): string|null {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteProfile (profile: P): void { }
|
deleteProfile (profile: P): void { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export abstract class ConnectableProfileProvider<P extends ConnectableProfile> extends ProfileProvider<P> {}
|
||||||
|
|
||||||
|
export abstract class QuickConnectProfileProvider<P extends ConnectableProfile> extends ConnectableProfileProvider<P> {
|
||||||
|
|
||||||
|
abstract quickConnect (query: string): PartialProfile<P>|null
|
||||||
|
|
||||||
|
abstract intoQuickConnectString (profile: P): string|null
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ export class CoreCommandProvider extends CommandProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
async activate () {
|
async activate () {
|
||||||
const profile = await this.profilesService.showProfileSelector()
|
const profile = await this.profilesService.showProfileSelector().catch(() => null)
|
||||||
if (profile) {
|
if (profile) {
|
||||||
this.profilesService.launchProfile(profile)
|
this.profilesService.launchProfile(profile)
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,7 @@ export class SelectorModalComponent<T> {
|
||||||
{ sort: true },
|
{ sort: true },
|
||||||
).search(f)
|
).search(f)
|
||||||
|
|
||||||
this.options.filter(x => x.freeInputPattern).forEach(freeOption => {
|
this.options.filter(x => x.freeInputPattern).sort(firstBy<SelectorOption<T>, number>(x => x.weight ?? 0)).forEach(freeOption => {
|
||||||
if (!this.filteredOptions.includes(freeOption)) {
|
if (!this.filteredOptions.includes(freeOption)) {
|
||||||
this.filteredOptions.push(freeOption)
|
this.filteredOptions.push(freeOption)
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ hotkeys:
|
||||||
profile-selectors:
|
profile-selectors:
|
||||||
__nonStructural: true
|
__nonStructural: true
|
||||||
profiles: []
|
profiles: []
|
||||||
|
groups: []
|
||||||
profileDefaults:
|
profileDefaults:
|
||||||
__nonStructural: true
|
__nonStructural: true
|
||||||
ssh:
|
ssh:
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { Injectable } from '@angular/core'
|
||||||
import { TranslateService } from '@ngx-translate/core'
|
import { TranslateService } from '@ngx-translate/core'
|
||||||
import { ProfilesService } from './services/profiles.service'
|
import { ProfilesService } from './services/profiles.service'
|
||||||
import { HotkeyDescription, HotkeyProvider } from './api/hotkeyProvider'
|
import { HotkeyDescription, HotkeyProvider } from './api/hotkeyProvider'
|
||||||
import { PartialProfile, Profile } from './api'
|
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -268,7 +267,7 @@ export class AppHotkeyProvider extends HotkeyProvider {
|
||||||
return [
|
return [
|
||||||
...this.hotkeys,
|
...this.hotkeys,
|
||||||
...profiles.map(profile => ({
|
...profiles.map(profile => ({
|
||||||
id: `profile.${AppHotkeyProvider.getProfileHotkeyName(profile)}`,
|
id: `profile.${ProfilesService.getProfileHotkeyName(profile)}`,
|
||||||
name: this.translate.instant('New tab: {profile}', { profile: profile.name }),
|
name: this.translate.instant('New tab: {profile}', { profile: profile.name }),
|
||||||
})),
|
})),
|
||||||
...this.profilesService.getProviders().map(provider => ({
|
...this.profilesService.getProviders().map(provider => ({
|
||||||
|
@ -278,7 +277,4 @@ export class AppHotkeyProvider extends HotkeyProvider {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
static getProfileHotkeyName (profile: PartialProfile<Profile>): string {
|
|
||||||
return (profile.id ?? profile.name).replace(/\./g, '-')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ import { FastHtmlBindDirective } from './directives/fastHtmlBind.directive'
|
||||||
import { DropZoneDirective } from './directives/dropZone.directive'
|
import { DropZoneDirective } from './directives/dropZone.directive'
|
||||||
import { CdkAutoDropGroup } from './directives/cdkAutoDropGroup.directive'
|
import { CdkAutoDropGroup } from './directives/cdkAutoDropGroup.directive'
|
||||||
|
|
||||||
import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider, PlatformService, FileProvider, ProfilesService, ProfileProvider, SelectorOption, Profile, SelectorService, CommandProvider } from './api'
|
import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider, PlatformService, FileProvider, ProfilesService, ProfileProvider, QuickConnectProfileProvider, SelectorOption, Profile, SelectorService, CommandProvider } from './api'
|
||||||
|
|
||||||
import { AppService } from './services/app.service'
|
import { AppService } from './services/app.service'
|
||||||
import { ConfigService } from './services/config.service'
|
import { ConfigService } from './services/config.service'
|
||||||
|
@ -177,7 +177,7 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
|
||||||
if (hotkey.startsWith('profile.')) {
|
if (hotkey.startsWith('profile.')) {
|
||||||
const id = hotkey.substring(hotkey.indexOf('.') + 1)
|
const id = hotkey.substring(hotkey.indexOf('.') + 1)
|
||||||
const profiles = await profilesService.getProfiles()
|
const profiles = await profilesService.getProfiles()
|
||||||
const profile = profiles.find(x => AppHotkeyProvider.getProfileHotkeyName(x) === id)
|
const profile = profiles.find(x => ProfilesService.getProfileHotkeyName(x) === id)
|
||||||
if (profile) {
|
if (profile) {
|
||||||
profilesService.openNewTabForProfile(profile)
|
profilesService.openNewTabForProfile(profile)
|
||||||
}
|
}
|
||||||
|
@ -191,7 +191,7 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
|
||||||
this.showSelector(provider)
|
this.showSelector(provider)
|
||||||
}
|
}
|
||||||
if (hotkey === 'command-selector') {
|
if (hotkey === 'command-selector') {
|
||||||
commands.showSelector()
|
commands.showSelector().catch(() => {return})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hotkey === 'profile-selector') {
|
if (hotkey === 'profile-selector') {
|
||||||
|
@ -214,7 +214,7 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
|
||||||
callback: () => this.profilesService.openNewTabForProfile(p),
|
callback: () => this.profilesService.openNewTabForProfile(p),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if (provider.supportsQuickConnect) {
|
if (provider instanceof QuickConnectProfileProvider) {
|
||||||
options.push({
|
options.push({
|
||||||
name: this.translate.instant('Quick connect'),
|
name: this.translate.instant('Quick connect'),
|
||||||
freeInputPattern: this.translate.instant('Connect to "%s"...'),
|
freeInputPattern: this.translate.instant('Connect to "%s"...'),
|
||||||
|
@ -229,7 +229,7 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.selector.show(this.translate.instant('Select profile'), options)
|
await this.selector.show(this.translate.instant('Select profile'), options).catch(() => {return})
|
||||||
}
|
}
|
||||||
|
|
||||||
static forRoot (): ModuleWithProviders<AppModule> {
|
static forRoot (): ModuleWithProviders<AppModule> {
|
||||||
|
|
|
@ -101,7 +101,7 @@ export class CommandService {
|
||||||
context.tab = tab.getFocusedTab() ?? undefined
|
context.tab = tab.getFocusedTab() ?? undefined
|
||||||
}
|
}
|
||||||
const commands = await this.getCommands(context)
|
const commands = await this.getCommands(context)
|
||||||
await this.selector.show(
|
return this.selector.show(
|
||||||
this.translate.instant('Commands'),
|
this.translate.instant('Commands'),
|
||||||
commands.map(c => ({
|
commands.map(c => ({
|
||||||
name: c.label,
|
name: c.label,
|
||||||
|
@ -109,6 +109,6 @@ export class CommandService {
|
||||||
description: c.sublabel,
|
description: c.sublabel,
|
||||||
icon: c.icon,
|
icon: c.icon,
|
||||||
})),
|
})),
|
||||||
)
|
).then(() => {return})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { PlatformService } from '../api/platform'
|
||||||
import { HostAppService } from '../api/hostApp'
|
import { HostAppService } from '../api/hostApp'
|
||||||
import { Vault, VaultService } from './vault.service'
|
import { Vault, VaultService } from './vault.service'
|
||||||
import { serializeFunction } from '../utils'
|
import { serializeFunction } from '../utils'
|
||||||
|
import { PartialProfileGroup, ProfileGroup } from '../api/profileProvider'
|
||||||
const deepmerge = require('deepmerge')
|
const deepmerge = require('deepmerge')
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
|
@ -364,6 +365,45 @@ export class ConfigService {
|
||||||
}
|
}
|
||||||
config.version = 4
|
config.version = 4
|
||||||
}
|
}
|
||||||
|
if (config.version < 5) {
|
||||||
|
const groups: PartialProfileGroup<ProfileGroup>[] = []
|
||||||
|
for (const p of config.profiles ?? []) {
|
||||||
|
if (!(p.group ?? '').trim()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let group = groups.find(x => x.name === p.group)
|
||||||
|
if (!group) {
|
||||||
|
group = {
|
||||||
|
id: `${uuidv4()}`,
|
||||||
|
name: `${p.group}`,
|
||||||
|
}
|
||||||
|
groups.push(group)
|
||||||
|
}
|
||||||
|
p.group = group.id
|
||||||
|
}
|
||||||
|
|
||||||
|
const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}')
|
||||||
|
for (const g of groups) {
|
||||||
|
if (profileGroupCollapsed[g.name]) {
|
||||||
|
const collapsed = profileGroupCollapsed[g.name]
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||||
|
delete profileGroupCollapsed[g.name]
|
||||||
|
profileGroupCollapsed[g.id] = collapsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.localStorage.profileGroupCollapsed = JSON.stringify(profileGroupCollapsed)
|
||||||
|
|
||||||
|
config.groups = groups
|
||||||
|
config.version = 5
|
||||||
|
}
|
||||||
|
if (config.version < 6) {
|
||||||
|
if (config.ssh.clearServiceMessagesOnConnect === false) {
|
||||||
|
config.profileDefaults.ssh.clearServiceMessagesOnConnect = false
|
||||||
|
delete config.ssh?.clearServiceMessagesOnConnect
|
||||||
|
}
|
||||||
|
config.version = 6
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async maybeDecryptConfig (store) {
|
private async maybeDecryptConfig (store) {
|
||||||
|
|
|
@ -13,8 +13,9 @@ export class FileProvidersService {
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
async selectAndStoreFile (description: string): Promise<string> {
|
async selectAndStoreFile (description: string): Promise<string> {
|
||||||
const p = await this.selectProvider()
|
return this.selectProvider().then(p => {
|
||||||
return p.selectAndStoreFile(description)
|
return p.selectAndStoreFile(description)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async retrieveFile (key: string): Promise<Buffer> {
|
async retrieveFile (key: string): Promise<Buffer> {
|
||||||
|
|
|
@ -2,12 +2,15 @@ import { Injectable, Inject } from '@angular/core'
|
||||||
import { TranslateService } from '@ngx-translate/core'
|
import { TranslateService } from '@ngx-translate/core'
|
||||||
import { NewTabParameters } from './tabs.service'
|
import { NewTabParameters } from './tabs.service'
|
||||||
import { BaseTabComponent } from '../components/baseTab.component'
|
import { BaseTabComponent } from '../components/baseTab.component'
|
||||||
import { PartialProfile, Profile, ProfileProvider } from '../api/profileProvider'
|
import { QuickConnectProfileProvider, PartialProfile, PartialProfileGroup, Profile, ProfileGroup, ProfileProvider } from '../api/profileProvider'
|
||||||
import { SelectorOption } from '../api/selector'
|
import { SelectorOption } from '../api/selector'
|
||||||
import { AppService } from './app.service'
|
import { AppService } from './app.service'
|
||||||
import { configMerge, ConfigProxy, ConfigService } from './config.service'
|
import { configMerge, ConfigProxy, ConfigService } from './config.service'
|
||||||
import { NotificationsService } from './notifications.service'
|
import { NotificationsService } from './notifications.service'
|
||||||
import { SelectorService } from './selector.service'
|
import { SelectorService } from './selector.service'
|
||||||
|
import deepClone from 'clone-deep'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import slugify from 'slugify'
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class ProfilesService {
|
export class ProfilesService {
|
||||||
|
@ -36,6 +39,126 @@ export class ProfilesService {
|
||||||
@Inject(ProfileProvider) private profileProviders: ProfileProvider<Profile>[],
|
@Inject(ProfileProvider) private profileProviders: ProfileProvider<Profile>[],
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Methods used to interract with ProfileProvider
|
||||||
|
*/
|
||||||
|
|
||||||
|
getProviders (): ProfileProvider<Profile>[] {
|
||||||
|
return [...this.profileProviders]
|
||||||
|
}
|
||||||
|
|
||||||
|
providerForProfile <T extends Profile> (profile: PartialProfile<T>): ProfileProvider<T>|null {
|
||||||
|
const provider = this.profileProviders.find(x => x.id === profile.type) ?? null
|
||||||
|
return provider as unknown as ProfileProvider<T>|null
|
||||||
|
}
|
||||||
|
|
||||||
|
getDescription <P extends Profile> (profile: PartialProfile<P>): string|null {
|
||||||
|
profile = this.getConfigProxyForProfile(profile)
|
||||||
|
return this.providerForProfile(profile)?.getDescription(profile) ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Methods used to interract with Profile
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Return ConfigProxy for a given Profile
|
||||||
|
* arg: skipUserDefaults -> do not merge global provider defaults in ConfigProxy
|
||||||
|
* arg: skipGroupDefaults -> do not merge parent group provider defaults in ConfigProxy
|
||||||
|
*/
|
||||||
|
getConfigProxyForProfile <T extends Profile> (profile: PartialProfile<T>, options?: { skipGlobalDefaults?: boolean, skipGroupDefaults?: boolean }): T {
|
||||||
|
const defaults = this.getProfileDefaults(profile, options).reduce(configMerge, {})
|
||||||
|
return new ConfigProxy(profile, defaults) as unknown as T
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an Array of Profiles
|
||||||
|
* arg: includeBuiltin (default: true) -> include BuiltinProfiles
|
||||||
|
* arg: clone (default: false) -> return deepclone Array
|
||||||
|
*/
|
||||||
|
async getProfiles (options?: { includeBuiltin?: boolean, clone?: boolean }): Promise<PartialProfile<Profile>[]> {
|
||||||
|
let list = this.config.store.profiles ?? []
|
||||||
|
if (options?.includeBuiltin ?? true) {
|
||||||
|
const lists = await Promise.all(this.config.enabledServices(this.profileProviders).map(x => x.getBuiltinProfiles()))
|
||||||
|
list = [
|
||||||
|
...this.config.store.profiles ?? [],
|
||||||
|
...lists.reduce((a, b) => a.concat(b), []),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortKey = p => `${this.resolveProfileGroupName(p.group ?? '')} / ${p.name}`
|
||||||
|
list.sort((a, b) => sortKey(a).localeCompare(sortKey(b)))
|
||||||
|
list.sort((a, b) => (a.isBuiltin ? 1 : 0) - (b.isBuiltin ? 1 : 0))
|
||||||
|
return options?.clone ? deepClone(list) : list
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a new Profile in config
|
||||||
|
* arg: genId (default: true) -> generate uuid in before pushing Profile into config
|
||||||
|
*/
|
||||||
|
async newProfile (profile: PartialProfile<Profile>, options?: { genId?: boolean }): Promise<void> {
|
||||||
|
if (options?.genId ?? true) {
|
||||||
|
profile.id = `${profile.type}:custom:${slugify(profile.name)}:${uuidv4()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const cProfile = this.config.store.profiles.find(p => p.id === profile.id)
|
||||||
|
if (cProfile) {
|
||||||
|
throw new Error(`Cannot insert new Profile, duplicated Id: ${profile.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.config.store.profiles.push(profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a Profile in config
|
||||||
|
*/
|
||||||
|
async writeProfile (profile: PartialProfile<Profile>): Promise<void> {
|
||||||
|
const cProfile = this.config.store.profiles.find(p => p.id === profile.id)
|
||||||
|
if (cProfile) {
|
||||||
|
if (!profile.group) {
|
||||||
|
delete cProfile.group
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(cProfile, profile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a Profile from config
|
||||||
|
*/
|
||||||
|
async deleteProfile (profile: PartialProfile<Profile>): Promise<void> {
|
||||||
|
this.providerForProfile(profile)?.deleteProfile(this.getConfigProxyForProfile(profile))
|
||||||
|
this.config.store.profiles = this.config.store.profiles.filter(p => p.id !== profile.id)
|
||||||
|
|
||||||
|
const profileHotkeyName = ProfilesService.getProfileHotkeyName(profile)
|
||||||
|
if (this.config.store.hotkeys.profile.hasOwnProperty(profileHotkeyName)) {
|
||||||
|
const profileHotkeys = deepClone(this.config.store.hotkeys.profile)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||||
|
delete profileHotkeys[profileHotkeyName]
|
||||||
|
this.config.store.hotkeys.profile = profileHotkeys
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all Profiles from config using option filter
|
||||||
|
* arg: filter (p: PartialProfile<Profile>) => boolean -> predicate used to decide which profiles have to be deleted
|
||||||
|
*/
|
||||||
|
async bulkDeleteProfiles (filter: (p: PartialProfile<Profile>) => boolean): Promise<void> {
|
||||||
|
for (const profile of this.config.store.profiles.filter(filter)) {
|
||||||
|
this.providerForProfile(profile)?.deleteProfile(this.getConfigProxyForProfile(profile))
|
||||||
|
|
||||||
|
const profileHotkeyName = ProfilesService.getProfileHotkeyName(profile)
|
||||||
|
if (this.config.store.hotkeys.profile.hasOwnProperty(profileHotkeyName)) {
|
||||||
|
const profileHotkeys = deepClone(this.config.store.hotkeys.profile)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||||
|
delete profileHotkeys[profileHotkeyName]
|
||||||
|
this.config.store.hotkeys.profile = profileHotkeys
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.config.store.profiles = this.config.store.profiles.filter(x => !filter(x))
|
||||||
|
}
|
||||||
|
|
||||||
async openNewTabForProfile <P extends Profile> (profile: PartialProfile<P>): Promise<BaseTabComponent|null> {
|
async openNewTabForProfile <P extends Profile> (profile: PartialProfile<P>): Promise<BaseTabComponent|null> {
|
||||||
const params = await this.newTabParametersForProfile(profile)
|
const params = await this.newTabParametersForProfile(profile)
|
||||||
if (params) {
|
if (params) {
|
||||||
|
@ -63,52 +186,40 @@ export class ProfilesService {
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
getProviders (): ProfileProvider<Profile>[] {
|
async launchProfile (profile: PartialProfile<Profile>): Promise<void> {
|
||||||
return [...this.profileProviders]
|
await this.openNewTabForProfile(profile)
|
||||||
|
|
||||||
|
let recentProfiles: PartialProfile<Profile>[] = JSON.parse(window.localStorage['recentProfiles'] ?? '[]')
|
||||||
|
if (this.config.store.terminal.showRecentProfiles > 0) {
|
||||||
|
recentProfiles = recentProfiles.filter(x => x.group !== profile.group || x.name !== profile.name)
|
||||||
|
recentProfiles.unshift(profile)
|
||||||
|
recentProfiles = recentProfiles.slice(0, this.config.store.terminal.showRecentProfiles)
|
||||||
|
} else {
|
||||||
|
recentProfiles = []
|
||||||
|
}
|
||||||
|
window.localStorage['recentProfiles'] = JSON.stringify(recentProfiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProfiles (): Promise<PartialProfile<Profile>[]> {
|
static getProfileHotkeyName (profile: PartialProfile<Profile>): string {
|
||||||
const lists = await Promise.all(this.config.enabledServices(this.profileProviders).map(x => x.getBuiltinProfiles()))
|
return (profile.id ?? profile.name).replace(/\./g, '-')
|
||||||
let list = lists.reduce((a, b) => a.concat(b), [])
|
|
||||||
list = [
|
|
||||||
...this.config.store.profiles ?? [],
|
|
||||||
...list,
|
|
||||||
]
|
|
||||||
const sortKey = p => `${p.group ?? ''} / ${p.name}`
|
|
||||||
list.sort((a, b) => sortKey(a).localeCompare(sortKey(b)))
|
|
||||||
list.sort((a, b) => (a.isBuiltin ? 1 : 0) - (b.isBuiltin ? 1 : 0))
|
|
||||||
return list
|
|
||||||
}
|
}
|
||||||
|
|
||||||
providerForProfile <T extends Profile> (profile: PartialProfile<T>): ProfileProvider<T>|null {
|
/*
|
||||||
const provider = this.profileProviders.find(x => x.id === profile.type) ?? null
|
* Methods used to interract with Profile Selector
|
||||||
return provider as unknown as ProfileProvider<T>|null
|
*/
|
||||||
}
|
|
||||||
|
|
||||||
getDescription <P extends Profile> (profile: PartialProfile<P>): string|null {
|
|
||||||
profile = this.getConfigProxyForProfile(profile)
|
|
||||||
return this.providerForProfile(profile)?.getDescription(profile) ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
selectorOptionForProfile <P extends Profile, T> (profile: PartialProfile<P>): SelectorOption<T> {
|
selectorOptionForProfile <P extends Profile, T> (profile: PartialProfile<P>): SelectorOption<T> {
|
||||||
const fullProfile = this.getConfigProxyForProfile(profile)
|
const fullProfile = this.getConfigProxyForProfile(profile)
|
||||||
const provider = this.providerForProfile(fullProfile)
|
const provider = this.providerForProfile(fullProfile)
|
||||||
const freeInputEquivalent = provider?.intoQuickConnectString(fullProfile) ?? undefined
|
const freeInputEquivalent = provider instanceof QuickConnectProfileProvider ? provider.intoQuickConnectString(fullProfile) ?? undefined : undefined
|
||||||
return {
|
return {
|
||||||
...profile,
|
...profile,
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
group: this.resolveProfileGroupName(profile.group ?? ''),
|
||||||
group: profile.group || '',
|
|
||||||
freeInputEquivalent,
|
freeInputEquivalent,
|
||||||
description: provider?.getDescription(fullProfile),
|
description: provider?.getDescription(fullProfile),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getRecentProfiles (): PartialProfile<Profile>[] {
|
|
||||||
let recentProfiles: PartialProfile<Profile>[] = JSON.parse(window.localStorage['recentProfiles'] ?? '[]')
|
|
||||||
recentProfiles = recentProfiles.slice(0, this.config.store.terminal.showRecentProfiles)
|
|
||||||
return recentProfiles
|
|
||||||
}
|
|
||||||
|
|
||||||
showProfileSelector (): Promise<PartialProfile<Profile>|null> {
|
showProfileSelector (): Promise<PartialProfile<Profile>|null> {
|
||||||
if (this.selector.active) {
|
if (this.selector.active) {
|
||||||
return Promise.resolve(null)
|
return Promise.resolve(null)
|
||||||
|
@ -118,12 +229,12 @@ export class ProfilesService {
|
||||||
try {
|
try {
|
||||||
const recentProfiles = this.getRecentProfiles()
|
const recentProfiles = this.getRecentProfiles()
|
||||||
|
|
||||||
let options: SelectorOption<void>[] = recentProfiles.map(p => ({
|
let options: SelectorOption<void>[] = recentProfiles.map((p, i) => ({
|
||||||
...this.selectorOptionForProfile(p),
|
...this.selectorOptionForProfile(p),
|
||||||
group: this.translate.instant('Recent'),
|
group: this.translate.instant('Recent'),
|
||||||
icon: 'fas fa-history',
|
icon: 'fas fa-history',
|
||||||
color: p.color,
|
color: p.color,
|
||||||
weight: -2,
|
weight: i - (recentProfiles.length + 1),
|
||||||
callback: async () => {
|
callback: async () => {
|
||||||
if (p.id) {
|
if (p.id) {
|
||||||
p = (await this.getProfiles()).find(x => x.id === p.id) ?? p
|
p = (await this.getProfiles()).find(x => x.id === p.id) ?? p
|
||||||
|
@ -177,30 +288,38 @@ export class ProfilesService {
|
||||||
})
|
})
|
||||||
} catch { }
|
} catch { }
|
||||||
|
|
||||||
this.getProviders().filter(x => x.supportsQuickConnect).forEach(provider => {
|
this.getProviders().forEach(provider => {
|
||||||
options.push({
|
if (provider instanceof QuickConnectProfileProvider) {
|
||||||
name: this.translate.instant('Quick connect'),
|
options.push({
|
||||||
freeInputPattern: this.translate.instant('Connect to "%s"...'),
|
name: this.translate.instant('Quick connect'),
|
||||||
description: `(${provider.name.toUpperCase()})`,
|
freeInputPattern: this.translate.instant('Connect to "%s"...'),
|
||||||
icon: 'fas fa-arrow-right',
|
description: `(${provider.name.toUpperCase()})`,
|
||||||
weight: provider.id !== this.config.store.defaultQuickConnectProvider ? 1 : 0,
|
icon: 'fas fa-arrow-right',
|
||||||
callback: query => {
|
weight: provider.id !== this.config.store.defaultQuickConnectProvider ? 1 : 0,
|
||||||
const profile = provider.quickConnect(query)
|
callback: query => {
|
||||||
resolve(profile)
|
const profile = provider.quickConnect(query)
|
||||||
},
|
resolve(profile)
|
||||||
})
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.selector.show(this.translate.instant('Select profile or enter an address'), options)
|
await this.selector.show(this.translate.instant('Select profile or enter an address'), options).catch(() => reject())
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject(err)
|
reject(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getRecentProfiles (): PartialProfile<Profile>[] {
|
||||||
|
let recentProfiles: PartialProfile<Profile>[] = JSON.parse(window.localStorage['recentProfiles'] ?? '[]')
|
||||||
|
recentProfiles = recentProfiles.slice(0, this.config.store.terminal.showRecentProfiles)
|
||||||
|
return recentProfiles
|
||||||
|
}
|
||||||
|
|
||||||
async quickConnect (query: string): Promise<PartialProfile<Profile>|null> {
|
async quickConnect (query: string): Promise<PartialProfile<Profile>|null> {
|
||||||
for (const provider of this.getProviders()) {
|
for (const provider of this.getProviders()) {
|
||||||
if (provider.supportsQuickConnect) {
|
if (provider instanceof QuickConnectProfileProvider) {
|
||||||
const profile = provider.quickConnect(query)
|
const profile = provider.quickConnect(query)
|
||||||
if (profile) {
|
if (profile) {
|
||||||
return profile
|
return profile
|
||||||
|
@ -211,27 +330,178 @@ export class ProfilesService {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
getConfigProxyForProfile <T extends Profile> (profile: PartialProfile<T>, skipUserDefaults = false): T {
|
/*
|
||||||
|
* Methods used to interract with Profile/ProfileGroup/Global defaults
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return global defaults for a given profile provider
|
||||||
|
* Always return something, empty object if no defaults found
|
||||||
|
*/
|
||||||
|
getProviderDefaults (provider: ProfileProvider<Profile>): any {
|
||||||
|
const defaults = this.config.store.profileDefaults
|
||||||
|
return defaults[provider.id] ?? {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set global defaults for a given profile provider
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
|
setProviderDefaults (provider: ProfileProvider<Profile>, pdefaults: any): void {
|
||||||
|
this.config.store.profileDefaults[provider.id] = pdefaults
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return defaults for a given profile
|
||||||
|
* Always return something, empty object if no defaults found
|
||||||
|
* arg: skipUserDefaults -> do not merge global provider defaults in ConfigProxy
|
||||||
|
* arg: skipGroupDefaults -> do not merge parent group provider defaults in ConfigProxy
|
||||||
|
*/
|
||||||
|
getProfileDefaults (profile: PartialProfile<Profile>, options?: { skipGlobalDefaults?: boolean, skipGroupDefaults?: boolean }): any[] {
|
||||||
const provider = this.providerForProfile(profile)
|
const provider = this.providerForProfile(profile)
|
||||||
const defaults = [
|
|
||||||
|
return [
|
||||||
this.profileDefaults,
|
this.profileDefaults,
|
||||||
provider?.configDefaults ?? {},
|
provider?.configDefaults ?? {},
|
||||||
!provider || skipUserDefaults ? {} : this.config.store.profileDefaults[provider.id] ?? {},
|
provider && !options?.skipGlobalDefaults ? this.getProviderDefaults(provider) : {},
|
||||||
].reduce(configMerge, {})
|
provider && !options?.skipGlobalDefaults && !options?.skipGroupDefaults ? this.getProviderProfileGroupDefaults(profile.group ?? '', provider) : {},
|
||||||
return new ConfigProxy(profile, defaults) as unknown as T
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
async launchProfile (profile: PartialProfile<Profile>): Promise<void> {
|
/*
|
||||||
await this.openNewTabForProfile(profile)
|
* Methods used to interract with ProfileGroup
|
||||||
|
*/
|
||||||
|
|
||||||
let recentProfiles: PartialProfile<Profile>[] = JSON.parse(window.localStorage['recentProfiles'] ?? '[]')
|
/**
|
||||||
if (this.config.store.terminal.showRecentProfiles > 0) {
|
* Synchronously return an Array of the existing ProfileGroups
|
||||||
recentProfiles = recentProfiles.filter(x => x.group !== profile.group || x.name !== profile.name)
|
* Does not return builtin groups
|
||||||
recentProfiles.unshift(profile)
|
*/
|
||||||
recentProfiles = recentProfiles.slice(0, this.config.store.terminal.showRecentProfiles)
|
getSyncProfileGroups (): PartialProfileGroup<ProfileGroup>[] {
|
||||||
} else {
|
return deepClone(this.config.store.groups ?? [])
|
||||||
recentProfiles = []
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an Array of the existing ProfileGroups
|
||||||
|
* arg: includeProfiles (default: false) -> if false, does not fill up the profiles field of ProfileGroup
|
||||||
|
* arg: includeNonUserGroup (default: false) -> if false, does not add built-in and ungrouped groups
|
||||||
|
*/
|
||||||
|
async getProfileGroups (options?: { includeProfiles?: boolean, includeNonUserGroup?: boolean }): Promise<PartialProfileGroup<ProfileGroup>[]> {
|
||||||
|
let profiles: PartialProfile<Profile>[] = []
|
||||||
|
if (options?.includeProfiles) {
|
||||||
|
profiles = await this.getProfiles({ includeBuiltin: options.includeNonUserGroup, clone: true })
|
||||||
}
|
}
|
||||||
window.localStorage['recentProfiles'] = JSON.stringify(recentProfiles)
|
|
||||||
|
let groups: PartialProfileGroup<ProfileGroup>[] = this.getSyncProfileGroups()
|
||||||
|
groups = groups.map(x => {
|
||||||
|
x.editable = true
|
||||||
|
|
||||||
|
if (options?.includeProfiles) {
|
||||||
|
x.profiles = profiles.filter(p => p.group === x.id)
|
||||||
|
profiles = profiles.filter(p => p.group !== x.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return x
|
||||||
|
})
|
||||||
|
|
||||||
|
if (options?.includeNonUserGroup) {
|
||||||
|
const builtInGroups: PartialProfileGroup<ProfileGroup>[] = []
|
||||||
|
builtInGroups.push({
|
||||||
|
id: 'built-in',
|
||||||
|
name: this.translate.instant('Built-in'),
|
||||||
|
editable: false,
|
||||||
|
profiles: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const ungrouped: PartialProfileGroup<ProfileGroup> = {
|
||||||
|
id: 'ungrouped',
|
||||||
|
name: this.translate.instant('Ungrouped'),
|
||||||
|
editable: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.includeProfiles) {
|
||||||
|
for (const profile of profiles.filter(p => p.isBuiltin)) {
|
||||||
|
let group: PartialProfileGroup<ProfileGroup> | undefined = builtInGroups.find(g => g.id === slugify(profile.group ?? 'built-in'))
|
||||||
|
if (!group) {
|
||||||
|
group = {
|
||||||
|
id: `${slugify(profile.group!)}`,
|
||||||
|
name: `${profile.group!}`,
|
||||||
|
editable: false,
|
||||||
|
profiles: [],
|
||||||
|
}
|
||||||
|
builtInGroups.push(group)
|
||||||
|
}
|
||||||
|
|
||||||
|
group.profiles!.push(profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
ungrouped.profiles = profiles.filter(p => !p.isBuiltin)
|
||||||
|
}
|
||||||
|
|
||||||
|
groups = groups.concat(builtInGroups)
|
||||||
|
groups.push(ungrouped)
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a new ProfileGroup in config
|
||||||
|
* arg: genId (default: true) -> generate uuid in before pushing Profile into config
|
||||||
|
*/
|
||||||
|
async newProfileGroup (group: PartialProfileGroup<ProfileGroup>, options?: { genId?: boolean }): Promise<void> {
|
||||||
|
if (options?.genId ?? true) {
|
||||||
|
group.id = `${uuidv4()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const cProfileGroup = this.config.store.groups.find(p => p.id === group.id)
|
||||||
|
if (cProfileGroup) {
|
||||||
|
throw new Error(`Cannot insert new ProfileGroup, duplicated Id: ${group.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.config.store.groups.push(group)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a ProfileGroup in config
|
||||||
|
*/
|
||||||
|
async writeProfileGroup (group: PartialProfileGroup<ProfileGroup>): Promise<void> {
|
||||||
|
delete group.profiles
|
||||||
|
delete group.editable
|
||||||
|
|
||||||
|
const cGroup = this.config.store.groups.find(g => g.id === group.id)
|
||||||
|
if (cGroup) {
|
||||||
|
Object.assign(cGroup, group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a ProfileGroup from config
|
||||||
|
*/
|
||||||
|
async deleteProfileGroup (group: PartialProfileGroup<ProfileGroup>, options?: { deleteProfiles?: boolean }): Promise<void> {
|
||||||
|
this.config.store.groups = this.config.store.groups.filter(g => g.id !== group.id)
|
||||||
|
if (options?.deleteProfiles) {
|
||||||
|
await this.bulkDeleteProfiles((p) => p.group === group.id)
|
||||||
|
} else {
|
||||||
|
for (const profile of this.config.store.profiles.filter(x => x.group === group.id)) {
|
||||||
|
delete profile.group
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve and return ProfileGroup Name from ProfileGroup ID
|
||||||
|
*/
|
||||||
|
resolveProfileGroupName (groupId: string): string {
|
||||||
|
return this.config.store.groups.find(g => g.id === groupId)?.name ?? groupId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return defaults for a given group ID and provider
|
||||||
|
* Always return something, empty object if no defaults found
|
||||||
|
* arg: skipUserDefaults -> do not merge global provider defaults in ConfigProxy
|
||||||
|
*/
|
||||||
|
getProviderProfileGroupDefaults (groupId: string, provider: ProfileProvider<Profile>): any {
|
||||||
|
return this.getSyncProfileGroups().find(g => g.id === groupId)?.defaults?.[provider.id] ?? {}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -285,7 +285,7 @@ export class VaultFileProvider extends FileProvider {
|
||||||
icon: 'fas fa-file',
|
icon: 'fas fa-file',
|
||||||
result: f,
|
result: f,
|
||||||
})),
|
})),
|
||||||
])
|
]).catch(() => null)
|
||||||
if (result) {
|
if (result) {
|
||||||
return `${this.prefix}${result.key.id}`
|
return `${this.prefix}${result.key.id}`
|
||||||
}
|
}
|
||||||
|
|
|
@ -149,7 +149,7 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
|
||||||
click: async () => {
|
click: async () => {
|
||||||
const modal = this.ngbModal.open(PromptModalComponent)
|
const modal = this.ngbModal.open(PromptModalComponent)
|
||||||
modal.componentInstance.prompt = this.translate.instant('Profile name')
|
modal.componentInstance.prompt = this.translate.instant('Profile name')
|
||||||
const name = (await modal.result)?.value
|
const name = (await modal.result.catch(() => null))?.value
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -262,7 +262,7 @@ export class ProfilesContextMenu extends TabContextMenuItemProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
async switchTabProfile (tab: BaseTabComponent) {
|
async switchTabProfile (tab: BaseTabComponent) {
|
||||||
const profile = await this.profilesService.showProfileSelector()
|
const profile = await this.profilesService.showProfileSelector().catch(() => null)
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,10 @@ import { SerialPortStream } from '@serialport/stream'
|
||||||
import { LogService, NotificationsService } from 'tabby-core'
|
import { LogService, NotificationsService } from 'tabby-core'
|
||||||
import { Subject, Observable } from 'rxjs'
|
import { Subject, Observable } from 'rxjs'
|
||||||
import { Injector, NgZone } from '@angular/core'
|
import { Injector, NgZone } from '@angular/core'
|
||||||
import { BaseSession, BaseTerminalProfile, InputProcessingOptions, InputProcessor, LoginScriptsOptions, SessionMiddleware, StreamProcessingOptions, TerminalStreamProcessor, UTF8SplitterMiddleware } from 'tabby-terminal'
|
import { BaseSession, ConnectableTerminalProfile, InputProcessingOptions, InputProcessor, LoginScriptsOptions, SessionMiddleware, StreamProcessingOptions, TerminalStreamProcessor, UTF8SplitterMiddleware } from 'tabby-terminal'
|
||||||
import { SerialService } from './services/serial.service'
|
import { SerialService } from './services/serial.service'
|
||||||
|
|
||||||
export interface SerialProfile extends BaseTerminalProfile {
|
export interface SerialProfile extends ConnectableTerminalProfile {
|
||||||
options: SerialProfileOptions
|
options: SerialProfileOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,14 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'
|
||||||
import slugify from 'slugify'
|
import slugify from 'slugify'
|
||||||
import deepClone from 'clone-deep'
|
import deepClone from 'clone-deep'
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { ProfileProvider, NewTabParameters, SelectorService, HostAppService, Platform, TranslateService } from 'tabby-core'
|
import { NewTabParameters, SelectorService, HostAppService, Platform, TranslateService, ConnectableProfileProvider } from 'tabby-core'
|
||||||
import { SerialProfileSettingsComponent } from './components/serialProfileSettings.component'
|
import { SerialProfileSettingsComponent } from './components/serialProfileSettings.component'
|
||||||
import { SerialTabComponent } from './components/serialTab.component'
|
import { SerialTabComponent } from './components/serialTab.component'
|
||||||
import { SerialService } from './services/serial.service'
|
import { SerialService } from './services/serial.service'
|
||||||
import { BAUD_RATES, SerialProfile } from './api'
|
import { BAUD_RATES, SerialProfile } from './api'
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class SerialProfilesService extends ProfileProvider<SerialProfile> {
|
export class SerialProfilesService extends ConnectableProfileProvider<SerialProfile> {
|
||||||
id = 'serial'
|
id = 'serial'
|
||||||
name = _('Serial')
|
name = _('Serial')
|
||||||
settingsComponent = SerialProfileSettingsComponent
|
settingsComponent = SerialProfileSettingsComponent
|
||||||
|
@ -32,6 +32,7 @@ export class SerialProfilesService extends ProfileProvider<SerialProfile> {
|
||||||
slowSend: false,
|
slowSend: false,
|
||||||
input: { backspace: 'backspace' },
|
input: { backspace: 'backspace' },
|
||||||
},
|
},
|
||||||
|
clearServiceMessagesOnConnect: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
|
|
|
@ -59,7 +59,7 @@ export class ConfigSyncSettingsTabComponent extends BaseComponent {
|
||||||
const modal = this.ngbModal.open(PromptModalComponent)
|
const modal = this.ngbModal.open(PromptModalComponent)
|
||||||
modal.componentInstance.prompt = this.translate.instant('Name for the new config')
|
modal.componentInstance.prompt = this.translate.instant('Name for the new config')
|
||||||
modal.componentInstance.value = name
|
modal.componentInstance.value = name
|
||||||
name = (await modal.result)?.value
|
name = (await modal.result.catch(() => null))?.value
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
.modal-header
|
||||||
|
h3.m-0 {{group.name}}
|
||||||
|
|
||||||
|
.modal-body
|
||||||
|
.row
|
||||||
|
.col-12.col-lg-4
|
||||||
|
.mb-3
|
||||||
|
label(translate) Name
|
||||||
|
input.form-control(
|
||||||
|
type='text',
|
||||||
|
autofocus,
|
||||||
|
[(ngModel)]='group.name',
|
||||||
|
)
|
||||||
|
|
||||||
|
.col-12.col-lg-8
|
||||||
|
.form-line.content-box
|
||||||
|
.header
|
||||||
|
.title(translate) Default profile group settings
|
||||||
|
.description(translate) These apply to all profiles of a given type in this group
|
||||||
|
|
||||||
|
.list-group.mt-3.mb-3.content-box
|
||||||
|
a.list-group-item.list-group-item-action.d-flex.align-items-center(
|
||||||
|
(click)='editDefaults(provider)',
|
||||||
|
*ngFor='let provider of providers'
|
||||||
|
) {{provider.name|translate}}
|
||||||
|
.me-auto
|
||||||
|
button.btn.btn-link.hover-reveal.ms-1((click)='$event.stopPropagation(); deleteDefaults(provider)')
|
||||||
|
i.fas.fa-trash-arrow-up
|
||||||
|
|
||||||
|
.modal-footer
|
||||||
|
button.btn.btn-primary((click)='save()', translate) Save
|
||||||
|
button.btn.btn-danger((click)='cancel()', translate) Cancel
|
|
@ -0,0 +1,54 @@
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
import { Component, Input } from '@angular/core'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { ConfigProxy, ProfileGroup, Profile, ProfileProvider, PlatformService, TranslateService } from 'tabby-core'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
@Component({
|
||||||
|
templateUrl: './editProfileGroupModal.component.pug',
|
||||||
|
})
|
||||||
|
export class EditProfileGroupModalComponent<G extends ProfileGroup> {
|
||||||
|
@Input() group: G & ConfigProxy
|
||||||
|
@Input() providers: ProfileProvider<Profile>[]
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private modalInstance: NgbActiveModal,
|
||||||
|
private platform: PlatformService,
|
||||||
|
private translate: TranslateService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
save () {
|
||||||
|
this.modalInstance.close({ group: this.group })
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel () {
|
||||||
|
this.modalInstance.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
editDefaults (provider: ProfileProvider<Profile>) {
|
||||||
|
this.modalInstance.close({ group: this.group, provider })
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteDefaults (provider: ProfileProvider<Profile>): Promise<void> {
|
||||||
|
if ((await this.platform.showMessageBox(
|
||||||
|
{
|
||||||
|
type: 'warning',
|
||||||
|
message: this.translate.instant('Restore settings to inherited defaults ?'),
|
||||||
|
buttons: [
|
||||||
|
this.translate.instant('Delete'),
|
||||||
|
this.translate.instant('Keep'),
|
||||||
|
],
|
||||||
|
defaultId: 1,
|
||||||
|
cancelId: 1,
|
||||||
|
},
|
||||||
|
)).response === 0) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||||
|
delete this.group.defaults?.[provider.id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditProfileGroupModalComponentResult<G extends ProfileGroup> {
|
||||||
|
group: G
|
||||||
|
provider?: ProfileProvider<Profile>
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
.modal-header(*ngIf='!defaultsMode')
|
.modal-header(*ngIf='defaultsMode === "disabled"')
|
||||||
h3.m-0 {{profile.name}}
|
h3.m-0 {{profile.name}}
|
||||||
|
|
||||||
.modal-header(*ngIf='defaultsMode')
|
.modal-header(*ngIf='defaultsMode !== "disabled"')
|
||||||
h3.m-0(
|
h3.m-0(
|
||||||
translate='Defaults for {type}',
|
translate='Defaults for {type}',
|
||||||
[translateParams]='{type: profileProvider.name}'
|
[translateParams]='{type: profileProvider.name}'
|
||||||
|
@ -10,7 +10,7 @@
|
||||||
.modal-body
|
.modal-body
|
||||||
.row
|
.row
|
||||||
.col-12.col-lg-4
|
.col-12.col-lg-4
|
||||||
.mb-3(*ngIf='!defaultsMode')
|
.mb-3(*ngIf='defaultsMode === "disabled"')
|
||||||
label(translate) Name
|
label(translate) Name
|
||||||
input.form-control(
|
input.form-control(
|
||||||
type='text',
|
type='text',
|
||||||
|
@ -18,17 +18,20 @@
|
||||||
[(ngModel)]='profile.name',
|
[(ngModel)]='profile.name',
|
||||||
)
|
)
|
||||||
|
|
||||||
.mb-3(*ngIf='!defaultsMode')
|
.mb-3(*ngIf='defaultsMode === "disabled"')
|
||||||
label(translate) Group
|
label(translate) Group
|
||||||
input.form-control(
|
input.form-control(
|
||||||
type='text',
|
type='text',
|
||||||
alwaysVisibleTypeahead,
|
alwaysVisibleTypeahead,
|
||||||
placeholder='Ungrouped',
|
placeholder='Ungrouped',
|
||||||
[(ngModel)]='profile.group',
|
[(ngModel)]='profileGroup',
|
||||||
[ngbTypeahead]='groupTypeahead',
|
[ngbTypeahead]='groupTypeahead',
|
||||||
|
[inputFormatter]="groupFormatter",
|
||||||
|
[resultFormatter]="groupFormatter",
|
||||||
|
[editable]="false"
|
||||||
)
|
)
|
||||||
|
|
||||||
.mb-3(*ngIf='!defaultsMode')
|
.mb-3(*ngIf='defaultsMode === "disabled"')
|
||||||
label(translate) Icon
|
label(translate) Icon
|
||||||
.input-group
|
.input-group
|
||||||
input.form-control(
|
input.form-control(
|
||||||
|
@ -74,9 +77,15 @@
|
||||||
)
|
)
|
||||||
option(ngValue='auto', translate) Auto
|
option(ngValue='auto', translate) Auto
|
||||||
option(ngValue='keep', translate) Keep
|
option(ngValue='keep', translate) Keep
|
||||||
option(*ngIf='profile.type == "serial" || profile.type == "telnet" || profile.type == "ssh"', ngValue='reconnect', translate) Reconnect
|
option(*ngIf='isConnectable()', ngValue='reconnect', translate) Reconnect
|
||||||
option(ngValue='close', translate) Close
|
option(ngValue='close', translate) Close
|
||||||
|
|
||||||
|
.form-line(*ngIf='isConnectable()')
|
||||||
|
.header
|
||||||
|
.title(translate) Clear terminal after connection
|
||||||
|
toggle(
|
||||||
|
[(ngModel)]='profile.clearServiceMessagesOnConnect',
|
||||||
|
)
|
||||||
.mb-4
|
.mb-4
|
||||||
|
|
||||||
.col-12.col-lg-8(*ngIf='this.profileProvider.settingsComponent')
|
.col-12.col-lg-8(*ngIf='this.profileProvider.settingsComponent')
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { Observable, OperatorFunction, debounceTime, map, distinctUntilChanged } from 'rxjs'
|
import { Observable, OperatorFunction, debounceTime, map, distinctUntilChanged } from 'rxjs'
|
||||||
import { Component, Input, ViewChild, ViewContainerRef, ComponentFactoryResolver, Injector } from '@angular/core'
|
import { Component, Input, ViewChild, ViewContainerRef, ComponentFactoryResolver, Injector } from '@angular/core'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { ConfigProxy, ConfigService, Profile, ProfileProvider, ProfileSettingsComponent, ProfilesService, TAB_COLORS } from 'tabby-core'
|
import { ConfigProxy, PartialProfileGroup, Profile, ProfileProvider, ProfileSettingsComponent, ProfilesService, TAB_COLORS, ProfileGroup, ConnectableProfileProvider } from 'tabby-core'
|
||||||
|
|
||||||
const iconsData = require('../../../tabby-core/src/icons.json')
|
const iconsData = require('../../../tabby-core/src/icons.json')
|
||||||
const iconsClassList = Object.keys(iconsData).map(
|
const iconsClassList = Object.keys(iconsData).map(
|
||||||
|
@ -19,8 +19,9 @@ export class EditProfileModalComponent<P extends Profile> {
|
||||||
@Input() profile: P & ConfigProxy
|
@Input() profile: P & ConfigProxy
|
||||||
@Input() profileProvider: ProfileProvider<P>
|
@Input() profileProvider: ProfileProvider<P>
|
||||||
@Input() settingsComponent: new () => ProfileSettingsComponent<P>
|
@Input() settingsComponent: new () => ProfileSettingsComponent<P>
|
||||||
@Input() defaultsMode = false
|
@Input() defaultsMode: 'enabled'|'group'|'disabled' = 'disabled'
|
||||||
groupNames: string[]
|
@Input() profileGroup: PartialProfileGroup<ProfileGroup> | undefined
|
||||||
|
groups: PartialProfileGroup<ProfileGroup>[]
|
||||||
@ViewChild('placeholder', { read: ViewContainerRef }) placeholder: ViewContainerRef
|
@ViewChild('placeholder', { read: ViewContainerRef }) placeholder: ViewContainerRef
|
||||||
|
|
||||||
private _profile: Profile
|
private _profile: Profile
|
||||||
|
@ -30,14 +31,14 @@ export class EditProfileModalComponent<P extends Profile> {
|
||||||
private injector: Injector,
|
private injector: Injector,
|
||||||
private componentFactoryResolver: ComponentFactoryResolver,
|
private componentFactoryResolver: ComponentFactoryResolver,
|
||||||
private profilesService: ProfilesService,
|
private profilesService: ProfilesService,
|
||||||
config: ConfigService,
|
|
||||||
private modalInstance: NgbActiveModal,
|
private modalInstance: NgbActiveModal,
|
||||||
) {
|
) {
|
||||||
this.groupNames = [...new Set(
|
if (this.defaultsMode === 'disabled') {
|
||||||
(config.store.profiles as Profile[])
|
this.profilesService.getProfileGroups().then(groups => {
|
||||||
.map(x => x.group)
|
this.groups = groups
|
||||||
.filter(x => !!x),
|
this.profileGroup = groups.find(g => g.id === this.profile.group)
|
||||||
)].sort() as string[]
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
colorsAutocomplete = text$ => text$.pipe(
|
colorsAutocomplete = text$ => text$.pipe(
|
||||||
|
@ -56,7 +57,7 @@ export class EditProfileModalComponent<P extends Profile> {
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this._profile = this.profile
|
this._profile = this.profile
|
||||||
this.profile = this.profilesService.getConfigProxyForProfile(this.profile, this.defaultsMode)
|
this.profile = this.profilesService.getConfigProxyForProfile(this.profile, { skipGlobalDefaults: this.defaultsMode === 'enabled', skipGroupDefaults: this.defaultsMode === 'group' })
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit () {
|
ngAfterViewInit () {
|
||||||
|
@ -72,13 +73,15 @@ export class EditProfileModalComponent<P extends Profile> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
groupTypeahead = (text$: Observable<string>) =>
|
groupTypeahead: OperatorFunction<string, readonly PartialProfileGroup<ProfileGroup>[]> = (text$: Observable<string>) =>
|
||||||
text$.pipe(
|
text$.pipe(
|
||||||
debounceTime(200),
|
debounceTime(200),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
map(q => this.groupNames.filter(x => !q || x.toLowerCase().includes(q.toLowerCase()))),
|
map(q => this.groups.filter(g => !q || g.name.toLowerCase().includes(q.toLowerCase()))),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
groupFormatter = (g: PartialProfileGroup<ProfileGroup>) => g.name
|
||||||
|
|
||||||
iconSearch: OperatorFunction<string, string[]> = (text$: Observable<string>) =>
|
iconSearch: OperatorFunction<string, string[]> = (text$: Observable<string>) =>
|
||||||
text$.pipe(
|
text$.pipe(
|
||||||
debounceTime(200),
|
debounceTime(200),
|
||||||
|
@ -86,7 +89,12 @@ export class EditProfileModalComponent<P extends Profile> {
|
||||||
)
|
)
|
||||||
|
|
||||||
save () {
|
save () {
|
||||||
this.profile.group ||= undefined
|
if (!this.profileGroup) {
|
||||||
|
this.profile.group = undefined
|
||||||
|
} else {
|
||||||
|
this.profile.group = this.profileGroup.id
|
||||||
|
}
|
||||||
|
|
||||||
this.settingsComponentInstance?.save?.()
|
this.settingsComponentInstance?.save?.()
|
||||||
this.profile.__cleanup()
|
this.profile.__cleanup()
|
||||||
this.modalInstance.close(this._profile)
|
this.modalInstance.close(this._profile)
|
||||||
|
@ -95,4 +103,9 @@ export class EditProfileModalComponent<P extends Profile> {
|
||||||
cancel () {
|
cancel () {
|
||||||
this.modalInstance.dismiss()
|
this.modalInstance.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isConnectable (): boolean {
|
||||||
|
return this.profileProvider instanceof ConnectableProfileProvider
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,9 +27,17 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
|
||||||
i.fas.fa-fw.fa-search
|
i.fas.fa-fw.fa-search
|
||||||
input.form-control(type='search', [placeholder]='"Filter"|translate', [(ngModel)]='filter')
|
input.form-control(type='search', [placeholder]='"Filter"|translate', [(ngModel)]='filter')
|
||||||
|
|
||||||
button.btn.btn-primary.flex-shrink-0.ms-3((click)='newProfile()')
|
div(ngbDropdown).d-inline-block.flex-shrink-0.ms-3
|
||||||
i.fas.fa-fw.fa-plus
|
button.btn.btn-primary(ngbDropdownToggle)
|
||||||
span(translate) New profile
|
i.fas.fa-fw.fa-plus
|
||||||
|
span(translate) New
|
||||||
|
div(ngbDropdownMenu)
|
||||||
|
button(ngbDropdownItem, (click)='newProfile()')
|
||||||
|
i.fas.fa-fw.fa-plus
|
||||||
|
span(translate) New profile
|
||||||
|
button(ngbDropdownItem, (click)='newProfileGroup()')
|
||||||
|
i.fas.fa-fw.fa-plus
|
||||||
|
span(translate) New profile Group
|
||||||
|
|
||||||
.list-group.mt-3.mb-3
|
.list-group.mt-3.mb-3
|
||||||
ng-container(*ngFor='let group of profileGroups')
|
ng-container(*ngFor='let group of profileGroups')
|
||||||
|
@ -37,17 +45,17 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
|
||||||
.list-group-item.list-group-item-action.d-flex.align-items-center(
|
.list-group-item.list-group-item-action.d-flex.align-items-center(
|
||||||
(click)='toggleGroupCollapse(group)'
|
(click)='toggleGroupCollapse(group)'
|
||||||
)
|
)
|
||||||
.fa.fa-fw.fa-chevron-right(*ngIf='group.collapsed')
|
.fa.fa-fw.fa-chevron-right(*ngIf='group.collapsed && group.profiles?.length > 0')
|
||||||
.fa.fa-fw.fa-chevron-down(*ngIf='!group.collapsed')
|
.fa.fa-fw.fa-chevron-down(*ngIf='!group.collapsed && group.profiles?.length > 0')
|
||||||
span.ms-3.me-auto {{group.name || ("Ungrouped"|translate)}}
|
span.ms-3.me-auto {{group.name || ("Ungrouped"|translate)}}
|
||||||
button.btn.btn-sm.btn-link.hover-reveal.ms-2(
|
button.btn.btn-sm.btn-link.hover-reveal.ms-2(
|
||||||
*ngIf='group.editable && group.name',
|
*ngIf='group.editable && group.name',
|
||||||
(click)='$event.stopPropagation(); editGroup(group)'
|
(click)='$event.stopPropagation(); editProfileGroup(group)'
|
||||||
)
|
)
|
||||||
i.fas.fa-pencil-alt
|
i.fas.fa-pencil-alt
|
||||||
button.btn.btn-sm.btn-link.hover-reveal.ms-2(
|
button.btn.btn-sm.btn-link.hover-reveal.ms-2(
|
||||||
*ngIf='group.editable && group.name',
|
*ngIf='group.editable && group.name',
|
||||||
(click)='$event.stopPropagation(); deleteGroup(group)'
|
(click)='$event.stopPropagation(); deleteProfileGroup(group)'
|
||||||
)
|
)
|
||||||
i.fas.fa-trash-alt
|
i.fas.fa-trash-alt
|
||||||
ng-container(*ngIf='!group.collapsed')
|
ng-container(*ngIf='!group.collapsed')
|
||||||
|
@ -67,7 +75,7 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
|
||||||
|
|
||||||
.me-auto
|
.me-auto
|
||||||
|
|
||||||
button.btn.btn-link.hover-reveal.ms-1((click)='$event.stopPropagation(); launchProfile(profile)')
|
button.btn.btn-link.hover-reveal.ms-1(*ngIf='!profile.isTemplate', (click)='$event.stopPropagation(); launchProfile(profile)')
|
||||||
i.fas.fa-play
|
i.fas.fa-play
|
||||||
|
|
||||||
.ms-1.hover-reveal(ngbDropdown, placement='bottom-right top-right auto')
|
.ms-1.hover-reveal(ngbDropdown, placement='bottom-right top-right auto')
|
||||||
|
@ -169,9 +177,12 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
|
||||||
.description(translate) These apply to all profiles of a given type
|
.description(translate) These apply to all profiles of a given type
|
||||||
|
|
||||||
.list-group.mt-3.mb-3.content-box
|
.list-group.mt-3.mb-3.content-box
|
||||||
a.list-group-item.list-group-item-action(
|
a.list-group-item.list-group-item-action.d-flex.align-items-center(
|
||||||
(click)='editDefaults(provider)',
|
(click)='editDefaults(provider)',
|
||||||
*ngFor='let provider of profileProviders'
|
*ngFor='let provider of profileProviders'
|
||||||
) {{provider.name|translate}}
|
) {{provider.name|translate}}
|
||||||
|
.me-auto
|
||||||
|
button.btn.btn-link.hover-reveal.ms-1((click)='$event.stopPropagation(); deleteDefaults(provider)')
|
||||||
|
i.fas.fa-trash-arrow-up
|
||||||
|
|
||||||
div([ngbNavOutlet]='nav')
|
div([ngbNavOutlet]='nav')
|
||||||
|
|
|
@ -1,32 +1,27 @@
|
||||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'
|
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 deepClone from 'clone-deep'
|
||||||
import { Component, Inject } from '@angular/core'
|
import { Component, Inject } from '@angular/core'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { ConfigService, HostAppService, Profile, SelectorService, ProfilesService, PromptModalComponent, PlatformService, BaseComponent, PartialProfile, ProfileProvider, TranslateService, Platform, AppHotkeyProvider } from 'tabby-core'
|
import { ConfigService, HostAppService, Profile, SelectorService, ProfilesService, PromptModalComponent, PlatformService, BaseComponent, PartialProfile, ProfileProvider, TranslateService, Platform, ProfileGroup, PartialProfileGroup, QuickConnectProfileProvider } from 'tabby-core'
|
||||||
import { EditProfileModalComponent } from './editProfileModal.component'
|
import { EditProfileModalComponent } from './editProfileModal.component'
|
||||||
|
import { EditProfileGroupModalComponent, EditProfileGroupModalComponentResult } from './editProfileGroupModal.component'
|
||||||
interface ProfileGroup {
|
|
||||||
name?: string
|
|
||||||
profiles: PartialProfile<Profile>[]
|
|
||||||
editable: boolean
|
|
||||||
collapsed: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
_('Filter')
|
_('Filter')
|
||||||
_('Ungrouped')
|
_('Ungrouped')
|
||||||
|
|
||||||
|
interface CollapsableProfileGroup extends ProfileGroup {
|
||||||
|
collapsed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: './profilesSettingsTab.component.pug',
|
templateUrl: './profilesSettingsTab.component.pug',
|
||||||
styleUrls: ['./profilesSettingsTab.component.scss'],
|
styleUrls: ['./profilesSettingsTab.component.scss'],
|
||||||
})
|
})
|
||||||
export class ProfilesSettingsTabComponent extends BaseComponent {
|
export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||||
profiles: PartialProfile<Profile>[] = []
|
|
||||||
builtinProfiles: PartialProfile<Profile>[] = []
|
builtinProfiles: PartialProfile<Profile>[] = []
|
||||||
templateProfiles: PartialProfile<Profile>[] = []
|
templateProfiles: PartialProfile<Profile>[] = []
|
||||||
profileGroups: ProfileGroup[]
|
profileGroups: PartialProfileGroup<CollapsableProfileGroup>[]
|
||||||
filter = ''
|
filter = ''
|
||||||
Platform = Platform
|
Platform = Platform
|
||||||
|
|
||||||
|
@ -59,7 +54,7 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||||
|
|
||||||
async newProfile (base?: PartialProfile<Profile>): Promise<void> {
|
async newProfile (base?: PartialProfile<Profile>): Promise<void> {
|
||||||
if (!base) {
|
if (!base) {
|
||||||
let profiles = [...this.templateProfiles, ...this.builtinProfiles, ...this.profiles]
|
let profiles = await this.profilesService.getProfiles()
|
||||||
profiles = profiles.filter(x => !this.isProfileBlacklisted(x))
|
profiles = profiles.filter(x => !this.isProfileBlacklisted(x))
|
||||||
profiles.sort((a, b) => (a.weight ?? 0) - (b.weight ?? 0))
|
profiles.sort((a, b) => (a.weight ?? 0) - (b.weight ?? 0))
|
||||||
base = await this.selector.show(
|
base = await this.selector.show(
|
||||||
|
@ -67,10 +62,13 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||||
profiles.map(p => ({
|
profiles.map(p => ({
|
||||||
icon: p.icon,
|
icon: p.icon,
|
||||||
description: this.profilesService.getDescription(p) ?? undefined,
|
description: this.profilesService.getDescription(p) ?? undefined,
|
||||||
name: p.group ? `${p.group} / ${p.name}` : p.name,
|
name: p.group ? `${this.profilesService.resolveProfileGroupName(p.group)} / ${p.name}` : p.name,
|
||||||
result: p,
|
result: p,
|
||||||
})),
|
})),
|
||||||
)
|
).catch(() => undefined)
|
||||||
|
if (!base) {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const profile: PartialProfile<Profile> = deepClone(base)
|
const profile: PartialProfile<Profile> = deepClone(base)
|
||||||
delete profile.id
|
delete profile.id
|
||||||
|
@ -90,8 +88,7 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||||
const cfgProxy = this.profilesService.getConfigProxyForProfile(profile)
|
const cfgProxy = this.profilesService.getConfigProxyForProfile(profile)
|
||||||
profile.name = this.profilesService.providerForProfile(profile)?.getSuggestedName(cfgProxy) ?? this.translate.instant('{name} copy', base)
|
profile.name = this.profilesService.providerForProfile(profile)?.getSuggestedName(cfgProxy) ?? this.translate.instant('{name} copy', base)
|
||||||
}
|
}
|
||||||
profile.id = `${profile.type}:custom:${slugify(profile.name)}:${uuidv4()}`
|
await this.profilesService.newProfile(profile)
|
||||||
this.config.store.profiles = [profile, ...this.config.store.profiles]
|
|
||||||
await this.config.save()
|
await this.config.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,6 +98,7 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Object.assign(profile, result)
|
Object.assign(profile, result)
|
||||||
|
await this.profilesService.writeProfile(profile)
|
||||||
await this.config.save()
|
await this.config.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,69 +142,80 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||||
cancelId: 1,
|
cancelId: 1,
|
||||||
},
|
},
|
||||||
)).response === 0) {
|
)).response === 0) {
|
||||||
this.profilesService.providerForProfile(profile)?.deleteProfile(
|
await this.profilesService.deleteProfile(profile)
|
||||||
this.profilesService.getConfigProxyForProfile(profile))
|
|
||||||
this.config.store.profiles = this.config.store.profiles.filter(x => x !== profile)
|
|
||||||
const profileHotkeyName = AppHotkeyProvider.getProfileHotkeyName(profile)
|
|
||||||
if (this.config.store.hotkeys.profile.hasOwnProperty(profileHotkeyName)) {
|
|
||||||
const profileHotkeys = deepClone(this.config.store.hotkeys.profile)
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
||||||
delete profileHotkeys[profileHotkeyName]
|
|
||||||
this.config.store.hotkeys.profile = profileHotkeys
|
|
||||||
}
|
|
||||||
await this.config.save()
|
await this.config.save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh (): void {
|
async newProfileGroup (): Promise<void> {
|
||||||
this.profiles = this.config.store.profiles
|
|
||||||
this.profileGroups = []
|
|
||||||
const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}')
|
|
||||||
|
|
||||||
for (const profile of this.profiles) {
|
|
||||||
// Group null, undefined and empty together
|
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
|
||||||
let group = this.profileGroups.find(x => x.name === (profile.group || ''))
|
|
||||||
if (!group) {
|
|
||||||
group = {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
|
||||||
name: profile.group || '',
|
|
||||||
profiles: [],
|
|
||||||
editable: true,
|
|
||||||
collapsed: profileGroupCollapsed[profile.group ?? ''] ?? false,
|
|
||||||
}
|
|
||||||
this.profileGroups.push(group)
|
|
||||||
}
|
|
||||||
group.profiles.push(profile)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.profileGroups.sort((a, b) => a.name?.localeCompare(b.name ?? '') ?? -1)
|
|
||||||
|
|
||||||
const builtIn = {
|
|
||||||
name: this.translate.instant('Built-in'),
|
|
||||||
profiles: this.builtinProfiles,
|
|
||||||
editable: false,
|
|
||||||
collapsed: false,
|
|
||||||
}
|
|
||||||
builtIn.collapsed = profileGroupCollapsed[builtIn.name ?? ''] ?? false
|
|
||||||
this.profileGroups.push(builtIn)
|
|
||||||
}
|
|
||||||
|
|
||||||
async editGroup (group: ProfileGroup): Promise<void> {
|
|
||||||
const modal = this.ngbModal.open(PromptModalComponent)
|
const modal = this.ngbModal.open(PromptModalComponent)
|
||||||
modal.componentInstance.prompt = this.translate.instant('New name')
|
modal.componentInstance.prompt = this.translate.instant('New group name')
|
||||||
modal.componentInstance.value = group.name
|
const result = await modal.result.catch(() => null)
|
||||||
const result = await modal.result
|
if (result?.value.trim()) {
|
||||||
if (result) {
|
await this.profilesService.newProfileGroup({ id: '', name: result.value })
|
||||||
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()
|
await this.config.save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteGroup (group: ProfileGroup): Promise<void> {
|
async editProfileGroup (group: PartialProfileGroup<CollapsableProfileGroup>): Promise<void> {
|
||||||
|
const result = await this.showProfileGroupEditModal(group)
|
||||||
|
if (!result) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Object.assign(group, result)
|
||||||
|
await this.profilesService.writeProfileGroup(ProfilesSettingsTabComponent.collapsableIntoPartialProfileGroup(group))
|
||||||
|
await this.config.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
async showProfileGroupEditModal (group: PartialProfileGroup<CollapsableProfileGroup>): Promise<PartialProfileGroup<CollapsableProfileGroup>|null> {
|
||||||
|
const modal = this.ngbModal.open(
|
||||||
|
EditProfileGroupModalComponent,
|
||||||
|
{ size: 'lg' },
|
||||||
|
)
|
||||||
|
|
||||||
|
modal.componentInstance.group = deepClone(group)
|
||||||
|
modal.componentInstance.providers = this.profileProviders
|
||||||
|
|
||||||
|
const result: EditProfileGroupModalComponentResult<CollapsableProfileGroup> | null = await modal.result.catch(() => null)
|
||||||
|
if (!result) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.provider) {
|
||||||
|
return this.editProfileGroupDefaults(result.group, result.provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.group
|
||||||
|
}
|
||||||
|
|
||||||
|
private async editProfileGroupDefaults (group: PartialProfileGroup<CollapsableProfileGroup>, provider: ProfileProvider<Profile>): Promise<PartialProfileGroup<CollapsableProfileGroup>|null> {
|
||||||
|
const modal = this.ngbModal.open(
|
||||||
|
EditProfileModalComponent,
|
||||||
|
{ size: 'lg' },
|
||||||
|
)
|
||||||
|
const model = group.defaults?.[provider.id] ?? {}
|
||||||
|
model.type = provider.id
|
||||||
|
modal.componentInstance.profile = Object.assign({}, model)
|
||||||
|
modal.componentInstance.profileProvider = provider
|
||||||
|
modal.componentInstance.defaultsMode = 'group'
|
||||||
|
|
||||||
|
const result = await modal.result.catch(() => null)
|
||||||
|
if (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)
|
||||||
|
if (!group.defaults) {
|
||||||
|
group.defaults = {}
|
||||||
|
}
|
||||||
|
group.defaults[provider.id] = model
|
||||||
|
}
|
||||||
|
return this.showProfileGroupEditModal(group)
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteProfileGroup (group: PartialProfileGroup<ProfileGroup>): Promise<void> {
|
||||||
if ((await this.platform.showMessageBox(
|
if ((await this.platform.showMessageBox(
|
||||||
{
|
{
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
|
@ -219,7 +228,8 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||||
cancelId: 1,
|
cancelId: 1,
|
||||||
},
|
},
|
||||||
)).response === 0) {
|
)).response === 0) {
|
||||||
if ((await this.platform.showMessageBox(
|
let deleteProfiles = false
|
||||||
|
if ((group.profiles?.length ?? 0) > 0 && (await this.platform.showMessageBox(
|
||||||
{
|
{
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: this.translate.instant('Delete the group\'s profiles?'),
|
message: this.translate.instant('Delete the group\'s profiles?'),
|
||||||
|
@ -230,19 +240,26 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||||
defaultId: 0,
|
defaultId: 0,
|
||||||
cancelId: 0,
|
cancelId: 0,
|
||||||
},
|
},
|
||||||
)).response === 0) {
|
)).response !== 0) {
|
||||||
for (const profile of this.profiles.filter(x => x.group === group.name)) {
|
deleteProfiles = true
|
||||||
delete profile.group
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.config.store.profiles = this.config.store.profiles.filter(x => x.group !== group.name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.profilesService.deleteProfileGroup(group, { deleteProfiles })
|
||||||
await this.config.save()
|
await this.config.save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isGroupVisible (group: ProfileGroup): boolean {
|
async refresh (): Promise<void> {
|
||||||
return !this.filter || group.profiles.some(x => this.isProfileVisible(x))
|
const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}')
|
||||||
|
const groups = await this.profilesService.getProfileGroups({ includeNonUserGroup: true, includeProfiles: true })
|
||||||
|
groups.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
groups.sort((a, b) => (a.id === 'built-in' || !a.editable ? 1 : 0) - (b.id === 'built-in' || !b.editable ? 1 : 0))
|
||||||
|
groups.sort((a, b) => (a.id === 'ungrouped' ? 0 : 1) - (b.id === 'ungrouped' ? 0 : 1))
|
||||||
|
this.profileGroups = groups.map(g => ProfilesSettingsTabComponent.intoPartialCollapsableProfileGroup(g, profileGroupCollapsed[g.id] ?? false))
|
||||||
|
}
|
||||||
|
|
||||||
|
isGroupVisible (group: PartialProfileGroup<ProfileGroup>): boolean {
|
||||||
|
return !this.filter || (group.profiles ?? []).some(x => this.isProfileVisible(x))
|
||||||
}
|
}
|
||||||
|
|
||||||
isProfileVisible (profile: PartialProfile<Profile>): boolean {
|
isProfileVisible (profile: PartialProfile<Profile>): boolean {
|
||||||
|
@ -270,11 +287,12 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||||
}[this.profilesService.providerForProfile(profile)?.id ?? ''] ?? 'warning'
|
}[this.profilesService.providerForProfile(profile)?.id ?? ''] ?? 'warning'
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleGroupCollapse (group: ProfileGroup): void {
|
toggleGroupCollapse (group: PartialProfileGroup<CollapsableProfileGroup>): void {
|
||||||
|
if (group.profiles?.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
group.collapsed = !group.collapsed
|
group.collapsed = !group.collapsed
|
||||||
const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}')
|
this.saveProfileGroupCollapse(group)
|
||||||
profileGroupCollapsed[group.name ?? ''] = group.collapsed
|
|
||||||
window.localStorage.profileGroupCollapsed = JSON.stringify(profileGroupCollapsed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async editDefaults (provider: ProfileProvider<Profile>): Promise<void> {
|
async editDefaults (provider: ProfileProvider<Profile>): Promise<void> {
|
||||||
|
@ -282,21 +300,40 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||||
EditProfileModalComponent,
|
EditProfileModalComponent,
|
||||||
{ size: 'lg' },
|
{ size: 'lg' },
|
||||||
)
|
)
|
||||||
const model = this.config.store.profileDefaults[provider.id] ?? {}
|
const model = this.profilesService.getProviderDefaults(provider)
|
||||||
model.type = provider.id
|
model.type = provider.id
|
||||||
modal.componentInstance.profile = Object.assign({}, model)
|
modal.componentInstance.profile = Object.assign({}, model)
|
||||||
modal.componentInstance.profileProvider = provider
|
modal.componentInstance.profileProvider = provider
|
||||||
modal.componentInstance.defaultsMode = true
|
modal.componentInstance.defaultsMode = 'enabled'
|
||||||
const result = await modal.result
|
const result = await modal.result.catch(() => null)
|
||||||
|
if (result) {
|
||||||
// Fully replace the config
|
// Fully replace the config
|
||||||
for (const k in model) {
|
for (const k in model) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||||
delete model[k]
|
delete model[k]
|
||||||
|
}
|
||||||
|
Object.assign(model, result)
|
||||||
|
this.profilesService.setProviderDefaults(provider, model)
|
||||||
|
await this.config.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteDefaults (provider: ProfileProvider<Profile>): Promise<void> {
|
||||||
|
if ((await this.platform.showMessageBox(
|
||||||
|
{
|
||||||
|
type: 'warning',
|
||||||
|
message: this.translate.instant('Restore settings to defaults ?'),
|
||||||
|
buttons: [
|
||||||
|
this.translate.instant('Delete'),
|
||||||
|
this.translate.instant('Keep'),
|
||||||
|
],
|
||||||
|
defaultId: 1,
|
||||||
|
cancelId: 1,
|
||||||
|
},
|
||||||
|
)).response === 0) {
|
||||||
|
this.profilesService.setProviderDefaults(provider, {})
|
||||||
|
await this.config.save()
|
||||||
}
|
}
|
||||||
Object.assign(model, result)
|
|
||||||
this.config.store.profileDefaults[provider.id] = model
|
|
||||||
await this.config.save()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
blacklistProfile (profile: PartialProfile<Profile>): void {
|
blacklistProfile (profile: PartialProfile<Profile>): void {
|
||||||
|
@ -314,6 +351,29 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
getQuickConnectProviders (): ProfileProvider<Profile>[] {
|
getQuickConnectProviders (): ProfileProvider<Profile>[] {
|
||||||
return this.profileProviders.filter(x => x.supportsQuickConnect)
|
return this.profileProviders.filter(x => x instanceof QuickConnectProfileProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save ProfileGroup collapse state in localStorage
|
||||||
|
*/
|
||||||
|
private saveProfileGroupCollapse (group: PartialProfileGroup<CollapsableProfileGroup>): void {
|
||||||
|
const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}')
|
||||||
|
profileGroupCollapsed[group.id] = group.collapsed
|
||||||
|
window.localStorage.profileGroupCollapsed = JSON.stringify(profileGroupCollapsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static collapsableIntoPartialProfileGroup (group: PartialProfileGroup<CollapsableProfileGroup>): PartialProfileGroup<ProfileGroup> {
|
||||||
|
const g: any = { ...group }
|
||||||
|
delete g.collapsed
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
private static intoPartialCollapsableProfileGroup (group: PartialProfileGroup<ProfileGroup>, collapsed: boolean): PartialProfileGroup<CollapsableProfileGroup> {
|
||||||
|
const collapsableGroup = {
|
||||||
|
...group,
|
||||||
|
collapsed,
|
||||||
|
}
|
||||||
|
return collapsableGroup
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,9 +35,11 @@ export class VaultSettingsTabComponent extends BaseComponent {
|
||||||
|
|
||||||
async enableVault () {
|
async enableVault () {
|
||||||
const modal = this.ngbModal.open(SetVaultPassphraseModalComponent)
|
const modal = this.ngbModal.open(SetVaultPassphraseModalComponent)
|
||||||
const newPassphrase = await modal.result
|
const newPassphrase = await modal.result.catch(() => null)
|
||||||
await this.vault.setEnabled(true, newPassphrase)
|
if (newPassphrase) {
|
||||||
this.vaultContents = await this.vault.load(newPassphrase)
|
await this.vault.setEnabled(true, newPassphrase)
|
||||||
|
this.vaultContents = await this.vault.load(newPassphrase)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async disableVault () {
|
async disableVault () {
|
||||||
|
@ -65,8 +67,10 @@ export class VaultSettingsTabComponent extends BaseComponent {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const modal = this.ngbModal.open(SetVaultPassphraseModalComponent)
|
const modal = this.ngbModal.open(SetVaultPassphraseModalComponent)
|
||||||
const newPassphrase = await modal.result
|
const newPassphrase = await modal.result.catch(() => null)
|
||||||
this.vault.save(this.vaultContents, newPassphrase)
|
if (newPassphrase) {
|
||||||
|
this.vault.save(this.vaultContents, newPassphrase)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleConfigEncrypted () {
|
async toggleConfigEncrypted () {
|
||||||
|
@ -118,7 +122,7 @@ export class VaultSettingsTabComponent extends BaseComponent {
|
||||||
modal.componentInstance.prompt = this.translate.instant('New name')
|
modal.componentInstance.prompt = this.translate.instant('New name')
|
||||||
modal.componentInstance.value = secret.key.description
|
modal.componentInstance.value = secret.key.description
|
||||||
|
|
||||||
const description = (await modal.result)?.value
|
const description = (await modal.result.catch(() => null))?.value
|
||||||
if (!description) {
|
if (!description) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { InfiniteScrollModule } from 'ngx-infinite-scroll'
|
||||||
import TabbyCorePlugin, { ToolbarButtonProvider, HotkeyProvider, ConfigProvider, HotkeysService, AppService } from 'tabby-core'
|
import TabbyCorePlugin, { ToolbarButtonProvider, HotkeyProvider, ConfigProvider, HotkeysService, AppService } from 'tabby-core'
|
||||||
|
|
||||||
import { EditProfileModalComponent } from './components/editProfileModal.component'
|
import { EditProfileModalComponent } from './components/editProfileModal.component'
|
||||||
|
import { EditProfileGroupModalComponent } from './components/editProfileGroupModal.component'
|
||||||
import { HotkeyInputModalComponent } from './components/hotkeyInputModal.component'
|
import { HotkeyInputModalComponent } from './components/hotkeyInputModal.component'
|
||||||
import { HotkeySettingsTabComponent } from './components/hotkeySettingsTab.component'
|
import { HotkeySettingsTabComponent } from './components/hotkeySettingsTab.component'
|
||||||
import { MultiHotkeyInputComponent } from './components/multiHotkeyInput.component'
|
import { MultiHotkeyInputComponent } from './components/multiHotkeyInput.component'
|
||||||
|
@ -48,6 +49,7 @@ import { HotkeySettingsTabProvider, WindowSettingsTabProvider, VaultSettingsTabP
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
EditProfileModalComponent,
|
EditProfileModalComponent,
|
||||||
|
EditProfileGroupModalComponent,
|
||||||
HotkeyInputModalComponent,
|
HotkeyInputModalComponent,
|
||||||
HotkeySettingsTabComponent,
|
HotkeySettingsTabComponent,
|
||||||
MultiHotkeyInputComponent,
|
MultiHotkeyInputComponent,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { BaseTerminalProfile, InputProcessingOptions, LoginScriptsOptions } from 'tabby-terminal'
|
import { ConnectableTerminalProfile, InputProcessingOptions, LoginScriptsOptions } from 'tabby-terminal'
|
||||||
|
|
||||||
export enum SSHAlgorithmType {
|
export enum SSHAlgorithmType {
|
||||||
HMAC = 'hmac',
|
HMAC = 'hmac',
|
||||||
|
@ -7,7 +7,7 @@ export enum SSHAlgorithmType {
|
||||||
HOSTKEY = 'serverHostKey',
|
HOSTKEY = 'serverHostKey',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SSHProfile extends BaseTerminalProfile {
|
export interface SSHProfile extends ConnectableTerminalProfile {
|
||||||
options: SSHProfileOptions
|
options: SSHProfileOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -113,8 +113,8 @@ export class SFTPPanelComponent {
|
||||||
|
|
||||||
async openCreateDirectoryModal (): Promise<void> {
|
async openCreateDirectoryModal (): Promise<void> {
|
||||||
const modal = this.ngbModal.open(SFTPCreateDirectoryModalComponent)
|
const modal = this.ngbModal.open(SFTPCreateDirectoryModalComponent)
|
||||||
const directoryName = await modal.result
|
const directoryName = await modal.result.catch(() => null)
|
||||||
if (directoryName !== '') {
|
if (directoryName?.trim()) {
|
||||||
this.sftp.mkdir(path.join(this.path, directoryName)).then(() => {
|
this.sftp.mkdir(path.join(this.path, directoryName)).then(() => {
|
||||||
this.notifications.notice('The directory was created successfully')
|
this.notifications.notice('The directory was created successfully')
|
||||||
this.navigate(path.join(this.path, directoryName))
|
this.navigate(path.join(this.path, directoryName))
|
||||||
|
|
|
@ -75,7 +75,7 @@ export class SSHProfileSettingsComponent {
|
||||||
modal.componentInstance.prompt = `Password for ${this.profile.options.user}@${this.profile.options.host}`
|
modal.componentInstance.prompt = `Password for ${this.profile.options.user}@${this.profile.options.host}`
|
||||||
modal.componentInstance.password = true
|
modal.componentInstance.password = true
|
||||||
try {
|
try {
|
||||||
const result = await modal.result
|
const result = await modal.result.catch(() => null)
|
||||||
if (result?.value) {
|
if (result?.value) {
|
||||||
this.passwordStorage.savePassword(this.profile, result.value)
|
this.passwordStorage.savePassword(this.profile, result.value)
|
||||||
this.hasSavedPassword = true
|
this.hasSavedPassword = true
|
||||||
|
@ -89,11 +89,13 @@ export class SSHProfileSettingsComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
async addPrivateKey () {
|
async addPrivateKey () {
|
||||||
const ref = await this.fileProviders.selectAndStoreFile(`private key for ${this.profile.name}`)
|
const ref = await this.fileProviders.selectAndStoreFile(`private key for ${this.profile.name}`).catch(() => null)
|
||||||
this.profile.options.privateKeys = [
|
if (ref) {
|
||||||
...this.profile.options.privateKeys!,
|
this.profile.options.privateKeys = [
|
||||||
ref,
|
...this.profile.options.privateKeys!,
|
||||||
]
|
ref,
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
removePrivateKey (path: string) {
|
removePrivateKey (path: string) {
|
||||||
|
|
|
@ -61,12 +61,4 @@ h3 SSH
|
||||||
(ngModelChange)='config.save()'
|
(ngModelChange)='config.save()'
|
||||||
)
|
)
|
||||||
|
|
||||||
.form-line
|
|
||||||
.header
|
|
||||||
.title(translate) Clear terminal after connection
|
|
||||||
toggle(
|
|
||||||
[(ngModel)]='config.store.ssh.clearServiceMessagesOnConnect',
|
|
||||||
(ngModelChange)='config.save()',
|
|
||||||
)
|
|
||||||
|
|
||||||
.alert.alert-info(translate) SSH connection management is now done through the "Profiles & connections" tab
|
.alert.alert-info(translate) SSH connection management is now done through the "Profiles & connections" tab
|
||||||
|
|
|
@ -83,7 +83,7 @@ export class SSHTabComponent extends ConnectableTerminalTabComponent<SSHProfile>
|
||||||
|
|
||||||
const jumpSession = await this.setupOneSession(
|
const jumpSession = await this.setupOneSession(
|
||||||
this.injector,
|
this.injector,
|
||||||
this.profilesService.getConfigProxyForProfile(jumpConnection),
|
this.profilesService.getConfigProxyForProfile<SSHProfile>(jumpConnection),
|
||||||
)
|
)
|
||||||
|
|
||||||
jumpSession.ref()
|
jumpSession.ref()
|
||||||
|
@ -163,10 +163,6 @@ export class SSHTabComponent extends ConnectableTerminalTabComponent<SSHProfile>
|
||||||
|
|
||||||
await session.start()
|
await session.start()
|
||||||
|
|
||||||
if (this.config.store.ssh.clearServiceMessagesOnConnect) {
|
|
||||||
this.frontend?.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.session?.resize(this.size.columns, this.size.rows)
|
this.session?.resize(this.size.columns, this.size.rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,6 @@ export class SSHConfigProvider extends ConfigProvider {
|
||||||
x11Display: null,
|
x11Display: null,
|
||||||
knownHosts: [],
|
knownHosts: [],
|
||||||
verifyHostKeys: true,
|
verifyHostKeys: true,
|
||||||
clearServiceMessagesOnConnect: true,
|
|
||||||
},
|
},
|
||||||
hotkeys: {
|
hotkeys: {
|
||||||
'restart-ssh-session': [],
|
'restart-ssh-session': [],
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Injectable, InjectFlags, Injector } from '@angular/core'
|
import { Injectable, InjectFlags, Injector } from '@angular/core'
|
||||||
import { ProfileProvider, NewTabParameters, PartialProfile, TranslateService } from 'tabby-core'
|
import { NewTabParameters, PartialProfile, TranslateService, QuickConnectProfileProvider } from 'tabby-core'
|
||||||
import * as ALGORITHMS from 'ssh2/lib/protocol/constants'
|
import * as ALGORITHMS from 'ssh2/lib/protocol/constants'
|
||||||
import { SSHProfileSettingsComponent } from './components/sshProfileSettings.component'
|
import { SSHProfileSettingsComponent } from './components/sshProfileSettings.component'
|
||||||
import { SSHTabComponent } from './components/sshTab.component'
|
import { SSHTabComponent } from './components/sshTab.component'
|
||||||
|
@ -8,10 +8,9 @@ import { ALGORITHM_BLACKLIST, SSHAlgorithmType, SSHProfile } from './api'
|
||||||
import { SSHProfileImporter } from './api/importer'
|
import { SSHProfileImporter } from './api/importer'
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class SSHProfilesService extends ProfileProvider<SSHProfile> {
|
export class SSHProfilesService extends QuickConnectProfileProvider<SSHProfile> {
|
||||||
id = 'ssh'
|
id = 'ssh'
|
||||||
name = 'SSH'
|
name = 'SSH'
|
||||||
supportsQuickConnect = true
|
|
||||||
settingsComponent = SSHProfileSettingsComponent
|
settingsComponent = SSHProfileSettingsComponent
|
||||||
configDefaults = {
|
configDefaults = {
|
||||||
options: {
|
options: {
|
||||||
|
@ -45,6 +44,7 @@ export class SSHProfilesService extends ProfileProvider<SSHProfile> {
|
||||||
reuseSession: true,
|
reuseSession: true,
|
||||||
input: { backspace: 'backspace' },
|
input: { backspace: 'backspace' },
|
||||||
},
|
},
|
||||||
|
clearServiceMessagesOnConnect: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
|
|
|
@ -34,7 +34,7 @@ export class SSHMultiplexerService {
|
||||||
if (!jumpConnection) {
|
if (!jumpConnection) {
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
const jumpProfile = this.profilesService.getConfigProxyForProfile(jumpConnection)
|
const jumpProfile = this.profilesService.getConfigProxyForProfile<SSHProfile>(jumpConnection)
|
||||||
key += '$' + await this.getMultiplexerKey(jumpProfile)
|
key += '$' + await this.getMultiplexerKey(jumpProfile)
|
||||||
}
|
}
|
||||||
return key
|
return key
|
||||||
|
|
|
@ -210,7 +210,6 @@ export class SSHSession {
|
||||||
if (!await this.verifyHostKey(handshake)) {
|
if (!await this.verifyHostKey(handshake)) {
|
||||||
this.ssh.end()
|
this.ssh.end()
|
||||||
reject(new Error('Host key verification failed'))
|
reject(new Error('Host key verification failed'))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
this.logger.info('Handshake complete:', handshake)
|
this.logger.info('Handshake complete:', handshake)
|
||||||
resolve()
|
resolve()
|
||||||
|
@ -300,7 +299,7 @@ export class SSHSession {
|
||||||
const modal = this.ngbModal.open(PromptModalComponent)
|
const modal = this.ngbModal.open(PromptModalComponent)
|
||||||
modal.componentInstance.prompt = `Username for ${this.profile.options.host}`
|
modal.componentInstance.prompt = `Username for ${this.profile.options.host}`
|
||||||
try {
|
try {
|
||||||
const result = await modal.result
|
const result = await modal.result.catch(() => null)
|
||||||
this.authUsername = result?.value ?? null
|
this.authUsername = result?.value ?? null
|
||||||
} catch {
|
} catch {
|
||||||
this.authUsername = 'root'
|
this.authUsername = 'root'
|
||||||
|
@ -428,11 +427,7 @@ export class SSHSession {
|
||||||
const modal = this.ngbModal.open(HostKeyPromptModalComponent)
|
const modal = this.ngbModal.open(HostKeyPromptModalComponent)
|
||||||
modal.componentInstance.selector = selector
|
modal.componentInstance.selector = selector
|
||||||
modal.componentInstance.digest = this.hostKeyDigest
|
modal.componentInstance.digest = this.hostKeyDigest
|
||||||
try {
|
return modal.result.catch(() => false)
|
||||||
return await modal.result
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -495,7 +490,7 @@ export class SSHSession {
|
||||||
modal.componentInstance.showRememberCheckbox = true
|
modal.componentInstance.showRememberCheckbox = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await modal.result
|
const result = await modal.result.catch(() => null)
|
||||||
if (result) {
|
if (result) {
|
||||||
if (result.remember) {
|
if (result.remember) {
|
||||||
this.savedPassword = result.value
|
this.savedPassword = result.value
|
||||||
|
|
|
@ -53,6 +53,6 @@ export class CommonSFTPContextMenu extends SFTPContextMenuItemProvider {
|
||||||
const modal = this.ngbModal.open(SFTPDeleteModalComponent)
|
const modal = this.ngbModal.open(SFTPDeleteModalComponent)
|
||||||
modal.componentInstance.item = item
|
modal.componentInstance.item = item
|
||||||
modal.componentInstance.sftp = session
|
modal.componentInstance.sftp = session
|
||||||
await modal.result
|
await modal.result.catch(() => {return})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { ProfileProvider, NewTabParameters, PartialProfile, TranslateService } from 'tabby-core'
|
import { NewTabParameters, PartialProfile, TranslateService, QuickConnectProfileProvider } from 'tabby-core'
|
||||||
import { TelnetProfileSettingsComponent } from './components/telnetProfileSettings.component'
|
import { TelnetProfileSettingsComponent } from './components/telnetProfileSettings.component'
|
||||||
import { TelnetTabComponent } from './components/telnetTab.component'
|
import { TelnetTabComponent } from './components/telnetTab.component'
|
||||||
import { TelnetProfile } from './session'
|
import { TelnetProfile } from './session'
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class TelnetProfilesService extends ProfileProvider<TelnetProfile> {
|
export class TelnetProfilesService extends QuickConnectProfileProvider<TelnetProfile> {
|
||||||
id = 'telnet'
|
id = 'telnet'
|
||||||
name = 'Telnet'
|
name = 'Telnet'
|
||||||
supportsQuickConnect = true
|
supportsQuickConnect = true
|
||||||
|
@ -21,6 +21,7 @@ export class TelnetProfilesService extends ProfileProvider<TelnetProfile> {
|
||||||
scripts: [],
|
scripts: [],
|
||||||
input: { backspace: 'backspace' },
|
input: { backspace: 'backspace' },
|
||||||
},
|
},
|
||||||
|
clearServiceMessagesOnConnect: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor (private translate: TranslateService) { super() }
|
constructor (private translate: TranslateService) { super() }
|
||||||
|
@ -95,4 +96,12 @@ export class TelnetProfilesService extends ProfileProvider<TelnetProfile> {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
intoQuickConnectString (profile: TelnetProfile): string | null {
|
||||||
|
let s = profile.options.host
|
||||||
|
if (profile.options.port !== 23) {
|
||||||
|
s = `${s}:${profile.options.port}`
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,11 @@ import colors from 'ansi-colors'
|
||||||
import stripAnsi from 'strip-ansi'
|
import stripAnsi from 'strip-ansi'
|
||||||
import { Injector } from '@angular/core'
|
import { Injector } from '@angular/core'
|
||||||
import { LogService } from 'tabby-core'
|
import { LogService } from 'tabby-core'
|
||||||
import { BaseSession, BaseTerminalProfile, InputProcessingOptions, InputProcessor, LoginScriptsOptions, SessionMiddleware, StreamProcessingOptions, TerminalStreamProcessor } from 'tabby-terminal'
|
import { BaseSession, ConnectableTerminalProfile, InputProcessingOptions, InputProcessor, LoginScriptsOptions, SessionMiddleware, StreamProcessingOptions, TerminalStreamProcessor } from 'tabby-terminal'
|
||||||
import { Subject, Observable } from 'rxjs'
|
import { Subject, Observable } from 'rxjs'
|
||||||
|
|
||||||
|
|
||||||
export interface TelnetProfile extends BaseTerminalProfile {
|
export interface TelnetProfile extends ConnectableTerminalProfile {
|
||||||
options: TelnetProfileOptions
|
options: TelnetProfileOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Injector, Component } from '@angular/core'
|
||||||
|
|
||||||
import { first } from 'rxjs'
|
import { first } from 'rxjs'
|
||||||
|
|
||||||
import { BaseTerminalProfile } from './interfaces'
|
import { ConnectableTerminalProfile } from './interfaces'
|
||||||
import { BaseTerminalTabComponent } from './baseTerminalTab.component'
|
import { BaseTerminalTabComponent } from './baseTerminalTab.component'
|
||||||
import { GetRecoveryTokenOptions, RecoveryToken } from 'tabby-core'
|
import { GetRecoveryTokenOptions, RecoveryToken } from 'tabby-core'
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ import { GetRecoveryTokenOptions, RecoveryToken } from 'tabby-core'
|
||||||
* A class to base your custom connectable terminal tabs on
|
* A class to base your custom connectable terminal tabs on
|
||||||
*/
|
*/
|
||||||
@Component({ template: '' })
|
@Component({ template: '' })
|
||||||
export abstract class ConnectableTerminalTabComponent<P extends BaseTerminalProfile> extends BaseTerminalTabComponent<P> {
|
export abstract class ConnectableTerminalTabComponent<P extends ConnectableTerminalProfile> extends BaseTerminalTabComponent<P> {
|
||||||
|
|
||||||
protected reconnectOffered = false
|
protected reconnectOffered = false
|
||||||
protected isDisconnectedByHand = false
|
protected isDisconnectedByHand = false
|
||||||
|
@ -57,6 +57,9 @@ export abstract class ConnectableTerminalTabComponent<P extends BaseTerminalProf
|
||||||
async initializeSession (): Promise<void> {
|
async initializeSession (): Promise<void> {
|
||||||
this.reconnectOffered = false
|
this.reconnectOffered = false
|
||||||
this.isDisconnectedByHand = false
|
this.isDisconnectedByHand = false
|
||||||
|
if (this.profile.clearServiceMessagesOnConnect) {
|
||||||
|
this.frontend?.clear()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Profile } from 'tabby-core'
|
import { ConnectableProfile, Profile } from 'tabby-core'
|
||||||
|
|
||||||
export interface ResizeEvent {
|
export interface ResizeEvent {
|
||||||
columns: number
|
columns: number
|
||||||
|
@ -19,3 +19,5 @@ export interface TerminalColorScheme {
|
||||||
export interface BaseTerminalProfile extends Profile {
|
export interface BaseTerminalProfile extends Profile {
|
||||||
terminalColorScheme?: TerminalColorScheme
|
terminalColorScheme?: TerminalColorScheme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConnectableTerminalProfile extends BaseTerminalProfile, ConnectableProfile {}
|
||||||
|
|
|
@ -175,7 +175,7 @@ export class SaveAsProfileContextMenu extends TabContextMenuItemProvider {
|
||||||
const modal = this.ngbModal.open(PromptModalComponent)
|
const modal = this.ngbModal.open(PromptModalComponent)
|
||||||
modal.componentInstance.prompt = this.translate.instant('New profile name')
|
modal.componentInstance.prompt = this.translate.instant('New profile name')
|
||||||
modal.componentInstance.value = tab.profile.name
|
modal.componentInstance.value = tab.profile.name
|
||||||
const name = (await modal.result)?.value
|
const name = (await modal.result.catch(() => null))?.value
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue