|
1 | | -<# |
| 1 | +<# |
2 | 2 | .SYNOPSIS |
3 | 3 | PowerShell GUI for Executing Scripts Organized by Tabs with Real-Time Search. |
4 | 4 |
|
|
11 | 11 | Luiz Hamilton Silva - @brazilianscriptguy |
12 | 12 |
|
13 | 13 | .VERSION |
14 | | - Updated: July 17, 2025 |
15 | | - Version: 2.2 (Fixed GUI layout issues) |
| 14 | + Updated: August 6, 2025 |
| 15 | + Version: 2.3 (Fixed GUI layout, inline search, logging compliance) |
16 | 16 | #> |
17 | 17 |
|
18 | 18 | #region --- Initialization and Configuration |
19 | 19 |
|
20 | 20 | # Hide the PowerShell console window |
21 | | -Add-Type @" |
22 | | -using System; |
23 | | -using System.Runtime.InteropServices; |
24 | | -public class Window { |
25 | | - [DllImport("kernel32.dll", SetLastError = true)] |
26 | | - static extern IntPtr GetConsoleWindow(); |
27 | | - [DllImport("user32.dll", SetLastError = true)] |
28 | | - [return: MarshalAs(UnmanagedType.Bool)] |
29 | | - static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); |
30 | | - public static void Hide() { |
31 | | - var handle = GetConsoleWindow(); |
32 | | - ShowWindow(handle, 0); // 0 = SW_HIDE |
33 | | - } |
34 | | - public static void Show() { |
35 | | - var handle = GetConsoleWindow(); |
36 | | - ShowWindow(handle, 5); // 5 = SW_SHOW |
| 21 | +if (-not ([System.Management.Automation.PSTypeName]'Window').Type) { |
| 22 | + Add-Type @" |
| 23 | + using System; |
| 24 | + using System.Runtime.InteropServices; |
| 25 | + public class Window { |
| 26 | + [DllImport("kernel32.dll", SetLastError = true)] |
| 27 | + static extern IntPtr GetConsoleWindow(); |
| 28 | + [DllImport("user32.dll", SetLastError = true)] |
| 29 | + [return: MarshalAs(UnmanagedType.Bool)] |
| 30 | + static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); |
| 31 | + public static void Hide() { |
| 32 | + var handle = GetConsoleWindow(); |
| 33 | + ShowWindow(handle, 0); // 0 = SW_HIDE |
| 34 | + } |
| 35 | + public static void Show() { |
| 36 | + var handle = GetConsoleWindow(); |
| 37 | + ShowWindow(handle, 5); // 5 = SW_SHOW |
| 38 | + } |
37 | 39 | } |
38 | | -} |
39 | 40 | "@ |
| 41 | +} |
40 | 42 | [Window]::Hide() |
41 | 43 |
|
42 | | -# Import necessary assemblies for Windows Forms and Drawing |
43 | 44 | Add-Type -AssemblyName System.Windows.Forms |
44 | 45 | Add-Type -AssemblyName System.Drawing |
45 | 46 |
|
46 | | -# Get the current script directory |
47 | 47 | $scriptDirectory = Split-Path -Parent $MyInvocation.MyCommand.Path |
48 | | -Write-Host "Current Script Directory: $scriptDirectory" -ForegroundColor Cyan |
49 | 48 |
|
50 | | -# Logging function |
51 | | -function Log-Message { |
| 49 | +function Write-Log { |
52 | 50 | param ( |
53 | 51 | [Parameter(Mandatory = $true)][string]$Message, |
54 | 52 | [ValidateSet("INFO", "WARN", "ERROR")][string]$MessageType = "INFO" |
55 | 53 | ) |
56 | 54 | $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" |
57 | | - $logEntry = "[$timestamp] [$MessageType] $Message" |
58 | 55 | $logPath = Join-Path $scriptDirectory "SysAdminToolSet.log" |
| 56 | + $entry = "[$timestamp] [$MessageType] $Message" |
59 | 57 | try { |
60 | | - if (-not (Test-Path (Split-Path $logPath))) { |
61 | | - New-Item -Path (Split-Path $logPath) -ItemType Directory -Force | Out-Null |
| 58 | + if (-not (Test-Path $logPath)) { |
| 59 | + $null = New-Item -Path $logPath -ItemType File -Force |
62 | 60 | } |
63 | | - Add-Content -Path $logPath -Value $logEntry -ErrorAction Stop |
| 61 | + Add-Content -Path $logPath -Value $entry |
64 | 62 | } catch { |
65 | | - Write-Warning "Failed to write to log: $_" |
| 63 | + Write-Warning "Failed to log message: $_" |
66 | 64 | } |
67 | 65 | } |
68 | 66 |
|
69 | | -Log-Message "Starting Launch Script Automatic Menu execution." |
70 | | - |
71 | 67 | #endregion |
72 | 68 |
|
73 | 69 | #region --- Core Functions |
74 | 70 |
|
75 | | -# Function to generate a dictionary of script filenames and paths from all subdirectories |
76 | 71 | function Get-ScriptDictionaries { |
77 | 72 | try { |
78 | 73 | $directories = Get-ChildItem -Path $scriptDirectory -Directory -Recurse -ErrorAction Stop |
79 | 74 | $scriptsByCategory = @{} |
80 | | - |
81 | 75 | foreach ($dir in $directories) { |
82 | | - $scriptFiles = Get-ChildItem -Path $dir.FullName -Filter "*.ps1" -File -ErrorAction Stop |
83 | | - if ($scriptFiles.Count -gt 0) { |
84 | | - $category = $dir.Name |
85 | | - $scriptsByCategory[$category] = $scriptFiles | Sort-Object -Property Name |
86 | | - Log-Message "Loaded $category category with $($scriptFiles.Count) scripts" |
| 76 | + $scripts = Get-ChildItem -Path $dir.FullName -Filter "*.ps1" -File -ErrorAction Stop |
| 77 | + if ($scripts.Count -gt 0) { |
| 78 | + $scriptsByCategory[$dir.Name] = $scripts | Sort-Object Name |
| 79 | + Write-Log "Loaded $($dir.Name) with $($scripts.Count) scripts" |
87 | 80 | } |
88 | 81 | } |
89 | | - if ($scriptsByCategory.Count -eq 0) { |
90 | | - Log-Message "No script categories found" -MessageType "WARN" |
91 | | - } |
92 | 82 | return $scriptsByCategory |
93 | 83 | } catch { |
94 | | - Log-Message "Failed to load script dictionaries: $_" -MessageType "ERROR" |
| 84 | + Write-Log "Script dictionary load failed: $_" "ERROR" |
95 | 85 | return @{} |
96 | 86 | } |
97 | 87 | } |
98 | 88 |
|
99 | | -# Function to update listbox items based on the search text with debouncing |
100 | 89 | function Update-ListBox { |
101 | 90 | param ( |
102 | | - [System.Windows.Forms.TextBox]$searchBox, |
103 | | - [System.Windows.Forms.CheckedListBox]$listBox, |
104 | | - [System.Collections.ObjectModel.Collection[System.IO.FileInfo]]$originalList |
| 91 | + [System.Windows.Forms.TextBox]$SearchBox, |
| 92 | + [System.Windows.Forms.CheckedListBox]$ListBox, |
| 93 | + [System.IO.FileInfo[]]$ScriptFiles |
105 | 94 | ) |
106 | | - |
107 | | - $searchText = $searchBox.Text.Trim().ToLower() |
108 | | - $listBox.BeginUpdate() |
109 | | - $listBox.Items.Clear() |
110 | | - |
111 | | - if ([string]::IsNullOrEmpty($searchText)) { |
112 | | - foreach ($file in $originalList) { |
113 | | - $listBox.Items.Add($file.Name, $false) |
114 | | - } |
115 | | - } else { |
116 | | - foreach ($file in $originalList) { |
117 | | - if ($file.Name.ToLower().Contains($searchText)) { |
118 | | - $listBox.Items.Add($file.Name, $false) |
119 | | - } |
| 95 | + $searchText = $SearchBox.Text.Trim().ToLowerInvariant() |
| 96 | + $ListBox.BeginUpdate() |
| 97 | + $ListBox.Items.Clear() |
| 98 | + foreach ($script in $ScriptFiles) { |
| 99 | + if ($searchText -eq "" -or $script.Name.ToLowerInvariant().Contains($searchText)) { |
| 100 | + $ListBox.Items.Add($script.Name, $false) |
120 | 101 | } |
121 | 102 | } |
122 | | - |
123 | | - if ($listBox.Items.Count -eq 0) { |
124 | | - $listBox.Items.Add("<No matching scripts found>", $false) |
| 103 | + if ($ListBox.Items.Count -eq 0) { |
| 104 | + $ListBox.Items.Add("<No matching scripts found>", $false) |
125 | 105 | } |
126 | | - |
127 | | - $listBox.EndUpdate() |
| 106 | + $ListBox.EndUpdate() |
128 | 107 | } |
129 | 108 |
|
130 | | -# Function to execute selected scripts |
131 | 109 | function Execute-Scripts { |
132 | | - param ([System.Windows.Forms.TabControl]$tabControl) |
133 | | - $anyExecuted = $false |
134 | | - |
135 | | - foreach ($tabPage in $tabControl.TabPages) { |
136 | | - $listBox = $tabPage.Controls | Where-Object { $_ -is [System.Windows.Forms.CheckedListBox] } |
137 | | - if ($listBox) { |
138 | | - foreach ($script in $listBox.CheckedItems) { |
139 | | - if ($script -eq "<No matching scripts found>") { continue } |
140 | | - |
141 | | - $scriptPath = $scriptsByCategory[$tabPage.Text] | Where-Object { $_.Name -eq $script } | Select-Object -ExpandProperty FullName |
142 | | - if (Test-Path $scriptPath) { |
143 | | - try { |
144 | | - $psi = New-Object System.Diagnostics.ProcessStartInfo |
145 | | - $psi.FileName = "powershell.exe" |
146 | | - $psi.Arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`"" |
147 | | - $psi.UseShellExecute = $false |
148 | | - $psi.RedirectStandardOutput = $true |
149 | | - $psi.RedirectStandardError = $true |
150 | | - $process = [System.Diagnostics.Process]::Start($psi) |
151 | | - $output = $process.StandardOutput.ReadToEnd() |
152 | | - $errorOutput = $process.StandardError.ReadToEnd() |
153 | | - $process.WaitForExit() |
154 | | - if ($process.ExitCode -eq 0) { |
155 | | - Log-Message "Successfully executed: $scriptPath" |
156 | | - Write-Host "Executed: $scriptPath`nOutput: $output" -ForegroundColor Green |
157 | | - } else { |
158 | | - $errorMessage = "Execution failed for ${scriptPath}: ${errorOutput}" |
159 | | - Log-Message $errorMessage -MessageType "ERROR" |
160 | | - [System.Windows.Forms.MessageBox]::Show("Error executing ${script}: ${errorOutput}", "Execution Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) |
161 | | - } |
162 | | - $anyExecuted = $true |
163 | | - } catch { |
164 | | - $errorMessage = "Failed to execute ${scriptPath}: $_" |
165 | | - Log-Message $errorMessage -MessageType "ERROR" |
166 | | - [System.Windows.Forms.MessageBox]::Show("Failed to execute ${script}: $_", "Execution Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) |
167 | | - } |
| 110 | + param ([System.Windows.Forms.TabControl]$TabControl) |
| 111 | + $executed = $false |
| 112 | + foreach ($tab in $TabControl.TabPages) { |
| 113 | + $listBox = $tab.Controls | Where-Object { $_ -is [System.Windows.Forms.CheckedListBox] } |
| 114 | + foreach ($item in $listBox.CheckedItems) { |
| 115 | + if ($item -eq "<No matching scripts found>") { continue } |
| 116 | + $scriptPath = ($scriptsByCategory[$tab.Text] | Where-Object { $_.Name -eq $item }).FullName |
| 117 | + if (-not (Test-Path $scriptPath)) { |
| 118 | + Write-Log "Script not found: $scriptPath" "ERROR" |
| 119 | + continue |
| 120 | + } |
| 121 | + try { |
| 122 | + $psi = New-Object System.Diagnostics.ProcessStartInfo -Property @{ |
| 123 | + FileName = "powershell.exe" |
| 124 | + Arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`"" |
| 125 | + UseShellExecute = $false |
| 126 | + RedirectStandardOutput = $true |
| 127 | + RedirectStandardError = $true |
| 128 | + } |
| 129 | + $process = [System.Diagnostics.Process]::Start($psi) |
| 130 | + $output = $process.StandardOutput.ReadToEnd() |
| 131 | + $errors = $process.StandardError.ReadToEnd() |
| 132 | + $process.WaitForExit() |
| 133 | + if ($process.ExitCode -eq 0) { |
| 134 | + Write-Log "Executed successfully: $scriptPath" |
168 | 135 | } else { |
169 | | - Log-Message "Script not found: $scriptPath" -MessageType "ERROR" |
170 | | - [System.Windows.Forms.MessageBox]::Show("Script not found: $scriptPath", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) |
| 136 | + Write-Log "Execution failed: $scriptPath - Error: $errors" "ERROR" |
171 | 137 | } |
| 138 | + $executed = $true |
| 139 | + } catch { |
| 140 | + Write-Log "Exception while executing ${scriptPath}: $_" "ERROR" |
172 | 141 | } |
173 | 142 | } |
174 | 143 | } |
175 | | - |
176 | | - if (-not $anyExecuted) { |
177 | | - [System.Windows.Forms.MessageBox]::Show("No scripts selected for execution.", "Info", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Information) |
| 144 | + if (-not $executed) { |
| 145 | + [System.Windows.Forms.MessageBox]::Show("No scripts selected.", "Info") |
178 | 146 | } |
179 | 147 | } |
180 | 148 |
|
181 | 149 | #endregion |
182 | 150 |
|
183 | | -#region --- GUI Implementation |
| 151 | +#region --- GUI Builder |
184 | 152 |
|
185 | 153 | function Create-GUI { |
186 | | - # Generate script dictionaries |
187 | | - $scriptsByCategory = Get-ScriptDictionaries |
| 154 | + $global:scriptsByCategory = Get-ScriptDictionaries |
188 | 155 | if ($scriptsByCategory.Count -eq 0) { |
189 | | - [System.Windows.Forms.MessageBox]::Show("No scripts found in $scriptDirectory or its subdirectories.", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) |
| 156 | + [System.Windows.Forms.MessageBox]::Show("No scripts found.", "Error") |
190 | 157 | return |
191 | 158 | } |
192 | 159 |
|
193 | | - # Initialize the Form |
194 | | - $form = [System.Windows.Forms.Form]::new() |
195 | | - $form.Text = 'Lauch Script Automatic Menu' |
196 | | - $form.Size = [System.Drawing.Size]::new(1200, 900) |
197 | | - $form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen |
198 | | - $form.BackColor = [System.Drawing.Color]::WhiteSmoke |
199 | | - $form.Add_Resize({ |
200 | | - $tabControl.Size = [System.Drawing.Size]::new($form.ClientSize.Width - 40, $form.ClientSize.Height - 150) |
201 | | - $executeButton.Location = [System.Drawing.Point]::new(($form.ClientSize.Width - 150) / 2, $form.ClientSize.Height - 80) |
202 | | - }) |
| 160 | + $form = New-Object Windows.Forms.Form -Property @{ |
| 161 | + Text = "Launch Script Menu" |
| 162 | + Size = New-Object Drawing.Size(1100, 800) |
| 163 | + StartPosition = "CenterScreen" |
| 164 | + BackColor = "WhiteSmoke" |
| 165 | + } |
203 | 166 |
|
204 | | - # Add TabControl for organizing script categories |
205 | | - $tabControl = [System.Windows.Forms.TabControl]::new() |
206 | | - $tabControl.Size = [System.Drawing.Size]::new($form.ClientSize.Width - 40, $form.ClientSize.Height - 150) |
207 | | - $tabControl.Location = [System.Drawing.Point]::new(10, 10) |
208 | | - $tabControl.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right -bor [System.Windows.Forms.AnchorStyles]::Bottom |
| 167 | + $tabControl = New-Object Windows.Forms.TabControl -Property @{ |
| 168 | + Dock = "Fill" |
| 169 | + } |
209 | 170 | $form.Controls.Add($tabControl) |
210 | 171 |
|
211 | | - # Store references to controls |
212 | | - $tabControls = @{} |
213 | | - |
214 | | - # Add Tabs for each category with debounced search |
215 | 172 | foreach ($category in $scriptsByCategory.Keys) { |
216 | | - $tabPage = [System.Windows.Forms.TabPage]::new() |
217 | | - $tabPage.Text = $category |
218 | | - $tabPage.AutoScroll = $true |
219 | | - |
220 | | - # Add Search Box |
221 | | - $searchBox = [System.Windows.Forms.TextBox]::new() |
222 | | - $searchBox.Size = [System.Drawing.Size]::new($tabPage.ClientSize.Width - 20, 25) |
223 | | - $searchBox.Location = [System.Drawing.Point]::new(10, 10) |
224 | | - $searchBox.Font = [System.Drawing.Font]::new("Arial", 10) |
225 | | - $searchBox.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right |
226 | | - $tabPage.Controls.Add($searchBox) |
227 | | - |
228 | | - # Add ListBox |
229 | | - $listBox = [System.Windows.Forms.CheckedListBox]::new() |
230 | | - $listBox.Size = [System.Drawing.Size]::new($tabPage.ClientSize.Width - 20, $tabPage.ClientSize.Height - 60) |
231 | | - $listBox.Location = [System.Drawing.Point]::new(10, 40) |
232 | | - $listBox.Font = [System.Drawing.Font]::new("Arial", 9) |
233 | | - $listBox.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right -bor [System.Windows.Forms.AnchorStyles]::Bottom |
234 | | - $listBox.ScrollAlwaysVisible = $true |
235 | | - $tabPage.Controls.Add($listBox) |
236 | | - |
237 | | - # Initial population |
238 | | - Update-ListBox -searchBox $searchBox -listBox $listBox -originalList $scriptsByCategory[$category] |
| 173 | + $tab = New-Object Windows.Forms.TabPage -Property @{ Text = $category; AutoScroll = $true } |
| 174 | + $searchBox = New-Object Windows.Forms.TextBox -Property @{ Location = '10,10'; Size = '1050,25' } |
| 175 | + $listBox = New-Object Windows.Forms.CheckedListBox -Property @{ Location = '10,45'; Size = '1050,650' } |
| 176 | + $tab.Controls.AddRange(@($searchBox, $listBox)) |
| 177 | + $tabControl.TabPages.Add($tab) |
| 178 | + |
| 179 | + Update-ListBox -SearchBox $searchBox -ListBox $listBox -ScriptFiles $scriptsByCategory[$category] |
239 | 180 |
|
240 | | - # Debounced search handler |
241 | | - $searchTimer = $null |
242 | 181 | $searchBox.Add_TextChanged({ |
243 | | - if ($searchTimer) { $searchTimer.Dispose() } |
244 | | - $searchTimer = New-Object System.Timers.Timer -ArgumentList 300 |
245 | | - $searchTimer.AutoReset = $false |
246 | | - $searchTimer.add_Elapsed({ |
247 | | - Update-ListBox -searchBox $searchBox -listBox $listBox -originalList $scriptsByCategory[$category] |
248 | | - }) |
249 | | - $searchTimer.Start() |
250 | | - }) |
251 | | - |
252 | | - # Dynamic resize handler for tab page controls |
253 | | - $tabPage.Add_Resize({ |
254 | | - $searchBox.Size = [System.Drawing.Size]::new($tabPage.ClientSize.Width - 20, 25) |
255 | | - $listBox.Size = [System.Drawing.Size]::new($tabPage.ClientSize.Width - 20, $tabPage.ClientSize.Height - 60) |
256 | | - }) |
257 | | - |
258 | | - $tabControls[$category] = @{ SearchBox = $searchBox; ListBox = $listBox } |
259 | | - $tabControl.TabPages.Add($tabPage) |
260 | | - } |
261 | | - |
262 | | - # Handle tab switch |
263 | | - $tabControl.Add_SelectedIndexChanged({ |
264 | | - $selectedTab = $tabControl.SelectedTab |
265 | | - if ($selectedTab -ne $null) { |
266 | | - $category = $selectedTab.Text |
267 | | - if ($tabControls.ContainsKey($category)) { |
268 | | - $controls = $tabControls[$category] |
269 | | - Update-ListBox -searchBox $controls.SearchBox -listBox $controls.ListBox -originalList $scriptsByCategory[$category] |
270 | | - } |
271 | | - } |
| 182 | + Update-ListBox -SearchBox $searchBox -ListBox $listBox -ScriptFiles $scriptsByCategory[$category] |
272 | 183 | }) |
| 184 | + } |
273 | 185 |
|
274 | | - # Add Execute Button with padding |
275 | | - $executeButton = [System.Windows.Forms.Button]::new() |
276 | | - $executeButton.Text = 'Execute' |
277 | | - $executeButton.Size = [System.Drawing.Size]::new(150, 40) |
278 | | - $executeButton.Location = [System.Drawing.Point]::new(($form.ClientSize.Width - 150) / 2, $form.ClientSize.Height - 100) # Adjusted for padding |
279 | | - $executeButton.Anchor = [System.Windows.Forms.AnchorStyles]::Bottom |
280 | | - $executeButton.BackColor = [System.Drawing.Color]::LightSkyBlue |
281 | | - $executeButton.FlatStyle = [System.Windows.Forms.FlatStyle]::Flat |
282 | | - $executeButton.Add_Click({ Execute-Scripts -tabControl $tabControl }) |
283 | | - $form.Controls.Add($executeButton) |
| 186 | + $btnExecute = New-Object Windows.Forms.Button -Property @{ |
| 187 | + Text = "Execute Selected" |
| 188 | + Size = '200,30' |
| 189 | + Location = New-Object Drawing.Point(440, 700) |
| 190 | + BackColor = 'LightSkyBlue' |
| 191 | + } |
| 192 | + $btnExecute.Add_Click({ Execute-Scripts -TabControl $tabControl }) |
| 193 | + $form.Controls.Add($btnExecute) |
284 | 194 |
|
285 | | - # Show the Form |
286 | | - [void] $form.ShowDialog() |
| 195 | + $form.ShowDialog() | Out-Null |
287 | 196 | } |
288 | 197 |
|
289 | 198 | #endregion |
290 | 199 |
|
291 | | -# Call the function to create the GUI |
| 200 | +# Entry Point |
292 | 201 | Create-GUI |
293 | 202 |
|
294 | 203 | # End of script |
0 commit comments