diff --git a/.gitignore b/.gitignore index c88f4efa..d7d09b70 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ publish/ Properties/launchSettings.json nuget.local.config GraphicalTools.Dev.slnx +artifacts/ +Thumbs.db +*.Thumbs.db diff --git a/README.md b/README.md index 72ea769b..870ae09f 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ PSTui adds interactive terminal-UI cmdlets to the PowerShell pipeline: ```powershell Install-Module PSTui +Import-Module PSTui ``` PSTui is the community continuation of Microsoft's now-archived @@ -27,6 +28,11 @@ It's part of the [tui-cs](https://github.com/tui-cs) family, alongside [Terminal.Gui](https://github.com/tui-cs/Terminal.Gui), [clet](https://github.com/tui-cs/clet), and [cli](https://github.com/tui-cs/cli). +![ls | ocgv and killp](docs/PSTui/hero.gif) + +> `ls | ocgv` then a `killp` process picker — interactive, filterable tables +> straight from the pipeline. + ## Installation Requires **PowerShell 7.6+** (the binary module targets .NET 10). Works on @@ -95,10 +101,16 @@ Install-Module PSTui * Multiple Selection - Select multiple items and send them down the pipeline. * Customizable - Customize the grid view window with the built-in parameters. -![Demo GIF](docs/PSTui/ocgv.gif) +**`Show-ObjectTree` (`shot`)** — explore any object graph as an interactive tree: + +![Get-Process | shot](docs/PSTui/shot.gif) ## Examples +Run [`demo.ps1`](demo.ps1) for a guided walkthrough of the examples below: + +![demo.ps1 walkthrough](docs/PSTui/demo.gif) + ### Example 1: Output processes to a grid view ```PowerShell @@ -212,6 +224,8 @@ PSTui includes a graphical command-history picker — the [F7History](https://github.com/tui-cs/F7History) module, **folded in and enabled by default** — no separate package to install. +![F7 command history](docs/PSTui/f7history.gif) + > **The key bindings register when PSTui is imported.** `Install-Module` alone > does *not* bind `F7`; PowerShell only loads the module (and its key handlers) > on `Import-Module`. To have `F7`/`Shift+F7` available in **every** session, add diff --git a/Scripts/tuirec/README.md b/Scripts/tuirec/README.md new file mode 100644 index 00000000..90d06924 --- /dev/null +++ b/Scripts/tuirec/README.md @@ -0,0 +1,136 @@ +# Recording PSTui GIFs with `tuirec` + +Use this guide to (re)generate the animated GIFs in the README and docs. The +recorder is [`tui-cs/tuirec`](https://github.com/tui-cs/tuirec) — a Go CLI that +spawns a process in a PTY, injects keystrokes, records an asciinema cast, and +renders a GIF via `agg`. This is the PSTui-specific companion to +[Terminal.Gui's `Scripts/tuirec/README.md`](https://github.com/tui-cs/Terminal.Gui/blob/main/Scripts/tuirec/README.md); +read that for the full keystroke-token reference. + +## Install + +```bash +# Requires Go 1.22+. agg is auto-downloaded on first use. +go install github.com/tui-cs/tuirec/cmd/tuirec@latest +export PATH="$(go env GOPATH)/bin:$PATH" # tuirec installs to GOPATH/bin +tuirec --version +``` + +## How PSTui differs from recording a Terminal.Gui app + +PSTui cmdlets are driven from a **PowerShell pipeline**, not a `ScenarioRunner` +DLL. Four things shape every recipe here: + +1. **Record a script file, not `-Command`.** `tuirec --args` is **comma-split**, + and PowerShell pipelines are full of commas (`Select-Object a, b, c`). So the + demos live in `*.ps1` files run with `-File`: + `--args "-NoLogo,-NoProfile,-File,./Scripts/tuirec/demo-ocgv.ps1"`. + +2. **The pwsh REPL does not render under a recording PTY** — only the + Terminal.Gui app it launches does. You cannot record "type a command, press + Enter, watch ocgv open" against the live prompt. Instead each demo script *is* + the scenario and invokes the cmdlet directly. + +3. **Show the prompt+command by echoing it from the script — before + `Import-Module`.** To make a GIF read like a real session, the demo `Write-Host`s + a synthetic prompt line (`PS ~/PStui> ls | ocgv`) and then sleeps briefly so it + lingers before the picker opens. Printing it *before* `Import-Module` means the + GIF opens on the command line instead of a ~1 s blank import lead-in. + +4. **Use `-FullScreen` + `--trim=false` for multi-step demos.** `-FullScreen` + gives each picker a clean alt-screen capture (inline rendering leaves residue + when a filtered grid is short). But `--trim` (on by default) treats everything + after the *first* alt-screen exit as postroll and drops it — fatal for a demo + with several steps — so pass `--trim=false`. + +The demo scripts: + +| Script | Records | +|--------|---------| +| `demo-ocgv.ps1` | `ls \| ocgv` then the `killp` process picker → `hero.gif` | +| `demo-shot.ps1` | `Get-Process \| shot` tree exploration → `shot.gif` | +| `demo-f7.ps1` | the `F7` command-history picker → `f7history.gif` | +| `../../demo.ps1` (repo root) | the full guided walkthrough → `demo.gif` | + +`killp` is **safe** in recordings — the demos `Esc` out of the picker, so no +process is ever killed. + +## Recipes + +Run from the repo root. PSTui must be importable (`Install-Module PSTui`, or it +auto-loads from the built `./module`). `tuirec` writes to `artifacts/` +(git-ignored); copy the result into `docs/PSTui/`. + +```bash +export PATH="$(go env GOPATH)/bin:$PATH" +``` + +### `hero.gif` — `ls | ocgv` + `killp` + +```bash +ks='wait:600,`src`,wait:1900,Escape,wait:1900,`Finder`,wait:1900,Escape,wait:1200' +tuirec record --binary pwsh --args "-NoLogo,-NoProfile,-File,./Scripts/tuirec/demo-ocgv.ps1" \ + --trim=false --name hero --title "PSTui — ocgv" --keystrokes "$ks" \ + --startup-delay 1900 --drain 1000 --cols 100 --rows 26 --keystroke-delay 120 +cp artifacts/hero.gif docs/PSTui/hero.gif +``` + +### `shot.gif` — `Get-Process | shot` + +```bash +ks='wait:1500,CursorDown,CursorDown,CursorDown,wait:1200,CursorUp,CursorUp,CursorUp,wait:500,CursorRight,wait:1700,CursorDown,CursorDown,CursorDown,CursorDown,wait:1500,Escape,wait:900' +tuirec record --binary pwsh --args "-NoLogo,-NoProfile,-File,./Scripts/tuirec/demo-shot.ps1" \ + --trim=false --name shot --title "PSTui — Show-ObjectTree (shot)" --keystrokes "$ks" \ + --startup-delay 2100 --drain 1000 --cols 100 --rows 28 +cp artifacts/shot.gif docs/PSTui/shot.gif +``` + +### `f7history.gif` — the F7 command-history picker + +```bash +ks='wait:1500,`Get`,wait:1800,`-S`,wait:1800,Escape,wait:900' +tuirec record --binary pwsh --args "-NoLogo,-NoProfile,-File,./Scripts/tuirec/demo-f7.ps1" \ + --trim=false --name f7history --title "PSTui — F7 history" --keystrokes "$ks" \ + --startup-delay 2100 --drain 1200 --cols 100 --rows 22 --keystroke-delay 130 +cp artifacts/f7history.gif docs/PSTui/f7history.gif +``` + +### `demo.gif` — the full `demo.ps1` walkthrough + +Records the repo-root [`demo.ps1`](../../demo.ps1) end-to-end; `Esc` advances +through each of its examples (the `killp` steps select nothing, so no process is +killed). + +```bash +ks='wait:800,Escape,wait:2300,Escape,wait:2300,Escape,wait:2300,Escape,wait:2300,Escape,wait:2300,CursorRight,wait:1600,Escape,wait:1000' +tuirec record --binary pwsh --args "-NoLogo,-NoProfile,-File,./demo.ps1" \ + --name demo --title "PSTui — demo.ps1" --keystrokes "$ks" \ + --startup-delay 3200 --drain 1200 --cols 100 --rows 28 +cp artifacts/demo.gif docs/PSTui/demo.gif +``` + +## Validate + +```bash +# No errors leaked into the cast: +grep -iE "error|not recognized|exception" artifacts/.cast + +# Eyeball a frame (needs python3 + Pillow). Frame 1 is usually the prompt: +python3 -c "from PIL import Image; im=Image.open('artifacts/.gif'); print(im.n_frames,'frames'); im.seek(im.n_frames//2); im.convert('RGB').save('/tmp/mid.png')" +``` + +A good recording is several frames and **> ~20 KB**; a near-blank capture is a +few KB / 2–3 frames (keystrokes went to the wrong focus, or no output rendered). + +## Troubleshooting + +| Problem | Cause | Fix | +|---------|-------|-----| +| "recording has no output events" / blank | Recording the bare pwsh REPL (doesn't render in the PTY) | Record a demo `.ps1` that invokes the cmdlet directly | +| Multi-step demo cut off after step 1 | `--trim` drops everything after the first alt-screen exit | `--trim=false` | +| Picker leaves residue / overlaps | Inline render of a short (filtered) grid | Use `-FullScreen` | +| Keystrokes seem ignored | Focus is on the tree/table, not the filter | `ocgv`: add `-Focus Filter`. `shot`: starts focused; `CursorRight` expands | +| `shot` renders mid-screen / empty | Inline tree positioning | Use `-FullScreen` | +| `--args` parsed into too many args | Commas in the PowerShell got split | Move the pipeline into a `-File` script | +| Backtick literal text dropped | Shell ate the backticks | Single-quote the whole `ks='...'` | +| ~1 s blank lead-in | `Import-Module` runs before any output | Echo the prompt line *before* `Import-Module` in the demo script | diff --git a/Scripts/tuirec/demo-f7.ps1 b/Scripts/tuirec/demo-f7.ps1 new file mode 100644 index 00000000..f551f683 --- /dev/null +++ b/Scripts/tuirec/demo-f7.ps1 @@ -0,0 +1,29 @@ +# F7 history demo for tuirec. The pwsh REPL does not render under a recording +# PTY, so this reproduces what pressing F7 shows: recent command history in +# Out-ConsoleGridView (the same shape Show-PSTuiHistory builds), with the typed +# prefix used as the live filter. +$prompt = 'PS ~/PStui> ' + +Write-Host '' +Write-Host ($prompt + 'Get ') -NoNewline; Write-Host '' +Import-Module PSTui -ErrorAction Stop + +$history = @( + 'Get-Process | Out-ConsoleGridView' + 'Get-ChildItem -Recurse -Filter *.cs' + 'Get-Service | Where-Object Status -eq Running' + 'git log --oneline -20' + 'dotnet build PSTui.slnx -c Release' + 'Get-Content README.md | Select-String PSTui' + 'Install-Module PSTui' + 'Get-Date -Format o' + 'Get-History | Format-Table -AutoSize' +) | ForEach-Object { [pscustomobject]@{ CommandLine = $_ } } + +Start-Sleep -Milliseconds 1200 +$history | + Out-ConsoleGridView -OutputMode Single -Title 'Command History (F7)' -Focus Filter -FullScreen | + Out-Null + +Write-Host $prompt +Start-Sleep -Milliseconds 800 diff --git a/Scripts/tuirec/demo-ocgv.ps1 b/Scripts/tuirec/demo-ocgv.ps1 new file mode 100644 index 00000000..d79d5ab8 --- /dev/null +++ b/Scripts/tuirec/demo-ocgv.ps1 @@ -0,0 +1,25 @@ +# Hero demo for tuirec: a pwsh session running `ls | ocgv` then `killp`. +# The prompt+command is printed *before* Import-Module so the GIF opens on the +# command line (no blank import lead-in), then the picker opens full-screen. +# We Esc out of both pickers, so nothing is actually killed. +$prompt = 'PS ~/PStui> ' + +Write-Host '' +Write-Host $prompt -NoNewline; Write-Host 'ls | ocgv' +Import-Module PSTui -ErrorAction Stop +Start-Sleep -Milliseconds 1100 +Get-ChildItem | + Out-ConsoleGridView -Title 'ls | ocgv' -Focus Filter -FullScreen | + Out-Null + +Write-Host $prompt -NoNewline; Write-Host 'killp' +Start-Sleep -Milliseconds 1400 +Get-Process | + Select-Object ProcessName, Id, + @{ Name = 'CPU(s)'; Expression = { [math]::Round($_.CPU, 2) } }, + @{ Name = 'WS(MB)'; Expression = { [math]::Round($_.WorkingSet64 / 1MB, 2) } } | + Out-ConsoleGridView -OutputMode Single -Title 'killp - pick a process' -Focus Filter -FullScreen | + Out-Null + +Write-Host $prompt +Start-Sleep -Milliseconds 900 diff --git a/Scripts/tuirec/demo-shot.ps1 b/Scripts/tuirec/demo-shot.ps1 new file mode 100644 index 00000000..a4fc229f --- /dev/null +++ b/Scripts/tuirec/demo-shot.ps1 @@ -0,0 +1,15 @@ +# SHOT demo for tuirec: a pwsh session running `Get-Process | shot`. +# Prompt printed before Import-Module so the GIF opens on the command line. +$prompt = 'PS ~/PStui> ' + +Write-Host '' +Write-Host $prompt -NoNewline; Write-Host 'Get-Process | shot' +Import-Module PSTui -ErrorAction Stop +Start-Sleep -Milliseconds 1200 +Get-Process | + Sort-Object -Property CPU -Descending | + Select-Object -First 15 | + Show-ObjectTree -Title 'Get-Process | shot' -FullScreen + +Write-Host $prompt +Start-Sleep -Milliseconds 800 diff --git a/docs/PSTui/demo.gif b/docs/PSTui/demo.gif new file mode 100644 index 00000000..8ce8f8fe Binary files /dev/null and b/docs/PSTui/demo.gif differ diff --git a/docs/PSTui/f7history.gif b/docs/PSTui/f7history.gif new file mode 100644 index 00000000..cdf10e6a Binary files /dev/null and b/docs/PSTui/f7history.gif differ diff --git a/docs/PSTui/hero.gif b/docs/PSTui/hero.gif new file mode 100644 index 00000000..2160b6b2 Binary files /dev/null and b/docs/PSTui/hero.gif differ diff --git a/docs/PSTui/ocgv.gif b/docs/PSTui/ocgv.gif deleted file mode 100644 index ac4212a9..00000000 Binary files a/docs/PSTui/ocgv.gif and /dev/null differ diff --git a/docs/PSTui/shot.gif b/docs/PSTui/shot.gif new file mode 100644 index 00000000..9d74abee Binary files /dev/null and b/docs/PSTui/shot.gif differ diff --git a/src/PSTui/PSTui.History.psm1 b/src/PSTui/PSTui.History.psm1 index f1427a75..fab8754f 100644 --- a/src/PSTui/PSTui.History.psm1 +++ b/src/PSTui/PSTui.History.psm1 @@ -68,17 +68,18 @@ function Show-PSTuiHistory { $selection = $history | Out-ConsoleGridView -OutputMode Single -Title $title -Filter $line - # Replace the current line with the selection (if any). + # Re-anchor PSReadLine's prompt at the *current* cursor row before touching + # the buffer. Under Terminal.Gui v2, Out-ConsoleGridView renders inline by + # default, so the screen has scrolled and PSReadLine's saved prompt row + # (_initialY) is now stale; a plain DeleteLine/Render would repaint the + # prompt in the wrong place (the F7History bug fixed in tui-cs/F7History#25). + # Passing CursorTop as the arg makes InvokePrompt re-anchor on this row. + [Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt($null, [Console]::CursorTop) + + # Replace the typed prefix (used above as the filter) with the selection. [Microsoft.PowerShell.PSConsoleReadLine]::DeleteLine() if ($selection) { - $command = $selection.CommandLine - [Microsoft.PowerShell.PSConsoleReadLine]::Insert($command) - if ($command.StartsWith($line)) { - [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor) - } - else { - [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($command.Length) - } + [Microsoft.PowerShell.PSConsoleReadLine]::Insert($selection.CommandLine) } }