Skip to content

PowerShell

Justin Puah edited this page Jul 27, 2026 · 2 revisions

PowerShell 7 profile and prompt for Windows (and Linux where applicable).

Prerequisites

Tool Purpose Install
PowerShell 7+ Shell winget install Microsoft.PowerShell
PSReadLine 2.2+ Line editing ships with pwsh 7; update via Install-Module PSReadLine
PSFzf Fzf integration Install-Module PSFzf
git-completion Git tab completion Install-Module git-completion
fzf Fuzzy finder winget install junegunn.fzf
zoxide Smart cd winget install ajeetdsouza.zoxide
Az.Tools.Predictor Azure command prediction Install-Module Az.Tools.Predictor
WinGet.CommandNotFound Package suggestions installed via Microsoft Store
jujutsu (jj) Optional — jj VCS segment in the prompt winget install jj-vcs.jj

Files

File Notes
Microsoft.PowerShell_profile.ps1 Shared profile — used on all machines
Profile/Set-Prompt.ps1 Prompt definition (jj/git + Azure context)
Profile/AzCliAccount.ps1 az CLI profile switcher (azs) — see below

The installer generates a stub at ~/Documents/PowerShell/Microsoft.PowerShell_profile.ps1 that dot-sources the repo file — changes are live immediately without re-running setup.

Scripts (Scripts/)

Standalone PowerShell tools (not loaded by the profile — run on demand). Each has comment-based help; run Get-Help .\Scripts\<Name>.ps1 -Full.

Script Purpose
Set-CodingFont.ps1 Switch the coding font across all dotfiles targets at once — Windows Terminal (live + repo), VS Code (live + repo snapshot), and Vim guifont. Set-CodingFont commit
Install-CodingFont.ps1 Download + per-user install + session-activate Nerd Font (Mono) faces from the nerd-fonts release. Install-CodingFont CommitMono
New-FontComparison.ps1 Self-contained side-by-side HTML/PNG comparison of the candidate fonts with a PowerShell/C#/Bicep language switcher. -Fonts filters the set
New-FontGlyphTest.ps1 Glyph-separation torture test (rn/m, cl/d, vv/w, 1lI|, 0O) across Commit/JetBrains/0xProto
New-FontLigatureTest.ps1 Ligatures-on vs -off comparison so you can see what each font actually fuses

The New-Font* tools subset each font to the sample glyphs and embed them as base64 woff2, so the output HTML renders the real fonts in any browser. They need Python with fonttools + brotli (pip install fonttools brotli) for subsetting and Edge for the PNG. Generated .font-*.html/.png land at the repo root and are git-ignored.

Profile architecture

The profile is structured in three phases to keep startup fast:

Phase 1 — blocking (runs before first prompt): Single PSModulePath scan into $global:ProfileModules cache, PSReadLine with PredictionSource History, all key bindings, prompt definition. Sets RIPGREP_CONFIG_PATH and FZF_DEFAULT_OPTS_FILE pointing at repo files (no copy needed). Reads AppsUseLightTheme from the registry once into $_isDark, then sets FZF_DEFAULT_OPTS, LG_CONFIG_FILE (lazygit base + theme), and EZA_CONFIG_DIR (eza theme dir) to catppuccin mocha or latte — mirrors nvim's theme detection. Sets _ZO_RESOLVE_SYMLINKS=1 so zoxide stores resolved paths; prevents duplicate database entries when navigating through junctions.

Phase 2a — first idle: PSFzf, zoxide, upgrade to PredictionSource HistoryAndPlugin, the fd-backed FZF_*_COMMAND vars (kept separate from ripgreprc so pickers stay .gitignore-clean while rg search stays exhaustive), the eza ll/la/lt helpers, and the Azure context timer (Initialize-AzTimer, moved off the load path). Guarded by $global:ProfileDeferredDone. Interactive tools needed immediately.

Phase 2b — next idle: az + zellij tab-completers, git-completion (+ g alias registration), WinGet CommandNotFound. Guarded by $global:ProfileDeferredSecondaryDone. Split from 2a so the first keypress isn't blocked by their import cost.

Phase 3 — async (background runspace): Azure context refresh on a 60-second timer. The timer is created in Phase 2a (Initialize-AzTimer); the refresh itself runs in a background runspace wired up in Set-Prompt.ps1.

Key bindings

Chord Action
Ctrl+r Reverse history search (fzf)
Ctrl+t FZF file picker
Alt+f FZF ripgrep search
Alt+b FZF git branch switcher
Alt+g FZF git worktree switcher
Ctrl+f Forward char
Ctrl+b Backward char
Ctrl+p / Ctrl+n Previous / next history
Ctrl+a / Ctrl+e Beginning / end of line
Ctrl+w Delete word backward
Ctrl+u Delete line backward
Ctrl+Space / Shift+Tab Menu complete
Ctrl+[ (Oem4) Vi command mode

Prompt (Set-Prompt.ps1)

  • jj (Jujutsu) change-id, closest bookmark, ahead count, and state — shown instead of git in jj repos (takes precedence in colocated repos; toggle ShowJj). Gate is a filesystem walk; up to 3 jj processes, all --ignore-working-copy. Renders jj:<change-id> <bookmark> ↑<dist> *<files> ∅ ✎ ( empty, no description, ! conflict)
  • Git branch and status — synchronous, 3 git processes per prompt (used when not in a jj repo)
  • Azure subscription context — async via background runspace, refreshed every 60s
  • az CLI profile tag (az:<name> / az:default) — see az CLI profile switcher below
  • Last command exit status (colour coded) and execution time
  • Truncated path for long directories
  • Windows Terminal OSC 9;9 CWD tracking; also syncs the Win32 process CWD so Zellij opens new panes in the current directory

az CLI profile switcher

Switch-AzProfile (alias azs) switches the Azure CLI between named profiles, each a self-contained AZURE_CONFIG_DIR under ~/.azure-profiles/<name>. The point is one identity per profile directory — see "why one identity per profile" below.

  • Switch by name: azs work, azs personal — creates ~/.azure-profiles/<name> if it doesn't exist yet and points AZURE_CONFIG_DIR at it.
  • Switch with a picker: azs with no argument opens an fzf picker listing every known profile plus default, with a preview column showing each profile's cached identity (or "not logged in" if it's never been used). The preview reads azureProfile.json directly — no az process per keystroke.
  • default is the reserved name for az's own ~/.azure — it means AZURE_CONFIG_DIR is unset. It's deliberately left alone rather than managed as a profile directory: on Windows ~/.azure is a single case-insensitive folder shared with Azure PowerShell (Get-Az* reads AzureRmContext.json from the same directory), so anything that swapped or redirected it would silently change the Az PowerShell context too.
  • Adding a new profile is just azs <newname> followed by az login — there's no manifest file to edit. The directory is the source of truth.
  • The prompt shows the active profile as az:<name> (or az:default), read straight from $env:AZURE_CONFIG_DIR — no az process, so it's always in sync with a silent shell-startup restore.

Why one identity per profile

az devops (and some other extensions) build their auth candidate list from whatever subscriptions/identities are cached in the current config dir, and use the first one that can successfully list a project — not necessarily the one you meant to use. A config directory holding two or more logged-in identities can therefore authenticate az devops calls as an account you didn't select, with no error or warning. Keeping exactly one identity per profile directory (log in once per profile, never share a dir between accounts) removes that ambiguity entirely.

Shared extension store

All profiles share one AZURE_EXTENSION_DIR (pointed at the existing ~/.azure/cliextensions), set unconditionally regardless of which profile is active. Without this, a non-default profile's fresh config dir has zero installed extensions, and az devops / az graph simply don't work under it.

Migrating from the old two-account setup

Earlier versions of this profile used an asymmetric two-account scheme (azw/azp: one alias unset AZURE_CONFIG_DIR for "work", the other pointed it at ~/.azure-personal for "personal"). That's gone — it couldn't express more than two accounts, and "work" was really just "whatever's logged into the default dir on this machine," which didn't travel between machines. If you're migrating:

  1. Move ~/.azure-personal to ~/.azure-profiles/personal.
  2. For every other account, run azs <name> (creates an empty profile dir), then az login once inside it — start each profile empty so it never caches more than one identity.
  3. Leave ~/.azure as-is; it keeps whatever the Azure PowerShell context and your "default" CLI login already are.
  4. Run azs once after migrating so the state file (~/.azure-active-profile) picks up the new layout.

Per-machine differences

There is a single shared profile — no per-machine variants. Machine-specific behaviour (work git identity, Azure subscription) is handled at other layers:

  • Git identity via [includeIf] in git/gitconfig
  • Az context loaded async in Profile/Set-Prompt.ps1

Install

./setup.ps1 -Module powershell

Stubs the profile into the pwsh 7 and VSCode profile locations (both under ~/Documents/PowerShell/) and junctions ~/Documents/PowerShell/Profilepowershell/Profile/. Windows PowerShell 5.1 is not targeted — the shared profile is #Requires -Version 7. The installer warns about (but never deletes) orphaned PS5 stubs under ~/Documents/WindowsPowerShell/ from older runs so you can remove them manually.


Source: powershell/README.md in the dotfiles repo.

dotfiles wiki

Getting started

Core (Windows)

Cross-platform

Claude Code & AI

Reference

Legacy (Linux)

Troubleshooting

Clone this wiki locally