Skip to content

Commit 9f01281

Browse files
fix(pwsh): stop async prompt clobbering third-party prompt wrappers
async: true deferred the entire init script, including the line that installs the global `prompt` function, to the first prompt draw. Anything wrapping `prompt` before that draw (e.g. zoxide's directory hook) got silently overwritten once the deferred script ran, since PowerShell's global prompt binding is written to unconditionally. Route the deferred install through a global function pointer variable instead: the trampoline installed at init time now owns the `prompt` binding permanently and never gets replaced, so anything wrapping it keeps a valid reference across the lazy load, re-init, and module removal. Fixes #7714 Entire-Checkpoint: 18db4675baa8
1 parent 218d736 commit 9f01281

3 files changed

Lines changed: 76 additions & 4 deletions

File tree

src/shell/init.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,17 @@ func sourceCommand(env runtime.Environment, scriptPath string, async bool) strin
281281
func sourceCommandAsync(shell, scriptPath string) string {
282282
switch shell {
283283
case PWSH:
284-
return fmt.Sprintf("function prompt() { & %s }", quotePwshOrElvishStr(scriptPath))
284+
// Get-Variable, not a bare $global: dereference, for the "has this
285+
// ever run before" check - on the very first run nothing has set
286+
// $global:_ompOriginalPromptFunction yet, and a bare read of an
287+
// unset variable throws under Set-StrictMode.
288+
return fmt.Sprintf(
289+
"if (-not (Get-Variable -Name _ompOriginalPromptFunction -Scope Global -ErrorAction Ignore -ValueOnly)) { $global:_ompOriginalPromptFunction = $Function:prompt }; "+
290+
"$global:_ompPromptFunction = $null; "+
291+
"$global:_ompInitialized = $false; "+
292+
"function prompt() { if (-not $global:_ompInitialized) { $global:_ompAsyncInit = $true; & %s; return }; if ($global:_ompPromptFunction) { & $global:_ompPromptFunction } }",
293+
quotePwshOrElvishStr(scriptPath),
294+
)
285295
case ZSH:
286296
return fmt.Sprintf("precmd() { source %s }", QuotePosixStr(scriptPath))
287297
case BASH:

src/shell/pwsh_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ Enable-PoshVIMode`
3030
assert.Equal(t, want, got)
3131
}
3232

33+
func TestSourceCommandAsyncPwsh(t *testing.T) {
34+
got := sourceCommandAsync(PWSH, "C:/cache/init.pwsh.ps1")
35+
36+
want := "if (-not (Get-Variable -Name _ompOriginalPromptFunction -Scope Global -ErrorAction Ignore -ValueOnly)) { $global:_ompOriginalPromptFunction = $Function:prompt }; " +
37+
"$global:_ompPromptFunction = $null; " +
38+
"$global:_ompInitialized = $false; " +
39+
"function prompt() { if (-not $global:_ompInitialized) { $global:_ompAsyncInit = $true; & 'C:/cache/init.pwsh.ps1'; return }; " +
40+
"if ($global:_ompPromptFunction) { & $global:_ompPromptFunction } }"
41+
42+
assert.Equal(t, want, got)
43+
}
44+
3345
func TestQuotePwshOrElvishStr(t *testing.T) {
3446
tests := []struct {
3547
str string

src/shell/scripts/omp.ps1

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,20 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock {
3737
# legacy per-prompt stream spawn.
3838
$script:ServeSupported = -not $script:ConstrainedLanguageMode -and $PSVersionTable.PSVersion.Major -ge 6
3939

40-
# Prompt related backup.
40+
# Async mode: state is threaded through $global:_ompAsyncInit (set by the
41+
# trampoline installed in the profile) and consumed once here, then
42+
# cleared so a later *sync* re-source of this same file takes the sync
43+
# branch below. Read via Get-Variable, not a bare $global: dereference -
44+
# a plain sync source never sets this global, and a bare read of an
45+
# unset variable throws under Set-StrictMode.
46+
$script:AsyncInit = [bool](Get-Variable -Name _ompAsyncInit -Scope Global -ErrorAction Ignore -ValueOnly)
47+
$global:_ompAsyncInit = $false
48+
49+
# Prompt related backup. In async mode this ends up capturing whatever
50+
# wraps the trampoline at first-draw time (e.g. another tool's prompt
51+
# hook), not the true pre-omp prompt - $global:_ompOriginalPromptFunction
52+
# (captured by the trampoline itself, before anything can wrap it) is
53+
# authoritative there instead. Kept here unconditionally for the sync path.
4154
$script:OriginalPromptFunction = $Function:prompt
4255
$originalPSReadLineOptions = Get-PSReadLineOption
4356
$script:OriginalContinuationPrompt = $originalPSReadLineOptions.ContinuationPrompt
@@ -986,7 +999,22 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock {
986999
$global:LASTEXITCODE = $script:OriginalLastExitCode
9871000
}
9881001

989-
$Function:prompt = $promptFunction
1002+
if ($script:AsyncInit) {
1003+
# Never touch the global prompt binding after the trampoline installed
1004+
# it - anything wrapping it (e.g. another tool's prompt hook) must
1005+
# keep a valid reference forever. Export-ModuleMember below becomes a
1006+
# silent no-op with no function named "prompt" in this module, which
1007+
# is intentional. $global:_ompInitialized is deliberately separate
1008+
# from $global:_ompPromptFunction: it only ever means "don't
1009+
# re-import", so OnRemove can restore a falsy original prompt without
1010+
# the trampoline mistaking that for "never initialized" and silently
1011+
# reinstalling this module on the next draw.
1012+
$global:_ompPromptFunction = $promptFunction
1013+
$global:_ompInitialized = $true
1014+
}
1015+
else {
1016+
$Function:prompt = $promptFunction
1017+
}
9901018

9911019
# set secondary prompt
9921020
Set-PSReadLineOption -ContinuationPrompt ((Invoke-Utf8Posh @("print", "secondary", "--shell=$script:ShellName")) -join "`n")
@@ -1328,7 +1356,29 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock {
13281356

13291357
Remove-Item Function:Get-PoshStackCount -ErrorAction SilentlyContinue
13301358

1331-
$Function:prompt = $script:OriginalPromptFunction
1359+
if ($script:AsyncInit) {
1360+
# Restore from the trampoline's own capture, never from
1361+
# $script:OriginalPromptFunction - in async mode that backup
1362+
# holds the wrapper chain (e.g. another tool's prompt hook),
1363+
# and restoring it here would recurse infinitely. Keep
1364+
# $global:_ompInitialized true even when the captured
1365+
# original is falsy, so the trampoline treats this as
1366+
# "restored", not "never initialized" - otherwise the next
1367+
# draw would silently reinstall the module Remove-Module just
1368+
# removed.
1369+
$global:_ompPromptFunction = $global:_ompOriginalPromptFunction
1370+
$global:_ompInitialized = $true
1371+
}
1372+
else {
1373+
# Only restore if this module's own prompt function is still
1374+
# the live global binding. If something replaced it since
1375+
# (e.g. a fresh async trampoline installed while switching
1376+
# this session from sync to async), leave it alone instead of
1377+
# clobbering whatever now owns the prompt.
1378+
if ($Function:prompt -eq $promptFunction) {
1379+
$Function:prompt = $script:OriginalPromptFunction
1380+
}
1381+
}
13321382

13331383
(Get-PSReadLineOption).ContinuationPrompt = $script:OriginalContinuationPrompt
13341384
(Get-PSReadLineOption).PromptText = $script:OriginalPromptText

0 commit comments

Comments
 (0)