The incident workbench and the other app demos are built from a handful of parts. Here each part is a whole app on its own, in PowerShell, C# and C# fluent, with the screen it drew after the keys or clicks in its caption. Every part works with the keyboard and the mouse, lists its keys in the footer, and keeps its value in app state under its Key, so a binding or a callback can read it.
Try it
Pick a part, then a language. The frame under the code is what that program drew; the three languages draw the same cells.
Install-Module ConsoleMonster -Scope CurrentUserShow-ConsoleApp -Content {
New-ConsoleSplitter -Key Panes -Ratio 0.3 -First {
New-ConsolePanel -Title Servers -Content {
New-ConsoleSelect -Key Server -PageSize 3 -Choices @(
[pscustomobject] @{ Value = 'WEB01'; Label = 'WEB01 Web' }
[pscustomobject] @{ Value = 'SQL01'; Label = 'SQL01 Database' }
[pscustomobject] @{ Value = 'FS01'; Label = 'FS01 File' }
)
}
} -Second {
New-ConsolePanel -Title Notes -Content {
'WEB01 serves the public site.'
'SQL01 holds the orders database.'
'FS01 keeps the shared folders.'
}
}
}Full program
Import-Module ConsoleMonster
Show-ConsoleApp -Content {
New-ConsoleSplitter -Key Panes -Ratio 0.3 -First {
New-ConsolePanel -Title Servers -Content {
New-ConsoleSelect -Key Server -PageSize 3 -Choices @(
[pscustomobject] @{ Value = 'WEB01'; Label = 'WEB01 Web' }
[pscustomobject] @{ Value = 'SQL01'; Label = 'SQL01 Database' }
[pscustomobject] @{ Value = 'FS01'; Label = 'FS01 File' }
)
}
} -Second {
New-ConsolePanel -Title Notes -Content {
'WEB01 serves the public site.'
'SQL01 holds the orders database.'
'FS01 keeps the shared folders.'
}
}
}Run: ./app-splitter.ps1
dotnet new console -n AppParts
cd AppParts
dotnet add package ConsoleMonsterSplitter panes = new Splitter(
new Panel(new Select("Server", new[] {
new Choice("WEB01", "WEB01 Web"),
new Choice("SQL01", "SQL01 Database"),
new Choice("FS01", "FS01 File")
}) { PageSize = 3 }) { Title = "Servers" },
new Panel(new Rows(
new TextBlock("WEB01 serves the public site."),
new TextBlock("SQL01 holds the orders database."),
new TextBlock("FS01 keeps the shared folders."))) { Title = "Notes" })
{
Key = "Panes",
Ratio = 0.3
};
ConsoleApp.Run(panes);Full program
using ConsoleMonster.Interactive;
using ConsoleMonster.Layout;
using ConsoleMonster.Widgets;
Splitter panes = new Splitter(
new Panel(new Select("Server", new[] {
new Choice("WEB01", "WEB01 Web"),
new Choice("SQL01", "SQL01 Database"),
new Choice("FS01", "FS01 File")
}) { PageSize = 3 }) { Title = "Servers" },
new Panel(new Rows(
new TextBlock("WEB01 serves the public site."),
new TextBlock("SQL01 holds the orders database."),
new TextBlock("FS01 keeps the shared folders."))) { Title = "Notes" })
{
Key = "Panes",
Ratio = 0.3
};
ConsoleApp.Run(panes);Run: dotnet run
dotnet new console -n AppParts
cd AppParts
dotnet add package ConsoleMonsterSplitter panes = Ui.Splitter().First(Ui.Panel().Title("Servers").Content(panelContent =>
{
panelContent.Select(select => select.Key("Server")
.AddChoices(new[]
{
new Choice("WEB01", "WEB01 Web"),
new Choice("SQL01", "SQL01 Database"),
new Choice("FS01", "FS01 File")
})
.PageSize(3));
})).Second(Ui.Panel().Title("Notes").Content(panelContent =>
{
panelContent.Rows(r => r.Content(rowsContent =>
{
rowsContent.Text("WEB01 serves the public site.");
rowsContent.Text("SQL01 holds the orders database.");
rowsContent.Text("FS01 keeps the shared folders.");
}));
})).Key("Panes").Ratio(0.3);
ConsoleApp.Run(panes);
Full program
using ConsoleMonster.Interactive;
using ConsoleMonster.Layout;
using ConsoleMonster.Widgets;
using ConsoleMonster;
Splitter panes = Ui.Splitter().First(Ui.Panel().Title("Servers").Content(panelContent =>
{
panelContent.Select(select => select.Key("Server")
.AddChoices(new[]
{
new Choice("WEB01", "WEB01 Web"),
new Choice("SQL01", "SQL01 Database"),
new Choice("FS01", "FS01 File")
})
.PageSize(3));
})).Second(Ui.Panel().Title("Notes").Content(panelContent =>
{
panelContent.Rows(r => r.Content(rowsContent =>
{
rowsContent.Text("WEB01 serves the public site.");
rowsContent.Text("SQL01 holds the orders database.");
rowsContent.Text("FS01 keeps the shared folders.");
}));
})).Key("Panes").Ratio(0.3);
ConsoleApp.Run(panes);
Run: dotnet run
╭─ Servers ────────────────────────╮│╭─ Notes ─────────────────────────────────╮ │ ╭─ › Server ───────────────────╮ │││ WEB01 serves the public site. │ │ │ › type to filter │ │││ SQL01 holds the orders database. │ │ ╰──────────────────────────────╯ │││ FS01 keeps the shared folders. │ ╰──────────────────────────────────╯│╰─────────────────────────────────────────╯ tab next shift+tab back esc quit
╭─ Servers ────────────────────────╮│╭─ Notes ─────────────────────────────────╮ │ ╭─ › Server ───────────────────╮ │││ WEB01 serves the public site. │ │ │ › type to filter │ │││ SQL01 holds the orders database. │ │ ╰──────────────────────────────╯ │││ FS01 keeps the shared folders. │ ╰──────────────────────────────────╯│╰─────────────────────────────────────────╯ tab next shift+tab back esc quit
Install-Module ConsoleMonster -Scope CurrentUserShow-ConsoleApp -Content {
New-ConsoleAccordion -Content {
New-ConsoleCollapsible -Key Synopsis -Expanded -Content {
'Lists servers and their roles.'
}
New-ConsoleCollapsible -Key Parameters -Summary '(2)' -Content {
'-Name server name or wildcard'
'-Role web, db or cache'
}
New-ConsoleCollapsible -Key Examples -Summary '(1)' -Content {
'Get-Inventory -Role web'
}
}
}Full program
Import-Module ConsoleMonster
Show-ConsoleApp -Content {
New-ConsoleAccordion -Content {
New-ConsoleCollapsible -Key Synopsis -Expanded -Content {
'Lists servers and their roles.'
}
New-ConsoleCollapsible -Key Parameters -Summary '(2)' -Content {
'-Name server name or wildcard'
'-Role web, db or cache'
}
New-ConsoleCollapsible -Key Examples -Summary '(1)' -Content {
'Get-Inventory -Role web'
}
}
}Run: ./app-accordion.ps1
dotnet new console -n AppParts
cd AppParts
dotnet add package ConsoleMonsterAccordion help = new Accordion(
new Collapsible("Synopsis", new TextBlock("Lists servers and their roles."))
{
Expanded = true
},
new Collapsible("Parameters", new Rows(
new TextBlock("-Name server name or wildcard"),
new TextBlock("-Role web, db or cache")))
{
Summary = "(2)"
},
new Collapsible("Examples", new TextBlock("Get-Inventory -Role web"))
{
Summary = "(1)"
});
ConsoleApp.Run(help);Full program
using ConsoleMonster.Interactive;
using ConsoleMonster.Layout;
using ConsoleMonster.Widgets;
Accordion help = new Accordion(
new Collapsible("Synopsis", new TextBlock("Lists servers and their roles."))
{
Expanded = true
},
new Collapsible("Parameters", new Rows(
new TextBlock("-Name server name or wildcard"),
new TextBlock("-Role web, db or cache")))
{
Summary = "(2)"
},
new Collapsible("Examples", new TextBlock("Get-Inventory -Role web"))
{
Summary = "(1)"
});
ConsoleApp.Run(help);Run: dotnet run
dotnet new console -n AppParts
cd AppParts
dotnet add package ConsoleMonsterAccordion help = Ui.Accordion().Content(accordionContent =>
{
accordionContent.Collapsible(collapsible => collapsible.Key("Synopsis")
.Content(Ui.Text("Lists servers and their roles."))
.Expanded());
accordionContent.Collapsible(collapsible => collapsible.Key("Parameters")
.Content(Ui.Rows().Content(rowsContent =>
{
rowsContent.Text("-Name server name or wildcard");
rowsContent.Text("-Role web, db or cache");
}))
.Summary("(2)"));
accordionContent.Collapsible(collapsible => collapsible.Key("Examples")
.Content(Ui.Text("Get-Inventory -Role web"))
.Summary("(1)"));
});
ConsoleApp.Run(help);
Full program
using ConsoleMonster.Interactive;
using ConsoleMonster.Layout;
using ConsoleMonster.Widgets;
using ConsoleMonster;
Accordion help = Ui.Accordion().Content(accordionContent =>
{
accordionContent.Collapsible(collapsible => collapsible.Key("Synopsis")
.Content(Ui.Text("Lists servers and their roles."))
.Expanded());
accordionContent.Collapsible(collapsible => collapsible.Key("Parameters")
.Content(Ui.Rows().Content(rowsContent =>
{
rowsContent.Text("-Name server name or wildcard");
rowsContent.Text("-Role web, db or cache");
}))
.Summary("(2)"));
accordionContent.Collapsible(collapsible => collapsible.Key("Examples")
.Content(Ui.Text("Get-Inventory -Role web"))
.Summary("(1)"));
});
ConsoleApp.Run(help);
Run: dotnet run
▸ Synopsis › ▾ Parameters -Name server name or wildcard -Role web, db or cache ▸ Examples (1) enter expand/collapse + expand all - collapse all tab next
▸ Synopsis › ▾ Parameters -Name server name or wildcard -Role web, db or cache ▸ Examples (1) enter expand/collapse + expand all - collapse all tab next
Install-Module ConsoleMonster -Scope CurrentUser$sections = New-ConsoleOutline -Key Sections -SelectedIndex 0 -Items @(
'Install'
@{ Title = 'Windows'; Level = 2 }
@{ Title = 'Linux'; Level = 2 }
'Configure'
'Run'
)
Show-ConsoleApp -Content {
New-ConsoleBreadcrumb -Key Where -Ancestors 'Guide' -Outline $sections
$sections
}Full program
Import-Module ConsoleMonster
$sections = New-ConsoleOutline -Key Sections -SelectedIndex 0 -Items @(
'Install'
@{ Title = 'Windows'; Level = 2 }
@{ Title = 'Linux'; Level = 2 }
'Configure'
'Run'
)
Show-ConsoleApp -Content {
New-ConsoleBreadcrumb -Key Where -Ancestors 'Guide' -Outline $sections
$sections
}Run: ./app-outline.ps1
dotnet new console -n AppParts
cd AppParts
dotnet add package ConsoleMonsterOutline sections = new Outline("Sections")
{
Items =
{
new OutlineItem("Install"),
new OutlineItem("Windows", 2),
new OutlineItem("Linux", 2),
new OutlineItem("Configure"),
new OutlineItem("Run")
},
SelectedIndex = 0
};
Breadcrumb where = new Breadcrumb("Where") { Ancestors = { "Guide" }, Outline = sections };
ConsoleApp.Run(new Rows(where, sections));Full program
using ConsoleMonster.Interactive;
using ConsoleMonster.Layout;
Outline sections = new Outline("Sections")
{
Items =
{
new OutlineItem("Install"),
new OutlineItem("Windows", 2),
new OutlineItem("Linux", 2),
new OutlineItem("Configure"),
new OutlineItem("Run")
},
SelectedIndex = 0
};
Breadcrumb where = new Breadcrumb("Where") { Ancestors = { "Guide" }, Outline = sections };
ConsoleApp.Run(new Rows(where, sections));Run: dotnet run
dotnet new console -n AppParts
cd AppParts
dotnet add package ConsoleMonsterOutline sections = ((Outline)Ui.Outline()
.Key("Sections")
.Build()).AddItem(new OutlineItem("Install"))
.AddItem(new OutlineItem("Windows", 2))
.AddItem(new OutlineItem("Linux", 2))
.AddItem(new OutlineItem("Configure"))
.AddItem(new OutlineItem("Run"))
.SelectedIndex(0);
Breadcrumb where = ((Breadcrumb)Ui.Breadcrumb().Key("Where").Build()).AddAncestor("Guide")
.Outline(sections);
ConsoleApp.Run(Ui.Rows().Content(rowsContent =>
{
rowsContent.Add(where);
rowsContent.Add(sections);
}));
Full program
using ConsoleMonster.Interactive;
using ConsoleMonster.Layout;
using ConsoleMonster;
Outline sections = ((Outline)Ui.Outline()
.Key("Sections")
.Build()).AddItem(new OutlineItem("Install"))
.AddItem(new OutlineItem("Windows", 2))
.AddItem(new OutlineItem("Linux", 2))
.AddItem(new OutlineItem("Configure"))
.AddItem(new OutlineItem("Run"))
.SelectedIndex(0);
Breadcrumb where = ((Breadcrumb)Ui.Breadcrumb().Key("Where").Build()).AddAncestor("Guide")
.Outline(sections);
ConsoleApp.Run(Ui.Rows().Content(rowsContent =>
{
rowsContent.Add(where);
rowsContent.Add(sections);
}));
Run: dotnet run
Guide › Install › Linux Install Windows › Linux Configure Run ↑↓ move enter open tab next shift+tab back esc quit
Guide › Install › Linux Install Windows › Linux Configure Run ↑↓ move enter open tab next shift+tab back esc quit
Install-Module ConsoleMonster -Scope CurrentUserShow-ConsoleApp -Content {
New-ConsoleTabs -Key Page -Content {
@{ Title = 'Overview'; Content = { 'SQL01 · Database · Warsaw'; 'Patched 2026-10-12' } }
@{ Title = 'Disks'; Content = { 'C: 41% used'; 'D: 97% used' } }
@{ Title = 'Services'; Content = { 'MSSQLSERVER Running'; 'SQLAgent Running' } }
}
}Full program
Import-Module ConsoleMonster
Show-ConsoleApp -Content {
New-ConsoleTabs -Key Page -Content {
@{ Title = 'Overview'; Content = { 'SQL01 · Database · Warsaw'; 'Patched 2026-10-12' } }
@{ Title = 'Disks'; Content = { 'C: 41% used'; 'D: 97% used' } }
@{ Title = 'Services'; Content = { 'MSSQLSERVER Running'; 'SQLAgent Running' } }
}
}Run: ./app-tabs.ps1
dotnet new console -n AppParts
cd AppParts
dotnet add package ConsoleMonsterTabs server = new Tabs("Page")
.Add("Overview", new Rows(
new TextBlock("SQL01 · Database · Warsaw"),
new TextBlock("Patched 2026-10-12")))
.Add("Disks", new Rows(
new TextBlock("C: 41% used"),
new TextBlock("D: 97% used")))
.Add("Services", new Rows(
new TextBlock("MSSQLSERVER Running"),
new TextBlock("SQLAgent Running")));
ConsoleApp.Run(server);Full program
using ConsoleMonster.Interactive;
using ConsoleMonster.Layout;
using ConsoleMonster.Widgets;
Tabs server = new Tabs("Page")
.Add("Overview", new Rows(
new TextBlock("SQL01 · Database · Warsaw"),
new TextBlock("Patched 2026-10-12")))
.Add("Disks", new Rows(
new TextBlock("C: 41% used"),
new TextBlock("D: 97% used")))
.Add("Services", new Rows(
new TextBlock("MSSQLSERVER Running"),
new TextBlock("SQLAgent Running")));
ConsoleApp.Run(server);Run: dotnet run
dotnet new console -n AppParts
cd AppParts
dotnet add package ConsoleMonsterTabs server = Ui.Tabs().Key("Page").Add("Overview", Ui.Rows().Content(rowsContent =>
{
rowsContent.Text("SQL01 · Database · Warsaw");
rowsContent.Text("Patched 2026-10-12");
})).Add("Disks", Ui.Rows().Content(rowsContent =>
{
rowsContent.Text("C: 41% used");
rowsContent.Text("D: 97% used");
})).Add("Services", Ui.Rows().Content(rowsContent =>
{
rowsContent.Text("MSSQLSERVER Running");
rowsContent.Text("SQLAgent Running");
}));
ConsoleApp.Run(server);
Full program
using ConsoleMonster.Interactive;
using ConsoleMonster.Layout;
using ConsoleMonster.Widgets;
using ConsoleMonster;
Tabs server = Ui.Tabs().Key("Page").Add("Overview", Ui.Rows().Content(rowsContent =>
{
rowsContent.Text("SQL01 · Database · Warsaw");
rowsContent.Text("Patched 2026-10-12");
})).Add("Disks", Ui.Rows().Content(rowsContent =>
{
rowsContent.Text("C: 41% used");
rowsContent.Text("D: 97% used");
})).Add("Services", Ui.Rows().Content(rowsContent =>
{
rowsContent.Text("MSSQLSERVER Running");
rowsContent.Text("SQLAgent Running");
}));
ConsoleApp.Run(server);
Run: dotnet run
Overview Disks Services ──────────━━━━━━━─────────────────────────────────────────── C: 41% used D: 97% used ←→ tabs 1-3 open tab tab next shift+tab back esc quit
Overview Disks Services ──────────━━━━━━━─────────────────────────────────────────── C: 41% used D: 97% used ←→ tabs 1-3 open tab tab next shift+tab back esc quit
Install-Module ConsoleMonster -Scope CurrentUser$times = '09:00', '09:05', '09:10', '09:15', '09:20', '09:25', '09:30', '09:35'
$cpu = 22, 25, 31, 64, 88, 71, 40, 28
$samples = for ($i = 0; $i -lt $times.Count; $i++) {
[pscustomobject] @{ Time = $times[$i]; Cpu = $cpu[$i] }
}
$chart = New-ConsoleChart -Type Line -Unit '%' -Series @([pscustomobject] @{ Key = 'cpu'
Values = $cpu })
Show-ConsoleApp -Content {
New-ConsoleSplitter -Ratio 0.3 -First {
New-ConsoleDataGrid -Key Samples -InputObject $samples -LinkKey Sample
} -Second {
New-ConsoleChartView -Key Cpu -Chart $chart -PointLabels $times -LinkKey Sample -Height 9
}
}Full program
Import-Module ConsoleMonster
$times = '09:00', '09:05', '09:10', '09:15', '09:20', '09:25', '09:30', '09:35'
$cpu = 22, 25, 31, 64, 88, 71, 40, 28
$samples = for ($i = 0; $i -lt $times.Count; $i++) {
[pscustomobject] @{ Time = $times[$i]; Cpu = $cpu[$i] }
}
$chart = New-ConsoleChart -Type Line -Unit '%' -Series @([pscustomobject] @{ Key = 'cpu'
Values = $cpu })
Show-ConsoleApp -Content {
New-ConsoleSplitter -Ratio 0.3 -First {
New-ConsoleDataGrid -Key Samples -InputObject $samples -LinkKey Sample
} -Second {
New-ConsoleChartView -Key Cpu -Chart $chart -PointLabels $times -LinkKey Sample -Height 9
}
}Run: ./app-chartview.ps1
dotnet new console -n AppParts
cd AppParts
dotnet add package ConsoleMonsterstring[] times = { "09:00", "09:05", "09:10", "09:15", "09:20", "09:25", "09:30", "09:35" };
double[] cpu = { 22, 25, 31, 64, 88, 71, 40, 28 };
LineChart chart = new LineChart { Unit = "%" };
foreach (double value in cpu) { chart.Add("cpu", value); }
ChartView view = new ChartView("Cpu", chart) { LinkKey = "Sample", Height = 9 };
foreach (string time in times) { view.PointLabels.Add(time); }
Grid samples = new Grid("Samples").AddColumn("Time").AddColumn("Cpu");
for (int i = 0; i < times.Length; i++) { samples.AddRow(times[i], times[i], cpu[i]); }
ConsoleApp.Run(new Splitter(new DataGrid("Samples", samples) { LinkKey = "Sample" }, view)
{
Ratio = 0.3
});Full program
using ConsoleMonster.Interactive;
using ConsoleMonster.Widgets;
string[] times = { "09:00", "09:05", "09:10", "09:15", "09:20", "09:25", "09:30", "09:35" };
double[] cpu = { 22, 25, 31, 64, 88, 71, 40, 28 };
LineChart chart = new LineChart { Unit = "%" };
foreach (double value in cpu) { chart.Add("cpu", value); }
ChartView view = new ChartView("Cpu", chart) { LinkKey = "Sample", Height = 9 };
foreach (string time in times) { view.PointLabels.Add(time); }
Grid samples = new Grid("Samples").AddColumn("Time").AddColumn("Cpu");
for (int i = 0; i < times.Length; i++) { samples.AddRow(times[i], times[i], cpu[i]); }
ConsoleApp.Run(new Splitter(new DataGrid("Samples", samples) { LinkKey = "Sample" }, view)
{
Ratio = 0.3
});Run: dotnet run
dotnet new console -n AppParts
cd AppParts
dotnet add package ConsoleMonsterstring[] times = { "09:00", "09:05", "09:10", "09:15", "09:20", "09:25", "09:30", "09:35" };
double[] cpu = { 22, 25, 31, 64, 88, 71, 40, 28 };
LineChart chart = Ui.LineChart().Unit("%");
foreach (double value in cpu) { chart.Add("cpu", value); }
ChartView view = ((ChartView)Ui.ChartView()
.Key("Cpu")
.Chart(chart)
.LinkKey("Sample")
.Height(9)
.Build());
foreach (string time in times) { view.PointLabels.Add(time); }
Grid samples = Ui.Grid().Title("Samples").AddColumn("Time").AddColumn("Cpu");
for (int i = 0; i < times.Length; i++) { samples.AddRow(times[i], times[i], cpu[i]); }
ConsoleApp.Run(Ui.Splitter()
.First(c => c.DataGrid(grid => grid.Key("Samples").Grid(samples).LinkKey("Sample")))
.Second(view)
.Ratio(0.3));
Full program
using ConsoleMonster.Interactive;
using ConsoleMonster.Widgets;
using ConsoleMonster;
string[] times = { "09:00", "09:05", "09:10", "09:15", "09:20", "09:25", "09:30", "09:35" };
double[] cpu = { 22, 25, 31, 64, 88, 71, 40, 28 };
LineChart chart = Ui.LineChart().Unit("%");
foreach (double value in cpu) { chart.Add("cpu", value); }
ChartView view = ((ChartView)Ui.ChartView()
.Key("Cpu")
.Chart(chart)
.LinkKey("Sample")
.Height(9)
.Build());
foreach (string time in times) { view.PointLabels.Add(time); }
Grid samples = Ui.Grid().Title("Samples").AddColumn("Time").AddColumn("Cpu");
for (int i = 0; i < times.Length; i++) { samples.AddRow(times[i], times[i], cpu[i]); }
ConsoleApp.Run(Ui.Splitter()
.First(c => c.DataGrid(grid => grid.Key("Samples").Grid(samples).LinkKey("Sample")))
.Second(view)
.Ratio(0.3));
Run: dotnet run
╭─ › Samples ─────────────╮│╭─ Cpu ──────────────────────────────────────────────────────╮ │ Time Cpu │││ 09:20 cpu 88% │ │ ───── ─── │││ 88%│ ⣀⠤⠒⠉⠑⠒⠤⢄⣀ │ │ 09:00 22 │││ │ ⣀⠤⠒⠉ │ ⠉⠑⠢⢄ │ │ 09:05 25 │││ │ ⡠⠔⠉ │ ⠉⠒⢄⡀ │ │ 09:10 31 │││ │ ⡠⠔⠉ │ ⠈⠒⠤⣀⡀ │ │ 09:15 64 │││ │ ⣀⣀⣀⣀⣀⡠⠤⠤⠤⠔⠒⠒⠉ │ ⠈⠉⠉⠒⠒⠢⠤ │ │ › 09:20 88 │││ │⠉⠉⠉⠉ │ │ │ 09:25 71 │││ 0%│ │ │ │ 09:30 40 │││ └────────────────────────────────────────────────────── │ ╰─────────────────────────╯│╰────────────────────────────────────────────────────────────╯ f3 sort tab next shift+tab back esc quit ctrl+shift+←/→ resize
╭─ › Samples ─────────────╮│╭─ Cpu ──────────────────────────────────────────────────────╮ │ Time Cpu │││ 09:20 cpu 88% │ │ ───── ─── │││ 88%│ ⣀⠤⠒⠉⠑⠒⠤⢄⣀ │ │ 09:00 22 │││ │ ⣀⠤⠒⠉ │ ⠉⠑⠢⢄ │ │ 09:05 25 │││ │ ⡠⠔⠉ │ ⠉⠒⢄⡀ │ │ 09:10 31 │││ │ ⡠⠔⠉ │ ⠈⠒⠤⣀⡀ │ │ 09:15 64 │││ │ ⣀⣀⣀⣀⣀⡠⠤⠤⠤⠔⠒⠒⠉ │ ⠈⠉⠉⠒⠒⠢⠤ │ │ › 09:20 88 │││ │⠉⠉⠉⠉ │ │ │ 09:25 71 │││ 0%│ │ │ │ 09:30 40 │││ └────────────────────────────────────────────────────── │ ╰─────────────────────────╯│╰────────────────────────────────────────────────────────────╯ f3 sort tab next shift+tab back esc quit ctrl+shift+←/→ resize
Install-Module ConsoleMonster -Scope CurrentUser$estate = [pscustomobject] @{
Warsaw = @(
[pscustomobject] @{ Name = 'WEB01'; Role = 'Web'; Patched = '2026-10-28' }
[pscustomobject] @{ Name = 'SQL01'; Role = 'Database'; Patched = '2026-10-12' }
[pscustomobject] @{ Name = 'FS01'; Role = 'File'; Patched = '2026-09-30' }
)
Berlin = @(
[pscustomobject] @{ Name = 'WEB11'; Role = 'Web'; Patched = '2026-10-28' }
[pscustomobject] @{ Name = 'SQL11'; Role = 'Database'; Patched = '2026-08-19' }
)
}
Show-ConsoleApp -Title Inventory -Content {
$treeSelectParameters = @{
Key = 'Tree'
Label = 'Servers'
InputObject = $estate
RootPath = '$estate'
Height = 10
}
New-ConsoleTreeSelect @treeSelectParameters
New-ConsoleBinding -Content {
param($State)
$path = if ($State.Tree) { $State.Tree.Path } else { 'nothing' }
New-ConsoleText -Text "Selected: $path"
}
}Full program
Import-Module ConsoleMonster
$estate = [pscustomobject] @{
Warsaw = @(
[pscustomobject] @{ Name = 'WEB01'; Role = 'Web'; Patched = '2026-10-28' }
[pscustomobject] @{ Name = 'SQL01'; Role = 'Database'; Patched = '2026-10-12' }
[pscustomobject] @{ Name = 'FS01'; Role = 'File'; Patched = '2026-09-30' }
)
Berlin = @(
[pscustomobject] @{ Name = 'WEB11'; Role = 'Web'; Patched = '2026-10-28' }
[pscustomobject] @{ Name = 'SQL11'; Role = 'Database'; Patched = '2026-08-19' }
)
}
Show-ConsoleApp -Title Inventory -Content {
$treeSelectParameters = @{
Key = 'Tree'
Label = 'Servers'
InputObject = $estate
RootPath = '$estate'
Height = 10
}
New-ConsoleTreeSelect @treeSelectParameters
New-ConsoleBinding -Content {
param($State)
$path = if ($State.Tree) { $State.Tree.Path } else { 'nothing' }
New-ConsoleText -Text "Selected: $path"
}
}Run: ./tree.ps1
dotnet new console -n AppParts
cd AppParts
dotnet add package ConsoleMonstervar estate = new
{
Warsaw = new[]
{
new Server("WEB01", "Web", "2026-10-28"),
new Server("SQL01", "Database", "2026-10-12"),
new Server("FS01", "File", "2026-09-30")
},
Berlin = new[]
{
new Server("WEB11", "Web", "2026-10-28"),
new Server("SQL11", "Database", "2026-08-19")
}
};
TreeSelect tree = new TreeSelect("Tree", new ObjectExplorer(estate, default, "$estate")) {
Label = "Servers",
Height = 10
};
AppBinding picked = new AppBinding(state =>
TextBlock.Plain("Selected: " + (state["Tree"] is ExplorerNode node ? node.Path : "nothing")));
ConsoleApp.Run(new Rows(new HeaderBar("Inventory"), tree, picked));
record Server(string Name, string Role, string Patched);Full program
using ConsoleMonster.Interactive;
using ConsoleMonster.Layout;
using ConsoleMonster.Widgets;
var estate = new
{
Warsaw = new[]
{
new Server("WEB01", "Web", "2026-10-28"),
new Server("SQL01", "Database", "2026-10-12"),
new Server("FS01", "File", "2026-09-30")
},
Berlin = new[]
{
new Server("WEB11", "Web", "2026-10-28"),
new Server("SQL11", "Database", "2026-08-19")
}
};
TreeSelect tree = new TreeSelect("Tree", new ObjectExplorer(estate, default, "$estate")) {
Label = "Servers",
Height = 10
};
AppBinding picked = new AppBinding(state =>
TextBlock.Plain("Selected: " + (state["Tree"] is ExplorerNode node ? node.Path : "nothing")));
ConsoleApp.Run(new Rows(new HeaderBar("Inventory"), tree, picked));
record Server(string Name, string Role, string Patched);Run: dotnet run
dotnet new console -n AppParts
cd AppParts
dotnet add package ConsoleMonstervar estate = new
{
Warsaw = new[]
{
new Server("WEB01", "Web", "2026-10-28"),
new Server("SQL01", "Database", "2026-10-12"),
new Server("FS01", "File", "2026-09-30")
},
Berlin = new[]
{
new Server("WEB11", "Web", "2026-10-28"),
new Server("SQL11", "Database", "2026-08-19")
}
};
TreeSelect tree = ((TreeSelect)Ui.TreeSelect()
.Key("Tree")
.Explorer(new ObjectExplorer(estate, default, "$estate"))
.Label("Servers")
.Height(10)
.Build());
ConsoleApp.Run(Ui.Rows().Content(rowsContent =>
{
rowsContent.HeaderBar(headerBar => headerBar.Title("Inventory"));
rowsContent.Add(tree);
rowsContent.Binding(binding => binding.Configure(o => o.Content = state => Ui.Text("Selected: "
+ (state["Tree"] is ExplorerNode node ? node.Path : "nothing"))));
}));
record Server(string Name, string Role, string Patched);
Full program
using ConsoleMonster.Interactive;
using ConsoleMonster.Layout;
using ConsoleMonster.Widgets;
using ConsoleMonster;
var estate = new
{
Warsaw = new[]
{
new Server("WEB01", "Web", "2026-10-28"),
new Server("SQL01", "Database", "2026-10-12"),
new Server("FS01", "File", "2026-09-30")
},
Berlin = new[]
{
new Server("WEB11", "Web", "2026-10-28"),
new Server("SQL11", "Database", "2026-08-19")
}
};
TreeSelect tree = ((TreeSelect)Ui.TreeSelect()
.Key("Tree")
.Explorer(new ObjectExplorer(estate, default, "$estate"))
.Label("Servers")
.Height(10)
.Build());
ConsoleApp.Run(Ui.Rows().Content(rowsContent =>
{
rowsContent.HeaderBar(headerBar => headerBar.Title("Inventory"));
rowsContent.Add(tree);
rowsContent.Binding(binding => binding.Configure(o => o.Content = state => Ui.Text("Selected: "
+ (state["Tree"] is ExplorerNode node ? node.Path : "nothing"))));
}));
record Server(string Name, string Role, string Patched);
Run: dotnet run
Inventory ╭─ › Servers ────────────────────────────────────────────────────────────────────────────╮ │ Name SQL11 │ │ Role Database │ │ > Patched 2026-08-19 │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ Selected: $estate.Berlin[1].Patched tab next shift+tab back esc quit
Inventory ╭─ › Servers ────────────────────────────────────────────────────────────────────────────╮ │ Name SQL11 │ │ Role Database │ │ > Patched 2026-08-19 │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ Selected: $estate.Berlin[1].Patched tab next shift+tab back esc quit
Install-Module ConsoleMonster -Scope CurrentUser$lines = @(
@{ Level = 'info'; Message = 'Deploy 4.12.2 started' }
@{ Level = 'info'; Message = 'web-01 drained' }
@{ Level = 'warn'; Message = 'web-01 health check timeout, retrying' }
@{ Level = 'info'; Message = 'web-01 updated' }
@{ Level = 'warn'; Message = 'web-02 health check timeout, retrying' }
@{ Level = 'info'; Message = 'web-02 updated' }
@{ Level = 'pass'; Message = 'Deploy complete' }
)
Show-ConsoleApp -Content {
'[bold]Deploy 4.12.2[/] [muted]· / finds text[/]'
New-ConsoleLogRegion -WidgetId deploy.log -NoElapsed -Height 8 -Content { $lines }
}Full program
Import-Module ConsoleMonster
$lines = @(
@{ Level = 'info'; Message = 'Deploy 4.12.2 started' }
@{ Level = 'info'; Message = 'web-01 drained' }
@{ Level = 'warn'; Message = 'web-01 health check timeout, retrying' }
@{ Level = 'info'; Message = 'web-01 updated' }
@{ Level = 'warn'; Message = 'web-02 health check timeout, retrying' }
@{ Level = 'info'; Message = 'web-02 updated' }
@{ Level = 'pass'; Message = 'Deploy complete' }
)
Show-ConsoleApp -Content {
'[bold]Deploy 4.12.2[/] [muted]· / finds text[/]'
New-ConsoleLogRegion -WidgetId deploy.log -NoElapsed -Height 8 -Content { $lines }
}Run: ./app-find.ps1
dotnet new console -n AppParts
cd AppParts
dotnet add package ConsoleMonster(string Level, string Message)[] lines =
{
("info", "Deploy 4.12.2 started"),
("info", "web-01 drained"),
("warn", "web-01 health check timeout, retrying"),
("info", "web-01 updated"),
("warn", "web-02 health check timeout, retrying"),
("info", "web-02 updated"),
("pass", "Deploy complete")
};
LogViewport log = new LogViewport("deploy.log") { ShowElapsed = false, Height = 8 };
foreach ((string level, string message) in lines) { log.Write(message, level); }
ConsoleApp.Run(new Rows(TextBlock.FromMarkup("[bold]Deploy 4.12.2[/] [muted]· / finds text[/]"),
log));Full program
using ConsoleMonster.Interactive;
using ConsoleMonster.Layout;
using ConsoleMonster.Widgets;
(string Level, string Message)[] lines =
{
("info", "Deploy 4.12.2 started"),
("info", "web-01 drained"),
("warn", "web-01 health check timeout, retrying"),
("info", "web-01 updated"),
("warn", "web-02 health check timeout, retrying"),
("info", "web-02 updated"),
("pass", "Deploy complete")
};
LogViewport log = new LogViewport("deploy.log") { ShowElapsed = false, Height = 8 };
foreach ((string level, string message) in lines) { log.Write(message, level); }
ConsoleApp.Run(new Rows(TextBlock.FromMarkup("[bold]Deploy 4.12.2[/] [muted]· / finds text[/]"),
log));Run: dotnet run
dotnet new console -n AppParts
cd AppParts
dotnet add package ConsoleMonster(string Level, string Message)[] lines =
{
("info", "Deploy 4.12.2 started"),
("info", "web-01 drained"),
("warn", "web-01 health check timeout, retrying"),
("info", "web-01 updated"),
("warn", "web-02 health check timeout, retrying"),
("info", "web-02 updated"),
("pass", "Deploy complete")
};
LogViewport log = Ui.LogViewport("deploy.log").ShowElapsed(false).Height(8);
foreach ((string level, string message) in lines) { log.Write(message, level); }
ConsoleApp.Run(Ui.Rows().Content(rowsContent =>
{
rowsContent.Markup("[bold]Deploy 4.12.2[/] [muted]· / finds text[/]");
rowsContent.Add(log);
}));
Full program
using ConsoleMonster.Interactive;
using ConsoleMonster.Layout;
using ConsoleMonster.Widgets;
using ConsoleMonster;
(string Level, string Message)[] lines =
{
("info", "Deploy 4.12.2 started"),
("info", "web-01 drained"),
("warn", "web-01 health check timeout, retrying"),
("info", "web-01 updated"),
("warn", "web-02 health check timeout, retrying"),
("info", "web-02 updated"),
("pass", "Deploy complete")
};
LogViewport log = Ui.LogViewport("deploy.log").ShowElapsed(false).Height(8);
foreach ((string level, string message) in lines) { log.Write(message, level); }
ConsoleApp.Run(Ui.Rows().Content(rowsContent =>
{
rowsContent.Markup("[bold]Deploy 4.12.2[/] [muted]· / finds text[/]");
rowsContent.Add(log);
}));
Run: dotnet run
Deploy 4.12.2 · / finds text ╭─ › deploy.log ─────────────────────────────────────────────────────╮ │ Deploy 4.12.2 started │ │ web-01 drained │ │ ▲ WARN web-01 health check timeout, retrying │ │ web-01 updated │ │ ▲ WARN web-02 health check timeout, retrying │ │ web-02 updated │ │ ✓ OK Deploy complete │ │ │ Find: timeout 2 of 2 enter next shift+enter previous esc close tab next
Deploy 4.12.2 · / finds text ╭─ › deploy.log ─────────────────────────────────────────────────────╮ │ Deploy 4.12.2 started │ │ web-01 drained │ │ ▲ WARN web-01 health check timeout, retrying │ │ web-01 updated │ │ ▲ WARN web-02 health check timeout, retrying │ │ web-02 updated │ │ ✓ OK Deploy complete │ │ │ Find: timeout 2 of 2 enter next shift+enter previous esc close tab next
How each part works
Splitter
Two panes side by side, or top and bottom with -Vertical (Vertical = true). Drag the divider, or press Ctrl+Shift+Left/Right while an input in either pane has focus. With a Key, app state holds the first pane's share, so a screen keeps its ratio when a binding rebuilds it. The frame is after drag:24,2,36,2.
New-ConsoleSplitter -Key Panes -Ratio 0.3 -First {
New-ConsolePanel -Title Servers -Content { New-ConsoleSelect -Key Server -Choices WEB01, SQL01, FS01 }
} -Second {
New-ConsolePanel -Title Notes -Content { 'WEB01 serves the public site.' }
}
Accordion and Collapsible
A Collapsible is a section with a header row; Enter, Space or a click folds it, and its value is whether it is open. While it is folded the header shows its Summary, and the inputs inside leave the focus order. An Accordion keeps one section open, and + and - open or fold them all. The frame is after Tab and Enter: Parameters opens and Synopsis folds.
New-ConsoleAccordion -Content {
New-ConsoleCollapsible -Key Synopsis -Expanded -Content { 'Lists servers and their roles.' }
New-ConsoleCollapsible -Key Parameters -Summary '(2)' -Content { '-Name server name or wildcard' }
}
Outline and Breadcrumb
An Outline lists entries with levels; Enter or a click makes an entry current, and an entry with a Target opens that control. A Breadcrumb given the outline shows where you are, after its Ancestors. The frame is after Down, Down, Enter: Linux is current and the breadcrumb reads Guide › Install › Linux.
$sections = New-ConsoleOutline -Key Sections -SelectedIndex 0 -Items @(
'Install'
@{ Title = 'Linux'; Level = 2 }
)
New-ConsoleBreadcrumb -Key Where -Ancestors 'Guide' -Outline $sections
Tabs
Pages under a strip of titles. Left/Right or Ctrl+PageDown/PageUp change page, a number key opens that page, and a click on a title opens it. Each page keeps its own state and focus while another page is showing, and the value of Key is the active page's title. The frame is after a click on Disks.
New-ConsoleTabs -Key Page -Content {
@{ Title = 'Overview'; Content = { 'SQL01 · Database · Warsaw' } }
@{ Title = 'Disks'; Content = { 'C: 41% used'; 'D: 97% used' } }
}
ChartView with LinkKey
ChartView puts a line chart in an app as a focusable control: Left/Right or a click moves the crosshair and the row above the plot reads every series there, a number key or a click on the legend hides a series, +/- or the wheel zoom and Home shows the whole range. Give a chart and a DataGrid the same LinkKey and moving one moves the other: the state key holds the position of the selected point or row. The frame is after Down four times in the grid; the crosshair followed to 09:20.
New-ConsoleDataGrid -Key Samples -InputObject $samples -LinkKey Sample
New-ConsoleChartView -Key Cpu -Chart $chart -PointLabels $times -LinkKey Sample -Height 9
TreeSelect
An object tree inside an app, with the explorer's budgets and safe member access. Right expands and Left folds, typing filters the open branches, and Enter or a double-click opens a row as its own tree; Backspace goes back. Its value is the selected node, whose Path the line under the tree prints. The frame is after Right, Down×4, Right, sql, a double-click on SQL11 and Down twice. The interactive page plays the whole run.
New-ConsoleTreeSelect -Key Tree -Label Servers -InputObject $estate -RootPath '$estate' -Height 10
New-ConsoleBinding -Content {
param($State)
New-ConsoleText -Text "Selected: $($State.Tree.Path)"
}
The find bar
LogViewport, a multiline text input and the document pager have a find bar. Press / in a log or the pager, or Ctrl+F in a text input where / is typing, then type: every match is highlighted and the bar counts them. Enter and Shift+Enter move to the next and previous match and Escape closes the bar; in a log or the pager the matches stay highlighted, so n and N keep moving, and a second Escape clears them. A query with a capital letter matches case. The frame is after /, timeout and Enter.
Show-ConsoleApp -Content {
New-ConsoleLogRegion -WidgetId deploy.log -NoElapsed -Height 8 -Content { $lines }
}
Inside Show-ConsoleApp content, New-ConsoleLogRegion makes the retained, scrollable log (LogViewport in C#, Ui.LogViewport() in fluent C#), and Write-ConsoleLogEntry -WidgetId deploy.log adds lines while the app runs. Watch-ConsoleLog and a multiline text input (New-ConsoleTextInput -Multiline) have the same find bar.
Variations
- Put any part inside another: a splitter of an outline and an accordion is the incident workbench's runbook, and tabs hold whole screens.
- Pin a value at start:
-Ratio,-Expanded,-SelectedIndexand-Value(or the C# properties of the same names) set the opening state. - Read the parts from code: each
Keyis a field of$Statein a binding or callback, so a line can follow the selected tab, outline entry or tree node. - Turn the mouse off for one app with
Show-ConsoleApp -NoMouse; every pointer action still has its key.
Put the parts together in the incident workbench, chart and table, help sections and settings tabs demos. The log tail demo uses the text input's find bar after a live run.
Next
- Forms and dialogs: ask several questions at once and confirm before acting.
- Help, errors and progress: PowerShell help, errors and progress drawn by ConsoleMonster.
- Test, record and drive apps: run any of these parts on a headless terminal.