<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>~iany/ PowerShell</title><link>https://blog.iany.me/tags/powershell/</link><description>Recent content in PowerShell «~iany/»</description><language>en-US</language><managingEditor>me@iany.me (Ian Yang)</managingEditor><webMaster>me@iany.me (Ian Yang)</webMaster><copyright>CC-BY-SA 4.0</copyright><lastBuildDate>Thu, 29 Jan 2026 23:30:42 +0800</lastBuildDate><atom:link href="https://blog.iany.me/tags/powershell/index.xml" rel="self" type="application/rss+xml"/><item><title>Use tmux for PowerShell in Windows Terminal</title><link>https://blog.iany.me/2026/01/use-tmux-for-powershell-in-windows-terminal/</link><pubDate>Thu, 29 Jan 2026 23:30:42 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2026/01/use-tmux-for-powershell-in-windows-terminal/</guid><description>&lt;p&gt;You can get tmux session persistence and multiplexing in Windows Terminal by running tmux inside WSL and setting the default shell to PowerShell. New panes and windows will then start &lt;code&gt;pwsh.exe&lt;/code&gt; instead of a Linux shell. Here’s a minimal setup using small wrappers and the &lt;code&gt;tmux -C attach&lt;/code&gt; trick to configure new sessions.&lt;/p&gt;
&lt;h2 id="why-tmux-under-wsl-with-powershell"&gt;Why tmux under WSL with PowerShell?&lt;/h2&gt;
&lt;p&gt;Since tmux does not run natively on Windows, the standard approach is to use it through WSL. However, if you launch &lt;code&gt;wsl tmux&lt;/code&gt; from Windows Terminal, all panes will default to your WSL shell. When working in a Windows directory, I prefer to use native PowerShell within the Windows environment.&lt;/p&gt;
&lt;p&gt;To ensure every tmux pane runs PowerShell, configure tmux to use &lt;code&gt;pwsh.exe&lt;/code&gt; as its default command. When you launch &lt;code&gt;pwsh.exe&lt;/code&gt; from WSL within a Windows directory, it brings us back to the Windows environment.&lt;/p&gt;
&lt;h2 id="wrappers-so-you-can-call-tmux-from-powershell"&gt;Wrappers so you can call tmux from PowerShell&lt;/h2&gt;
&lt;p&gt;Put these in a directory on your &lt;code&gt;PATH&lt;/code&gt; (e.g. &lt;code&gt;~/Documents/PowerShell/bin&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PowerShell&lt;/strong&gt; — &lt;code&gt;tmux.ps1&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;wsl tmux $args
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Cmd&lt;/strong&gt; — &lt;code&gt;tmux.cmd&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-batch"&gt;@echo off
wsl tmux %*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From PowerShell or Cmd you can then run &lt;code&gt;tmux new&lt;/code&gt;, &lt;code&gt;tmux attach&lt;/code&gt;, etc., and they all go to WSL’s tmux.&lt;/p&gt;
&lt;h2 id="creating-powershell-sessions"&gt;Creating PowerShell Sessions&lt;/h2&gt;
&lt;p&gt;The next piece is a script that creates a new session whose default command is &lt;code&gt;pwsh.exe&lt;/code&gt;, or attaches if that session already exists. Example usage: &lt;code&gt;tmux-pwsh dev&lt;/code&gt; or &lt;code&gt;tmux-pwsh work&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The script sets the default command only for sessions it creates; it does not affect sessions started with a standard &lt;code&gt;tmux new-session&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;tmux-pwsh.ps1&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;param(
[Parameter(Mandatory=$true)]
[string]$SessionName
)
$pwshCommand = &amp;quot;exec pwsh.exe -nologo&amp;quot;
# Check if session exists using exact name match
wsl tmux has-session -t &amp;quot;\=$SessionName&amp;quot; 2&amp;gt;$null
if ($LASTEXITCODE -ne 0) {
wsl tmux new-session -d -s $SessionName $pwshCommand
echo &amp;quot;set-option default-command `&amp;quot;$pwshCommand`&amp;quot;&amp;quot; | `
wsl tmux -C attach -t &amp;quot;\=$SessionName&amp;quot; &amp;gt;$null
}
wsl tmux attach -t &amp;quot;\=$SessionName&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;=&lt;/code&gt; prefix does an exact session name match and must be escaped when sending the command from PowerShell to WSL.&lt;/li&gt;
&lt;li&gt;The new session specifies command &lt;code&gt;pwsh.exe&lt;/code&gt; via the command argument.&lt;/li&gt;
&lt;li&gt;Running &lt;code&gt;wsl tmux set-option&lt;/code&gt; immediately after creating the session does not work because there&amp;rsquo;s a brief window where the session is not available for setting options. Instead, sending &lt;code&gt;set-option&lt;/code&gt; via &lt;code&gt;tmux -C attach&lt;/code&gt; reliably applies the setting.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After this, splitting panes and creating new windows in that session will all start &lt;code&gt;pwsh.exe -nologo&lt;/code&gt; in Windows Terminal.&lt;/p&gt;
&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Use WSL for tmux and PowerShell as the default command by setting tmux’s default command to &lt;code&gt;exec pwsh.exe -nologo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;tmux.ps1&lt;/code&gt;&lt;/strong&gt; / &lt;strong&gt;&lt;code&gt;tmux.cmd&lt;/code&gt;&lt;/strong&gt; — thin wrappers so &lt;code&gt;tmux&lt;/code&gt; from Windows means &lt;code&gt;wsl tmux&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;tmux-pwsh.ps1 &amp;lt;name&amp;gt;&lt;/code&gt;&lt;/strong&gt; — create (or attach to) a named session that uses PowerShell as the default command.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once these are on your &lt;code&gt;PATH&lt;/code&gt;, run e.g. &lt;code&gt;tmux-pwsh dev&lt;/code&gt; from Windows Terminal to get a tmux session where every pane is PowerShell.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/tmux/">Tmux</category><category domain="https://blog.iany.me/tags/powershell/">PowerShell</category><category domain="https://blog.iany.me/tags/wsl/">WSL</category><category domain="https://blog.iany.me/tags/windows-terminal/">Windows Terminal</category></item><item><title>Temporary Vi Mode in PowerShell</title><link>https://blog.iany.me/2025/11/temporary-vi-mode-in-powershell/</link><pubDate>Sat, 22 Nov 2025 23:17:05 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2025/11/temporary-vi-mode-in-powershell/</guid><description>&lt;p&gt;I miss the feature in readline (Bash/Zsh) where &lt;code&gt;Ctrl+x, Ctrl+v&lt;/code&gt; switches to Vi command mode temporarily. In that workflow, entering Insert mode switches back to Emacs mode. &lt;code&gt;PSReadLine&lt;/code&gt; has a command &lt;code&gt;ViCommandMode&lt;/code&gt;, but binding it directly in Emacs mode will report errors on every key input.&lt;/p&gt;
&lt;p&gt;The solution requires handling the mode change event to toggle the global &lt;code&gt;EditMode&lt;/code&gt; between &lt;code&gt;Emacs&lt;/code&gt; and &lt;code&gt;Vi&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;When entering &amp;ldquo;Insert&amp;rdquo; mode (the default for Emacs users), we force the mode to &lt;code&gt;Emacs&lt;/code&gt; and set up the trigger for the temporary Vi mode. When that trigger (&lt;code&gt;Ctrl+x, Ctrl+v&lt;/code&gt;) is fired, we switch the edit mode to &lt;code&gt;Vi&lt;/code&gt; and jump to command mode.&lt;/p&gt;
&lt;p&gt;Here is the configuration for your &lt;code&gt;$PROFILE&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;function OnViModeChange {
if ($args[0] -eq 'Insert') {
Set-PSReadLineOption -EditMode Emacs
Set-PSReadlineKeyHandler -Chord &amp;quot;Ctrl+x,Ctrl+v&amp;quot; -ScriptBlock {
Set-PSReadLineOption -EditMode Vi
[Microsoft.PowerShell.PSConsoleReadLine]::ViCommandMode()
}
# Add other Emacs key bindings here.
# Switching `EditMode` resets key bindings to their defaults.
# Remember to reconfigure them.
# Set-PSReadlineKeyHandler -Chord &amp;quot;Ctrl+w&amp;quot; -Function BackwardKillWord
}
}
Set-PSReadLineOption -ViModeIndicator Script
Set-PSReadLineOption -ViModeChangeHandler $Function:OnViModeChange
Set-PSReadLineOption -EditMode Emacs
# Reuse the function to start Emacs mode
OnViModeChange Insert
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Switching &lt;code&gt;EditMode&lt;/code&gt; resets key bindings to their defaults. Remember to reconfigure them.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/powershell/">PowerShell</category><category domain="https://blog.iany.me/tags/vim/">Vim</category><category domain="https://blog.iany.me/tags/emacs/">Emacs</category><category domain="https://blog.iany.me/tags/console/">Console</category></item><item><title>Envrc Alternative for PowerShell in Windows</title><link>https://blog.iany.me/2024/04/envrc-alternative-for-powershell-in-windows/</link><pubDate>Sat, 06 Apr 2024 08:55:34 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2024/04/envrc-alternative-for-powershell-in-windows/</guid><description>&lt;p&gt;This post introduces a solution for automatically setting up and tearing down shell environments for PowerShell in Windows. It is proposed as a potential alternative to the bash-based tool &lt;a href="https://github.com/direnv/direnv"&gt;direnv&lt;/a&gt;, which, while effective at loading &lt;code&gt;.envrc&lt;/code&gt; files in the current or nearest ancestor directory, has limited compatibility with PowerShell in Windows.&lt;/p&gt;
&lt;h2 id="envrcps1"&gt;.envrc.ps1&lt;/h2&gt;
&lt;p&gt;The basic idea is creating a file &lt;code&gt;.envrc.ps1&lt;/code&gt; with two entries: setup and teardown. The setup entry is responsible for setting up the environment up entering the directory, and the teardown entry is used when leaving it.&lt;/p&gt;
&lt;p&gt;PowerShell allows code to be executed from a file using the dot command. This means that the entire file can be used as the setup entry. For the teardown entry, a function can be defined within the same file, and I use the function name &lt;code&gt;down&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The layout of the file &lt;code&gt;.envrc.ps1&lt;/code&gt; looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;# setup
$env:GH_TOKEN = &amp;quot;secret&amp;quot;
# teardown
function global:down {
$env:GH_TOKEN = &amp;quot;&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;global:&lt;/code&gt; scope before the function name is necessary when this file is loaded in the hook, as I will do later.&lt;/p&gt;
&lt;p&gt;The corresponding commands for setup and teardown entries:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Setup: &lt;code&gt;. .envrc.ps1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Teardown: &lt;code&gt;down&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="hook"&gt;Hook&lt;/h2&gt;
&lt;p&gt;To hook the setup and teardown entries, I reference &lt;a href="https://github.com/PowerShell/PowerShell/issues/14484#issuecomment-1731647083"&gt;this example&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;using namespace System;
using namespace System.Management.Automation;
$hook = [EventHandler[LocationChangedEventArgs]] {
param([object] $source, [LocationChangedEventArgs] $eventArgs)
end {
# 1. `down` for $eventArgs.OldPath
# 2. `. .envrc.ps1` for $eventArgs.NewPath
}
};
$currentAction = $ExecutionContext.SessionState.InvokeCommand.LocationChangedAction;
if ($currentAction) {
$ExecutionContext.SessionState.InvokeCommand.LocationChangedAction = [Delegate]::Combine($currentAction, $hook);
} else {
$ExecutionContext.SessionState.InvokeCommand.LocationChangedAction = $hook;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To run the code snippet, PowerShell 7 or later is required. You can install it from the &lt;a href="https://github.com/PowerShell/PowerShell"&gt;repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here is the algorithm for identifying setup and teardown entries:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Find the nearest &lt;code&gt;.envrc.ps1&lt;/code&gt; for the old path.&lt;/li&gt;
&lt;li&gt;Find the nearest &lt;code&gt;.envrc.ps1&lt;/code&gt; for the new path.&lt;/li&gt;
&lt;li&gt;If the two paths are the same, skip the next step.&lt;/li&gt;
&lt;li&gt;If they differ, invoke the &lt;code&gt;down&lt;/code&gt; function and remove it if it exists. Then, if found, load the &lt;code&gt;.envrc.ps1&lt;/code&gt; file for the new path.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The full code example to be added into PowerShell &lt;code&gt;$PROFILE&lt;/code&gt; file.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;using namespace System;
using namespace System.Management.Automation;
# existing PowerShell profile contents
# skip when version is less than 7
if ($PSVersionTable.PSVersion.Major -lt 7) {
return
}
# find the .envrc.ps1 in the currenct directory or the nearest ancestor directory.
function Find-NearestEnvrc {
param (
[Parameter(Mandatory=$true)]
[string]$StartDir
)
$currentDir = Resolve-Path $StartDir
while ($currentDir -ne &amp;quot;&amp;quot;) {
$envrcPath = Join-Path $currentDir &amp;quot;.envrc.ps1&amp;quot;
if (Test-Path $envrcPath) {
return $envrcPath
}
$currentDir = Split-Path $currentDir -Parent
}
return &amp;quot;&amp;quot;
}
# hook the setup and teardown entries
$hook = [EventHandler[LocationChangedEventArgs]] {
param([object] $source, [LocationChangedEventArgs] $eventArgs)
end {
$oldEnvrc = Find-NearestEnvrc $eventArgs.OldPath
$newEnvrc = Find-NearestEnvrc $eventArgs.NewPath
if ($oldEnvrc -ne $newEnvrc) {
Get-Command down -ErrorAction SilentlyContinu
if (Get-Command down -ErrorAction SilentlyContinu) {
down
Remove-Item Function:down
}
if ($newEnvrc -ne &amp;quot;&amp;quot;) {
. $newEnvrc
}
}
}
};
$currentAction = $ExecutionContext.SessionState.InvokeCommand.LocationChangedAction;
if ($currentAction) {
$ExecutionContext.SessionState.InvokeCommand.LocationChangedAction = [Delegate]::Combine($currentAction, $hook);
} else {
$ExecutionContext.SessionState.InvokeCommand.LocationChangedAction = $hook;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="example-usage"&gt;Example Usage&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;# setup
# set environment variable
$env:GH_TOKEN = &amp;quot;secret&amp;quot;
# load python virtual env
. .venv\Scripts\Activate.ps1
# add a helper function
function global:build {
cargo build
}
# teardown
function global:down {
# unset environment variable
$env:GH_TOKEN = &amp;quot;&amp;quot;
# unload python virtual env
deactivate
# remove the helper function
Remove-Item Function:build
}
&lt;/code&gt;&lt;/pre&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/environment-variables/">Environment Variables</category><category domain="https://blog.iany.me/tags/powershell/">PowerShell</category><category domain="https://blog.iany.me/tags/windows/">Windows</category></item></channel></rss>