diff --git a/tabby-core/src/directives/fastHtmlBind.directive.ts b/tabby-core/src/directives/fastHtmlBind.directive.ts
index 0693b65045..255c27dce1 100644
--- a/tabby-core/src/directives/fastHtmlBind.directive.ts
+++ b/tabby-core/src/directives/fastHtmlBind.directive.ts
@@ -7,6 +7,7 @@ import { PlatformService } from '../api/platform'
})
export class FastHtmlBindDirective implements OnChanges {
@Input() fastHtmlBind?: string
+ private _lastValue?: string
constructor (
private el: ElementRef,
@@ -14,6 +15,10 @@ export class FastHtmlBindDirective implements OnChanges {
) { }
ngOnChanges (): void {
+ if (this.fastHtmlBind === this._lastValue) {
+ return
+ }
+ this._lastValue = this.fastHtmlBind
this.el.nativeElement.innerHTML = this.fastHtmlBind ?? ''
for (const link of this.el.nativeElement.querySelectorAll('a')) {
link.addEventListener('click', event => {
diff --git a/tabby-core/src/services/profiles.service.ts b/tabby-core/src/services/profiles.service.ts
index d0b9f0fec2..40388b0799 100644
--- a/tabby-core/src/services/profiles.service.ts
+++ b/tabby-core/src/services/profiles.service.ts
@@ -14,7 +14,7 @@ import slugify from 'slugify'
@Injectable({ providedIn: 'root' })
export class ProfilesService {
- private profileDefaults: Profile = {
+ private profileDefaults = {
id: '',
type: '',
name: '',
@@ -26,6 +26,7 @@ export class ProfilesService {
weight: 0,
isBuiltin: false,
isTemplate: false,
+ terminalColorScheme: null,
behaviorOnSessionEnd: 'auto',
}
@@ -52,8 +53,8 @@ export class ProfilesService {
}
getDescription
(profile: PartialProfile
): string|null {
- profile = this.getConfigProxyForProfile(profile) as PartialProfile
- return this.providerForProfile(profile)?.getDescription(profile) ?? null
+ const fullProfile = this.getConfigProxyForProfile(profile)
+ return this.providerForProfile(fullProfile)?.getDescription(fullProfile) ?? null
}
/*
@@ -65,7 +66,7 @@ export class ProfilesService {
* arg: skipUserDefaults -> do not merge global provider defaults in ConfigProxy
* arg: skipGroupDefaults -> do not merge parent group provider defaults in ConfigProxy
*/
- getConfigProxyForProfile
(profile: PartialProfile
, options?: { skipGlobalDefaults?: boolean, skipGroupDefaults?: boolean }): FullyDefined
& ConfigProxy> {
+ getConfigProxyForProfile (profile: PartialProfile, options?: { skipGlobalDefaults?: boolean, skipGroupDefaults?: boolean }): FullyDefined & ConfigProxy> {
const defaults = this.getProfileDefaults(profile, options).reduce(configMerge, {})
return new ConfigProxy(profile, defaults) as any
}
@@ -213,10 +214,9 @@ export class ProfilesService {
const provider = this.providerForProfile(fullProfile)
const freeInputEquivalent = provider instanceof QuickConnectProfileProvider ? provider.intoQuickConnectString(fullProfile) ?? undefined : undefined
return {
- name: profile.name,
+ ...profile,
icon: profile.icon ?? undefined,
color: profile.color ?? undefined,
- weight: profile.weight,
group: this.resolveProfileGroupName(profile.group ?? ''),
freeInputEquivalent,
description: provider?.getDescription(fullProfile),
@@ -510,7 +510,7 @@ export class ProfilesService {
* arg: skipUserDefaults -> do not merge global provider defaults in ConfigProxy
*/
getProviderProfileGroupDefaults (groupId: string, provider: ProfileProvider): any {
- return this.getSyncProfileGroups().find(g => g.id === groupId)?.defaults?.[provider.id] ?? {}
+ return (this.config.store.groups ?? []).find(g => g.id === groupId)?.defaults?.[provider.id] ?? {}
}
}
diff --git a/tabby-electron/src/shells/windowsStock.ts b/tabby-electron/src/shells/windowsStock.ts
index e2eaafa4f6..d1094a97f5 100644
--- a/tabby-electron/src/shells/windowsStock.ts
+++ b/tabby-electron/src/shells/windowsStock.ts
@@ -72,22 +72,26 @@ export class WindowsStockShellsProvider extends WindowsBaseShellProvider {
}
private async getPowerShellPath () {
- for (const name of ['pwsh.exe', 'powershell.exe']) {
- if (await which(name, { nothrow: true })) {
- return name
- }
- }
+ // Check well-known paths first to avoid slow PATH scanning via `which`
for (const psPath of [
`${process.env.USERPROFILE}\\AppData\\Local\\Microsoft\\WindowsApps\\pwsh.exe`,
+ `${process.env.ProgramFiles}\\PowerShell\\7\\pwsh.exe`,
+ `${process.env['ProgramFiles(x86)']}\\PowerShell\\7\\pwsh.exe`,
`${process.env.SystemRoot}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`,
`${process.env.SystemRoot}\\System32\\powershell.exe`,
- (process.env.SystemRoot ?? 'C:\\Windows') + '\\powerhshell.exe',
]) {
try {
await fs.stat(psPath)
return psPath
} catch { }
}
+ // Fall back to PATH search only if not found in standard locations
+ for (const name of ['pwsh.exe', 'powershell.exe']) {
+ const found = await which(name, { nothrow: true })
+ if (found) {
+ return found
+ }
+ }
return 'powershell.exe'
}
}
diff --git a/tabby-electron/src/sshImporters.ts b/tabby-electron/src/sshImporters.ts
index f301bc8f6e..3de2e44ee7 100644
--- a/tabby-electron/src/sshImporters.ts
+++ b/tabby-electron/src/sshImporters.ts
@@ -354,21 +354,72 @@ async function convertToSSHProfiles (config: SSHConfig): Promise[] | null = null
+let _openSSHCacheMtime: number | null = null
+let _openSSHCachePromise: Promise[]> | null = null
+
+interface SSHProfileDiskCache {
+ mtime: number
+ profiles: PartialProfile[]
+}
+
@Injectable({ providedIn: 'root' })
export class OpenSSHImporter extends SSHProfileImporter {
+ private diskCachePath: string
+
+ constructor (electron: ElectronService) {
+ super()
+ this.diskCachePath = path.join(electron.app.getPath('userData'), 'ssh-profiles-cache.json')
+ }
+
async getProfiles (): Promise[]> {
+ if (_openSSHCachePromise) {
+ return _openSSHCachePromise
+ }
const configPath = path.join(process.env.HOME ?? '~', '.ssh', 'config')
- try {
- const config: SSHConfig = await parseSSHConfigFile(configPath)
- return await convertToSSHProfiles(config)
- } catch (e) {
- if (e.code === 'ENOENT') {
- return []
+ _openSSHCachePromise = (async () => {
+ try {
+ const stat = await fs.stat(configPath)
+ const mtime = stat.mtimeMs
+
+ if (_openSSHCache && _openSSHCacheMtime === mtime) {
+ return _openSSHCache
+ }
+
+ // Try disk cache first
+ try {
+ const diskCache: SSHProfileDiskCache = JSON.parse(
+ await fs.readFile(this.diskCachePath, 'utf8'),
+ )
+ if (diskCache.mtime === mtime) {
+ _openSSHCache = diskCache.profiles
+ _openSSHCacheMtime = mtime
+ return _openSSHCache
+ }
+ } catch { /* no cache or invalid */ }
+
+ const config: SSHConfig = await parseSSHConfigFile(configPath)
+ _openSSHCache = await convertToSSHProfiles(config)
+ _openSSHCacheMtime = mtime
+
+ // Save to disk cache (fire and forget)
+ fs.writeFile(this.diskCachePath, JSON.stringify({ mtime, profiles: _openSSHCache }))
+ .catch(() => { /* ignore write errors */ })
+
+ return _openSSHCache
+ } catch (e) {
+ if (e.code === 'ENOENT') {
+ return []
+ }
+ throw e
+ } finally {
+ _openSSHCachePromise = null
}
- throw e
- }
+ })()
+
+ return _openSSHCachePromise
}
}
diff --git a/tabby-settings/src/components/profilesSettingsTab.component.ts b/tabby-settings/src/components/profilesSettingsTab.component.ts
index e7a810bd14..bff6def9ed 100644
--- a/tabby-settings/src/components/profilesSettingsTab.component.ts
+++ b/tabby-settings/src/components/profilesSettingsTab.component.ts
@@ -26,6 +26,7 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
profileGroups: PartialProfileGroup[]
filter = ''
Platform = Platform
+ private descriptionCache = new Map()
constructor (
public config: ConfigService,
@@ -49,10 +50,17 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
}
async refreshProfiles (): Promise {
- this.builtinProfiles = (await this.profilesService.getProfiles()).filter(x => x.isBuiltin)
- this.customProfiles = (await this.profilesService.getProfiles()).filter(x => !x.isBuiltin)
- this.templateProfiles = this.builtinProfiles.filter(x => x.isTemplate)
- this.builtinProfiles = this.builtinProfiles.filter(x => !x.isTemplate)
+ const allProfiles = await this.profilesService.getProfiles()
+ this.builtinProfiles = allProfiles.filter(x => x.isBuiltin && !x.isTemplate)
+ this.templateProfiles = allProfiles.filter(x => x.isBuiltin && x.isTemplate)
+ this.customProfiles = allProfiles.filter(x => !x.isBuiltin)
+
+ this.descriptionCache.clear()
+ for (const p of allProfiles) {
+ if (p.id) {
+ this.descriptionCache.set(p.id, this.profilesService.getDescription(p))
+ }
+ }
}
launchProfile (profile: PartialProfile): void {
@@ -265,6 +273,9 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
}
getDescription (profile: PartialProfile): string|null {
+ if (profile.id) {
+ return this.descriptionCache.get(profile.id) ?? null
+ }
return this.profilesService.getDescription(profile)
}