From 234a0b74a1bb21107ec6bc2a869eb3eccd8232eb Mon Sep 17 00:00:00 2001 From: megaproxy Date: Mon, 25 May 2026 19:13:03 +0100 Subject: [PATCH] Add PowerShell as a selectable shell in the distro dropdown --- src-tauri/src/pty.rs | 56 +++++++++++++++++++++++++++++--------------- src/App.tsx | 10 ++++++-- 2 files changed, 45 insertions(+), 21 deletions(-) diff --git a/src-tauri/src/pty.rs b/src-tauri/src/pty.rs index 094d847..117d97a 100644 --- a/src-tauri/src/pty.rs +++ b/src-tauri/src/pty.rs @@ -13,6 +13,10 @@ use portable_pty::{CommandBuilder, MasterPty, PtySize, native_pty_system}; use serde::Serialize; use tauri::{AppHandle, Emitter}; +/// Sentinel "distro" name used to spawn Windows PowerShell instead of WSL. +/// Frontend appends this to the distro list it shows in the dropdown. +pub const POWERSHELL_DISTRO: &str = "PowerShell"; + pub type PaneId = u64; /// What we keep alive for each spawned PTY. @@ -62,26 +66,40 @@ impl PtyManager { }) .context("openpty failed")?; - let mut cmd = CommandBuilder::new("wsl.exe"); - if let Some(d) = distro.as_deref() { - cmd.arg("-d"); - cmd.arg(d); - } - // Default new panes to the WSL user's home (~) rather than the - // Windows-side cwd we inherit from the launcher (typically - // C:\Users\, which shows up as /mnt/c/Users/ inside WSL). - // wsl.exe resolves `~` against the distro's default shell. - let resolved_cwd = cwd.as_deref().unwrap_or("~"); - cmd.arg("--cd"); - cmd.arg(resolved_cwd); - // Force a login shell so .bashrc etc. run and PATH is populated. - // wsl.exe without an explicit command launches the default shell - // interactively, which is exactly what we want. + let is_powershell = distro.as_deref() == Some(POWERSHELL_DISTRO); - let child = pair - .slave - .spawn_command(cmd) - .context("failed to spawn wsl.exe; is WSL installed?")?; + let cmd = if is_powershell { + // cwd from the leaf is ignored — leaves may carry Linux-style + // paths (e.g. `~`, `/mnt/d/...`) from a previously-assigned WSL + // distro that PowerShell wouldn't understand. PowerShell starts + // in its own default cwd; user can `cd` if they want. + let mut c = CommandBuilder::new("powershell.exe"); + c.arg("-NoLogo"); + c + } else { + let mut c = CommandBuilder::new("wsl.exe"); + if let Some(d) = distro.as_deref() { + c.arg("-d"); + c.arg(d); + } + // Default new panes to the WSL user's home (~) rather than the + // Windows-side cwd we inherit from the launcher (typically + // C:\Users\, which shows up as /mnt/c/Users/ inside WSL). + // wsl.exe resolves `~` against the distro's default shell. + let resolved_cwd = cwd.as_deref().unwrap_or("~"); + c.arg("--cd"); + c.arg(resolved_cwd); + // wsl.exe without an explicit command launches the default shell + // interactively, which is exactly what we want. + c + }; + + let spawn_err = if is_powershell { + "failed to spawn powershell.exe" + } else { + "failed to spawn wsl.exe; is WSL installed?" + }; + let child = pair.slave.spawn_command(cmd).context(spawn_err)?; // We need to keep the master alive (drop = close the PTY), but we // also need the reader and writer split from it. diff --git a/src/App.tsx b/src/App.tsx index 9c40c98..f3c72c9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -49,6 +49,9 @@ import "./lib/layout/Gutter.css"; const LEGACY_STORAGE_KEY = "tiletopia.tree.v1"; const SAVE_DEBOUNCE_MS = 500; +/** Sentinel "distro" the backend recognises to spawn powershell.exe instead + * of wsl.exe. Must match `POWERSHELL_DISTRO` in `src-tauri/src/pty.rs`. */ +const POWERSHELL_DISTRO = "PowerShell"; function isInteractiveDistro(name: string): boolean { return !name.toLowerCase().startsWith("docker-desktop"); @@ -100,11 +103,14 @@ export default function App() { let resolvedDefault: string | undefined; try { resolvedDistros = await listDistros(); - resolvedDefault = - resolvedDistros.find(isInteractiveDistro) ?? resolvedDistros[0]; } catch (e) { console.warn("list_distros failed:", e); } + // Append PowerShell as a pseudo-distro so it appears in the titlebar + // default-picker and the per-pane dropdown. + resolvedDistros = [...resolvedDistros, POWERSHELL_DISTRO]; + resolvedDefault = + resolvedDistros.find(isInteractiveDistro) ?? resolvedDistros[0]; if (cancelled) return; if (loaded) {