This commit is contained in:
Eugene
2026-06-23 09:22:15 +02:00
parent d44a9b40e0
commit c79d1498da
4 changed files with 162 additions and 149 deletions
+149
View File
@@ -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<string, string>|null = null
/** Strips `undefined` values from a process-style environment object. */
function normalizeEnv (env: Record<string, string|undefined>): Record<string, string> {
const result: Record<string, string> = {}
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<string, string>, 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<string, string>): 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<string, string> {
const env: Record<string, string> = {}
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<string, string>, source: Record<string, string>): 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<string, string> {
const merged: Record<string, string> = {}
// 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<string, string> {
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<string, string>): Record<string, string> {
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
}
+4 -19
View File
@@ -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<string, string>) {
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,
-130
View File
@@ -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<string, string>): Record<string, string> {
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<string, string> {
const env: Record<string, string> = {}
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<string, string> {
if (!wnr) {
const result: Record<string, string> = {}
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<string, string> = {}
// 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)
}
@@ -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()',
)