From c79d1498da69ca76f2ed528d4c6f84e1765dd243 Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 23 Jun 2026 09:22:15 +0200 Subject: [PATCH] cleanup --- tabby-local/src/environment.ts | 149 ++++++++++++++++++ tabby-local/src/session.ts | 23 +-- tabby-local/src/windowsEnvironment.ts | 130 --------------- .../terminalSettingsTab.component.pug | 9 ++ 4 files changed, 162 insertions(+), 149 deletions(-) create mode 100644 tabby-local/src/environment.ts delete mode 100644 tabby-local/src/windowsEnvironment.ts 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 4d345acf..6927e152 100644 --- a/tabby-local/src/session.ts +++ b/tabby-local/src/session.ts @@ -4,7 +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 { getWindowsEnvironment } from './windowsEnvironment' +import { getEnvironment, substituteEnv } from './environment' const windowsDirectoryRegex = /([a-zA-Z]:[^\:\[\]\?\"\<\>\|]+)/mi @@ -22,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 @@ -68,9 +53,9 @@ export class Session extends BaseSession { } if (!pty) { - const baseEnv = this.hostApp.platform === Platform.Windows && this.config.store.terminal.windowsRefreshEnvironment - ? getWindowsEnvironment() - : process.env + const baseEnv = getEnvironment( + this.hostApp.platform === Platform.Windows && this.config.store.terminal.windowsRefreshEnvironment, + ) let env = mergeEnv( baseEnv, diff --git a/tabby-local/src/windowsEnvironment.ts b/tabby-local/src/windowsEnvironment.ts deleted file mode 100644 index e9ce4c41..00000000 --- a/tabby-local/src/windowsEnvironment.ts +++ /dev/null @@ -1,130 +0,0 @@ -let wnr: any = null - -try { - wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires -} catch { } - -function expandWindowsEnv (env: Record): Record { - const result = { ...env } - const pattern = /%([^%]+)%/g - let changed = true - let iterations = 0 - while (changed && iterations < 10) { - changed = false - iterations++ - for (const [key, value] of Object.entries(result)) { - const expanded = value.replace(pattern, (match, varName) => { - const lookup = varName.toLowerCase() - const foundKey = Object.keys(result).find(k => k.toLowerCase() === lookup) - if (foundKey !== undefined) { - return result[foundKey] - } - const processKey = Object.keys(process.env).find(k => k.toLowerCase() === lookup) - if (processKey !== undefined) { - return process.env[processKey]! - } - return match - }) - if (expanded !== result[key]) { - result[key] = expanded - changed = true - } - } - } - return result -} - -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 { - return name.toLowerCase() === 'path' || name.toLowerCase() === 'pathext' -} - -function mergePathValue (existing: string, additional: string): string { - if (!existing) { - return additional - } - if (!additional) { - return existing - } - return existing + ';' + additional -} - -/** - * Rebuilds the environment block from Windows registry sources, - * mimicking the behavior of Windows Terminal's environment refresh. - * Falls back to process.env if windows-native-registry is unavailable. - */ -export function getWindowsEnvironment (): Record { - if (!wnr) { - const result: Record = {} - for (const [key, value] of Object.entries(process.env)) { - if (value !== undefined) { - result[key] = value - } - } - return result - } - - const systemEnv = readRegistryEnv(wnr.HK.LM, 'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment') - const userEnv = readRegistryEnv(wnr.HK.CU, 'Environment') - const volatileEnv = readRegistryEnv(wnr.HK.CU, 'Volatile Environment') - - const merged: Record = {} - - // System env vars as base - for (const [key, value] of Object.entries(systemEnv)) { - merged[key] = value - } - - // User env vars override system, but Path/PATHEXT are merged - for (const [key, value] of Object.entries(userEnv)) { - if (isPathLikeVar(key) && merged[key]) { - merged[key] = mergePathValue(merged[key], value) - } else { - merged[key] = value - } - } - - // Volatile env vars override, but Path/PATHEXT are merged - for (const [key, value] of Object.entries(volatileEnv)) { - if (isPathLikeVar(key) && merged[key]) { - merged[key] = mergePathValue(merged[key], value) - } else { - merged[key] = value - } - } - - // 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 && !(key in merged)) { - merged[key] = value - } - } - - return expandWindowsEnv(merged) -} 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()', + )