diff --git a/tabby-local/src/config.ts b/tabby-local/src/config.ts index e6dbc2e1..6df505ae 100644 --- a/tabby-local/src/config.ts +++ b/tabby-local/src/config.ts @@ -8,6 +8,7 @@ export class TerminalConfigProvider extends ConfigProvider { useConPTY: true, environment: {}, setComSpec: false, + windowsRefreshEnvironment: true, }, } diff --git a/tabby-local/src/environment.ts b/tabby-local/src/environment.ts new file mode 100644 index 00000000..96ebfd34 --- /dev/null +++ b/tabby-local/src/environment.ts @@ -0,0 +1,149 @@ +let wnr: any = null + +try { + wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires +} catch { } + +let cachedEnvironment: Record|null = null + +/** Strips `undefined` values from a process-style environment object. */ +function normalizeEnv (env: Record): Record { + const result: Record = {} + for (const [key, value] of Object.entries(env)) { + if (value !== undefined) { + result[key] = value + } + } + return result +} + +/** + * Resolves a variable name to its actual key in `env`. Lookups are + * case-insensitive on Windows, where environment variable names are. + */ +function findKey (env: Record, name: string): string|undefined { + if (name in env) { + return name + } + if (process.platform !== 'win32') { + return undefined + } + const lower = name.toLowerCase() + return Object.keys(env).find(k => k.toLowerCase() === lower) +} + +/** Iteratively expands `%VAR%` references within an environment block in place. */ +function expandWindowsEnv (env: Record): void { + const pattern = /%([^%]+)%/g + let changed = true + let iterations = 0 + while (changed && iterations < 10) { + changed = false + iterations++ + for (const [key, value] of Object.entries(env)) { + const expanded = value.replace(pattern, (match, varName) => { + const found = findKey(env, varName) + return found !== undefined ? env[found] : match + }) + if (expanded !== env[key]) { + env[key] = expanded + changed = true + } + } + } +} + +function readRegistryEnv (hive: any, path: string): Record { + const env: Record = {} + if (!wnr) { + return env + } + try { + const key = wnr.getRegistryKey(hive, path) + if (!key) { + return env + } + for (const [name, entry] of Object.entries(key)) { + if (!name || typeof name !== 'string') { + continue + } + const e = entry as any + if (e && typeof e.value === 'string') { + env[name] = e.value + } + } + } catch { + // ignore registry read errors + } + return env +} + +function isPathLikeVar (name: string): boolean { + const lower = name.toLowerCase() + return lower === 'path' || lower === 'pathext' +} + +/** + * Merges a registry environment source into `target`. Most variables override, + * but `Path`/`PATHEXT` are appended to the existing value (resolved + * case-insensitively so e.g. system `Path` and user `PATH` don't split). + */ +function mergeRegistryEnv (target: Record, source: Record): void { + for (const [key, value] of Object.entries(source)) { + const existingKey = isPathLikeVar(key) ? findKey(target, key) : undefined + if (existingKey !== undefined) { + target[existingKey] = target[existingKey] && value + ? target[existingKey] + ';' + value + : target[existingKey] || value + } else { + target[key] = value + } + } +} + +/** + * Rebuilds the environment block from Windows registry sources, + * mimicking the behavior of Windows Terminal's environment refresh. + */ +function buildWindowsEnvironment (): Record { + const merged: Record = {} + + // System env vars as base, then user and volatile overrides (Path/PATHEXT appended) + mergeRegistryEnv(merged, readRegistryEnv(wnr.HK.LM, 'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment')) + mergeRegistryEnv(merged, readRegistryEnv(wnr.HK.CU, 'Environment')) + mergeRegistryEnv(merged, readRegistryEnv(wnr.HK.CU, 'Volatile Environment')) + + // Preserve process-specific vars (e.g. Electron, Node, Angular) + // that aren't defined in the registry + for (const [key, value] of Object.entries(process.env)) { + if (value !== undefined && findKey(merged, key) === undefined) { + merged[key] = value + } + } + + expandWindowsEnv(merged) + return merged +} + +export function getEnvironment (refreshFromRegistry = false): Record { + if (process.platform === 'win32' && refreshFromRegistry) { + cachedEnvironment = buildWindowsEnvironment() + } else { + cachedEnvironment ??= normalizeEnv(process.env) + } + return cachedEnvironment +} + +/** Expands `%VAR%` (Windows) / `$VAR` (POSIX) references using the cached environment. */ +export function substituteEnv (env: Record): Record { + const base = getEnvironment() + env = { ...env } + const pattern = process.platform === 'win32' ? /%(\w+)%/g : /\$(\w+)\b/g + for (const [key, value] of Object.entries(env)) { + env[key] = value.toString().replace(pattern, (substring, p1) => { + const found = findKey(base, p1) + return found !== undefined ? base[found] : '' + }) + } + return env +} diff --git a/tabby-local/src/session.ts b/tabby-local/src/session.ts index 0847139c..6927e152 100644 --- a/tabby-local/src/session.ts +++ b/tabby-local/src/session.ts @@ -4,6 +4,7 @@ import { Injector } from '@angular/core' import { HostAppService, ConfigService, WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild, Platform, BootstrapData, BOOTSTRAP_DATA, LogService } from 'tabby-core' import { BaseSession } from 'tabby-terminal' import { SessionOptions, ChildProcess, PTYInterface, PTYProxy } from './api' +import { getEnvironment, substituteEnv } from './environment' const windowsDirectoryRegex = /([a-zA-Z]:[^\:\[\]\?\"\<\>\|]+)/mi @@ -21,21 +22,6 @@ function mergeEnv (...envs) { return result } -function substituteEnv (env: Record) { - env = { ...env } - const pattern = process.platform === 'win32' ? /%(\w+)%/g : /\$(\w+)\b/g - for (const [key, value] of Object.entries(env)) { - env[key] = value.toString().replace(pattern, function (substring, p1) { - if (process.platform === 'win32') { - return Object.entries(process.env).find(x => x[0].toLowerCase() === p1.toLowerCase())?.[1] ?? '' - } else { - return process.env[p1] ?? '' - } - }) - } - return env -} - /** @hidden */ export class Session extends BaseSession { private pty: PTYProxy|null = null @@ -67,8 +53,12 @@ export class Session extends BaseSession { } if (!pty) { + const baseEnv = getEnvironment( + this.hostApp.platform === Platform.Windows && this.config.store.terminal.windowsRefreshEnvironment, + ) + let env = mergeEnv( - process.env, + baseEnv, { COLORTERM: 'truecolor', TERM: 'xterm-256color', diff --git a/tabby-terminal/src/components/terminalSettingsTab.component.pug b/tabby-terminal/src/components/terminalSettingsTab.component.pug index f5fb4ffc..3657b5e9 100644 --- a/tabby-terminal/src/components/terminalSettingsTab.component.pug +++ b/tabby-terminal/src/components/terminalSettingsTab.component.pug @@ -245,3 +245,12 @@ div.mt-4(*ngIf='hostApp.platform === Platform.Windows') [(ngModel)]='config.store.terminal.setComSpec', (ngModelChange)='config.save()', ) + + .form-line + .header + .title(translate) Refresh environment variables from registry + .description(translate) Reload PATH and other variables from the Windows registry for each new session + toggle( + [(ngModel)]='config.store.terminal.windowsRefreshEnvironment', + (ngModelChange)='config.save()', + )