Powershell GUI fronted (WPF) to run categorized console scripts

⌈⌋ branch:  ClickyColoury


Check-in [06df8ad9bd]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Derived from current base version 0.8.0; some sample and generic scripts added.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: 06df8ad9bd4a649fdd91da4fd6dba04b33996856
User & Date: mario 2017-09-22 23:34:42
Context
2017-09-22
23:40
Minor text fixes. check-in: 15d9b1576a user: mario tags: trunk
23:34
Derived from current base version 0.8.0; some sample and generic scripts added. check-in: 06df8ad9bd user: mario tags: trunk
22:52
initial empty check-in check-in: 7e2f1fd24a user: mario tags: trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Added ClickyColoury.cmd.





>
>
1
2
@echo off
powershell.exe -Version 2.0 -STA -WindowStyle Hidden -File modules/starter.ps1

Added TextyTypey.cmd.







>
>
>
1
2
3
@echo off
rem -- This runs in PS5.0 already
powershell.exe -STA -File modules/starter.ps1 -CLI

Added UserTools/reddit.url.





>
>
1
2
[InternetShortcut]
URL=http://reddit.com/

Added data/UserTools.description.csv.





>
>
1
2
file,icon,desc
reddit.url,cmd.png,Reddit homepage

Added data/combobox.automap.txt.





>
>
1
2
$true
$false

Added data/combobox.exchangeserver.txt.





>
>
1
2
YOURSMTP01
YOURSMTP03

Added data/combobox.mailperm.txt.









































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FullAccess
Send-as
ExternalAccount
DeleteItem
ReadPermission
ChangePermission
ChangeOwner
###--for-rooms--###
Owner
Author
Contributor
PublishingEditor
PublishingAuthor
Reviewer
None
CreateItems
CreateSubfolders
EditAllItems
FolderContact
FolderOwner

Added data/combobox.minicmdlets.txt.







>
>
>
1
2
3
Get-ADDomain
Get-ADDefaultDomainPasswordPolicy
[windows.forms.clipboard]::GetDataObject().getData("HTML Format")

Added data/servers.sample.csv.







>
>
>
1
2
3
Hostname,Owner,Description
localhost,Emily Examplary,well it's your host
server1,Emily Examplary,some server perhaps

Added dev/create-psd1.ps1.





















































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# api: ps
# type: cli
# title: create .psd1
# description: module manifest from script meta header
# version: 0.1
#
# Creates a .psd1 file for a .psm1/.ps1

[CmdletBinding()]
Param($fn=(ReadHost ".psm1 filename:"));

$target = $fn -replace "\.\w+$",".psd1"
$base = $fn -replace "^.+[\\\\/]",""

# plugin meta block
$src = (Get-Content $fn | Out-String)
$meta = @{}
$src | Select-String "(?m)^#\s*(\w+):\s*([^\n]+)" -AllMatches |
     % { $_.Matches } | % { ,($_.Groups | % { $_.Value }) } |
     % { $meta[$_[1]] = $_[2] }

# create .psd1 file
$psd = @{
    Path = $target
    RootModule = $base
    ModuleToProcess = $base
    Author = $meta.author
    CompanyName = "-"
    Description = $meta.description
    ModuleVersion = $meta.version
    #ProjectUri = "$meta.url"
    Guid = [guid]::newguid()
    PowerShellVersion = "2.0"
    #FormatsToProcess = "$fn.ps1xml"
    ProcessorArchitecture = 'amd64'
    FunctionsToExport = "*"
    AliasesToExport = "*"
    VariablesToExport = ""
    CmdletsToExport = ""
    PassThru = $true
}
New-ModuleManifest @psd | Out-File $target -Encoding UTF8

Added img/back.png.

cannot compute difference between binary files

Added img/clear.png.

cannot compute difference between binary files

Added img/clickycoloury.ico.

cannot compute difference between binary files

Added img/clipboard.png.

cannot compute difference between binary files

Added img/computer.png.

cannot compute difference between binary files

Added img/copy.png.

cannot compute difference between binary files

Added img/csv.png.

cannot compute difference between binary files

Added img/ethernet.png.

cannot compute difference between binary files

Added img/html.png.

cannot compute difference between binary files

Added img/icon.beta.png.

cannot compute difference between binary files

Added img/icon.cloud.png.

cannot compute difference between binary files

Added img/icon.cmd.png.

cannot compute difference between binary files

Added img/icon.controller.png.

cannot compute difference between binary files

Added img/icon.copy.png.

cannot compute difference between binary files

Added img/icon.date.png.

cannot compute difference between binary files

Added img/icon.events.png.

cannot compute difference between binary files

Added img/icon.exchange.png.

cannot compute difference between binary files

Added img/icon.extras.png.

cannot compute difference between binary files

Added img/icon.finduser.png.

cannot compute difference between binary files

Added img/icon.fire.png.

cannot compute difference between binary files

Added img/icon.firewall.png.

cannot compute difference between binary files

Added img/icon.folder.png.

cannot compute difference between binary files

Added img/icon.godzilla.png.

cannot compute difference between binary files

Added img/icon.info.png.

cannot compute difference between binary files

Added img/icon.install.png.

cannot compute difference between binary files

Added img/icon.key.png.

cannot compute difference between binary files

Added img/icon.license.png.

cannot compute difference between binary files

Added img/icon.lnk.png.

cannot compute difference between binary files

Added img/icon.log.png.

cannot compute difference between binary files

Added img/icon.mfcmapi.png.

cannot compute difference between binary files

Added img/icon.network.png.

cannot compute difference between binary files

Added img/icon.notes.png.

cannot compute difference between binary files

Added img/icon.office.png.

cannot compute difference between binary files

Added img/icon.powershell.png.

cannot compute difference between binary files

Added img/icon.printer.png.

cannot compute difference between binary files

Added img/icon.registry.png.

cannot compute difference between binary files

Added img/icon.samba.png.

cannot compute difference between binary files

Added img/icon.server.png.

cannot compute difference between binary files

Added img/icon.tools.png.

cannot compute difference between binary files

Added img/icon.unlock.png.

cannot compute difference between binary files

Added img/icon.users.png.

cannot compute difference between binary files

Added img/icon.wmi.png.

cannot compute difference between binary files

Added img/info.extras.png.

cannot compute difference between binary files

Added img/ping.png.

cannot compute difference between binary files

Added img/sweep.png.

cannot compute difference between binary files

Added img/to-user.png.

cannot compute difference between binary files

Added img/user.png.

cannot compute difference between binary files

Added modules/clipboard.psm1.

























































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# api: ps
# type: functions
# title: clipboard
# description: Reads/writes to clipboard
# version: 0.8
# status: stable
# category: win32
# config: -
#
# Provides clipboard read and write shortcuts, with an HTML formatter.
#

# syslib
Add-Type -AN System.Windows.Forms


#-- Send HTML formatted content to clipboard
function Set-ClipboardHtml($html) {
  #-- escape non-ASCII characters first
  $html = [regex]::replace($html, "([^\x01-\x7F])", {
      "&#" + ([int][char][string]$args[0]) + ";"
  })
  #-- wrap with HTML Format header
  $len = $html.length;  # section sizes
  $pfx = 190; $start = 165; $end = 35;
  $data = @"
Version:1.0
StartHTML:$(($pfx).toString().PadLeft(9, '0'))
EndHTML:$(($pfx+$start+$len+$end).toString().PadLeft(9, '0'))
StartFragment:$(($pfx+$start).toString().PadLeft(9, '0'))
EndFragment:$(($pfx+$start+$len).toString().PadLeft(9, '0'))
StartSelection:$(($pfx+$start).toString().PadLeft(9, '0'))
EndSelection:$(($pfx+$start+$len).toString().PadLeft(9, '0'))
SourceURL:http://localhost/#cmd-table
   <!DOCTYPE html><HTML> 
   <HEAD><TITLE>Clibboard</TITLE><META HTTP-EQUIV='Content-Type' CONTENT='text/html; charset=UTF-8'></HEAD>
   <BODY><!--Start-->      
$html
   <!--End--></BODY> </HTML>      
"@
  #-- send to system clipboard
  [System.Windows.Forms.Clipboard]::setData([System.Windows.Forms.Dataformats]::Html, $data)
}

#-- Write clipboard (text/plain)
function Set-Clipboard($text) {
    if ($text.length) {
        [void] [System.Windows.Forms.Clipboard]::setText($text)
    }
}

#-- Return current content (text/plain)
function Get-Clipboard() {
    return [System.Windows.Forms.Clipboard]::getText().trim()
}

<#
 #@tests
 return [System.Windows.Forms.Clipboard]::GetDataObject().getFormats()
#>

Added modules/menu.psm1.



















































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# api: ps
# type: functions
# title: Utility code
# description: Output and input shortcuts, screen setup, menu handling, meta extraction
# version: 0.8.4
# category: misc
# author: mario
# config:
#    { name: cfg.hidden, type: bool, value: 0, description: also show hidden menu entries in CLI version }
# status: beta
# priority: core
#
# A couple of utility funcs:
#  · Get-Machine        → Read-Host "machine"
#  · echo_n             → echo wo/ CRLF
#  · Edit-Config        → edit config file
#  · Extrac-PluginMeta  → tokenize comment fields
#  · preg_match_all     → PHP-esque regex matching
#  · Init-Screen        → print script summary
#  · Print-Menu         → output $menu 
#  · Print-MenuHelp     → show help= entries
#  · Process-Menu       → input/run $menu prompt
#
# Load with:
#  Import-Module. ".\modules\menu.psm1"


#-- get $machine
function Get-Machine() {
    [CmdletBinding()]
    Param($current = $global:machine)
    # Ask for '$machine'
    Write-Host -N -f Yellow '$machine'
    # Add (default/last)
    if ($current) {
        Write-Host -N -f Gray "($current)"
    }
    Write-Host -N -f Yellow ': '
    $new = (Read-Host).trim().toUpper()
    if (!$new) {
        return $current
    }
    # Ping test
    if (!(Test-Connection -ComputerName $new -Count 1 -BufferSize 128 -Quiet -ErrorAction SilentlyContinue)) {
        Write-Host -f Red " → not online"
    }
    $global:machine = $new
    return $new
}

#-- opens notepad for editing a list/csv file in-place
function Get-NotepadCSV() {
    Param($text="", $EDITOR=$env:EDITOR)
    $tmpfn = [IO.Path]::GetTempFileName()
    $text | Out-File $tmpfn -Encoding UTF8
    [void](Start-Process $EDITOR $tmpfn -Wait)
    $text = Get-Content $tmpfn | Out-String
    [void](Remove-Item $tmpfn)
    return $text
}

#-- echo sans newline
function echo_n($str) {
    [void](Write-Host -NoNewLine $str)
}

#-- start notepad on config file (obsolete, now in separate script)
function Edit-Config() {
    Param($fn, $options, $overwrite=0, $CRLF="`r`n", $EDITOR=$ENV:EDITOR)
    if ($overwrite -or !(Test-Path $fn)) {
        # create parent dir
        $dir = Split-Path $fn -parent
        if ($dir -and !(Test-Path $dir)) { md -Force "$dir" }
        # assemble defaults
        $out = "# type: config$CRLF# fn: $fn$CRLF$CRLF"
        $options | % {
            $v = $_.value
            switch ($_.type) {
                bool { $v = @('$False', '$True')[[Int32]$v] }
                default { $v = "'$v'" }
            }
            $out += '$' + $_.name + " = " + $v + "; # $($_.description)$CRLF"
        }
        # write
        $out | Out-File $fn -Encoding UTF8
    }
    & $EDITOR "$fn"
}

#-- Regex/Select-String -Allmatches as nested array (convenience shortcut)
function preg_match_all() {
    Param($rx, $str)
    $str | Select-String $rx -AllMatches | % { $_.Matches } | % { ,($_.Groups | % { $_.Value }) }
}

#-- baseline plugin meta data support
function Extract-PluginMeta() {
    <#
      .SYNOPSIS
         Reads top-comment block and plugin meta data from given filename
      .DESCRIPTION
         Plugin meta data is a cross-language documentation scheme to manage
         application-level feature plugins. This function reads the leading
         comment and convers key:value entries into a hash. Also prepares the
         config{} parameter list.
      .PARAMERER fn
         Script to read from
      .OUTPUTS
         Returns a HashTable of field: values, including the config: list/hash.
      .EXAMPLE
         In ClickyColoury ir reads the "plugins" at once like this:
           $menu = (Get-Iten tools*/*.ps1 | % { Extract-PluginMeta $_ })
         Entries than can be accessed like:
           $menu | % { $_.title -and $_.category -eq "network" }
      .NOTES
         Each entry contains an .id basename and .fn field, additionaly to what
         the plugin itself defines.
         Packs comment remainder as .doc field.
    #>
    Param($fn, $meta=@{}, $cfg=@())

    # read file
    $str = Get-Content $fn | Out-String

    # look for first comment block
    if ($m = [regex]::match($str, '(?m)((?:^\s*[#]+.*$\n?)+)')) {

        # remove leading #␣ from lines, then split remainder comment
        $str = $m.groups[1] -replace "(?m)^\s*#[ \t]*", ""
        $str, $doc = [regex]::split($str, '\r?\n\r?\n')

        # find all `key:value` pairs
        preg_match_all -rx "(?m)^([\w-]+):\s*(.*(?:$)(?:\r?\n(?!\w+:).+$)*)" -str $str | % { $meta[$_[1]] = $_[2].trim() }

        # split out config: and crude-parse it (JSONish serialization)
        preg_match_all -rx "\{(.+?)\}" -str $meta.config | % { $r = @{};
            preg_match_all -rx "([\w.-]+)\s*[:=]\s*(?:[']?([^,;}]+)[']?)" -str $_[1] | % {  $r[$_[1]] = $_[2] }; $cfg += $r;
        }

        # merge into hashtable
        $meta.fn = "$fn"
        $meta.id = ($fn -replace "^.+[\\/]|\.\w+$","") -replace "[^\w]","_"
        $meta.doc = ($doc -join "`r`n")
        $meta.config = $cfg
    }
    return $meta  # or return as (New-Object PSCustomObject -Prop $meta)
}


#-- script header
function Init-Screen() {
    param($x=80,$y=45)
    #-- screen size better than `mode con` as it retains scrolling:
    if ($host.Name -match "Console") {
        $con = $host.UI.RawUI
        $buf = $con.BufferSize; $buf.height = 16*$y; $buf.width = $x; $con.BufferSize = $buf;
        $win = $con.WindowSize; $win.height =    $y; $win.width = $x; $con.WindowSize = $win;
    }
    #-- header
    $meta = $cfg.main
    Write-Host -b DarkBlue -f White ("  {0,-60} {1,15} " -f $meta.title, $meta.version)
    Write-Host -b DarkBlue -f Gray  ("  {0,-60} {1,15} " -f $meta.description, $meta.category)
    try { $host.UI.RawUI.WindowTitle = $meta.title } catch { }
}

#-- group plugin list by category, sort by sort: / key: / title:
function Sort-Menu($menu) {
    $usort_cat = { (@{cmd=1; powershell=2; onbehalf=3; exchange=4; empirum=5; network=6; info=7; wmi=8}[$_.category], $_.category) -ne $null }
    $usort_key = { if ($_.key -match "(\d+)") { [int]$matches[1] } else { $_.key } }
    return ($menu | Sort-Object $usort_cat, {$_.sort}, $usort_key, {$_.title})
}

#-- string cutting
function substr($str, $from, $to) {
    if ($to -lt $str.length) {
        $str.substring($from, $to)
    }
    else {
        $str
    }
}

#-- Write out menu list (sorted, 3 columns, with category headers)
function Print-Menu() {
    param($menu, $cat=".+", $last_cat="", $i=0)
    # group by category
    $ls = Sort-Menu ($menu | ? { $_.title -and $_.key -and ($_.category -match $cat) -and ((!$cfg.hidden) -or !$_.hidden) })
    $ls | % {
        if ($last_cat -ne $_.category) {
            if ($line) { Write-Host ""}
            Write-Host -f Black ("     {0,-74}" -f ($last_cat = $_.category))
            $i = 0
        }
        $line = (($i++) % 3 -ne 2)
        Write-Host -N -f Green ("{0,4}" -f (substr $_.key.split("|")[0] 0 4))
        Write-Host -N -f DarkRed  ("→")
        Write-Host -N:$line -f White ("{0,-21}" -f (substr $_.title 0 21))
    }
    echo ""
}

#-- print help= entries from $menu (→ not very pretty)
function Print-MenuHelp($menu) {
    $menu | ? { $_.title -and $_.key } | % {
        Write-Host -N -f Green (" " + $_.key.split("|")[0..2] -join ", ").PadRight(15)
        Write-Host -f White (" " + $_.title)
        Write-Host -f Gray ("                " + ($_.description))
    }
}

#-- Invoked on one menu entry → executes .command, or .func, or loads .fn script
filter Process-MenuTask() {
    Param($params)
    $host.UI.RawUI.WindowTitle = "MultiTool → $($_.title)"
    Write-Host -b DarkBlue -f Cyan ("{0,-60} {1,18}" -f $_.title, $_.version)
    echo ""
    #-- commands or function
    try {
        if ($_.command) {
            Invoke-Expression $_.command  # no options
        }
        elseif ($_.func) {
            Invoke-Expression "$($_.func) $($params)" # pass optional flags
        }
        #-- no fn?
        elseif (!$_.fn) {
            Write-Host -f Red "No processor for task:"
            $_ | FT | Write-Host
        }
        #-- start in separate "window"
        elseif ($_.type -eq "window") {
            Start-Process powershell.exe -ArgumentList "-STA -ExecutionPolicy ByPass -File $($_.fn) $global:machine"
        }
        #-- dot-source file simply (for e.g. "inline" and "cli" type)
        else {
            Invoke-Expression ". $($_.fn) $($params)"  # run script
        }
    }
    catch {
        Write-Host -b DarkRed -f White ($_ | Out-String)
        $Error.Clear()
    }
    $host.UI.RawUI.WindowTitle = "MultiTool"
}

#-- Promp/input loop (REPL)
function Process-Menu() {
    param($menu, $prompt="Func")

    #-- prompt+exec loop
    while ($True) {
        Write-Host -N -f Yellow "$prompt> "
        $which = (Read-Host).trim()
        if ($which -match "^([\w.-]+)(?:\b|\s|$)(.*)$") { $params = $matches[2] } else { $params = $null }

        # find according menu entry: run func or expression
        $menu  |  ? { $_.key -and $which -match "^($($_['key']))\b" }  |  % { 
            while ($true) {
                $_ | Process-MenuTask $params
                if ((!$_.repeat) -or ((Read-Host "Wiederholen?") -notmatch "^[jy1rw]")) { break; }
            }
        }
    }
}

Added modules/starter.ps1.

























































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# api: ps
# type: main
# title: ClickyColoury + TextyTypey - GUI+CLI tool frontend
# description: Convenience invocation of various Powershell and CMD scripts
# version: 0.8.0
# depends: menu, wpf, clipboard
# category: misc
# config:
#   { name: cfg.gridview, type: select, value: Format-Table, select: Format-Table|Out-GridView, description: default table display mode }
#   { name: cfg.cli, type: bool, value: 0, description: Start console (CLI) version per default? }
#   { name: cfg.cached, type: bool, value: 0, description: Use CLIXML script cache on startup }
#   { name: debug, type: bool, value: 0, description: Powershell-internal } 
# author: mario
# license: MITL
# priority: core
# status: testing
#
# Note that this is the WindowsPresentationForm version, but also renders a
# classic -CLI menu otherwise. Utilizes:
#
#   · wpf.psm1 = graphical toolkit features
#   · menu.psm1 = mostly CLI features
#   · clipboard.psm1 = for the HTML output
#
# Whereas scripts (and plugins) reside in tools*/*.ps1
#
# Starting up with `-cli` parameter should yield the text version.
#
# Only works with powershell.exe -Version 2.0 -STA -File ... at the moment.
#


#-- params
[CmdletBinding()]
Param(
    [switch]$CLI = $false
)

#-- config
$global:cfg = @{
    domain = "WORKWORKWORKWORKWORK"  # (Get-ADDomain).Netbiosname
    threading = 1
    autoclear = 300
    exchange = @{
        ConfigurationName = "Microsoft.Exchange"
        ConnectionUri = "http://YOUREXCHANGESRV/psremote/"
    }
    main = @{} 
    curr_script_fn = $MyInvocation.MyCommand.Path
    multitool_base = (Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path))
    user_config_fn  = "$env:APPDATA\multitool\config.ps1"
    user_plugins_d  = "$env:APPDATA\multitool"
    tools_cache_fn = "./data/tools.cache.clixml"
    tool_dirs = @("./tools/*/*.ps1")  # add custom dirs here!
}
$global:plugins = @{
    "init" = @()
    "before" = @()
    "after" = @()
    "menu" = @()
}
#-- load user config
cd ($cfg.multitool_base)
if (Test-Path ($cfg.user_config_fn)) {
    . ($cfg.user_config_fn)
}
$cfg|FL

#-- restart if not in single-thread-apartment mode
if (-not [System.Management.Automation.Runspaces.Runspace]::DefaultRunSpace.ApartmentState -eq "STA") {
#    powershell.exe -Version 2.0 -STA -ExecutionPolicy unrestricted -File $curr_script_fn
#    break 2
}

#-- modules
$global:GUI = [hashtable]::Synchronized(@{Host=$Host})
Import-Module -DisableNameChecking "$($cfg.multitool_base)\modules\wpf.psm1"
Import-Module -DisableNameChecking "$($cfg.multitool_base)\modules\menu.psm1"
Import-Module -DisableNameChecking "$($cfg.multitool_base)\modules\clipboard.psm1"
if (!(Get-Module -Name ActiveDirectory)) { Import-Module ActiveDirectory }

#-- post init
$cfg.main = (Extract-PluginMeta $cfg.curr_script_fn)
#$global:GUI|FL



#-- predefined menu entries
$menu = @(
)
#-- load cache?
if ($cfg.cached -and (Test-Path ($cfg.tools_cache_fn))) {
    $menu = Import-CliXml ($cfg.tools_cache_fn)
}
#-- add menu entries from scripts
else {
    $menu += @(Get-Item ($cfg.tool_dirs) | % { Extract-PluginMeta $_ } | ? { $_.api -and $_.type -and $_.title })
}
#-- add user plugins
if (Test-Path ($cfg.user_plugins_d)) {
    $menu += @(Get-Item "$($cfg.user_plugins_d)/*.ps1" | % { Extract-PluginMeta $_ } | ? { $_.type -and $_.api -eq "multitool" })
}
#-- run `type:init` plugins here
$menu | ? { $_.type -eq "init" -and $_.fn } | % { . $_.fn }


#-- CLI mode
if ($CLI) {
    Init-Screen
    $menu = $menu + @{key="m|menu"; category="extras"; title="print menu"; func='Print-Menu $menu'}
    Print-Menu $menu
    Process-Menu -Menu $menu -Prompt "TextyTypey"
}
#-- WPF multi-tool
else {
    echo "Starting GUI version..."
    $shell = Start-Win (Sort-Menu $menu)
}

#-- cleanup
if ($Debug) {
    Remove-Module wpf
    $Error
}

Added modules/tasksched.psm1.



























































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# api: ps
# type: init
# category: function
# title: Start-Sched
# description: runs a GUI app via task scheduler (once)
# version: 0.1
# src: https://msdn.microsoft.com/en-us/library/aa389389(v=vs.85).aspx
#
# Starts a GUI task via hidden task scheduler ("at"),
# with a delay of 3 seconds to make up for network latency
# and remote systime deviations.


$command = "cmd.exe"

# time = [datetime]::parse(GWMI -Class Win32_LocalTime -ComputerName $machine)
$time = (Get-Date).AddSeconds(3)
$args = @(
   $command, # cmd
   $time,    # current +3s
   $False,   # no repeat
   $Null,    # days of week
   $Null,    # days of month
   $True     # interact with desktop
)

# [wmiclass]"\\$machine\ROOT\CIMV2:Win32_ScheduledJob"
Invoke-WmiMethod -class "Win32_ScheduledJob" -name "Create" -ArgumentList $args -ComputerName $machine

Added modules/wpf.psm1.











































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
# api: ps
# title: WPF + WinForms
# description: WinForm and WPF shortcuts, GUI and thread handling
# depends: menu, clipboard, sys:presentationframework, sys:system.windows.forms, sys:system.drawing
# version: 1.0.7
# type: functions
# category: ui
# config:
#   { name: cfg.threading, type: bool, value: 1, description: Enable threading/runspaces for GUI/WPF version }
#   { name: cfg.autoclear, type: int, value: 300, description: Clear output box after N seconds. }
#   { name: cfg.noheader, type: bool, value: 0, description: Disable script info header in output. }
# status: beta
# priority: default
#
# Handles $menu list in WPF window. Creates widgets and menu entries
# for plugin list / $menu entries.
#   · `type: inline` is the implied default, renders output in TextBlock 
#   · `type: cli` or `window` plugins are run in a separate window
#   · `hidden: 1` tools only show up in menus
#   · `keycode:` is used for shortcuts; the CLI `key:` regex ignored
#   · `type: init-gui` plugins are run once during GUI construction
#   · Whereas `type: init` execute in the main/script RunSpace
#
# The responsive UI is achieved through:
#   · A new runspace for the GUI and a trivial message queue in $GUI.tasks.
#   · Main loop simply holds the window open, then executes $GUI.tasks events.
#   · That event queue simply holds the exact entries from $menu.
#   · Pipes them through `Run-GuiTask` (Should ideally be identical to the
#     one in menu.psm1, but needs a few customizations here.)
#
# All widget interactions are confined to the WPF runspace/thread.
#   · WPF interaction through `$GUI` would often hang both processes.
#   · Thus `Out-Gui` not only manages output, but also variable injection.
#
# Scripts/tools should work identically as for the CLI version:
#   · Aliases for `Write-Host` and `Read-Host` should make it transparent.
#   · However simple console output (typically to the stdout stream) will
#     have to be pipe-captured.
#   · Thus there's no guaranteed order for them and direct `Write-Host` calls.
#
# ToDo:
#   · split up into gui.psm1 and wpf.psm1 (actual GUI runspace)


#-- register libs
Add-Type -AN PresentationCore, PresentationFramework, WindowsBase
Add-Type -AN System.Drawing, System.Windows.Forms, Microsoft.VisualBasic
# [System.Windows.Forms.Application]::EnableVisualStyles()
# [System.Windows.Forms.Application]::SetCompatibleTextRenderingDefault($true)

#-- init vars
$ModuleDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$BaseDir = Split-Path -Parent $ModuleDir
$global:GUI = [hashtable]::Synchronized(@{Host=$Host})
$menu = @()
$global:last_output = 1.5


#-- WPF/WinForms widget wrapper
function W {
    <#
      .SYNOPSIS
         Convenient wrapper to create WPF or WinForms widgets.
      .DESCRIPTION
         Allows to instantiate and modify WPF/XAML or WinForms UI widgets.
         The $prop hash unifies property assignments and widget method calls.
         Notably this implementation is a bit slow, due to this abstraction and
         probing different object trees.          
      .PARAMERER type
         Widget type (e.g. "Button" or "Label") or existing $w widget object.
      .PARAMETER prop
         HashTable listing widget attributes or methods to call:
           @{Color="Red"; Text="Hello World"; Add=$other_widget}
         Property+method names depend on whether WPF or WinForms widgets are used. 
      .PARAMETER type
         Defaults to "Controls" for WPF widgets, but can be set to "Forms" for WF.
      .OUTPUTS
         Returns the instantiated Widget.
      .EXAMPLE
         Create a button:
           $w_btn = W Button @{Content="Text"; Border=2; add_Click=$cb}
         Add child widgets per list:
           $w_grd = W Grid @{Add=$w1, $w2, $w3}
         Or chain creation of multiple widgets:
           $w_all = W StackPanel {Spacing=5; Add=(
              (W Button @{Content="OK"}),
              (W Label @{Content="Really?"})
           )}
         The nesting gets confusing for WPF, but often simplifies WinForm structures.
      .NOTES
         The shortcut method `WF` creates WinForms widgets, whereas `WD` is
         for TextBlock/document inlines.
    #>
    [CmdletBinding()]
    Param($type = "Button", $prop = @{}, $Base="Controls")

    #-- new object
    if ($type.getType().Name -eq "String") {
        $w = New-Object System.Windows.$Base.$type
    }
    else {
        $w = $type      
    }
    #@bug on FOOTERM01 w/ PS 3.0
    if (($PSVersionTable.PSVersion.Major -eq 3) -and ($w -is [System.Windows.Thickness])) { return $w; }

    #-- apply options+methods
    $prop.keys | % {
        $key = $_
        $val = $prop[$_]
        if ($pt = ($w | Get-Member -Force -Name $key)) { $pt = $pt.MemberType }

        #-- properties
        if ($pt -eq "Property") {
            if (($Base -eq "Forms") -and (@("Size" , "ItemSize", "Location") -contains $key)) {
                $val = New-Object System.Drawing.Size($val[0], $val[1])
            }
            $w.$key = $val
        }
        #-- check for methods in widget and common subcontainers
        else {
            ForEach ($obj in @($w, $w.Children, $w.Child, $w.Container, $w.Controls)) {
                if ($obj.psobject -and $obj.psobject.methods.match($key) -and $obj.$key) {
                    ([array]$val) | ? { $obj.$key.Invoke } | % { $obj.$key.Invoke($_) } | Out-Null
                    break
                }
            }
        }
    }
    return $w
}

#-- WinForms version
function WF {
    Param($type, $prop, $add=@{}, $click=$null)
    W -Base Forms -Type $type -Prop ($prop + @{add=$add; add_click=$click})
}

#-- Document "widgets"
function WD {
    Param($type, $prop=@{})
    W -Base Documents -Type $type -Prop $prop
}


#-- WPF main window
function WPF-Window {
    <#
      .SYNOPSIS
         Loads XAML file `wpf.xaml` from same directory as this module.
      .DESCRIPTION
         Also populates $GUI.$_ for a select few global widget names.
      .NOTES
         Workaround img\ cache dir not enabled.
    #>

    #-- ImgDir
    $ImgDir = $BaseDir
    #if (Test-Path "$($env:APPDATA)\multitool\img") { $ImgDir = "$($env:APPDATA)/multitool" }

    #-- load
    $xaml = ((Get-Content "$BaseDir/modules/wpf.xaml" | Out-String) -replace "e:/","$ImgDir/")
    $w = [Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader ([xml]$xaml)))

    #-- save aliases int $GUI (sync hash across all threads/runspaces)
    $GUI.w = $w
    $GUI.styles = $w.Resources
    $shortcuts = "Window,Menu,Ribbon,Grid_EXTRAS,Output,machine,username,bulkcsv"
    $shortcuts.split(",") | % { $GUI.$_ = $w.findName($_) } | Out-Null

    #--- return
    return $w
}


#-- create new runspace
function New-GuiThread {
    <#
      .SYNOPSIS
         Creates a new Runspace for the GUI interface.
      .DESCRIPTION
         Copies functions over into new Runspace/Pipeline and attaches shared $GUI
         hashtable for data exchange.
      .EXAMPLE
         New-GuiThread {code} -Funcs "copy1,copy2" -Vars "menu,GUI" -Modules "WPF"
      .NOTES
         Does not work with PS 5.0 yet (presumably scoping or syntax issue with func duplication).
    #>
    Param(
        $code = {},
        $funcs = "WPF-Window,W,WF,WD,Add-GuiMenu,Get-IconPath,Create-GuiParamFields,"+
                 "Out-Gui,Out-Html,Ask-Gui,Add-ButtonHooks,AD-Search,Get-MachineCurrentUser,"+
                 "Set-Clipboard,Set-ClipboardHtml,Get-Clipboard,Get-NotepadCSV,Extract-PluginMeta,preg_match_all",
        $modules = "",
        $vars = "menu",
        $level = "",
        [switch]$invoke=$true
    )

    #-- create, add functions and code
    $shell = [PowerShell]::Create()
    $shell.AddScript((
        ($funcs.split(",")  |  ? { $_ }  |  % { 
            $func = Get-Command $_ -CommandType Filter,Function,Cmdlet
           "$($func.CommandType) $($func.Name) { $($func.Definition) };`r`n"
        }) -join "`r`n"
    )) | Out-Null
#Write-Host -f green "'''$code'''"
    $shell.AddScript($code) | Out-Null

    #-- separate thread
    $shell.Runspace = [runspacefactory]::CreateRunspace()
    $shell.Runspace.ApartmentState = "STA"
    $shell.Runspace.ThreadOptions = "ReuseThread"         
    $shell.Runspace.Open() | out-null

    #-- add vars, modules
    $shell.Runspace.SessionStateProxy.SetVariable("GUI", $GUI) | out-null
    $shell.Runspace.SessionStateProxy.SetVariable("vars", @{}) | out-null
    $shell.Runspace.SessionStateProxy.SetVariable("in_thread", 1) | out-null
    $vars.split(",")  |  ? { $_ } |  % { $shell.Runspace.SessionStateProxy.SetVariable($_, (get-variable $_).value) } | Out-Null
    #$modules.split(",") | ?{$_} | % { $shell.Runspace.SessionStateProxy.ImportPSModule($_) } | Out-Null

    #-- start
    if ($invoke) {
       $handle = $shell.BeginInvoke()
    }
    return $shell
}
#-- no worky
function Attach-StreamEventHandler {
    #-- attaches an event handler to runspace streams (.error and .warning) to capture output
    #$shell.AddScript({
    #    $GUI.stream_error = $Error
    #})
    #Register-ObjectEvent -InputObject $shell.streams.error -EventName DataAdded -Action ({
    #    @{error="err"} | Out-String | Out-Gui -f Red
    #})
    #$shell.streams.error.add_DataAdded({
       # Param($sender, $event)
       #  $Error | Out-Gui -f Red
       #  @($Sender) | out-string | Out-Gui -f Red
       #  [void]($sender.ReadAll() | % { $_.Message | Out-Gui })
       #  Out-Gui "error" -f Red
    #})
}


#-- Look up icon basename path variations
function Get-IconPath {
    <#
      .SYNOPSIS
         Looks up alternative filenames for icons.
      .DESCRIPTION9
         Scans for PNGs (filename from PMD .icon or .img field) in $BaseDir/img/
      .PARAMETER basenames
         Can be a list of icon names "user", "icon.user", "user2.png" etc.
    #>
    [CmdletBinding()]
    Param([Parameter(ValueFromRemainingArguments=$true)]$basenames)
    ForEach ($fn in ($basenames | ? { $_ -ne $null })) {
        ForEach ($png in "icon.$fn.png", "icon.$fn", "$fn.png", "$fn") {
            if (Test-Path ($path = "$BaseDir/img/$png")) {
                return $path
            }
        }
    }
}

#-- Additional input boxes / or combobox if according data/fieldname.txt file exists
function Create-GuiParamFields() {
    <#
      .SYNOPSIS
         Creates input field widgets for extra #param: names
      .DESCRIPTION
         Some plugins may depend on more input than just `machine` and `username`.
         In order to avoid pesky VBS input field popups, plugins may specify additional
         fields with `# param: username,accesslevel,othervar3`

         This function crafts TextBox or ComboBox fields for those. Assigns them a
         unique (per-plugin) widget name "var_$plugin_$paramname". So it can later be read
         out by the "Read-Host" wrapper.
      .NOTES
         Combobox fields are created when there's an according data/combobox.$paramname.txt file.
    #>
    Param(
        $s = "extra,field,names",
        $prefix = "plugin",
        $extra_params = @()
    )
    "$s".split("[;,]") | ? { $_ -and $_ -notmatch "^\s*(machine|host|computer|adname|account|user|bulk)" } | % {
        $key = $_.trim()
        if (Test-Path ($fn="data/combobox.$key.txt")) {
            $field = (W ComboBox @{Width=120; IsEditable="True"})
            Get-Content $fn | % { $field.Items.Add((W ComboBoxItem @{Content=$_})) } | Out-Null
            $field.text = $field.Items[0].content  # use first entry as default
        }
        else {
            $field = (W TextBox @{Width=120})
        }
        $extra_params += (W WrapPanel @{Add=(W Label @{Content="$key"; FontWeight="Bold"}), $field})
        $GUI.w.registerName("var_$($prefix)_$($key)", $field)
    }
    return $extra_params
}

#-- Add tool buttons to main window
function Add-GuiMenu {
    <#
      .SYNOPSIS
         Adds menu entries and button blocks for each plugin from $menu
      .DESCRIPTION
         Iterators over the $menu list
          - skips "hidden:" or "nomenu:" entries, or "type:init*" plugins
          - adds a MenuItem and callback
          - uses the PMD .category to find the right menu or grid/notebook tab
          - looks up plugin or category icons (WPF requires unique instances each?!)
          - calls Create-GuiParamFields for extra input variables
         Also handles shortcut icons section.
      .NOTES
         This is what causes the slow startup! (perhaps due to `W` being too convenient)
    #>
    Param($menu)
    $icon_default = W Image
    ForEach ($e in $menu) {
    
        #-- prepare params
        $e.hidden = ($e.hidden -match "1|yes|hide|hidden|true")
        if ($e.type -eq "init") { continue; }
        $CAT = $e.category.toUpper();
        $GRID = (@($GUI.w.findName("Grid_$CAT"), $GUI.Grid_EXTRAS) -ne $null)[0]

        #-- output category header
        if (($e.category -ne $category) -and (-not $e.hidden)) {
            $category = $e.category
            $GRID.Children.Add((W Label @{Content="  → $category"; Foreground="White"; Background="#443377"; Font="Verdana"; FontSize=17; Width=775}))
        }

        #-- callback (= now just appends to event queue)
        $cb = { $GUI.tasks += $e }.getnewclosure()

        #-- action block/button
        if (-not $e.hidden) {
            $border = W Border @{Style=$GUI.styles.ToolBlock; ToolTip=$e.doc; set_Child=(
                W StackPanel @{Orientation="Horizontal"; Add=
                    (W Button @{Style=$GUI.styles.ToolButton; Width=120; Add_Click=$cb; Content=(
                       W WrapPanel @{Add=
                          (W Image @{Source=(Get-IconPath $e.img $e.icon $e.category); Height=20; Width=22}),
                          (W TextBlock @{Text=$e.title; TextWrapping="Wrap"})
                       }
                    )}),
                    (W StackPanel @{Padding=2; Margin=4; Width=200; Add=
                       @(
                          (W TextBlock @{Text=$e.description; TextWrapping="Wrap"}),
                          (W TextBlock @{Text="v$($e.version) - $($e.type)"; Foreground="#777777"})
                       ) + (Create-GuiParamFields $e.param $e.id)
                    })
                }
            )}
            $GRID.Children.Add($border)
        }
        
        #-- add menu entry
        if (($e.type -notmatch "^init") -and ($e.category)) {
            $m = $GUI.w.findName("Menu_$($CAT)")
            # new Extras > submenu if not found
            if (-not $m) {
                $m = (W MenuItem @{Name=$CAT; Header=$e.category})
                $GUI.w.findName("Menu_EXTRAS").Items.Add($m)
                $GUI.w.registerName("Menu_$CAT", $m)
            }
            # add
            $ICON = W Image @{Source=(Get-IconPath $e.img $e.icon $e.category); Height=20; Width=20}
            $m.Items.Add((W MenuItem @{Header=$e.title; InputGestureText=$e.keycode; Icon=$ICON; ToolTip=(W ToolTip @{Content=$e.description}); Add_Click=$cb}))
        }
    }

    #-- and a shortcut - have their own custom sorting from `#shortcut: 123`
    if ($m = $GUI.w.findName("Shortcuts")) {
        ForEach ($e in ($menu | ? { $_.shortcut -match "\d+|true|yes" } | Sort-Object @{expression={$_.shortcut.trim()}} )) {
            $cb = { $GUI.tasks += $e }.getnewclosure()
            $ICON = W Image @{Source=(Get-IconPath $e.img $e.icon $e.category); Height=22; Width=22}
            $BTN = W Button @{Style=$GUI.styles.ActionButton; ToolTip=(W ToolTip @{Content=$e.title}); Add_click=$cb; Height=22; Width=22; Content=$ICON}
            $m.Children.Add($BTN)
        }
    }
}


#-- attach callbacks for main UI buttons
function Add-ButtonHooks {
    <#
      .SYNOPSIS
         Prepares callbacks for buttons/toolbar in main window.
      .DESCRIPTION
         Such as the computer and username fields, or the clipboard functionality.
      .NOTES
         Ping and username lookup can freeze the UI, as they run in the WPF runspace already.
         $GUI.machine and $GUI.username are shared with the main thread.
         As is the clipboards $GUI.html shadow content.
    #>

    #-- computer name
    $GUI.w.findName("BtnComputer").add_Click({ 
        if ($m = Get-Clipboard) {
            $GUI.machine.Text = $m
            $col = if (Test-Connection $m -Count 1 -Quiet -TTL 10 -ErrorAction SilentlyContinue) {"#5599ff99"} else {"#55ff7755"}
            $GUI.machine.Items.Insert(0, (W ComboBoxItem @{Content=$m; Background=$col}))
            $GUI.machine.Background = "$col"
        }
    })
    $GUI.w.findName("BtnComputerClr").add_Click({ $GUI.machine.Text = ""; $GUI.machine.Background = "White" })
    $GUI.w.findName("BtnComputerCpy").add_Click({ Set-Clipboard $GUI.machine.Text })
    $GUI.w.findName("BtnComputerPng").add_Click({ })
    $GUI.w.findName("BtnComputerUsr").add_Click({ if ($u = Get-MachineCurrentUser $GUI.machine.Text) { $GUI.username.Text = $u } })

    #-- user name
    $GUI.w.findName("BtnUsername").add_Click({
        $u = Get-Clipboard
        if ($u -match "\w+[@, ]+\w+") { $u = (AD-Search $u -only 1) }
        $GUI.username.Text = "$u"
    })
    $GUI.username.add_DropDownOpened({
        if (($u = $GUI.username.Text).length -gt 2) {
            $GUI.username.Items.Clear()
            AD-Search $u | % { $GUI.username.Items.Add($_) }
        }
    })
    $GUI.username.add_DropDownClosed({
        $GUI.username.Text = $GUI.username.Text -replace "\s*\|.+$", ""
    })
    $GUI.w.findName("BtnUsernameClr").add_Click({ $GUI.username.Text = "" })
    $GUI.w.findName("BtnUsernameCpy").add_Click({ Set-Clipboard $GUI.username.Text })
    $GUI.w.findName("BtnUsernameCom").add_Click({  })

    #-- bulk/csv
    $GUI.w.findName("BtnBulkimport").add_Click({ $GUI.bulkcsv.text = Get-NotepadCSV $GUI.bulkcsv.text "notepad" })

    #-- clipboard tools
    $GUI.w.findName("BtnClipText").add_Click({ Set-Clipboard $GUI.output.text })
    $GUI.w.findName("BtnClipHtml").add_Click({
        # use $html when available
        if ($GUI.html) {
            Set-ClipboardHtml $GUI.html
        }
        else {
            #@ToDo: convert TextBlock.Inlines to HTML
            Set-Clipboard ($GUI.output.text)
        }
    })
    $GUI.w.findName("BtnClipFree").add_Click({
        $prev_output = ($GUI.output.Inlines | % { $_ })
        $GUI.html = ""
        $GUI.output.text = ""
    })
    $GUI.w.findName("BtnClipSwap").add_Click({
        $GUI.output.text = ""
        $prev_output | % { $GUI.output.Inlines.Add($_) }
    })
    $global:prev_output = @()
    
    #-- window closing
    $GUI.w.add_Closed({
        $GUI.closed = $true
    })

    #-- unicode symbols
    if ($ls = $GUI.w.findName("UnicodeClip")) {
        $cb = {Set-Clipboard $this.Content}
        ForEach ($btn in $ls.Children) {
            $btn.Add_click($cb)
        }
    }
}

#-- User lookup (simple `finduser` variant)
function AD-Search {
    <#
      .SYNOPSIS
         Simple AD username search
      .DESCRIPTION
         Gets invoked whenever somethings is pasted into `username` input field.
         Defaults to search for "Last, First" names and spit out "UserName123".
         Also may scan for phone numbers or email addresses (used in UI for dropdown).
      .NOTES
         Defaults to [adsisearched], but may use Get-ADUser when available. By default
         the ActiveDirectory module is not loaded again into the WPF/UI Runspace.
         May require customization if AD displayname does not follow "First, Last" scheme.
    #>
    Param(
        $s = "",
        $only = 0
    )
    $s = ($s -replace "%","*")
    #-- AD search
    if (Test-Path function:Get-ADUser) {
        $filter = switch -regex ($s) {
           "^[*?\d]+$" {   "telephoneNumber -like '*$s'"                              }
           "^\w+, \w+" {   "displayname -like '$s*'"                                  }
             "\w+@\w+" {   "mail -like '$s'"                                          }
               default {   "displayname -like '$s*' -or samaccountname -like '$s*'"   }
        }
        #-- find
        $u = Get-ADUser -Filter $filter -Properties samaccountname,displayname,telephonenumber
    }
    #-- ADSI query
    else {
        $s = $s -replace '([;\\"#+=<>])', '\$1'
        $filter = switch -regex ($s) {
           "^[*?\d]+$" {   "telephonenumber=*$s"                              }
        "^\w+\\?, \w+" {   "displayname=$s"                                   }
           ".+\w+@\w+" {   "mail=$s"                                          }
               default {   "|(displayname=$s*)(samaccountname=$s*)"           }
        }
        $adsi = [adsisearcher]"(&(objectClass=user)(objectCategory=person)($filter))"
        [void]$adsi.PropertiesToLoad.AddRange(("telephonenumber","displayname","samaccountname","mail","*","+"))
        $u = ($adsi.findAll() | % { $_.properties })
    }
    #-- result
    if ($only) { return [string]$u.samaccountname }
    else { return @($u | % { "{0} | {1} | {2}" -f [string]$_.samaccountname, [string]$_.displayname, [string]$_.telephonenumber }) }
}

#-- get current user
function Get-MachineCurrentUser($m) {
    <#
      .SYNOPSIS
         Get current user from remote machine name.
      .DESCRIPTION
         Runs a quick WMI query. Returns user name (domain prefix stripped).
      .NOTES
         Does not fallback to alternative Explorer.exe process scan.
    #>
    if ($m -and ($w = gwmi win32_computersystem -computername $m) -and ($u = $w.username)) {
        return $u -replace "^\w+[\\\\]",""
    }
}


#-- HTML output
#   · invoked by Out-Gui
function Out-Html {
    <#
      .SYNOPSIS
         Assembles HTML (clipboard) output from each Out-Gui (Write-Host alias) call.
      .DESCRIPTION
         Converts -F foreground and -B background colors and appends to $GUI.html.
         Additionally provides -Bold and -Underline support (but those aren't Write-Host/CLI compatible then).
      .NOTES
         That's the lazy approach. Original plan was to convert WPF TextBlock inlines in a HTML clipboard function.
         Postponed since Out-Gui does not implement any images or table output yet anyway.
    #>
    param(
        [Parameter(ValueFromPipeline=$true)]$str = $null,
        [alias("Foreground")]$f = $null,
        [alias("Background")]$b = $null,
        [alias("Bld")]$bold = $null,
        [alias("Strikethrough")]$S = $null,
        [alias("Underline")]$U = $null,
        [alias("NoNewLine")][switch]$N = $false,
        $css = ""
    )

    #-- foreground/background maps (for white HTML background)
    $map_f = @{
        Yellow = "#5554400"
        Red = "#55070F"
        Blue = "#111144"
        Green = "#004400"
        Cyan = "#004455"
        White = "#111"
        Gray = "#666"
        Black = "#222"
    }
    $map_b = @{
        Yellow = "#f3f3aa"
        Green = "#aaeeaa"
        Blue = "#a5a5f5"
        Cyan = "#a1ece5"
        Red = "#ef8a77"
        White = "#444"
        Gray = "#bbb"
        Black = "#555"
    }

    #-- normalize string
    #if (!$str) { return }
    $str = ($str | Out-String -Width 120).trim()
    $str = $str -replace "(?m)\s*$", ""
    $str = $str -replace "<","&lt;"
    $str = $str -replace ">","&gt;"
    $str = $str -replace "\r?\n", "<br>`r`n"

    #-- colorize
    $str = $str -replace "✔","<font color=#115511>✔</font>"
    $str = $str -replace "✘","<font color=#771122>✘</font>"
    if ($f -eq "#ff9988dd") {
        $f = $null;      # detect script title/description block
        $bold = "1";
        $b = "#eeeef5";
    }
    if ($f -and $map_f.containsKey($f)) {
        $f = $map_f[$f]       # remap to darker foreground
    }
    if ($b) {
        if ($map_b.containsKey($b)) {
            $b = $map_b[$b]   # lighter background colors
        }
        $css += "background-color:$b;"
    }
    if ($bold) {
        $str = "<strong>$str</strong>"
    }
    if ($U) {
        $str = "<u>$str</u>"
    }
    if ($css -or $f) {
        $str = "<font style='$css' color='$f'>$str</font>"
    }
    if ($N) { $NL = "" } else { $NL = "<br>`r`n" }
    $GUI.html += $str + $NL
}


#-- GUI interactions
#   · Primarily appends to `output` TextBlock
#   · Is called from main thread (and the only allowed interaction method),
#     itself uses w.Dispatcher.Invoke to execute codeblock in GUI runspace
#   · Also updates window `-Title`
#   · And provides `-GetVars` workaround
filter Out-Gui {
    <#
      .SYNOPSIS
         Fills the $GUI.output TextBlock from calls to Write-Host (aliased to Out-Gui).
      .DESCRIPTION
         Mainly converts any objects (HashTables/PSObjects) to strings, then appends them to main output pane.
         Since this is called from the main script Runspace, uses the WPF dispatcher to execute the appending
         in the WPF runspace (factually WPF does it in another separate thread).
      .PARAM str
         Input string or object
      .PARAM f
         Foreground color
      .PARAM b
         Background color
      .PARAM N
         NoNewLine
      .PARAM bold, s
         Not CLI/Write-Host compatible. Should not be used unless the plugin/script was meant for GUI usage only.
      .NOTES
         Also implements another few UI interactions, such as -title setting, or output -clear.
         For example -getvars returns a HashTable of toolbar and plugin/ToolBlock input fields.

         Ultimately this should handle error/exception highlighting and creating OUtGrid-like display
         for HashTables and Objects. Not implemented yet, as it doesn't even run on PS 5.0 yet.
         Also should be rewritten to Begin{} Process{} End{} and split up to handle proper piping.

         (Currently this does not allow to synchronously handle output from tools/scripts. Write-Host
         calls get processed before any Out-Default remains...)
    #>
    param(
        [Parameter(ValueFromPipeline=$true)]$str = $null,
        [alias("Foreground")]$f = $null,
        [alias("Background")]$b = $null,
        [alias("Bld")]$bold = $null,
        [alias("Strikethrough")]$S = $null,
        [alias("Underline")]$U = $null,
        [alias("NoNewLine")][switch]$N = $false,
       [string]$title = $null,
       [string]$getvars = $null,
       [string]$testpfx = $null,
       [switch]$clear = $false
    )

    trap { $_ | Out-String >> ./data/log.errors.wpf.txt }

    Out-Html $str -f $f -b $b -bold $bold -s $s -U $U -N:$N
    

    #-- wrap in scriptblock for GUI thread dispatcher
    $callback = [action]{
        $inlines = @()
        trap { $_ | Out-String >> ./data/log.errors.out-gui.txt }
    
        #-- clear stale contents
        #   · if last output over 5 minutes ago (`-clear:$true` is set by caller)
        if ($clear) {
            $GUI.prev_output = $GUI.output.inlines
            $GUI.output.text = ""
            $GUI.html = ""
        }

        #-- just -Title update
        if ($title) {
            $GUI.w.title = $title
            return
        }

        #-- Read input fields
        #  · this is a workaround, because accessing $GUI.machine.text
        #    directly from parent runspace would hang up WPF
        #  · for some reason also needs hashtable recreated
        if ($getvars) {
            $GUI.vars = @{}
            $getvars.split("[,;]") | % {
                if (($key = $_.trim()) -and ($f = $GUI.w.findName($key)) -or ($f = $GUI.w.findName("var_$($testpfx)_$($key)"))) { 
                    $GUI.vars.$key = $f.text
                }
            } | Out-Null
            return $GUI.vars
        }

        # see if ErrorRecord
        try {
            $type = $str.GetType().Name
        }
        catch {
            $type = "unknown"
        }
        if ($type -eq "--ErrorRecord") {
            $inlines += WD Figure @{Background="#ff773322"; Content=($str|Out-String)}
        }
        
        # or HashTable
        elseif ($type -eq "---HashTable") {
            $inlines += ($str|Out-String)
        }

        # or image
        elseif ($type -eq "Image" -or $type -eq "Paragraph" -or $type -eq "InlineUIContainer") {
            # leave as-is
        }

        # plain string
        else {
            $str = $str | Out-String -Width 100
            $a = WD Run @{Text=$str}
            if ($f) { $a.Foreground=$f }
            if ($b) { $a.Background=$b }
            if ($bold) { $a.FontWeight="Bold" }
            if ($S) { $a.TextDecorations="Strikethrough" }
            if ($U) { $a.TextDecorations="Underline" }
            $inlines += $a
        }

        #-- append
        $inlines | % { $GUI.output.Inlines.Add($_) } | Out-Null
        [void]$GUI.output.Parent.ScrollToBottom()
    }
    
    #-- PowerShell >= 3.0 does need the parameters swapped
    #   (@src: https://gallery.technet.microsoft.com/Script-New-ProgressBar-329abbfd)
    if ($PSVersionTable.PSVersion.Major -eq 2) {
        [void]$GUI.w.Dispatcher.Invoke("Normal", $callback)
    }
    else {
        [void]$GUI.w.Dispatcher.Invoke($callback)
    }
}


##########################################################################################
########################   everything below runs in main thread   ########################
##########################################################################################


#-- User input
#   · this is aliased over `Read-Host`
#   → so scripts can be used unaltered in standalone/CLI or GUI mode
#   · for standard field names just returns the GUI $machine/$username input
#   · else shows a VB input box window
function Ask-Gui {
    <#
      .SYNOPSIS
         Alias over `Read-Host`
      .DESCRIPTION
         Compares Read-Host requests against a list of know variable names/titles,
         such as "computer" or "username". If a match is found, returns the current
         value from the GUI input field. Else shows a VBS popup.

         The purpose is to allow tools/scripts to run in CLI mode as well as in GUI
         frontend unchanged. But to avoid needless popups for predefined input fields.
      .PARAM str
         Textual input query. Usually just the variable name '$computer' or '$user'.
         Aliases like 'Machine' and 'PC' and 'Hostname' are known, as well as 'AD-Account'
         or 'AD-User-Name' e.g.
      .NOTES
         Scripts should preferrably query for input in their Param() section once,
         and list custom fields per plugin meta #param: list.
         The GUI frontend does not implement a full PSHost, just an output TextBlock.
         (Input fields might be feasible, but too much work IMHO.)
    #>
    param($str, $title="Input", [switch]$N=$false)
    if ($str -match "^(?i)[$]?(AD[-\s]?)?(User|Account)(?:[-\s]?Name)?\s*[=:]*\s*?$") {
        return $GUI.vars.username
    }
    elseif ($str -match "^(?i)[$]?(Computer|Machine|PC|Host)([-\s]?Name)?\s*[=:]*\s*$") {
        return $GUI.vars.machine
    }
    elseif ($str -match "^(?i)[$]?(Bulk|bulk.?csv|bulkfn|list|CSV)") {
        return $GUI.vars.bulkcsv  # should be exported to filename?
    }
    elseif ($GUI.vars.containsKey($str)) {  #-- per-plugin input boxes
        return $GUI.vars[$str]
    }
    else {
        #-- Trivial input box for everything else
        return [Microsoft.VisualBasic.Interaction]::InputBox($title, $str, '')
    }
}

# alias for CMD `CHOICE` function
function Ask-GuiVar {
    param($P, $VAR, $M, $TEXT)
    $v = Ask-Gui $TEXT "Choice"
    Invoke-Expression ('$global'+":$VAR = '$v'")
}

# alias for CMD `CHOICE` function
function Ask-GuiMachine {
    return $global:machine
}

# alias for CMD `CHOICE` function
function Ask-GuiUser {
    return $global:username
}

#-- Converts `# param: name,list` from vars{} to quoted cmdline "arg" "strings"
function Get-ParamVarsCmd {
    <#
      .SYNOPSIS
         Crafts a list of cmd/Invoke-quoted strings from params list
      .DESCRIPTION
         Is used for type:window and type:cli scripts/plugins. Those get executed
         in a separate Powershell process, thus need input variables as exec arguments.
      .NOTES
         Somewhat tedious, as this is the third duplication of field/varname alias handling.
    #>
    Param(
        $param = "machine,username",         # from plugin meta field
        $vars = @{machine=""; username=""},  # from GUI -GetVars
        $out = ""
    )

    if (!$param) { $param = "machine,username" }   # default list
    ForEach ($key in $param.split("[,;]")) {
        if (!($key = $key.trim()).length) { continue }
        # aliases
        if ($key -match '^(host|hostname|computer|pc-?name)$') { $key = "machine" }
        if ($key -match '^(user|ad-?name|account|accountname)$') { $key = "username" }
        if ($key -match '^(bulk|bulkcsv|bulkfn|bulklist)$') {
            $tmpfn = [IO.Path]::GetTempFileName()
            $vars[$key] | Out-File $tmpfn -Encoding UTF8 | Out-Null
            $vars[$key] = $tmpfn   # ToDo: clean tmp via $plugins.after[]
        }
        # quote + append
        $out += ' "'+($vars[$key] -replace '([\\"^])','^$1')+'"'
    }
    return $out
}

#-- wrapper around Process-MenuTask
function Run-GuiTask {
    <#
      .SYNOPSIS
         Executes the given $e event queue entry returned from GUI (menu or button callback).
      .DESCRIPTION
         Checks the $menu.type and handles output start, script execution, and result collection.
          - Just prints .title and .description first
          - Then assembles GUI variables ($machine, $username and extra script fields)
          - Runs `type:inline` plugins in main thread.
          - But `type:window` in separate window/CLI process.
          - Captures and outputs any errors
          - Then returns tp the Start-Win loop
      .PARAM e
         The script/tool entry as returned from $GUI.tasks (just one of the $menu entries really).
         Said queue gets filled by any of the menu or toolblock buttons in the main window.
      .NOTES
         Ideally this should just wrap the CLI Process-MenuTask function.
         The | Out-Gui piping does not mix direct Write-Host calls in order yet. Thus any extra
         script output gets shown AFTERWARDS. (to be fixed with proper pipe handling)
    #>
    [CmdletBinding()]
    param($e, $clear=$false) # one of the $menu entries{}

    #-- check last output time (>= 5 minutes)
    if ($last_output -and (([double]::parse((Get-Date -u %s)) - $last_output) -gt $cfg.autoclear)) {
        $clear = $true
    }
    #-- print header
    Out-Gui -title "➱ KlickiBunti → $($e.title)" -clear:$clear
    if ((!$e.noheader) -and (!$cfg.noheader)) {
        Out-Gui -f '#ff9988dd' -b '#ff102070' ("$($e.title) - $($e.description)")
    }

    #-- Get-GuiVars (fetch input from "Ribbon"-fields: machine, username, ...)
    Out-Gui -GetVars "machine,username,bulkcsv,$($e.param)" -testpfx $e.id  # populates $GUI.vars{}
    $GUI.vars.GetEnumerator() | % { Set-Variable $_.name $_.value -Scope Global }
    
    #-- plugins
    TRAP { $_ | out-gui -b red }
    $plugins.before | % { Invoke-Expression ($_.ToString()) }

    #-- Run $menu entry rules (command=, func=, or fn=)
    try {
        #-- Internal commands
        if ($e.command) {
            [void]((Invoke-Expression $e.command) | Out-String | Out-Gui -f Yellow)
        }
        elseif ($e.func) {
            [void]((Invoke-Expression "$e.func $machine $username") | Out-String | Out-Gui -f Yellow)
        }
        #-- Start script
        elseif ($e.fn) {
            if ($e.type -match "window|cli") {  # in separate window
                $cmd_params = Get-ParamVarsCmd ($e.param) ($GUI.vars)
                Start-Process powershell.exe -Argumentlist "-STA -ExecutionPolicy ByPass -File $($e.fn) $cmd_params"
            }
            else {  # dot-source all "inline" type: plugins
                . $e.fn | Out-String | Out-Gui # -f Green
            }
        }
        #-- No handler
        else {
            Out-Gui -f Red "No Run-GuiTask handler for:"
            [void](@($e) | Out-String | Out-Gui)
        }
    }
    #-- Error cleanup
    catch {
        Out-Gui -f Yellow $_
    }
    if ($Error) {
        $Error | % { $_ | Out-Gui -f Red } | Out-Null
        [void]($Error.Clear())
    }

    #-- Run plugins / cleanup
    $plugins.after | % { Invoke-Expression ($_.ToString()) }
    $global:last_output = [double]::parse((Get-Date -u %s))
    Out-Gui -title "➱ KlickiBunti"
}


#-- verify runspace and window are still active
function Test-GuiRunning($shell, $GUI) {
    return ($shell.Runspace.RunspaceStateinfo.State -eq "Opened") -and (-not $GUI.closed)
}


#-- Initialize Window, Buttons, Subshell+GUI thread
function Start-Win {
    <#
      .SYNOPSIS
         Starts the WPF window and Runspace, then waits for GUI events
      .DESCRIPTION
         Sets up GUI environment from $menu entries, then polls the shared $GUI. variables
      .NOTES
         Perhaps the least interesting function and self-explanatory.
    #>
    Param($menu)

    #-- no threading / manual
    #    $GUI.w = $w = WPF-Window; $w.showdialog(); Write-Host "Ran WPF-Window without thread"

    #-- start GUI in separate thread/runspace
    $shell = New-GuiThread -Code {
        $GUI.w = $w = WPF-Window
        #TRAP { $_ | Out-Gui -f '#ffee99' -b '#cc5544'; $Error.Clear() }
        #TRAP { $GUI.w.Host.UI.Write($_);  }
        #Import-Module ActiveDirectory
        Add-ButtonHooks
        Add-GuiMenu $menu
        #-- execute `type:init` files right away
        $menu | ? { $_.fn -and ($_.type -eq "init-gui") } | % { . $_.fn }
        $w.ShowDialog()
    } -Vars "menu,BaseDir,cfg,plugins"
    $GUI.tasks = @()

    #-- wait for window to be visible
    while (!($GUI.w.IsInitialized)) { 
        Start-Sleep -milliseconds 275
        Write-Host 'wait on $GUI.w.isInitialized'
        #$global:GUI     |FT  |Out-String|Write-Host -f Blue
        #$shell.runspace   |FL -Prop *|Out-String|Write-Host -f Yellow
        #$shell.Streams    |FL|Out-String|Write-Host -f DarkGreen
        if ($Debug -and $shell.streams.error) {
            $shell.streams | FL | Out-String | Write-Host -b Red
        }
    } 

    #-- Alias console functions
    # `echo` is already alias to `Write-Output`
    Set-Alias -Name Write-Host  -Value Out-Gui  -Scope Global
    Set-Alias -Name Read-Host   -Value Ask-Gui  -Scope Global
    Set-Alias -Name Get-Machine -Value Ask-GuiMachine
    Set-Alias -Name choice      -Value Ask-GuiVar
    
    #-- basic error catching
    TRAP { $_ | Out-Gui -f Red; $Error.Clear() }

    #-- main loop / trivial message queue
    #   test for window state, pause a few seconds, then resolves $tasks[]
    while (Test-GuiRunning $shell $GUI) {
        if ($GUI.tasks) {
            $GUI.tasks | % { Run-GuiTask $_ }
            $GUI.tasks = @()
        }
        Start-Sleep -milliseconds 175
    }
}


Added modules/wpf.xaml.























































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
<!--
# api: wpf
# type: gui
# title: MultiTool window
# version: 1.0.8
-->
<Window
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:r="http://schemas.microsoft.com/winfx/2006/xaml/presentation/ribbon"   
  x:Name="Window" Title="➱ ClickyColoury" WindowStartupLocation="CenterScreen" 
  Width="980" Height="720"
  ShowInTaskbar="True">

  <!-- Styles -->
  <Window.Resources>
    <Style x:Key="RibbonGradient" TargetType="DockPanel">
      <Setter Property="Background"><Setter.Value>
      <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
        <GradientStop Color="#FFd1ddeb" Offset="0.00" />
        <GradientStop Color="#ffc9d9ed" Offset="0.30" />
        <GradientStop Color="#ffcdddef" Offset="0.70" />
        <GradientStop Color="#ffd7e6f6" Offset="1.00" />
      </LinearGradientBrush>
      </Setter.Value></Setter>
    </Style>

    <Style x:Key="RibbonBorder" TargetType="Border">
      <Setter Property="BorderBrush" Value="#FF556699" />
      <Setter Property="BorderThickness" Value="1" />
      <Setter Property="CornerRadius" Value="8" />
      <Setter Property="Margin" Value="3" />
      <Setter Property="Padding" Value="4,3,4,0" />
      <Setter Property="Background"><Setter.Value>
      <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
        <GradientStop Color="#00d5e3f2" Offset="0.00" />
        <GradientStop Color="#ffc9d9ed" Offset="0.20" />
        <GradientStop Color="#ffcdddef" Offset="0.50" />
        <GradientStop Color="#ffcdddef" Offset="0.51" />
        <GradientStop Color="#ffd7e6f6" Offset="0.70" />
        <GradientStop Color="#ffc2d9f1" Offset="0.71" />
        <GradientStop Color="#ffa2c9e1" Offset="1.00" />
      </LinearGradientBrush>
      </Setter.Value></Setter>
    </Style>

    <Style x:Key="RibbonBorderGreen" TargetType="Border" BasedOn="{StaticResource RibbonBorder}">
      <Setter Property="Background"><Setter.Value>
      <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
        <GradientStop Color="#00d5f2e3" Offset="0.00" />
        <GradientStop Color="#ffc9edd9" Offset="0.20" />
        <GradientStop Color="#ffb9e7d7" Offset="0.70" />
        <GradientStop Color="#ffa0e4c5" Offset="0.71" />
        <GradientStop Color="#ffa2e1c9" Offset="1.00" />
      </LinearGradientBrush>
      </Setter.Value></Setter>
    </Style>

    <Style x:Key="ActionButton" TargetType="Button">
      <Setter Property="TabIndex" Value="0" />
      <Setter Property="FontSize" Value="18" />
      <Setter Property="Foreground" Value="#ff333366" />
      <Setter Property="Template">
       <Setter.Value>
        <ControlTemplate TargetType="Button">
         <Border>
          <Border.Style>
           <Style TargetType="{x:Type Border}">
            <Style.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
              <Setter Property="Background" Value="#ffeeddaa"/>
              <Setter Property="Opacity" Value="1.0"/>
            </Trigger>
            <!--Trigger Property="IsPressed" Value="True">
              <Setter Property="Background" Value="#ffffcc99"/>
              <Setter Property="Opacity" Value="0.6"/></Trigger-->
            </Style.Triggers>
           </Style>
          </Border.Style>
          <Border Opacity="0.9"> <ContentPresenter/> </Border>
         </Border>
        </ControlTemplate>
       </Setter.Value>
      </Setter>
    </Style>

    <Style x:Key="ToolBlock" TargetType="Border">
      <Setter Property="BorderBrush" Value="#ff707280" />
      <Setter Property="BorderThickness" Value="2" />
      <Setter Property="CornerRadius" Value="5" />
      <Setter Property="Width" Value="350" />
      <!--Setter Property="Height" Value="65" /-->
      <Setter Property="Margin" Value="5" />
      <Setter Property="Padding" Value="4" />
      <Setter Property="Background"><Setter.Value>
      <LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
        <GradientStop Color="#ffeeeeff" Offset="0.00" />
        <GradientStop Color="#fff7f7f7" Offset="1.00" />
      </LinearGradientBrush>
      </Setter.Value></Setter>
    </Style>

    <Style x:Key="ToolButton" TargetType="Button">
      <Setter Property="FontSize" Value="15" />
      <Setter Property="FontWeight" Value="Bold" />
      <Setter Property="BorderThickness" Value="3" />
    </Style>

    <Style x:Key="MainTab" TargetType="TabItem">
      <Setter Property="HeaderTemplate">
        <Setter.Value>
          <DataTemplate>
            <ContentPresenter Content="{TemplateBinding Content}">
              <ContentPresenter.LayoutTransform>
                <RotateTransform Angle="270" />
              </ContentPresenter.LayoutTransform>
            </ContentPresenter>
          </DataTemplate>
        </Setter.Value>
      </Setter>
      <Setter Property="Padding" Value="0" />
      <Setter Property="Margin" Value="0" />
      <!--Setter Property="Width" Value="30" /-->
    </Style>
  </Window.Resources>

  <!-- Widgets --> 
  <DockPanel Background="#77797c">

  <Menu DockPanel.Dock="Top" Background="#ffc0d0ef">
     <MenuItem Header="CMD" x:Name="Menu_CMD" ToolTip="Computer/command-line tools">
       <MenuItem Header="OnBehalf" x:Name="Menu_ONBEHALF" ToolTip="OnBehalf remote execution" />
     </MenuItem>
     <MenuItem Header="User" x:Name="Menu_USER" ToolTip="User accounts" />
     <MenuItem Header="PowerShell" x:Name="Menu_POWERSHELL" ToolTip="Powershell scripts">
       <MenuItem Header="Bulk" x:Name="Menu_BULK"><MenuItem.Icon><Image Source="e:/img/csv.png" Width="18" Height="18"/></MenuItem.Icon></MenuItem>
     </MenuItem>
     <MenuItem Header="Exchange" x:Name="Menu_EXCHANGE" ToolTip="Exchange/Outlook" />
     <MenuItem Header="Network" x:Name="Menu_NETWORK" ToolTip="Network and server">
        <MenuItem Header="Server" x:Name="Menu_SERVER"><MenuItem.Icon><Image Source="e:/img/icon.server.png"/></MenuItem.Icon></MenuItem>
     </MenuItem>
     <MenuItem Header="Info" x:Name="Menu_INFO" ToolTip="Info (read-only) tools for users/machines" />
     <MenuItem Header="WMI" x:Name="Menu_WMI" ToolTip="Windows Management Interface" />
     <MenuItem Header="UserTools" x:Name="Menu_USERTOOLS" ToolTip="Shortcuts installed on \\$machine\c:\Users\$username\Desktop" />
     <MenuItem Header="Beta" x:Name="Menu_BETA" ToolTip="New/experimental scripts" />
     <MenuItem Header="_Extras" x:Name="Menu_EXTRAS" ToolTip="Config, Miscellaneous, Shortcuts">
        <MenuItem Header="Misc" x:Name="Menu_MISC"><MenuItem.Icon><Image Source="e:/img/icon.controller.png"/></MenuItem.Icon></MenuItem>
        <MenuItem Header="Config" x:Name="Menu_CONFIG"><MenuItem.Icon><Image Source="e:/img/icon.tools.png"/></MenuItem.Icon></MenuItem>
        <MenuItem Header="Update" x:Name="Menu_UPDATE"><MenuItem.Icon><Image Source="e:/img/icon.log.png"/></MenuItem.Icon></MenuItem>
        <!--MenuItem Header="TEST scripts" x:Name="Menu_TEST"><MenuItem.Icon><Image Source="e:/img/icon.godzilla.png"/></MenuItem.Icon></MenuItem-->
     </MenuItem>
     <MenuItem Header="✐" x:Name="Menu_EDIT" ToolTip="Script editing" />
     <MenuItem Header="䷰"  HorizontalAlignment="Right" x:Name="Menu_DOCS" ToolTip="w/ blackjack" />
  </Menu>

  <DockPanel x:Name="Ribbon" DockPanel.Dock="Top" Height="80" Style="{StaticResource RibbonGradient}">

    <!-- Computer -->
    <Border Style="{StaticResource RibbonBorder}" Width="170" DockPanel.Dock="Left">
      <DockPanel>
        <!-- Btn -->
        <Label DockPanel.Dock="Bottom" HorizontalAlignment="Center" VerticalAlignment="Bottom" FontWeight="Bold" Foreground="#ff223366" Content="Machine/Hostname"/>
        <Button DockPanel.Dock="Left" x:Name="BtnComputer" Width="64" Height="64" Style="{StaticResource ActionButton}" ToolTip="Computer (←clipbrd)"><Image Source="e:/img/computer.png" Width="64" Height="64" /></Button>
        <DockPanel>
          <!-- Clear/Copy/Ping/ToUser -->
          <WrapPanel DockPanel.Dock="Top">
            <Button x:Name="BtnComputerClr" Style="{StaticResource ActionButton}" ToolTip="Clear" Margin="0,0,5,0"><Image Source="e:/img/clear.png" Width="16" Height="16" Opacity="0.2"  /></Button>
            <Button x:Name="BtnComputerCpy" Style="{StaticResource ActionButton}" ToolTip="Copy" Margin="0,0,10,0"><Image Source="e:/img/copy.png" Width="16" Height="16" Opacity="0.5" /></Button>
            <Button x:Name="BtnComputerPng" Style="{StaticResource ActionButton}" ToolTip="ping" Margin="0,0,5,0"><Image Source="e:/img/ping.png" Width="16" Height="16" Opacity="0.4" /></Button>
            <Button x:Name="BtnComputerUsr" Style="{StaticResource ActionButton}" ToolTip="Get current user →"><Image Source="e:/img/to-user.png" Width="18" Height="16" Opacity="0.8" /></Button>
          </WrapPanel>
          <!-- Input -->
          <ComboBox x:Name="machine" IsEditable="True" Height="22" Width="90" FontSize="13" FontWeight="Bold" />
        </DockPanel>
      </DockPanel>
    </Border>

    <!-- User -->
    <Border Style="{StaticResource RibbonBorder}" Width="175" DockPanel.Dock="Left">
      <DockPanel>
        <Label DockPanel.Dock="Bottom" HorizontalAlignment="Center" FontWeight="Bold" Foreground="#ff223366" Content="User target"/>
        <Button DockPanel.Dock="Left" x:Name="BtnUsername" Style="{StaticResource ActionButton}" ToolTip="User (←clipbrd)"><Image Source="e:/img/user.png" /></Button>
        <DockPanel>
          <!-- Clear/Copy/ToComputer -->
          <WrapPanel DockPanel.Dock="Top">
            <Button x:Name="BtnUsernameClr" Style="{StaticResource ActionButton}" ToolTip="Clear" Margin="0,0,5,0"><Image Source="e:/img/clear.png" Width="16" Height="16" Opacity="0.2"  /></Button>
            <Button x:Name="BtnUsernameCpy" Style="{StaticResource ActionButton}" ToolTip="Copy" Margin="0,0,50,0"><Image Source="e:/img/copy.png" Width="16" Height="16" Opacity="0.5" /></Button>
            <Button x:Name="BtnUsernameCom" Style="{StaticResource ActionButton}" ToolTip="← To primary computer"><Image Source="e:/img/to-user.png" Width="18" Height="16" Opacity="0.2" /></Button>
          </WrapPanel>
          <!-- Input -->
          <ComboBox x:Name="username" IsEditable="True" Height="22" Width="120" FontSize="13" FontWeight="Bold" />
        </DockPanel>
      </DockPanel>
    </Border> 

    <!-- Bulk -->
    <Border Style="{StaticResource RibbonBorder}" Width="135" DockPanel.Dock="Left">
      <DockPanel>
        <DockPanel DockPanel.Dock="Left">
          <Label DockPanel.Dock="Bottom" HorizontalAlignment="Center" FontWeight="Bold" Foreground="#ff223366" Content="Bulk"/>
          <Button DockPanel.Dock="Top" x:Name="BtnBulkimport" Style="{StaticResource ActionButton}"><Image Source="e:/img/csv.png" /></Button>
        </DockPanel>
        <TextBox DockPanel.Dock="Right" x:Name="bulkcsv" AcceptsReturn="True" Height="64" Width="120" FontSize="10" />
      </DockPanel>
    </Border> 

    <!-- Shortcuts -->
    <Border Style="{StaticResource RibbonBorder}" Width="85" DockPanel.Dock="Left">
      <DockPanel>
        <Label DockPanel.Dock="Bottom" HorizontalAlignment="Center" FontWeight="Bold" Foreground="#ff223366" Content="Shortcuts"/>
        <WrapPanel DockPanel.Dock="Top" x:Name="Shortcuts" Width="82" Height="80" />
      </DockPanel>
    </Border> 

    <!-- Unicode -->
    <Border Style="{StaticResource RibbonBorderGreen}" Width="155" DockPanel.Dock="Left">
      <DockPanel>
        <Label DockPanel.Dock="Bottom" HorizontalAlignment="Center" FontWeight="Bold" Foreground="#ff223366" Content="Unicode"/>
        <ScrollViewer DockPanel.Dock="Top"> <WrapPanel x:Name="UnicodeClip" Width="140" Height="66">
          <Button Style="{StaticResource ActionButton}" Content="❏" />
          <Button Style="{StaticResource ActionButton}" Content="✔" />
          <Button Style="{StaticResource ActionButton}" Content="✘" />
          <Button Style="{StaticResource ActionButton}" Content="➜" />
          <Button Style="{StaticResource ActionButton}" Content="➩" />
          <Button Style="{StaticResource ActionButton}" Content="▶" />
          <Button Style="{StaticResource ActionButton}" Content="⏩" />
          <Button Style="{StaticResource ActionButton}" Content="✎" />

          <Button Style="{StaticResource ActionButton}" Content="❍"  />
          <Button Style="{StaticResource ActionButton}" Content="☑" />
          <Button Style="{StaticResource ActionButton}" Content="☒" />
          <Button Style="{StaticResource ActionButton}" Content="⟳" />
          <Button Style="{StaticResource ActionButton}" Content="❎" />
          <Button Style="{StaticResource ActionButton}" Content="☛" />
          <Button Style="{StaticResource ActionButton}" Content="♻" />
          <Button Style="{StaticResource ActionButton}" Content="⚙" />

          <Button Style="{StaticResource ActionButton}" Content="✰" />
          <Button Style="{StaticResource ActionButton}" Content="✱" />
          <Button Style="{StaticResource ActionButton}" Content="☎" />
          <Button Style="{StaticResource ActionButton}" Content="➟" />
          <Button Style="{StaticResource ActionButton}" Content="⚡" />
          <Button Style="{StaticResource ActionButton}" Content="⤷" />
          <Button Style="{StaticResource ActionButton}" Content="❚" />
          <Button Style="{StaticResource ActionButton}" Content="〓" />
          <Button Style="{StaticResource ActionButton}" Content="❯" />
        </WrapPanel> </ScrollViewer>
      </DockPanel>
    </Border> 

    <!-- Clipboard -->
    <Border Style="{StaticResource RibbonBorderGreen}" Width="125" DockPanel.Dock="Right">
      <DockPanel>
        <Label DockPanel.Dock="Bottom" HorizontalAlignment="Center" FontWeight="Bold" Foreground="#ff223366" Content="Clipboard"/>
        <Button DockPanel.Dock="Left" x:Name="BtnClipText" ToolTip="Copy (text)" Width="48" Height="48" Style="{StaticResource ActionButton}"><Image Source="e:/img/clipboard.png" /></Button>
        <Button DockPanel.Dock="Left" x:Name="BtnClipHtml" ToolTip="Copy (colored)" Width="48" Height="48" Style="{StaticResource ActionButton}"><Image Source="e:/img/html.png" /></Button>
        <Button DockPanel.Dock="Top" x:Name="BtnClipFree" ToolTip="Clear" Width="24" Height="20" Style="{StaticResource ActionButton}"><Image Source="e:/img/sweep.png" /></Button>
        <Button DockPanel.Dock="Bottom" x:Name="BtnClipSwap" ToolTip="Last buffer" Width="24" Height="20" Style="{StaticResource ActionButton}"><Image Source="e:/img/back.png" /></Button>
      </DockPanel>
    </Border> 
  </DockPanel>

  <!-- Main -->
  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*" />
      <ColumnDefinition Width="2" />
      <ColumnDefinition Width="5" />
      <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>

    <TabControl TabStripPlacement="Left" Background="White">
      <TabItem Style="{StaticResource MainTab}">
        <TabItem.Header> <StackPanel Orientation="Horizontal"> <Image Source="e:/img/icon.cmd.png" Width="20" Height="20" /> <Label Content="CMD" FontSize="14" FontWeight="Bold" /> </StackPanel> </TabItem.Header>
        <ScrollViewer> <WrapPanel x:Name="Grid_CMD" Background="#ff777a87" /> </ScrollViewer>
      </TabItem>
      <TabItem Style="{StaticResource MainTab}">
        <TabItem.Header> <StackPanel Orientation="Horizontal"> <Image Source="e:/img/icon.user.png" Width="20" Height="20" /> <Label Content="Empirum" FontSize="14" FontWeight="Bold" /> </StackPanel> </TabItem.Header>
        <ScrollViewer> <WrapPanel x:Name="Grid_USER" Background="#ff777a87" /> </ScrollViewer>
      </TabItem>
      <TabItem Style="{StaticResource MainTab}">
        <TabItem.Header> <StackPanel Orientation="Horizontal"> <Image Source="e:/img/icon.powershell.png" Width="20" Height="20" /> <Label Content="Power" FontSize="14" FontWeight="Bold" /> </StackPanel> </TabItem.Header>
        <ScrollViewer> <WrapPanel x:Name="Grid_POWERSHELL" Background="#ff777a87" /> </ScrollViewer>
      </TabItem>
      <TabItem Style="{StaticResource MainTab}">
        <TabItem.Header> <StackPanel Orientation="Horizontal"> <Image Source="e:/img/icon.exchange.png" Width="20" Height="20" /> <Label Content="Exchange" FontSize="14" FontWeight="Bold" /> </StackPanel> </TabItem.Header>
        <ScrollViewer> <WrapPanel x:Name="Grid_EXCHANGE" Background="#ff777a87" /> </ScrollViewer>
      </TabItem>
      <TabItem Style="{StaticResource MainTab}">
        <TabItem.Header> <StackPanel Orientation="Horizontal"> <Image Source="e:/img/icon.info.png" Width="20" Height="20" /> <Label Content="Info" FontSize="14" /> </StackPanel> </TabItem.Header>
        <ScrollViewer> <WrapPanel x:Name="Grid_INFO" Background="#ff777a87" /> </ScrollViewer>
      </TabItem>
      <TabItem Style="{StaticResource MainTab}">
        <TabItem.Header> <StackPanel Orientation="Horizontal"> <Image Source="e:/img/icon.beta.png" Width="20" Height="20" /> <Label Content="Beta" FontSize="14" FontWeight="Bold" /> </StackPanel> </TabItem.Header>
        <ScrollViewer> <WrapPanel x:Name="Grid_BETA" Background="#ff777a87" /> </ScrollViewer>
      </TabItem>
      <TabItem Style="{StaticResource MainTab}">
        <TabItem.Header> <StackPanel Orientation="Horizontal"> <Image Source="e:/img/icon.tools.png" Width="20" Height="20" /> <Label Content="Extras" FontSize="14" /> </StackPanel> </TabItem.Header>
        <ScrollViewer> <WrapPanel x:Name="Grid_EXTRAS" Background="#ff777a87" /> </ScrollViewer>
      </TabItem>
    </TabControl>

    <Grid Grid.Column="2" Width="1" HorizontalAlignment="Stretch" Background="#ff444444" />
    <GridSplitter Grid.Column="3" Width="7" HorizontalAlignment="Stretch" Background="#ff444444" />

    <ScrollViewer Grid.Column="4">
       <TextBlock x:Name="Output" Padding="2"
       FontSize="12" FontFamily="Consolas,Mono" FontWeight="Normal" TextWrapping="Wrap"
       HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
       Foreground="White" Background="#012356"></TextBlock>
    </ScrollViewer>
  </Grid>

  </DockPanel>
</Window>

Added tools/beta/copy_driver.ps1.

























































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# api: multitool
# version: 0.3.7
# title: Copy driver
# description: from X:\Treiber\ to user \Temp
# type: window
# category: powershell
# img: copy
# hidden: 0
# status: untested
# param: machine, driver
#
# ServiceDesk Driver Collection
#  ❏ from X:\drivers
#  ❏ coypied to user \\$machine\C$\Temp\
#


#-- vars
Param(
    $machine = (Read-Host "Computer"),
    $driver = (Read-Host "Driver"),
    $cache_fn = "data/combobox.driver.txt",
    $driver_d = "X:\Drivers",
    $CRLF = "`r`n"
)

#-- update list
if ($driver -match "^-*update-*(list)?$") {
    Write-Host -f Green "❏ updating $cache_fn"
    $r = "update-list"
    ForEach ($fn in GCI $driver_d) {
        $r += "$CRLF$fn"
    }
    $r | Out-File $cache_fn -Encoding UTF8
}

#-- else copy
elseif (Test-Connection -Quiet $machine) {
    md "\\$machine\c$\Temp\$driver"
    robocopy /E /V /B  "$driver_d\$driver" "\\$machine\c$\Temp\$driver"

    Write-Host -f Green "-- Close me window --"
    Start-Sleep -seconds 20
}

Added tools/cmd/cmd_boottime.ps1.



















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# api: multitool
# version: 2.0
# title: Boot time
# description: Ermittle System-Boot-Zeit
# type: psexec
# category: cmd
# img: date.png
# hidden: 0
# key: 3|boot
# src: https://github.com/lazywinadmin/LazyWinAdmin_GUI/blob/master/LazyWinAdmin/LazyWinAdmin.ps1#L159
#
# Now queries via WMI Win32_OperatingSystem ➔ Lastbootuptime
#
# CMD approach:
#  ❏ systeminfo /s $machine
#

Param($machine = (Read-Host "Computer"))


#-- new WMI
$wmi = Get-WmiObject -class Win32_OperatingSystem -computer $machine
$uptime = $wmi.ConvertToDateTime($wmi.Lastbootuptime)
Write-Host "Last boot time: $uptime"

Added tools/cmd/cmd_currentuser.ps1.







































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# api: multitool
# version: 1.0
# title: Current user on PC
# description: Get currently logged on user (via PSLoggedOn.exe)
# type: psexec
# category: cmd
# img: users.png
# hidden: 0
# key: 6|cu|current|currentuser|loggedon|psloggedon
# config: -
#
# Via PsLoggedOn.exe
#  · There are quicker alternatives for Powershell now...
#  · e.g. WMI Win32_user

Param($machine = (Read-Host "Computer"))

Write-Host "PSLoggedOn..."
U:\Tools\PsLoggedon.exe \\$machine

Added tools/cmd/cmd_sfc.ps1.





































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# api: multitool
# version: 1.0
# title: SFC /SCANNOW
# description: scan protected system files (and replace)
# type: window
# category: cmd
# hidden: 0
# key: 5|sfc
# config: -
#
# runs sfc /scannow on remote computer

Param($machine = (Read-Host "Computer"))

Write-Host "Starting SFC /SCANNOW..."
psexec \\$machine sfc /scannow

Read-Host "---END---"

Added tools/exchange/serverchecks.ps1.





































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# api: multitool
# version: 0.5
# title: Exchange health checks
# description: probe various server performance/database stats
# type: inline
# category: exchange
# icon: firewall
# param: exchangeserver
# hidden: 0
# key: m10|exchangetests?
# status: beta
# config: -
#
# Run basic Exchange server health checks
#  ❏ Test-ServiceHealth
#  ❏ Test-Mailflow
#  ❏ Get-MailboxDatabase
#  ❏ Get-MailboxDatabase


Param($server = (Read-Host "exchangeserver"));


#-- conn
Import-ExchangeSession


#-- tests
Write-Host -f Green "❏ Test-ServiceHealth"
Test-ServiceHealth | FL | Out-String | Write-Host

Write-Host -f Green "❏ Test-EcpConnectivity"
Test-EcpConnectivity -ClientAccessServer $server | Out-String | Write-Host

Write-Host -f Green "❏ Test-Mailflow"
Test-Mailflow -Targetmailboxserver $server | FL -Prop * | Out-String | Write-Host

Write-Host -f Green "❏ Get-MailboxDatabase -Status"
Get-MailboxDatabase -Status -Server $server | FT name,server,mounted,replicationtype,recovery -Auto -Wrap | Out-String | Write-Host

Write-Host -f Green "❏ Get-Queue"
Get-Queue -Server $server | FL -Prop PSComputerName,Identity,IsValid,Status,MessageCount,RetryCount,LastError,RiskLevel,IncomingRate,OutgoingRate,PriorityDescriptions,DeferredMessageCount,LockedMessageCount | Out-String | Write-Host








Added tools/extras/eventvwr.ps1.

































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# api: multitool
# version: 0.1
# title: EventVwr
# description: start EventViewer on remote computer
# type: inline
# category: extras
# img: tools.png
# hidden: 1
# noheader: 1
# key: t9|eventvwr
# config: -
#
# End

Start-Process eventvwr.exe -ArgumentList $machine

Added tools/extras/exit.ps1.

































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# api: multitool
# version: 0.0
# title: Exit MultiTool
# description: end WPF multitool
# type: inline
# category: extras
# hidden: 1
# icon: fire.png
# key: x0|x|quit|exit
# keycode: Alt+F4
# config: -
#
#
# End

break 5

Added tools/extras/powershell_info.ps1.







































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# api: multitool
# version: 0.0
# title: PowerShell info
# description: runtime details
# type: inline
# category: misc
# hidden: 1
# icon: powershell
# config: -
#

Write-Host "❏ Host"
($Host |FL | out-string).trim() |write-host

Write-Host "❏ VerTbl"
($PSVersionTable |Ft | out-string).trim() |write-host

Write-Host "❏ [Accels]"
([psobject].Assembly.GetType("System.Management.Automation.TypeAccelerators")::get | fT -auto -wrap | out-string -width 60).trim() | write-host

Added tools/info/DCs.ps1.































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# api: multitool
# version: 1.3
# title: Domain controllers
# description: List DCs
# type: inline
# category: info
# hidden: 0
# key: i1|DCs|domain|controllers|pdc
# config: {}
# 
# Shows list of active domain controlles


Get-ADDomainController -Filter * | FT -Auto -Wrap Name,Enabled,Site,IPV4Address,SslPort,LdapPort,IsReadOnly | Out-String -Width 100

Added tools/info/dhcp.ps1.































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# api: multitool
# version: 1.0
# title: DHCP servers
# description: List domain DHCP servers
# type: cmd
# category: info
# hidden: 0
# key: i2|dhcp|lsdh
# config: {}
# 
# List domain DHCP servers


netsh dhcp show server

Added tools/info/dns.ps1.































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# api: multitool
# version: 0.1
# title: DNS servers
# description: List domain DNS via dsquery
# type: cmd
# category: info
# hidden: 0
# key: i3|dns|dsquery
# config: {}
# 
# Runs `dsquery` to show all DNS servers for domain.


dsquery server -limit 0

Added tools/info/findlocaladmins.ps1.



























































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# api: multitool
# version: 0.5
# title: Detect admins
# description: list local admins on remote machine
# type: inline
# category: info
# img: users.png
# hidden: 0
# key: i13|localadmins
# config: {}
# 
# list local admins on computer
# - via WMI win32_groupuser
# - shortened to DOMAIN\UserName


Param($machine = (Read-Host "Machine"))

Write-Host -f Green "WMI query 'win32_groupuser'..."
$admins = Gwmi win32_groupuser –computer $machine

Write-Host -f Green "Extract account infos"
$admins = $admins |? {$_.groupcomponent –like '*"Administrators"'}
$admins | ? {
    $_.PartComponent -match 'Domain="(.*)",Name="(.*)"'
} | % { 
    Write-Host -f Yellow "♞ $($matches[1])\$($matches[2])"
}

Added tools/info/fu.ps1.









































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# api: multitool
# version: 1.4
# title: Find user 
# description: Performs an AD search for phone numbers or employee numbers
# type: inline
# category: info
# hidden: 0
# icon: finduser
# key: i4|fu|f|find|find-?user
# keycode: F6
# shortcut: 4
# config: -
# 
# Scans AD user list for all phone numbers, or for employee IDs.
#
#  → The graphical WPF-MultiTool has a simpler version built into
#    the `User` field/dropdown already (- only scans for the main
#    telephone number though).
#
#  → Should use Out-GridView or Format-Table per config option.
#    (Not reimplemented yet for GUI or CLI version).


Param($find = (Read-Host "User"));

# adapt for LIKE
if ($find -match "%") {        # SQL placeholders
    $find = $find -replace "%","*"
}
if ($find -notmatch "\*") {
    if ($find -match "[A-Z]{3,}") {   # usernames
        $find = "*$find*"
    }
    else {                         # numbers only
        $find = "*$find"
    }
}

# search
$ls = (Get-ADUser -Filter {
  (TelephoneNumber -like $find) -or (MobilePhone -like $find) -or
  (employeeNumber -like $find) -or (homePhone -like $find) -or (displayname -like $find)
} -Properties samaccountname,displayname,telephoneNumber,mobilePhone,homePhone,employeeNumber |
Select-Object samaccountname,displayname,telephonenumber,mobilephone,homePhone,employeeNumber)
                      
# output
if ($cfg.gridview -match "GridView") {
    $ls | Out-GridView
}
else {
    $ls | Format-Table -Auto -Wrap | Out-String -Width 120
}

Added tools/info/get_rdp_perm.ps1.

































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# api: multitool
# version: 0.3
# title: get RDP permissions
# description: on remote PC
# type: inline
# category: info
# img: registry
# hidden: 0
# 
# SYSTEM\CurrentControlSet\\Control\\Terminal Server\fDenyTSConnections


$v = Get-RemoteRegistry("\\$machine\HKLM\SYSTEM\CurrentControlSet\\Control\\Terminal Server\fDenyTSConnections")
Write-Host "RDP = $(!$v)"


Added tools/info/lockedusers.ps1.

































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# api: multitool
# version: 1.0
# title: Locked out users
# description: Find locked out user names
# type: inline
# category: info
# icon: user
# hidden: 0
# key: i5|lock|locked|lockedout|ll
# config: {}
# 
# Scans AD for locked-out accounts


Search-ADAccount -LockedOut | Select samaccountname, name

Added tools/info/minicmdlets.ps1.

















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# api: multitool
# version: 0.1
# title: mini tools
# description: Small PS cmdlet collection
# type: inline
# category: info
# hidden: 0
# param: machine,username,minicmdlets
# config: {}
# 
# Various functions


Param(
    $machine = (Read-Host "Machine"),
    $user = (Read-Host "User"),
    $cmd = (Read-Host "minicmdlets")
)

#-- run
Write-Host -f Green "❏ $cmd"
$cmd = $cmd -replace '\$(machine|host|computer)',"'$machine'"
$cmd = $cmd -replace '\$(username|user|account)',"'$user'"
(Invoke-Expression $cmd | Out-String).trim() | Write-Host

Added tools/info/netuser.ps1.

































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# api: multitool
# version: 0.1
# title: NET USER
# description: Get NET USER details
# type: inline
# category: info
# hidden: 0
# key: i6|netuser
# config: {}
# 
# NET USER


Param($username = (Read-Host "USer"))

NET USER $username /DOMAIN

Added tools/info/printserver.ps1.







































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# api: multitool
# version: 1.1
# title: Printservers
# description: scans AD for print servers
# type: inline
# category: info
# hidden: 0
# key: i7|print|print-?se?rve?r?
# config: {}
# 
# scans AD for print servers


$ls = Get-ADObject -LDAPFilter "(&(&(&(uncName=*)(objectCategory=printQueue))))" -Prop * |
        Sort-Object -Unique -Property servername |
        Select servername

$ls | Format-Table -Auto -Wrap

Added tools/info/service.ps1.





































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# api: multitool
# version: 0.2
# title: Services
# description: list services on remote machine
# type: inline
# category: info
# hidden: 0
# key: i8|service|services
# config: {}
# 
# list services on remote machine


Param($machine = (Read-Host "Machine"))

Get-Service -computer $machine | Select-Object status,name,description | FT


Added tools/network/nslookup.ps1.

























>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
# api: multitool
# version: 0.1
# title: NSlookup
# description: nameserver lookup
# type: inline
# category: network
# key: n2|tracert
# config: -
#
# nslookup

nslookup $machine 

Added tools/network/ping.ps1.



























>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
# api: multitool
# version: 0.1
# title: Ping
# description: Ping computer
# type: inline
# category: network
# key: n1|ping
# config: -
#
# Ping

ping $machine -n 3

Added tools/network/tracert.ps1.

























>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
# api: multitool
# version: 0.1
# title: Tracert
# description: Traceoute to computer
# type: inline
# category: network
# key: n3|tracert
# config: -
#
# Ping

tracert $machine 

Added tools/plugins/configedit.ps1.

















































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# api: multitool
# version: 0.4
# title: Config file
# description: Create and edit config file
# type: inline
# category: config
# hidden: 1
# key: config|cfg
# img: tools
# config: -
#
#
# Create/load main config file


#-- init
Param($fn="$env:APPDATA\multitool\config.ps1", $options=@(), $overwrite=0, $CRLF="`r`n", $EDITOR="notepad")

# create parent dir
if ($overwrite -or !(Test-Path $fn)) {
    $dir = Split-Path $fn -parent
    if ($dir -and !(Test-Path $dir)) { md -Force "$dir" }
}

#-- read file
if (Test-Path $fn) {
    $src = (Get-Content $fn) | Out-String
}
else {
    $src = "# type: config$CRLF# fn: $fn$CRLF$CRLF"
}

#-- fetch options from all plugins
$options = @($menu | ? { $_.config -and $_.config.count } | % { $_.config })
#-- and main
$options += (Extract-PluginMeta "./modules/starter.ps1").config
$options += (Extract-PluginMeta "./modules/menu.psm1").config
$options += (Extract-PluginMeta "./modules/wpf.psm1").config

#-- assemble defaults
$options | % {
    $v = $_.value
    switch ($_.type) {
        bool { $v = @('$False', '$True')[[Int32]$v] }
        default { $v = "'$v'" }
    }
    if ($src -notmatch "(?mi)^[$]$($_.name)") {
        $src += '$' + $_.name + " = " + $v + "; # $($_.description)$CRLF"
    }
}

# (over)write
$src | Out-File $fn -Encoding UTF8

#-- start notepad
& $EDITOR "$fn"

Added tools/plugins/funcs_base.ps1.













































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# encoding: utf-8
# api: ps
# title: functions
# description: Some utility functions for scripts
# version: 1.0
# type: init
# category: misc
# hidden: 1
# priority: optional
#
# Defines:
#  · PSExec                  → wrap psexec.exe
#  · Check-PSExecResult      → eval $LASTEXITCODE
#  · Invoke-ExchangeCommand  → simpler remoting
#  · Open-RemoteRegistry     → -hive LocalMachine -machine A0004915 -path SOFTWARE
#


#-- check $LASTEXITCODE from psexec invocations
function Check-PSExecResult {
    Param($err=$LASTEXITCODE)
    switch ($err) {
        0 { Write-Host -f Yellow "✔ okay" }
        1 { Write-Host -f Yellow "✔ okay" }
        2 { Write-Host -f Red "✘ failed" }
        default { Write-Host -f Gray "➗ Exitcode=$err" }
    }
}
#-- might as well define a PSExec wrapper then...
function PSExec {
    [CmdletBinding()]
    Param($machine, [Parameter(ValueFromRemainingArguments=$true)]$cmd)
    $cmd = (($cmd | % { if ($_ -match ' ') { '"'+$_+'"' } else { $_ }}) -join ' ')

    #Write-Host -f DarkYellow "❏ PSExec($machine $cmd)"
    Invoke-Expression "& PSExec.exe $machine $cmd 2>&1" | Select -Skip 3 | Write-Host -b DarkGray
                                                   # 
    [void](Check-PSExecResult $LASTEXITCODE)
}


#-- default session (just started once, then kept in memory)
function Import-ExchangeSession {
    $params = $cfg.exchange
    if (! (Test-Path function:Get-Mailbox)) {
        $null = ( Write-Host -f Green "❏ Exchange connection..." )
        $global:Exchange_Session = New-PSSession @params
        $null = Import-PSSession -Session $global:Exchange_Session
    }
    else {
        $null = ( Write-Host -f DarkGray "✔ Exchange conn active." )
    }
}

#-- via WMI _ComputerSystem or looking up Owner of Explorer _Process
function Get-CurrentUser {
    Param($machine)
    if (($W = GWMI win32_computersystem -comp $machine) -and ($W.username)) {
        $r = $W.username
    }
    elseif ($W = GWMI Win32_Process -ComputerName $machine -filter "name='Explorer.exe'") {
        $r = ($w | % { $_.getOwner().user } | ? { $_ -notmatch "^SYSTEM$" })
        if ($r -is [array]) { $r = $r[0] }
    }
    else {
        $r = "nobody"
    }
    return $r -replace "^\w+\\(?=\w+)",""
}

#-- via WMI _userAccount
function Get-UserSID {
    Param($user)
    ([WMI]"win32_userAccount.Domain='$($cfg.domain)',Name='$user'").SID
}


#-- remote registry
function Open-RemoteRegistry {
    <#
    .SYNOPSIS
        Open remote registry tree
    .DESCRIPTION
        Establish a Win32.Registry connection to remote machine. Does not retrieve Leafes/Values itself.
    .PARAMETER  Path
        Can either be a full path such as "\\HOSTNAME\HKLM\SOFTWARE\Windows"
        Or just the regpath "SOFTWARE\Windows" when both -Hive and -Machine are given
    .PARAMETER  Hive
        If no full -Path given, should name "HKLM", "HKCR", or "HKCU" (current user is looked up automatically).
    .PARAMETER  Machine
        If no full -Path given, lists the remote hostname to connect to.
    .EXAMPLE
        $R = Open-RemoteRegisty "\\localhost\HKLM\SW\WindowsCurrentControlSet"
        $R = Open-RemoteRegisty -Machine "localhost" -Hive "HKLM" -Path "SW\WindowsCurrentControlSet"
    #>
    Param(
        $path = $null,            # preferred: full specifier "\\HOSTNAME\HKLM\RegPath" (host+hive+path; no leaf/value)
        $hive = "LocalMachine",
        $machine = $null,
        $writemode = $true,
        [switch]$silent = $false
    )
 
    #-- combine path if it starts with "\\" two backslashes
    if ((!$machine) -and ($path -match "^\\\\(\w+)\\(\w+)\\(.+)$")) {
        $machine = $matches[1]
        $hive = $matches[2]
        $path = $matches[3]
    }

    #-- hive aliases
    $hive = switch -regex ($hive) {
        ".*LM" { "LocalMachine" }
        ".*CR" { "ClassesRoot" }
        ".*CU" {
             $path = (Get-UserSID (Get-CurrentUser $machine)) + "\" + $path
             [Microsoft.Win32.RegistryHive]::Users
        }
        ".*USERS" { [Microsoft.Win32.RegistryHive]::Users }
    }
    if (!$hive) {
        $hive = "LocalMachine"
    }

    #-- open
    if (!$silent) {
        $null = ( Write-Host -f DarkGray  "❏ Remote registry connection [$machine\$hive]..." )
    }
    $R = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey("$hive", $machine)
    if (!$silent) {
        $null = ( Write-Host -f DarkGray  "❏ Open subkey [$path]..." )
    }
    return $R.OpenSubKey($path, $writemode)
}

#-- shortcuts
function Set-RemoteRegistry {
    Param($path = "\\localhost\HKLM\SOFTWARE\Etc\Key", $value="", $type="String")
    if ($path -match "^\\\\(\w+)\\(\w+)\\(.+)\\([^\\]+)$") {
        $R = Open-RemoteRegistry -machine $matches[1] -hive $matches[2] -path $matches[3] -Silent
        $R.setValue($matches[4], $value, $type)
    }
}
function Get-RemoteRegistry {
    Param($path = "\\localhost\HKLM\SOFTWARE\Etc\Key")
    if ($path -match "^\\\\(\w+)\\(\w+)\\(.+)\\([^\\]+)$") {
        $R = Open-RemoteRegistry -machine $matches[1] -hive $matches[2] -path $matches[3] -writemode $false -Silent
        return $R.getValue($matches[4])
    }
}

Added tools/plugins/funcs_json.ps1.















































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# encoding: utf-8
# api: ps
# title: JSON functions
# description: JSON decoder for PS 2.0
# version: 0.4
# type: init
# category: misc
# hidden: 1
# priority: optional
#
# Defines:
#  · Convert-FromJSON20

#-- deserialize JSON to hashtable/array
function Convert-FromJSON20 {
    Param($json)
    try { 
        $void = [System.Reflection.Assembly]::LoadWithPartialName("System.Web.Extensions")
        $parser = New-Object System.Web.Script.Serialization.JavaScriptSerializer
        #$obj = New-Object PSObject -Property 
        $obj = $parser.DeserializeObject($json)
    }
    catch {
        $obj = @{}
    }
    return $obj
}

#-- converts nested hashtables/dictionaries/psobjects to string (visual tree indenting)
function Out-Struct {
    Param($obj, $SPC = "")
    ForEach-KV $obj {
        Param($k,$v)
        if ($v -is [string] -or $v -is [int]) {
            "$SPC➜ $k = $v "
        }
        else {
            "$SPC➩ $k"
            Out-Struct $v "  $SPC"
        }
    }
}

#-- iterate over dicts/objects/arrays using scriptblock with Param($k,$v)
function ForEach-KV {
    Param($var, $cb, $i=0)
    switch ($var.GetType().Name) {
        Array          { $var | % { $cb.Invoke($i++, $_) } }
        HashTable      { $var.Keys | % { $cb.Invoke($_, $var[$_]) } }
       "Dictionary``2" { $var.Keys | % { $cb.Invoke($_, $var.Item($_)) } }
        PSobject       { $var.GetIterator() | % { $cb.Invoke($_.Key, $_.Value) } }
        PSCustomObject { $var.GetIterator() | % { $cb.Invoke($_.Key, $_.Value) } }
        default        { $cb.Invoke($i++, $_) }
    }
}

Added tools/plugins/funcs_remoting.ps1.





































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# api: multitool
# version: 0.6
# title: Remoting funcs
# description: Start-SchedTask, WMI ScheduledJob, PSRemoting
# type: init
# category: functions
# status: TESTING
# hidden: 0
# config: -
# 
# Fail:
#  - psexec does not allow interactive apps in foreground even with -i and session id (from WMI/win32_process)
#  - Win32_ScheduledJob cannot be run immediately; requires latency timeing and timestamp crafting
#
# Working approach:
#  ❏ schtasks.exe /Create /S A0004915 /TN TeamViewer /RU DOMAIN\TestAccount
#    /IT /TR "'C:\Program Files\TeamViewerQS\Team ViewerQS-idcm7ct8bq.exe'"
#    /SC ONCE /SD 05/05/2022 /ST 23:59 /F
#  ❏ schtasks /run /S A0004915 /TN TeamViewer
#


#-- run as scheduled job
#
function Start-SchedTask {
    Param(
        $machine = "TESTHOST",
        $cmd = "'cmd.exe'",           # Note the double string context quoting "'...'" for paths with spaces!
        $taskname = "psonce"
    )

    #-- ping
    if (!(Test-Connection $machine -Quiet -ErrorAction SilentlyContinue)) {
        Write-Host -f Red " $machine offline"
        return
    }

    #-- current user
    $user = GWMI win32_ComputerSystem -ComputerName $machine | select -expand username
    Write-Host -f Yellow "❏ $user on $machine"

    #-- sched task
    Write-Host -f Yellow "❏ scheduling $cmd"
    schtasks.exe /Create /S $machine /TN $taskname /RU "$($cfg.domain)\$username" /IT /TR "$cmd" /SC ONCE /SD 05/05/2022 /ST 23:59 /F

    Write-Host -f Yellow "❏ starting task..."
    schtasks.exe /run /S $machine /TN $taskname
}

    
#-- check for session id (`QUSER` doesn't work)
#
function Get-RemoteSessionID {
    Param(
        $machine
    )
    $sid = Get-WmiObject Win32_Process -ComputerName $machine -Filter 'Name="explorer.exe"' | Select -First 1 -Expand SessionId
    if ($sid) {
        return $sid
    }
    else {
        Write-Host -f Red "✘ No user desktop session found; assuming default 1"
        return 1
    }
}

Added tools/plugins/init_env.ps1.

































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# api: ps
# title: initialize $ENV
# description: predefines some Powershell and system environment variables
# version: 0.5
# type: init
# category: misc
# hidden: 1
# priority: core

#-- powershell
#$Debug = $true
#$ErrorActionPreference = "SilentlyContinue"
#$DebugPreference = "SilentlyContinue"
#$ProgressPreference = "Continue"
#$VerbosePreference = "SilentlyContinue"
$WarningPreference = "Continue"
#$WhatIfPreference = $False
#$ConfirmPreference = "High"
$OFS=" "

#-- environment
if (!$ENV:EDITOR) {
    $ENV:EDITOR = "notepad"
}
if (!$ENV:XDG_CONFIG_HOME) {
    $ENV:XDG_CONFIG_HOME = $ENV:APPDATA
}

#-- create config dir
if (!(Test-Path ($cfg.user_plugins_d))) {
    md ($cfg.user_plugins_d)
}

Added tools/plugins/init_intro.ps1.





































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# api: ps
# title: init screen
# description: writes something on the Output window on startup
# version: 0.0.3
# type: init-gui
# category: misc
# hidden: 1
# priority: core

if ($GUI.w -and !$CLI) {

    # combine version of all scripts
    $sigma_ver = ($menu | % { $_.version } | ? { $_ -match "\d" } | % { $_ -replace "-.+$","" -replace "\D","" } | Measure -Sum).Sum -replace "(?<=\d)(?!$)","."

    # output
    Out-Gui -f Yellow -b "#223388" "ClickyColoury frontend to Multi-Tools  Σ ≈ $sigma_ver"
    Out-Gui -f "#88bb22" " ✉ Color clipboard enabled (➤HTML icon)"
}

Added tools/plugins/menu_editscripts.ps1.



































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# api: multitool
# type: init-gui
# version: 0.5
# title: --- edit ---
# description: edit multitool tools/ and main script, modules
# hidden: 1
# category: edit
# config: {}
#
# Adds to Config > Edit scripts menu

if ((!$CLI) -and (!$e) -and ($GUI.w)) {

    $wm = $GUI.w.findName("Menu_EDIT").Items
    $submenus = @{}

    #-- prepend main scripts
    $add = @(
       @{fn = ".\modules\starter.ps1"}
       @{fn = ".\modules\wpf.psm1"},
       @{fn = ".\modules\wpf.xaml"}
       @{fn = ".\modules\menu.psm1"}
       @{fn = ".\modules\clipboard.psm1"}
    )

    #-- add edit entries
    ($add+$menu) | ? {$_.fn} | Sort-Object {$_.fn} | % {

        #-- dir and path
        if (($_fn = $_.fn) -match "([.\w]+)[\\//]([^\\//]+)$") {
            $dir = $matches[1] -replace "tools\.",""
            $fn = $matches[2]
        }
        else {
            continue
        }

        #-- find/add dir submenu
        if ($m = $submenus[$dir]) {
        }
        else {
            $m = W MenuItem @{Header=$dir}
            $submenus[$dir] = $m
            $wm.Add($m)
        }
        
        $m.Items.Add((W MenuItem @{Header="_$fn"; Add_click={notepad "$_fn"}.getnewclosure()}))
    }
}

Added tools/plugins/update_cache_adsearch.ps1.



















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# api: multitool
# version: 0.1
# title: update ADSearch.txt
# description: Update ADSearch cache file
# type: inline
# category: update
# hidden: 1
# img: log
# status: obsolete
#
# Update data\adsearch.txt
#
#  - used by WPF multiTool user search
#  - is a plain line-wise text file of format:
#    "ADUser | Name, USer | +1-234-567890"
#

$cache_fn = ".\data\adsearch.txt"

echo "Get-ADUser ... > $cache_fn"

Get-ADUser -Filter * -Properties SAMAccountName,DisplayName,TelephoneNumber | % {
   "{0} | {1} | {2}" -f @($_.SAMAccountName, $_.DisplayName, $_.TelephoneNumber)
} | Out-File $cache_fn -Encoding UTF8

Added tools/test/test_htmlclip.ps1.

























>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
# api: multitool
# title: test html clip
# description: Set-clipboardHtml
# version: 0.2
# category: test
# type: inline
#
# testy test

$email ="test@test"
$user = "user123"
Set-ClipboardHtml "<p><font style='color:green'>&#x2714;➟</font> The mailbox &lt;<font style='color:blue'>$email</font>&gt; for the account <b style='color:#553311'>$user</b> has been created.</p>"

Added tools/test/test_ping.ps1.



















>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
# api: multitool
# type: inline
# category: test
# title: ping
# description: localhost
# version: 0.1

ping $machine

Added tools/test/test_processes.ps1.























>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
# api: multitool
# version: 0.1
# title: processes
# description: lists local machine tasks
# type: inline
# category: test
# hidden: 1
#
# Dummy test script

Get-Process

Added tools/test/test_readhost.ps1.





















>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
# api: multitool
# type: inline
# category: test
# title: Read-Host
# description: runs the Ask-Gui() wrapper
# version: 0.1

Read-Host "User"  # should get the $username variable without prompt
Read-Host "Else"  # should pop up a message box

Added tools/test/test_vars.ps1.



































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# api: multitool
# title: test vars
# description: $GUI.vars and Get-ParamVarsCmd interpol
# version: 0.0
# param: machine, username, field1, field2
# category: test
# type: window
#
# testy test

param($mach=0, $user=0, $field1=0, $field2=0)

Write-Host "host=$mach"
Write-Host "user=$user"
Write-Host "f1=$field1"
Write-Host "f2=$field2"
Read-host "Wait"

Added tools/test/test_writehost.ps1.





















>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
# api: multitool
# type: inline
# category: test
# title: Write-Host
# description: test color output
# version: 0.1

"Red,Green,Yellow,Black,Orange".split(",") | % { Write-Host $_ -f $_ }


Added tools/wmi/diskspace.ps1.





































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# api: multitool
# version: 0.1
# title: Disk space
# description: List logicaldisks / free space
# type: inline
# category: wmi
# hidden: 0
# key: w3|service|services
# config: {}
# 
# list services on remote machine


Param($machine = (Read-Host "Machine"))

Get-WmiObject win32_logicaldisk -computer $machine | FL


Added tools/wmi/network.ps1.











































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# api: multitool
# version: 0.4
# title: Network adapters
# description: Scans remote PC via WMI win32_networkadapters
# type: inline
# category: wmi
# tag: proxy
# hidden: 0
# key: w4|network|network-?adapters|ada?pt[ers]*|nwa
# config: {}
# 
# Useful for detecting parallel LAN and WLAN connections.
#
#  → proxy issues
#


Param($machine = (Read-Host "Machine"));

$adapters = Get-WMIObject win32_networkadapter -filter "netconnectionstatus=2" -ComputerName $machine
Format-List -InputObject $adapters -Property NetConnectionID,Name,MACaddress,ServiceName,InterfaceIndex

Added tools/wmi/sid.ps1.



































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# api: multitool
# version: 1.0
# title: [WMI] $user
# description: Show SID and other AD user properties
# type: inline
# category: wmi
# hidden: 0
# key: w1|sid|wmi_user|userid
# config: {}
# 
# Get detailed user info (such as SID for AD name) via WMI query.


Param($user = (Read-Host User))

[wmi] "win32_userAccount.Domain='$($cfg.domain)',Name='$user'" | Format-List -Prop *