|
| 1 | +# PLAN: Push-Location Stack Display in Oh My Posh |
| 2 | + |
| 3 | +## Problem |
| 4 | + |
| 5 | +When using `Push-Location`/`Pop-Location` to navigate directories, the stack is invisible — no standard prompt segment shows it. You can get lost: how many `Pop-Location` calls do you need? Where will you land? |
| 6 | + |
| 7 | +**Goal:** show the pushd stack (or at minimum the depth) in the oh-my-posh prompt, with no display when the stack is empty. |
| 8 | + |
| 9 | +## Scope |
| 10 | + |
| 11 | +Two decisions to make before implementing: |
| 12 | + |
| 13 | +1. **What to show:** count-only (`2x ~/proj`) vs top-of-stack path (`↩ ~/proj`) vs full stack (`~/proj ← ~/tmp`) |
| 14 | +2. **How to implement:** built-in `.StackCount` (OMP Path segment) vs env vars via `Set-PoshContext` hook |
| 15 | + |
| 16 | +## Background |
| 17 | + |
| 18 | +### Getting the stack in PowerShell |
| 19 | + |
| 20 | +[`Get-Location -Stack`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-location?view=powershell-7.5) exposes the current location stack. Most-recently pushed path is first: |
| 21 | + |
| 22 | +```powershell |
| 23 | +# All paths |
| 24 | +Get-Location -Stack | Select-Object -ExpandProperty Path |
| 25 | +
|
| 26 | +# Top of stack only |
| 27 | +(Get-Location -Stack | Select-Object -First 1).Path |
| 28 | +``` |
| 29 | + |
| 30 | +### Option A: `.StackCount` in the built-in Path segment |
| 31 | + |
| 32 | +[OMP's Path segment](https://ohmyposh.dev/docs/segments/system/path) exposes `.StackCount` — shows only the count, not paths: |
| 33 | + |
| 34 | +```json |
| 35 | +{ |
| 36 | + "type": "path", |
| 37 | + "template": "{{ if gt .StackCount 0 }}{{ .StackCount }}x {{ end }}{{ .Path }}" |
| 38 | +} |
| 39 | +``` |
| 40 | + |
| 41 | +- (+) Zero config, no profile changes |
| 42 | +- (-) Count only — no idea which directory is on top |
| 43 | + |
| 44 | +### Option B: `Set-PoshContext` hook + env vars + text segment |
| 45 | + |
| 46 | +Since [OMP v28](https://github.com/JanDeDobbeleer/oh-my-posh/releases/tag/v28.0.0) removed the old `command` segment, the recommended pattern is to compute values in `Set-PoshContext` (PowerShell's prompt hook) and read them via `.Env.VAR` in a `text` segment. |
| 47 | + |
| 48 | +> **Note:** In pwsh the hook is `Set-PoshContext` (an alias). The lowercase `set_poshcontext()` is the zsh/bash/fish variant. The docs show both; use the alias form for pwsh. |
| 49 | +
|
| 50 | +Add to `$PROFILE`: |
| 51 | + |
| 52 | +```powershell |
| 53 | +function Set-EnvVar([bool]$originalStatus) { |
| 54 | + $stack = @(Get-Location -Stack | Select-Object -ExpandProperty Path) |
| 55 | + $env:POSH_PUSHD_TOP = if ($stack.Count) { $stack[0] } else { "" } |
| 56 | + $env:POSH_PUSHD_STACK = if ($stack.Count) { $stack -join " <- " } else { "" } |
| 57 | + $env:POSH_PUSHD_COUNT = $stack.Count |
| 58 | +} |
| 59 | +New-Alias -Name 'Set-PoshContext' -Value 'Set-EnvVar' -Scope Global -Force |
| 60 | +``` |
| 61 | + |
| 62 | +Add a `text` segment to the OMP config: |
| 63 | + |
| 64 | +```json |
| 65 | +{ |
| 66 | + "type": "text", |
| 67 | + "template": "{{ if .Env.POSH_PUSHD_TOP }}↩ {{ .Env.POSH_PUSHD_TOP }}{{ end }}" |
| 68 | +} |
| 69 | +``` |
| 70 | + |
| 71 | +Or show the full stack: |
| 72 | + |
| 73 | +```json |
| 74 | +{ |
| 75 | + "type": "text", |
| 76 | + "template": "{{ if .Env.POSH_PUSHD_STACK }}⇄ {{ .Env.POSH_PUSHD_STACK }}{{ end }}" |
| 77 | +} |
| 78 | +``` |
| 79 | + |
| 80 | +- (+) Shows actual paths, not just count |
| 81 | +- (+) Composable — can format/truncate arbitrarily in PowerShell |
| 82 | +- (-) One extra function in `$PROFILE`; env vars persist in child processes (cosmetic issue) |
| 83 | + |
| 84 | +## POC Steps |
| 85 | + |
| 86 | +- [ ] Try Option A first (`.StackCount` in existing Path segment template) — zero friction, check if count-only is good enough |
| 87 | +- [ ] If count isn't enough: add `Set-PoshContext` + `POSH_PUSHD_TOP` env var + `text` segment |
| 88 | +- [ ] Verify the env var approach works with the existing `Set-PoshContext` alias if one already exists in `$PROFILE` |
| 89 | +- [ ] Test with: 0 pushes (nothing shows), 1 push (top shown), 3 pushes (only top shown or full stack) |
| 90 | +- [ ] Decide on truncation: show full path, or `~/...` abbreviated? Add home-path substitution if needed: |
| 91 | + ```powershell |
| 92 | + $env:POSH_PUSHD_TOP = $stack[0] -replace [regex]::Escape($HOME), "~" |
| 93 | + ``` |
| 94 | + |
| 95 | +## Testing: Pester + `oh-my-posh print primary --plain` |
| 96 | + |
| 97 | +`oh-my-posh print primary` renders the prompt from a config file without a live terminal. `--plain` strips ANSI codes for exact string comparison. Combined with Pester, this gives snapshot tests for the rendered prompt. |
| 98 | + |
| 99 | +Key flags for deterministic output: |
| 100 | + |
| 101 | +| Flag | Purpose | |
| 102 | +|---|---| |
| 103 | +| `--config <path>` | Which OMP theme to render | |
| 104 | +| `--plain` | Strip ANSI, makes string comparison work | |
| 105 | +| `--terminal-width 120` | Fixed width so line-breaking is deterministic | |
| 106 | +| `--pwd` / `--pswd` | Override current directory shown in prompt | |
| 107 | +| `--stack-count <n>` | Override stack count (for `.StackCount`-based approach) | |
| 108 | + |
| 109 | +> **Note:** `--stack-count` only feeds the `.StackCount` variable in the Path segment. To test the env-var approach (Option B), set `$env:POSH_PUSHD_TOP` before calling `oh-my-posh print`. |
| 110 | +
|
| 111 | +### Testing the `Set-PoshContext` logic independently |
| 112 | + |
| 113 | +The hook is plain PowerShell — test it in isolation before wiring to OMP: |
| 114 | + |
| 115 | +```powershell |
| 116 | +Describe "Set-PoshContext populates env vars" { |
| 117 | + BeforeEach { |
| 118 | + $env:POSH_PUSHD_TOP = "" |
| 119 | + $env:POSH_PUSHD_COUNT = 0 |
| 120 | + } |
| 121 | +
|
| 122 | + It "clears env vars when stack is empty" { |
| 123 | + # Start fresh, don't push anything |
| 124 | + Set-EnvVar $true |
| 125 | + $env:POSH_PUSHD_TOP | Should -BeExactly "" |
| 126 | + [int]$env:POSH_PUSHD_COUNT | Should -Be 0 |
| 127 | + } |
| 128 | +
|
| 129 | + It "shows top of stack after Push-Location" { |
| 130 | + $tmp = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ("posh-test-" + [guid]::NewGuid())) |
| 131 | + Push-Location $tmp.FullName |
| 132 | + Set-EnvVar $true |
| 133 | + $env:POSH_PUSHD_TOP | Should -BeExactly $tmp.FullName |
| 134 | + Pop-Location |
| 135 | + Remove-Item -Recurse -Force $tmp |
| 136 | + } |
| 137 | +} |
| 138 | +``` |
| 139 | + |
| 140 | +### Snapshot testing rendered prompt output |
| 141 | + |
| 142 | +Spawn a fresh `pwsh` process so the location stack is isolated (the stack is per-runspace): |
| 143 | + |
| 144 | +```powershell |
| 145 | +Describe "OMP prompt rendering" { |
| 146 | + BeforeAll { |
| 147 | + $script:Config = Join-Path $PSScriptRoot "../.go-my-posh.yaml" |
| 148 | + } |
| 149 | +
|
| 150 | + It "shows no pushd indicator when stack is empty" { |
| 151 | + $out = & oh-my-posh print primary ` |
| 152 | + --config $script:Config ` |
| 153 | + --shell pwsh ` |
| 154 | + --plain ` |
| 155 | + --terminal-width 120 ` |
| 156 | + --pwd (Get-Location).Path ` |
| 157 | + --pswd (Get-Location).Path ` |
| 158 | + --stack-count 0 |
| 159 | +
|
| 160 | + $out | Should -Not -Match "↩" |
| 161 | + $out | Should -Not -Match " +" # no double spaces from missing segment |
| 162 | + } |
| 163 | +
|
| 164 | + It "renders pushd indicator via env var (full chain)" { |
| 165 | + $script = @" |
| 166 | +`$env:POSH_PUSHD_TOP = "$HOME" |
| 167 | +oh-my-posh print primary --config '$script:Config' --plain --terminal-width 120 |
| 168 | +"@ |
| 169 | + $out = pwsh -NoProfile -NonInteractive -Command $script |
| 170 | + $out | Should -Match ([regex]::Escape("↩")) |
| 171 | + } |
| 172 | +} |
| 173 | +``` |
| 174 | + |
| 175 | +**Snapshot approach (recommended for spacing/alignment regressions):** once output looks right, store it: |
| 176 | + |
| 177 | +```powershell |
| 178 | +$expected = Get-Content "$PSScriptRoot/snapshots/prompt-no-stack.txt" -Raw |
| 179 | +$out | Should -BeExactly $expected |
| 180 | +``` |
| 181 | + |
| 182 | +Update snapshots intentionally when you change the template; `git diff snapshots/` is the review UI. See [[testing-golden]] for the general pattern. |
| 183 | + |
| 184 | +### What can't be integration-tested here |
| 185 | + |
| 186 | +- Terminal reflow/resize behavior (terminal-specific, not OMP's output) |
| 187 | +- iTerm2/Windows Terminal rendering of specific Unicode glyphs |
| 188 | + |
| 189 | +## Risks |
| 190 | + |
| 191 | +**`Set-PoshContext` alias conflict:** if `$PROFILE` already has a `Set-PoshContext` alias or function, `New-Alias -Force` will overwrite it silently. Check first; consider merging into one function. |
| 192 | + |
| 193 | +**Env var persistence:** `$env:POSH_PUSHD_TOP` is visible to child processes. Unlikely to cause issues but worth knowing. |
| 194 | + |
| 195 | +**Stack is per named stack:** `Push-Location` supports named stacks via `-StackName`. `Get-Location -Stack` returns the *default* stack only. If using named stacks, the hook needs `Get-Location -StackName <name>` for each stack. |
| 196 | + |
| 197 | +**Prompt perf:** `Get-Location -Stack` is fast (in-memory), but if the prompt is already slow (check with `oh-my-posh debug`), any hook adds up. See [[shell.prompt#debugging prompt being slow]]. |
| 198 | + |
| 199 | +## Unresolved Questions |
| 200 | + |
| 201 | +- [ ] Is count-only (Option A) good enough, or is seeing the actual path important enough to add the hook? |
| 202 | +- [ ] What truncation/formatting for long paths? Home substitution (`~`)? Max chars? |
| 203 | +- [ ] If using Option B: should the full stack be shown, or just top-of-stack? |
| 204 | +- [ ] Does `POSH_PUSHD_COUNT` need to be an int in the env var, or is string fine for the template? |
| 205 | + |
| 206 | +## Related |
| 207 | + |
| 208 | +- [[shell.prompt#app-idea pushd stack display in prompt]] — origin of this plan |
| 209 | +- [[testing-golden]] — golden/snapshot testing patterns; Pester snapshots follow the same principles |
| 210 | +- [[pwsh.raii.PLAN]] — `Push-Location`/`Pop-Location` as RAII; related to stack management |
| 211 | +- [OMP Path segment `.StackCount`](https://ohmyposh.dev/docs/segments/system/path) |
| 212 | +- [OMP Templates `.Env`](https://ohmyposh.dev/docs/configuration/templates) |
| 213 | +- [OMP v28 release — removed `command` segment](https://github.com/JanDeDobbeleer/oh-my-posh/releases/tag/v28.0.0) |
0 commit comments