PowerControl is a PowerShell 7 / WPF desktop shell that turns small XML-defined building blocks into a working tool collection: menus that produce rows, actions that work on the selected rows, background jobs that never block the window, and everything you need around it (filtering, export, import, comparison, scheduling, favorites, bundles).
New menus and actions are created inside PowerControl itself: Customization > Create a new element opens the
element editor (script with syntax check, icon picker, key binding, workflow, children), saves the element into an
engine file and it shows up right away. Anyone who can write a few lines of PowerShell is done in minutes. The
XML files in engine\ can also be edited by hand, and Python or Node.js scripts plug in the same way. No build
step, no module installation.
Deutsche Fassung: README.de.md
| Component | Version |
|---|---|
| Windows | 10 / 11 (the shell is WPF; the engine itself is portable, see -Headless) |
| PowerShell | 7.6 or later (x64; the Fluent theme needs .NET 10 WPF) |
| Optional | Python 3 and/or Node.js 18+ for elements that call Invoke-PowerControlExternal |
The code editor (AvalonEdit) ships in dll\.
git clone <this repository>
cd PowerControl
pwsh -NoProfile -ExecutionPolicy Bypass -File .\PowerControl.ps1The first start creates ini\settings.json, temp\ (element index, icon cache) and Logs\ with defaults:
theme follows Windows, navigation and actions collapsed to icon rails, DebugLevel 2.
Useful parameters:
.\PowerControl.ps1 -Theme Dark # Light | Dark | System | Settings
.\PowerControl.ps1 -Run 'Recently changed' # run an element right after start
.\PowerControl.ps1 -Headless -Run 'Import clipboard' # no window, rows go to the pipeline
.\PowerControl.ps1 -Headless -Run 'List all PCtrl Elements' -OutFile out.json # .json | .csv | .xml | .txt
.\PowerControl.ps1 -DebugLevel 5 # verbose logs in Logs\Every file in engine\*.xml is a list of PCtrl elements. The element type decides where it shows up:
| Type | Shows up as | Script receives | Script returns |
|---|---|---|---|
Menu |
navigation rail / tree, start page, palette | nothing (or a dialog) | rows via $Script:PowerControlItem_Output |
MenuBar |
entry of the top menu (View, Customization, ...) |
nothing (or a dialog) | rows |
Action |
actions panel, offered for the rows that match its DisplayCondition |
$script:SelectedEntries |
rows (become the output of the job) |
Thread |
job list context menu (tabs, breadcrumbs); Toolbar="Jobs" puts it on the jobs bar |
the selected jobs | nothing (works on the job objects) |
Command |
shortcuts of the shell (Ctrl+K, Ctrl+Tab, ...) | nothing | nothing, usually Invoke-PowerControlUICommand |
OutputGridHeaderMenu |
right click on a column header | the column | nothing |
EditTreeView |
the element editor | the element | nothing |
A minimal menu:
<PCtrl type="Menu" Label="Services" ID="Services-0001" Icon="lucide\server.svg" ToolTip="Running services">
<Script><![CDATA[
$Script:PowerControlItem_Output += Get-Service | Where-Object Status -eq Running |
ForEach-Object { [PSCustomObject]@{ Name = $_.Name; DisplayName = $_.DisplayName } }
]]></Script>
</PCtrl>An action that applies to those rows (the DisplayCondition is evaluated against each selected row as $_):
<PCtrl type="Action" Label="Restart service" ID="RestartService-0001" Icon="lucide\refresh-cw.svg">
<DisplayCondition Categorie="Services" SortOrder="1"><![CDATA[ $null -ne $_.Name -and $null -ne $_.DisplayName ]]></DisplayCondition>
<KeyBinding Key="R" Modifiers="Ctrl+Shift" />
<Script><![CDATA[
foreach ($row in @($script:SelectedEntries)) { Restart-Service -Name $row.Name; $Script:PowerControlItem_Output += [PSCustomObject]@{ Name = $row.Name; Result = 'restarted' } }
]]></Script>
</PCtrl>More attributes and child nodes: Children (comma separated IDs for groups and sub menus), PrevAction /
NextAction with Priority (chains of elements, see "Workflows" below), ApartmentState="STA" (dialogs,
clipboard), KeyBinding, Toolbar="Jobs". Every script runs in its own runspace, so a hanging script never
freezes the window; the job list shows it and Stop ends it.
Dialogs are described as hashtables and drawn by the built-in dialog builder, see the next section.
Other languages: Invoke-PowerControlExternal -Language Python|Node -InputObject $rows -Code '...' sends the rows
as JSON to stdin and turns every JSON line on stdout into a row. Interpreter paths are detected automatically or
set in View > Settings.
Most elements need a few inputs before they can work: a file, a delimiter, a choice, a flag. Writing XAML for that
every time is the part nobody wants to do, so PowerControl ships a dialog builder. You describe the dialog as a
hashtable, call Show-PCtrlGUI, and get the values back as $GUIResults. The builder draws the window in the
current theme, wires validation, default button, Enter/Esc, file pickers and icon previews.
The builder lives in the action Draw GUI (engine\main.xml). An element that wants a dialog declares it as
PrevAction; the chain then runs STA, so your element needs no further attribute:
<PCtrl type="Menu" Label="Import CSV file" ID="ImportCsv-0001" Icon="lucide\file-spreadsheet.svg">
<PrevAction ID="DrawGUI-e7670e8344194a828558f726" Priority="100" />
<Script><![CDATA[
$GUItable = @{
'window' = @{ 'title' = 'Import CSV'; 'description' = 'The first line of the file provides the column names.' }
'dialog' = @{
0 = @{ 'Label' = 'File'; 'OpenFile' = @{ 'Name' = 'Path'; 'Filter' = 'CSV (*.csv;*.txt)|*.csv;*.txt|All files (*.*)|*.*' } }
1 = @{ 'Label' = 'Delimiter'; 'ComboBox' = @{ 'Name' = 'Delimiter'; 'IsEditable' = $true; 'Text' = ';'; 'Elements' = @(';', ',', 'Tab', '|') } }
2 = @{ 'CheckBox' = @{ 'Name' = 'Trim'; 'Text' = 'Trim values'; 'IsChecked' = $true } }
}
'actionButtons' = @{ 0 = @{ 'Label' = '_Import'; 'Name' = 'Import'; 'isDefault' = $true } }
'OnResult' = {
$path = ([string]$GUIResults.Path).Trim('"', ' ')
$delimiter = if ($GUIResults.Delimiter -is [string]) { $GUIResults.Delimiter } else { [string]$GUIResults.Delimiter.Content }
if ($delimiter -eq 'Tab') { $delimiter = "`t" }
foreach ($row in Import-Csv -LiteralPath $path -Delimiter $delimiter[0]) { $Script:PowerControlItem_Output += $row }
}
}
Show-PCtrlGUI -GUItable $GUItable
]]></Script>
</PCtrl>The hashtable has four parts:
| Key | Content |
|---|---|
window |
title, optional description (text under the title bar), Width / MinWidth |
dialog |
the rows, numbered from 0; each row is @{ 'Label' = '...'; '<Control>' = @{ 'Name' = '...'; ... } }. The label becomes the left column, the control the right one. Label is optional. |
actionButtons |
numbered buttons with Label (_ marks the access key), Name, isDefault. A button without Code validates the fields and closes the dialog with "OK"; Code = { ... } runs your own script block instead (set $DrawGuiWindow.DialogResult = $false to cancel). |
OnResult |
script block that runs after "OK" with the values in $GUIResults, keyed by the Name of each control. It runs inside the element's job, so $Script:PowerControlItem_Output, $script:SelectedEntries and all engine functions are available. |
| Control | Options | Value in $GUIResults |
|---|---|---|
TextBox |
Text, MultiLine, Width, Height, IsEnabled, ToolTip, Focus, check |
string |
PasswordBox |
ToolTip |
SecureString |
CheckBox |
Text (the caption), IsChecked, ToolTip |
$true / $false |
ComboBox |
Elements, Text (preselected), IsEditable, IconPreview, Width, ToolTip, check |
the selected item; read it as a string with $GUIResults.X.Content (or $GUIResults.X when the user typed free text) |
OpenFile |
Text, Filter (Windows file dialog filter), InitialDirectory |
path as string |
ListBox |
Items, Text (preselected entries), SelectionMode, ToolTip |
the selected items |
RadioButton |
Text (the caption) |
the caption of the checked button |
CodeEditor |
text, Syntax (default PowerShell), MinWidth, MinHeight, check |
string; syntax colored editor (AvalonEdit) |
DatePicker, Slider, ToggleButton, RichTextBox |
WPF defaults | date, number, bool, text |
DataGrid |
Columns (each Type = TextColumn, CheckBoxColumn or ComboBoxColumn with Header, Binding, Width, Items), Rows, AddRows, IsReadOnly, Filter (text box that narrows the choice lists) |
the rows as objects |
GroupBox, Expander, TabControl, StackPanel |
Header / Tabs (@{ Header; Elements }) and Elements: nested rows in the same syntax |
the nested controls report under their own names |
TextBlock, Separator, Image, ProgressBar, Button |
static or decorative | none |
Validation: 'check' = { $sender.Text -match '^[0-6]$' } runs when the field loses focus and again on "OK". A
failing field gets a red border and the dialog stays open and names it. Focus = $true puts the cursor into that
field. Events = @{ TextChanged = '...' } attaches WPF events; the handler text is compiled into a script block.
ComboBoxvalues arrive asComboBoxItem; the helper pattern in the examples ($asText) turns both the item and free text into a plain string.- Do not name variables you read inside
OnResultlike the builder's own loop variables ($item,$element,$control):OnResultruns inside the builder and they would be shadowed. - Larger dialogs:
TabControlwith one tab per topic (the element editor does this),GroupBoxfor related fields,DataGridwithComboBoxColumnfor lists of choices. - Test a dialog without running the real work: select the element in
List all PCtrl Elementsand useDry run, orSave + Dry runin the editor. The values the dialog would deliver appear as rows. - Everything also works outside PowerControl:
tools\Test-DrawGUI.ps1is the builder as a standalone script (generated from main.xml), handy for designing a dialog in a plain console before pasting it into an element. - Complete real examples:
Settings...,Compare job outputs...,Export settings...and the element editor inengine\main.xml, all import menus inengine\ImportData.xml.
An element rarely does everything itself. It declares other elements as PrevAction (run before the element's
own script) or NextAction (run after it), each with a Priority. The element itself does not even need a
script: it can be nothing more than the sum of the steps it chains. That is how PowerControl stays modular:
small actions such as "ask for a user", "fetch the password from the vault", "start PuTTY" are written once and
reused by every element that needs them. The dialog builder is the same mechanism, Draw GUI is simply a
PrevAction.
- All
PrevActionelements, highestPriorityfirst. - The element's own
Script. - All
NextActionelements, highestPriorityfirst.
Chains nest: a PrevAction can have its own PrevAction/NextAction entries (Draw GUI pulls in
Convert-XAMLtoWindow and Show-WPFWindow this way). Cycles are detected and skipped with a warning. If any
member carries ApartmentState="STA", the whole chain runs STA.
The whole chain runs as one job in one runspace. Everything a step leaves behind is available to the next steps:
$script:variables ($script:PasswordFilter = '...'in a dialog step, read by the credential step)- functions declared as
function script:Name { ... }(this is howShow-PCtrlGUIreaches your element) $GUIResultsafter a dialog,$script:SelectedEntries(the selected rows),$Script:PowerControlItem_Output(the rows the job returns; every step may add to it)
Helper actions have no DisplayCondition and no KeyBinding. They never show up in the actions panel, the
palette or the start page; they exist only to be chained. The core ships Draw GUI, History helper,
Convert-XAMLtoWindow and Show-WPFWindow this way.
An example: one action opens SSH with the stored credentials, the other asks for a user first. Both reuse the same helper actions, only the position and the order differ:
<PCtrl type="Action" Label="open SSH" ID="OpenSSH-0001" Icon="lucide\terminal.svg">
<KeyBinding Key="P" Modifiers="Ctrl"/>
<DisplayCondition Categorie="Machine Control" SortOrder="3"><![CDATA[ $_.Protocol -eq 'SSH2' ]]></DisplayCondition>
<PrevAction ID="CollectActions-0001" Priority="999"/> <!-- 1. log who did what on which machine (audit trail) -->
<PrevAction ID="GetCredentialFromVault-0001" Priority="100"/> <!-- 2. password from the credential vault -->
<NextAction ID="ProgramStartPutty-0001" Priority="90"/> <!-- 4. start PuTTY with host + credentials -->
</PCtrl> <!-- 3. = the element itself, it has no script -->
<PCtrl type="Action" Label="open SSH as ..." ID="OpenSSHAs-0001" Icon="lucide\terminal.svg">
<KeyBinding Key="P" Modifiers="Ctrl+Alt"/>
<DisplayCondition><![CDATA[ $_.Protocol -eq 'SSH2' ]]></DisplayCondition>
<PrevAction ID="CollectActions-0001" Priority="999"/>
<PrevAction ID="DrawGUI-e7670e8344194a828558f726" Priority="100"/> <!-- the dialog builder -->
<NextAction ID="GetCredentialFromVault-0001" Priority="100"/> <!-- credentials only after the dialog -->
<NextAction ID="ProgramStartPutty-0001" Priority="50"/>
<Script><![CDATA[
# asks for user name and password source; the credential step reads $GUIResults, PuTTY reads the result
Show-PCtrlGUI -GUItable $GUItable
]]></Script>
</PCtrl>The credential and PuTTY actions know nothing about SSH dialogs; they read $GUIResults or $script: variables
if present and fall back to the selected row otherwise. New protocols (RDP, WinRM, a web console) are then one
small element each.
DisplayConditiondecides where an action appears. Its script runs for every row with the row as$_; the action is offered for the rows where it returns$true.Categoriegroups the actions in the panel and the palette,SortOrderorders them inside the group. A condition may also look at the context, e.g.$PowerControl.ActiveMenu.ID -eq '...'for actions that belong to one menu only. NoDisplayConditionat all means helper element, never shown.Childrenexist only onMenuandMenuBarelements: a comma separated list of IDs that become the entries of a navigation group or of a menu bar submenu. The IDs may come from any engine file; the editor'sChildrentab offers all elements of the same type.KeyBindingis per element, and the same key may serve several actions:Con "connect SSH" and on "connect RDP" does the right thing for the selected row. If several actions of one row share a key, the lowestSortOrderwins, the same rule that picks the default action for Enter and double click.- Everything is reachable without a mouse: menus and commands through their bindings and the palette (Ctrl+K),
actions through their bindings, Enter for the default action,
efor the editor on element rows.
- Element editor, tab
Workflow: a table of thePrevAction/NextActionentries with priority and target, filter box over the element list. display action timelineon a row ofList all PCtrl Elements: the complete chain in execution order, with priority, owner, apartment state and whether the step has a script.Check engine: danglingPrevAction/NextActionreferences, duplicate IDs.Dry run: runs the dialog of a chain and reports its values without executing the real work.tools\Get-ActionTimeline.ps1: the same timeline outside PowerControl. It readsengine\*.xmldirectly and prints the chain in execution order, as text or Markdown (-Markdown), handy for long workflows and docs:
pwsh -NoProfile -File .\tools\Get-ActionTimeline.ps1 -Element 'open SSH as ...'
pwsh -NoProfile -File .\tools\Get-ActionTimeline.ps1 -Element 'Settings...' -Markdown > workflow.md- Light / dark / Windows theme (Fluent), collapsed icon rails with hover popups, pinnable panels
- Output tabs: one per pinned job, each with its own filter, selection and action breadcrumb; Ctrl+Tab cycles
- Command palette (Ctrl+K), filter box over the grid (Ctrl+Shift+F), column profiles per menu, start page
- Jobs bar: running / finished / pinned, right click menu (pin, stop, rerun, repeat, compare, favorite, ...)
- Repeat a job every N seconds or minutes, with change notification and a history of previous outputs
- Compare the outputs of two jobs or of two runs of the same job: as a table with
old → newcells or as a line diff; colored HTML export - Favorites for menus and actions (star in the popups and trees), tray icon with tabs and repeating jobs, configurable global hotkey, window title follows the shown job
- Error banner for script errors, finished-job notification,
Logs\with configurable detail - Import: CSV, JSON, CliXML, clipboard (tab separated, CSV, JSON, Markdown tables), REST, SQL, folder contents
- Export: clipboard, TXT, CSV, HTML (sortable), Markdown, JSON, CliXML
Output grid with the dark theme, a pinned tab, the navigation rail on the left and the actions rail on the right:
Navigation popup of a group and the actions popup of a category (hover over the rail icons):
Command palette (Ctrl+K) and the jobs bar (hover over the bottom bar):
"Compare with..." from the job menu and the result as a table (changed cells as old → new):
Settings dialog and element editor, both described as hashtables and drawn by the dialog builder:
Customization in the top menu:
List all PCtrl Elements/KeyBindings: every element with its file, condition and shortcut; select a row and presseto edit it (script with syntax check, icon picker, key binding, workflow, children);Duplicate,Move to file,Dry run(runs the dialog without the real work),Export elements...Create a new elementImport elements...andFetch bundles: bundles are plain engine files; subscriptions (folders,.xmlURLs or GitHub repository / folder links inView > Settings) are merged intoengine\Subscribed.xmlat every startRecently changed: who changed which element when;Show diffandRestore versionuse the history inini\history\Check engine: parse errors, duplicate IDs, dangling references, missing iconsExport settings.../Import settings...: favorites, column profiles, bundle sources, hotkey, ...
This repository ships the core only: the shell, the engine and the three core engine files. Ready-made packages for specific tasks (machine management, protocol connections, reporting, ...) will be published as separate repositories. They are plain engine files, so they plug in like any other bundle:
View > Settings > Bundle sources: the repository or folder URL, fetched at every start intoengine\Subscribed.xml- or
Customization > Import elements...for a one-time import of a downloaded file
A list of available packages will appear here once the first ones are published.
PowerControl.ps1 bootstrap: parameters, platform, $PowerControl object, dot-sources src\
src\01..07-Core.* engine: index, execution, runspaces, jobs, output, sequences, headless
src\10..17-UI.* Windows shell (WPF)
engine\ your elements: main.xml (shell, editor, bundles), ExportData.xml, ImportData.xml
icons\lucide\ Lucide icons (ISC license), referenced as Icon="lucide\name.svg"
dll\ AvalonEdit code editor (6.3.1.120)
images\ screenshots for this README
tools\ Test-EngineXml.ps1 (validator), Get-ActionTimeline.ps1 (workflow printer), Export-DrawGuiStandalone.ps1,
Test-DrawGUI.ps1 (dialog playground)
tests\ Test-CoreFunctions.ps1 (89 checks, run with pwsh -NoProfile -File)
ini\, temp\, Logs\, Export\ created at runtime, not part of the repository
pwsh -NoProfile -File .\tests\Test-CoreFunctions.ps1 # engine, settings, jobs, bundles, history, headless actions
pwsh -NoProfile -File .\tools\Test-EngineXml.ps1 # validates engine\*.xml (exit code = number of errors)
pwsh -NoProfile -File .\tools\Export-DrawGuiStandalone.ps1 # regenerates tools\Test-DrawGUI.ps1 after editing the dialog builder
pwsh -NoProfile -File .\tools\Get-ActionTimeline.ps1 -Element 'Edit element' # workflow of an element, no shell neededConventions: functions Verb-PowerControlNoun, four spaces, no hard-coded shortcuts in the shell (they are
Command elements with KeyBinding), every new core function that engine scripts may call goes into the
$functions list of Invoke-ActionScriptThreading and into the $wanted list of the tests.
Apache License 2.0, see LICENSE. Lucide icons: ISC license. AvalonEdit: MIT license.









