Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ publish/
Properties/launchSettings.json
nuget.local.config
GraphicalTools.Dev.slnx
artifacts/
Thumbs.db
*.Thumbs.db
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
136 changes: 136 additions & 0 deletions Scripts/tuirec/README.md
Original file line number Diff line number Diff line change
@@ -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/<name>.cast

# Eyeball a frame (needs python3 + Pillow). Frame 1 is usually the prompt:
python3 -c "from PIL import Image; im=Image.open('artifacts/<name>.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 |
29 changes: 29 additions & 0 deletions Scripts/tuirec/demo-f7.ps1
Original file line number Diff line number Diff line change
@@ -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 '<F7>'
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
25 changes: 25 additions & 0 deletions Scripts/tuirec/demo-ocgv.ps1
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions Scripts/tuirec/demo-shot.ps1
Original file line number Diff line number Diff line change
@@ -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
Binary file added docs/PSTui/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/PSTui/f7history.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/PSTui/hero.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/PSTui/ocgv.gif
Binary file not shown.
Binary file added docs/PSTui/shot.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 10 additions & 9 deletions src/PSTui/PSTui.History.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
Loading