Powershell GUI fronted (WPF) to run categorized console scripts

⌈⌋ branch:  ClickyColoury


Check-in [59be64686c]

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

Overview
Comment:More abstractions for standard variable names (GUI widget mappings and default callbacks for Add-ButtonHooks). Renamed and resorted input variable functions, mostly using "Vars" now instead of "Params". Support for old $meta.param field removed from Get-GuiExtra.. Mapping now in central Get-ScriptVars. Shortens Run-GuiTask.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1:59be64686c7fcc03381b808ed73b1c9ad2d6b37a
User & Date: mario 2018-04-11 20:02:34
Context
2018-04-11
20:03
bump version check-in: bf2db273a8 user: mario tags: trunk
20:02
More abstractions for standard variable names (GUI widget mappings and default callbacks for Add-ButtonHooks). Renamed and resorted input variable functions, mostly using "Vars" now instead of "Params". Support for old $meta.param field removed from Get-GuiExtra.. Mapping now in central Get-ScriptVars. Shortens Run-GuiTask. check-in: 59be64686c user: mario tags: trunk
15:37
Fix [adsisearcher] ldap expression to omit disabled users; also prevent multiple rows from being returned again (-only 1). Readd |Out-String prior default |Out-Gui pipe; because objects/lists in $str might not transfer over Dispatch-GUI after all. check-in: 3ee7c5a5fd user: mario tags: trunk
Changes

Changes to modules/guimenu.psm1.

53
54
55
56
57
58
59



















60
61
62
63
64
65
66
...
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
...
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
...
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
...
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
...
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
...
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
...
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
...
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
...
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
...
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
...
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008

#-- init a few local vars
#   - $GUI, $last_output etc. get created in Start-Win
#   - New-GuiThread inherits $menu,$cfg,$plugins from global
$ModuleDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$BaseDir = Split-Path -Parent $ModuleDir





















#-- WPF/WinForms widget wrapper
function W {
    <#
      .SYNOPSIS
         Convenient wrapper to create WPF or WinForms widgets.
      .DESCRIPTION
................................................................................
    $xaml = (Get-Content "$BaseDir/modules/$main" | Out-String)
    $xaml = $xaml -replace "e:/","$ImgDir/"
    if (Test-Path ($fn = "modules/theme.$theme.xaml")) { $xaml = $xaml -replace "modules/theme.\w+.xaml", $fn }
    $w = [Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader ([xml]$xaml)))

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

    #--- return
    return $w
}


................................................................................
      .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({  })

    #-- other fields
    #

    #-- clipboard tools
    $GUI.w.findName("BtnClipText").add_Click({
................................................................................

     END {
         Dispatch-GUI ([action]{ [void]$GUI.output.Parent.ScrollToEnd() })
     }

}


#-- Read standard input fields
#  · this is a workaround, because accessing $GUI.machine.text
#    directly from parent runspace would hang up WPF
#  · for some reason also needs its hashtable recreated
function Get-StandardGuiVars {
    Dispatch-Gui ([action]{
        $GUI.vars = @{
            machine = "$($GUI.machine.Text)"
            username = "$($GUI.username.Text)"
        }
    })

}

#-- just -Title update
function Set-GuiTitle($title) {
    Dispatch-Gui ([action]{ $GUI.w.title = $title })
}

................................................................................
    $GUI.html = ""
    Dispatch-Gui ([action]{
        $GUI.prev_output = (Get-OutputTextRange).Text
        $GUI.output.blocks.clear()
    })
}



#-- invoke [action] callback via main window dispatcher
#   PowerShell >= 3.0 does need the parameters swapped
#   @src: https://gallery.technet.microsoft.com/Script-New-ProgressBar-329abbfd
#
function Dispatch-Gui($action, $args=@()) {
    #$GUI.w|GM|Out-String|Write-Host -f Green

    if ($PSVersionTable.PSVersion.Major -eq 2) {
        [void]$GUI.w.Dispatcher.Invoke("Normal", $action)
    }
    else {
        [void]$GUI.w.Dispatcher.Invoke($action, "Normal") #, $args)
    }
}


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

................................................................................
          @{name="bulkcsv"; type="text"; value="flag,user,group"; description="CSV input"}
          @{name="pick"; type="select"; select="a|b|c|d"; value="c"; description="Select test"}
       )},
       $height = 450, $CURRENT_STORE=$GUI.vars_previous,
       $vars = @{}, $btns = @(), $name2type = @{},
       $results = @{}, $proceed = $false, $SELECT_FN = "./data/combobox.{0}.txt"
    )

    #-- params
    if ($meta.param -and -not $meta.vars) {
        if ($meta.param -is "string") {
            $meta.param = $meta.param.trim() -split "\s*[,;]\s*"
        }
        $meta.vars = $meta.param | % {
            if (Test-Path ($SELECT_FN -f $_)) { $t = "select" } else { $t = "str" }
            @{name=$_; description="..."; type="$t"; value=$GUI.vars[$_]}
        }
    }
    if (!$meta.vars) { return }

    #-- input widgets
    $var_widgets = $meta.vars | ? { $_.name } | % {
         $PARAM = $_
         $NAME = $PARAM.name
         $ALIAS = Get-ParamAlias $NAME
         $VALUE = $PARAM.value
         $name2type[$NAME] = $PARAM.type
		 
         # previous values
         if (($v = $CURRENT_STORE[$NAME]) -or ($v = $GUI.vars[$ALIAS])) {
             $VALUE = $v
         }
................................................................................
                W TextBox @{ Height=120; Width=280; Text=$VALUE+"`r`n"; AcceptsReturn=1 }
            }
            "select|combo" {
                if ($PARAM.select) { $ls = $PARAM.select.split("[,;|]") }
                elseif (Test-Path ($SELECT_FN -f $NAME)) { $ls = Get-Content ($SELECT_FN -f $NAME) }
                else { $ls = @("Null") }
                if (!$VALUE) { $VALUE = $ls[0] }
                W ComboBox @{Height=20; Width=260; IsEditable=$true; Text=$VALUE; ItemsSource=$ls }
            }
            "btn|button|action" {
                W Button @{Content=$PARAM.description; Add_Click={$results[$NAME] = $results._proceed = 1; $w.Close()}}
                #$btns += ...;continue;
            }
            "str|int|^$" { 
                W TextBox @{Height=20; Width=270; Text=$VALUE}
................................................................................
    if ($results._proceed) {
        #$results|FL|Out-String|Write-Host
        $results.getEnumerator() | % { $CURRENT_STORE[$_.Name] = $_.Value }
        return $results
    }
}

#-- Parameter/varname aliasing
function Get-ParamAlias {
    <#
      .SYNOPSIS
         Aliases like "Computer" for $machine variable
      .DESCRIPTION
         Allows flexible title matching for standard/internal/default variables.
         Which permits scripts to list arguments by different monikers. For instance
         Read-Host '$computer-name:' would still match `$machine`.
    #>
    Param($name)
    $aliases = @{
        username = "^(?i)[$]?(AD[-\s]?)?(User|Account)(?:[-\s]?Name)?\s*[=:?]*\s*$"
        machine =  "^(?i)[$]?(Computer|Machine|PC|Host)([-\s]?Name)?\s*[=:?]*\s*$"
        #bulkcsv = "^(?i)[$]?(Bulk|bulk.?csv|bulkfn|list|CSV)" # obsolete
    }
    ForEach ($key in $aliases.keys) {
        if ($name -match $aliases[$key]) { return $key }
    }
    return $name;
}

#-- 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 {
................................................................................
         Textual input query. Usually just the variable name '$computer' or '$user'.
         Recognizes common aliases like 'Machine' 'PC' 'Hostname' 'AD-Account' or 'AD-User-Name'
      .NOTES
         Scripts should preferrably query for input in their Param() section once,
         and list custom fields per plugin meta #param: list. NEW: per #vars: block.
    #>
    param($name, $title="Input", [switch]$N=$false)
    $alias = Get-ParamAlias $name
    if ($GUI.vars.containsKey($alias)) {
        return $GUI.vars[$alias]
    }
    else {
        #-- undefined/non-Param() input request / stray Read-Host calls
        $results = Read-GuiExtraParams @{
            title="Input"; description="Read-Host"; doc="stray Read-Host call from script";
................................................................................
            api="clicky"; type="io"; category="extra"; version="1.1";
            vars=@(@{name=$name; type="str"; description="$name"; value=""})
        }
        if (!$results) { throw "❎ Cancelled at Read-Host request." }
        return $results[$name]
    }
}


















#-- 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
................................................................................
        $meta_params = @(),    # meta.vars[] list of dicts
        $vals = @{machine=""; username=""},  # from $GUI.vars[]
        $out = ""
    )

    ForEach ($key in ($meta_params | % { $_.name })) {
        # aliases
        $key = Get-ParamAlias $key
        # quote + append
        $out += ' "'+($vals[$key] -replace '([\\"^])','^$1')+'"'
    }
    return $out
}


























#-- print header/title
function Out-GuiHeader($e) {
   #$Host.UI.Write("TITLE`r`n")
   Set-GuiTitle "➱ $($cfg.main.title) → $($e.title)"
   if ($e.noheader -or $cfg.noheader) { return; }
   $Host.UI.Write("HDR`r`n")
   $XAML = @"
      <Paragraph xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
       Foreground="#ff9988dd" Background="#ff102070" Margin="0,3">
         <Image Source='$(Get-IconPath $e.icon $e.img $e.category)' Width='16' Height='16' />
................................................................................

    #-- check last output time (>= 5 minutes per default)
    if ($last_output -and (([double]::parse((Get-Date -u %s)) - $last_output) -ge $cfg.autoclear)) {
        Clear-GuiOutput
    }

    #-- get vars (fetch input from "Ribbon"-fields: machine, username, etc)
    Get-StandardGuiVars
    if ($e.param -or $e.vars -and -not ($e.vars | ? {$GUI.vars.keys -contains (Get-ParamAlias $_.name)})) {
        $add = Read-GuiExtraParams $e
        if ($add) { $add.getEnumerator() | % { $GUI.vars[$_.Name] = $_.Value } }
        else { Write-Host -f Red "Cancelled."; return; }        
    }
    $GUI.vars.GetEnumerator() | % { Set-Variable $_.name $_.value -Scope Global }

    #-- print header
    Out-GuiHeader $e

    #-- plugins
    TRAP { $_ | out-gui -b red }
................................................................................
        }
        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.vars) ($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 -Width 120 | Out-Gui
            }
        }
        #-- No handler







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







 







|







 







>
>
>
>
>
>
>
>
>



|






<
<





|












<
<







 







|
|
|
|
|
|
|
|
|
|
|
|
>







 







|
|
|
|
|
|
|
|
|
|
|
<
<
<
<







 







<
<
<
<
<
<
<
<
<
<
<






|







 







|







 







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







 







|







 








>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

<
>







 







|






>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>



<







 







<
<
<
<
|
<







 







|







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
...
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
...
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
...
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
...
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
...
744
745
746
747
748
749
750











751
752
753
754
755
756
757
758
759
760
761
762
763
764
...
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
...
843
844
845
846
847
848
849





















850
851
852
853
854
855
856
...
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
...
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
...
994
995
996
997
998
999
1000




1001

1002
1003
1004
1005
1006
1007
1008
....
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031

#-- init a few local vars
#   - $GUI, $last_output etc. get created in Start-Win
#   - New-GuiThread inherits $menu,$cfg,$plugins from global
$ModuleDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$BaseDir = Split-Path -Parent $ModuleDir

#-- standard UI input widgets and handler names
$cfg.standard_fields = @{
    machine = "Computer"
    username = "Username"
    #bulkcsv = "BulkCSV"
    ticketnum = "ticketnum"
}
$cfg.standard_aliases = @{
    username = "^(?i)[$]?(AD[-\s]?)?(User|Account)(?:[-\s]?Name)?\s*[=:?]*\s*$"
    machine =  "^(?i)[$]?(Computer|Machine|PC|Host)([-\s]?Name)?\s*[=:?]*\s*$"
    #bulkcsv = "^(?i)[$]?(Bulk|bulk.?csv|bulkfn|list|CSV)" # obsolete
}
$cfg.standard_actions = @{   # Add-ButtonHooks
    Cpy = 'Set-Clipboard $GUI.{0}.Text'
    Clr = '$GUI.{0}.Text = ""; $GUI.{0}.Background = "White"'
    "" = '$GUI.{0}.Text = (Get-Clipboard).trim()'
}



#-- WPF/WinForms widget wrapper
function W {
    <#
      .SYNOPSIS
         Convenient wrapper to create WPF or WinForms widgets.
      .DESCRIPTION
................................................................................
    $xaml = (Get-Content "$BaseDir/modules/$main" | Out-String)
    $xaml = $xaml -replace "e:/","$ImgDir/"
    if (Test-Path ($fn = "modules/theme.$theme.xaml")) { $xaml = $xaml -replace "modules/theme.\w+.xaml", $fn }
    $w = [Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader ([xml]$xaml)))

    #-- save aliases int $GUI (sync hash across all threads/runspaces)
    $GUI.styles = $w.Resources
    $shortcuts = "Window,Menu,Ribbon,Grid_ALL,Output,Cancel," + ($cfg.standard_fields.Keys -join ",")
    $shortcuts.split(",") | % { $GUI.$_ = $w.findName($_) } | Out-Null

    #--- return
    return $w
}


................................................................................
      .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.
    #>
    
    #-- default clipboard actions (Clear+Copy) for primary GUI input boxes
    ForEach ($key in $cfg.standard_fields.Keys) {
        ForEach ($suffix in $cfg.standard_actions.Keys) {
            if ($btn = $GUI.w.findName("Btn" + $cfg.standard_fields[$key] + $suffix)) {
                $btn.add_Click([scriptblock]::create($cfg.standard_actions[$suffix] -f $key))
            }           
        }
    }

    #-- computer name
    $GUI.w.findName("BtnComputer").add_Click({ 
        if ($m = (Get-Clipboard).trim()) {
            $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("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).trim()
        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("BtnUsernameCom").add_Click({  })

    #-- other fields
    #

    #-- clipboard tools
    $GUI.w.findName("BtnClipText").add_Click({
................................................................................

     END {
         Dispatch-GUI ([action]{ [void]$GUI.output.Parent.ScrollToEnd() })
     }

}

#-- invoke [action] callback via main window dispatcher
#   PowerShell >= 3.0 does need the parameters swapped
#   @src: https://gallery.technet.microsoft.com/Script-New-ProgressBar-329abbfd
#
function Dispatch-Gui($action, $args=@()) {
    #$GUI.w|GM|Out-String|Write-Host -f Green

    if ($PSVersionTable.PSVersion.Major -eq 2) {
        [void]$GUI.w.Dispatcher.Invoke("Normal", $action)
    }
    else {
        [void]$GUI.w.Dispatcher.Invoke($action, "Normal") #, $args)
    }
}

#-- just -Title update
function Set-GuiTitle($title) {
    Dispatch-Gui ([action]{ $GUI.w.title = $title })
}

................................................................................
    $GUI.html = ""
    Dispatch-Gui ([action]{
        $GUI.prev_output = (Get-OutputTextRange).Text
        $GUI.output.blocks.clear()
    })
}

#-- Read standard input fields
#  · this is a workaround, because accessing $GUI.machine.text
#    directly from parent runspace would hang up WPF
#  · for some reason also needs its hashtable recreated
function Get-GuiStandardVars {
    Dispatch-Gui ([action]{
        $GUI.vars = @{
            machine = $GUI.machine.Text
            username = $GUI.username.Text
        }
    })




}


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

................................................................................
          @{name="bulkcsv"; type="text"; value="flag,user,group"; description="CSV input"}
          @{name="pick"; type="select"; select="a|b|c|d"; value="c"; description="Select test"}
       )},
       $height = 450, $CURRENT_STORE=$GUI.vars_previous,
       $vars = @{}, $btns = @(), $name2type = @{},
       $results = @{}, $proceed = $false, $SELECT_FN = "./data/combobox.{0}.txt"
    )











    if (!$meta.vars) { return }

    #-- input widgets
    $var_widgets = $meta.vars | ? { $_.name } | % {
         $PARAM = $_
         $NAME = $PARAM.name
         $ALIAS = Get-VarsAlias $NAME
         $VALUE = $PARAM.value
         $name2type[$NAME] = $PARAM.type
		 
         # previous values
         if (($v = $CURRENT_STORE[$NAME]) -or ($v = $GUI.vars[$ALIAS])) {
             $VALUE = $v
         }
................................................................................
                W TextBox @{ Height=120; Width=280; Text=$VALUE+"`r`n"; AcceptsReturn=1 }
            }
            "select|combo" {
                if ($PARAM.select) { $ls = $PARAM.select.split("[,;|]") }
                elseif (Test-Path ($SELECT_FN -f $NAME)) { $ls = Get-Content ($SELECT_FN -f $NAME) }
                else { $ls = @("Null") }
                if (!$VALUE) { $VALUE = $ls[0] }
                W ComboBox @{Height=22; Width=260; IsEditable=$true; Text=$VALUE; ItemsSource=$ls }
            }
            "btn|button|action" {
                W Button @{Content=$PARAM.description; Add_Click={$results[$NAME] = $results._proceed = 1; $w.Close()}}
                #$btns += ...;continue;
            }
            "str|int|^$" { 
                W TextBox @{Height=20; Width=270; Text=$VALUE}
................................................................................
    if ($results._proceed) {
        #$results|FL|Out-String|Write-Host
        $results.getEnumerator() | % { $CURRENT_STORE[$_.Name] = $_.Value }
        return $results
    }
}























#-- 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 {
................................................................................
         Textual input query. Usually just the variable name '$computer' or '$user'.
         Recognizes common aliases like 'Machine' 'PC' 'Hostname' 'AD-Account' or 'AD-User-Name'
      .NOTES
         Scripts should preferrably query for input in their Param() section once,
         and list custom fields per plugin meta #param: list. NEW: per #vars: block.
    #>
    param($name, $title="Input", [switch]$N=$false)
    $alias = Get-VarsAlias $name
    if ($GUI.vars.containsKey($alias)) {
        return $GUI.vars[$alias]
    }
    else {
        #-- undefined/non-Param() input request / stray Read-Host calls
        $results = Read-GuiExtraParams @{
            title="Input"; description="Read-Host"; doc="stray Read-Host call from script";
................................................................................
            api="clicky"; type="io"; category="extra"; version="1.1";
            vars=@(@{name=$name; type="str"; description="$name"; value=""})
        }
        if (!$results) { throw "❎ Cancelled at Read-Host request." }
        return $results[$name]
    }
}

#-- Parameter/varname aliasing
function Get-VarsAlias {
    <#
      .SYNOPSIS
         Aliases like "Computer" for $machine variable
      .DESCRIPTION
         Allows flexible title matching for standard/internal/default variables.
         Which permits scripts to list arguments by different monikers. For instance
         Read-Host '$computer-name:' would still match $GUI.vars."machine"
    #>
    Param($name)
    ForEach ($key in $cfg.standard_aliases.keys) {
        if ($name -match $cfg.standard_aliases[$key]) { return $key }
    }
    return $name;
}

#-- Converts `# param: name,list` from vars{} to quoted cmdline "arg" "strings"

function Convert-VarsToCmdArgs {
    <#
      .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
................................................................................
        $meta_params = @(),    # meta.vars[] list of dicts
        $vals = @{machine=""; username=""},  # from $GUI.vars[]
        $out = ""
    )

    ForEach ($key in ($meta_params | % { $_.name })) {
        # aliases
        $key = Get-VarsAlias $key
        # quote + append
        $out += ' "'+($vals[$key] -replace '([\\"^])','^$1')+'"'
    }
    return $out
}

#-- fetch all tool input vars (ribbon fields, custom vars)
function Get-ScriptVars($e) {
    #-- machine, user
    $null = Get-GuiStandardVars
    
    #-- convert obsolete param: scheme to vars: list
    if ($e.param -and -not $e.vars) {
        $e.vars = ($e.param.trim() -split "\s*[,;]\s*") | % {
            if (Test-Path "data/combobox.$_.txt") { $t = "select" } else { $t = "str" }
            @{name=$_; description="..."; type="$t"; value=$GUI.vars[$_]}
        }
    }

    #-- input dialog, if vars: lists non-standard fields
    if ($e.vars -and ($e.vars | % { Get-VarsAlias ($_.name) } | ? { $GUI.vars.keys -notcontains $_ })) {
        if ($add = Read-GuiExtraParams $e) {
            $add.getEnumerator() | % { $GUI.vars[$_.Name] = $_.Value }
        }
        else {
            return $false;
        }
    }
    return $GUI.vars
}

#-- print header/title
function Out-GuiHeader($e) {

   Set-GuiTitle "➱ $($cfg.main.title) → $($e.title)"
   if ($e.noheader -or $cfg.noheader) { return; }
   $Host.UI.Write("HDR`r`n")
   $XAML = @"
      <Paragraph xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
       Foreground="#ff9988dd" Background="#ff102070" Margin="0,3">
         <Image Source='$(Get-IconPath $e.icon $e.img $e.category)' Width='16' Height='16' />
................................................................................

    #-- check last output time (>= 5 minutes per default)
    if ($last_output -and (([double]::parse((Get-Date -u %s)) - $last_output) -ge $cfg.autoclear)) {
        Clear-GuiOutput
    }

    #-- get vars (fetch input from "Ribbon"-fields: machine, username, etc)




    if (!(Get-ScriptVars $e)) { Write-Host -f Red "Cancelled."; return; }

    $GUI.vars.GetEnumerator() | % { Set-Variable $_.name $_.value -Scope Global }

    #-- print header
    Out-GuiHeader $e

    #-- plugins
    TRAP { $_ | out-gui -b red }
................................................................................
        }
        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 = Convert-VarsToCmdArgs ($e.vars) ($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 -Width 120 | Out-Gui
            }
        }
        #-- No handler