Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions tabby-core/src/directives/fastHtmlBind.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@ import { PlatformService } from '../api/platform'
})
export class FastHtmlBindDirective implements OnChanges {
@Input() fastHtmlBind?: string
private _lastValue?: string

constructor (
private el: ElementRef,
private platform: PlatformService,
) { }

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 => {
Expand Down
14 changes: 7 additions & 7 deletions tabby-core/src/services/profiles.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import slugify from 'slugify'

@Injectable({ providedIn: 'root' })
export class ProfilesService {
private profileDefaults: Profile = {
private profileDefaults = {
id: '',
type: '',
name: '',
Expand All @@ -26,6 +26,7 @@ export class ProfilesService {
weight: 0,
isBuiltin: false,
isTemplate: false,
terminalColorScheme: null,
behaviorOnSessionEnd: 'auto',
}

Expand All @@ -52,8 +53,8 @@ export class ProfilesService {
}

getDescription <P extends Profile> (profile: PartialProfile<P>): string|null {
profile = this.getConfigProxyForProfile(profile) as PartialProfile<P>
return this.providerForProfile(profile)?.getDescription(profile) ?? null
const fullProfile = this.getConfigProxyForProfile(profile)
return this.providerForProfile(fullProfile)?.getDescription(fullProfile) ?? null
}

/*
Expand All @@ -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 <P extends Profile> (profile: PartialProfile<P>, options?: { skipGlobalDefaults?: boolean, skipGroupDefaults?: boolean }): FullyDefined<P> & ConfigProxy<FullyDefined<P>> {
getConfigProxyForProfile <T extends Profile> (profile: PartialProfile<T>, options?: { skipGlobalDefaults?: boolean, skipGroupDefaults?: boolean }): FullyDefined<T> & ConfigProxy<FullyDefined<T>> {
const defaults = this.getProfileDefaults(profile, options).reduce(configMerge, {})
return new ConfigProxy(profile, defaults) as any
}
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -510,7 +510,7 @@ export class ProfilesService {
* 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] ?? {}
return (this.config.store.groups ?? []).find(g => g.id === groupId)?.defaults?.[provider.id] ?? {}
}

}
16 changes: 10 additions & 6 deletions tabby-electron/src/shells/windowsStock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
}
67 changes: 59 additions & 8 deletions tabby-electron/src/sshImporters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
SSHProfile,
AutoPrivateKeyLocator,
ForwardedPortConfig,
} from 'tabby-ssh'

Check failure on line 14 in tabby-electron/src/sshImporters.ts

View workflow job for this annotation

GitHub Actions / Lint

Cannot find module 'tabby-ssh' or its corresponding type declarations.

Check failure on line 14 in tabby-electron/src/sshImporters.ts

View workflow job for this annotation

GitHub Actions / Windows-Build (arm64, aarch64-pc-windows-msvc)

Cannot find module 'tabby-ssh' or its corresponding type declarations.

Check failure on line 14 in tabby-electron/src/sshImporters.ts

View workflow job for this annotation

GitHub Actions / macOS-Build (x86_64, x86_64-apple-darwin)

Cannot find module 'tabby-ssh' or its corresponding type declarations.

Check failure on line 14 in tabby-electron/src/sshImporters.ts

View workflow job for this annotation

GitHub Actions / Linux-Build (arm64, arm64, aarch64-unknown-linux-gnu, aarch64-linux-gnu-, ubuntu-24.04-arm)

Cannot find module 'tabby-ssh' or its corresponding type declarations.

Check failure on line 14 in tabby-electron/src/sshImporters.ts

View workflow job for this annotation

GitHub Actions / Linux-Build (x64, amd64, x86_64-unknown-linux-gnu, ubuntu-24.04)

Cannot find module 'tabby-ssh' or its corresponding type declarations.

Check failure on line 14 in tabby-electron/src/sshImporters.ts

View workflow job for this annotation

GitHub Actions / Windows-Build (x64, x86_64-pc-windows-msvc)

Cannot find module 'tabby-ssh' or its corresponding type declarations.

Check failure on line 14 in tabby-electron/src/sshImporters.ts

View workflow job for this annotation

GitHub Actions / Linux-Build (arm, armhf, arm-unknown-linux-gnueabihf, arm-linux-gnueabihf-, ubuntu-24.04)

Cannot find module 'tabby-ssh' or its corresponding type declarations.

Check failure on line 14 in tabby-electron/src/sshImporters.ts

View workflow job for this annotation

GitHub Actions / macOS-Build (arm64, aarch64-apple-darwin)

Cannot find module 'tabby-ssh' or its corresponding type declarations.

import { ElectronService } from './services/electron.service'
import SSHConfig, { Directive, LineType } from 'ssh-config'
Expand Down Expand Up @@ -354,21 +354,72 @@
}


let _openSSHCache: PartialProfile<SSHProfile>[] | null = null
let _openSSHCacheMtime: number | null = null
let _openSSHCachePromise: Promise<PartialProfile<SSHProfile>[]> | null = null

interface SSHProfileDiskCache {
mtime: number
profiles: PartialProfile<SSHProfile>[]
}

@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<PartialProfile<SSHProfile>[]> {
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
}
}

Expand Down
19 changes: 15 additions & 4 deletions tabby-settings/src/components/profilesSettingsTab.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
profileGroups: PartialProfileGroup<CollapsableProfileGroup>[]
filter = ''
Platform = Platform
private descriptionCache = new Map<string, string|null>()

constructor (
public config: ConfigService,
Expand All @@ -49,10 +50,17 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
}

async refreshProfiles (): Promise<void> {
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<Profile>): void {
Expand Down Expand Up @@ -265,6 +273,9 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
}

getDescription (profile: PartialProfile<Profile>): string|null {
if (profile.id) {
return this.descriptionCache.get(profile.id) ?? null
}
return this.profilesService.getDescription(profile)
}

Expand Down
Loading