# FileOrganizer.ps1 - Modern File Organizer with GUI & CLI # A professional-grade Windows utility for organizing files by type # # Default config.json (auto-created if missing): # <# # { # "categories": { # "Word Files": ["doc", "docx", "docm", "dotx"], # "PowerPoint Files": ["ppt", "pptx", "pptm"], # "Excel Files": ["xls", "xlsx", "xlsm", "csv"], # "PDF Files": ["pdf"], # "Images": ["jpg", "jpeg", "png", "gif", "bmp", "svg", "webp", "tiff"], # "Videos": ["mp4", "avi", "mkv", "mov", "wmv", "flv"], # "Executables": ["exe", "msi", "bat", "cmd", "ps1"], # "Archives": ["zip", "rar", "7z", "tar", "gz"], # "Audio": ["mp3", "wav", "flac", "ogg", "aac", "wma"], # "Code": ["ps1", "py", "js", "ts", "cpp", "c", "h", "cs", "java", "rb", "go", "rs"], # "Text": ["txt", "md", "log", "rtf", "json", "xml", "yaml", "yml", "ini", "cfg"] # }, # "excludePatterns": ["Thumbs.db", ".DS_Store", "desktop.ini", "*.tmp", "~$*"], # "createTypeSubfolder": true, # "defaultAction": "Move", # "defaultDepth": 100 # } # #> # # Usage: # .\FileOrganizer.ps1 # Interactive GUI mode # .\FileOrganizer.ps1 -SourcePath "C:\Downloads" -DestPath "D:\Organized" -DryRun # .\FileOrganizer.ps1 -SourcePath "C:\Src" -DestPath "D:\Dest" -Action Copy -Depth 2 # .\FileOrganizer.ps1 -CategoriesFile "custom.json" # Get-Help .\FileOrganizer.ps1 -Detailed #Requires -Version 5.1 # ============================================================ # IMPORTANT: param() MUST come first before any executable code # ============================================================ [CmdletBinding()] param ( [Parameter(HelpMessage = "Source folder path to scan")] [string]$SourcePath, [Parameter(HelpMessage = "Destination folder path")] [string]$DestPath, [Parameter(HelpMessage = "Scan depth: 0=Main only, 1=Subfolders, 2=SubSubfolders, 100=All")] [ValidateRange(0, 100)] [int]$Depth = 100, [Parameter(HelpMessage = "Action to perform: Move or Copy")] [ValidateSet("Move", "Copy")] [string]$Action = "Move", [Parameter(HelpMessage = "Simulate operation without moving/copying files")] [switch]$DryRun, [Parameter(HelpMessage = "Path to custom categories JSON file")] [string]$CategoriesFile, [Parameter(HelpMessage = "Run without looping for additional tasks")] [switch]$NoLoop, [Parameter(HelpMessage = "Use separate destination folder per file type")] [switch]$SeparateDestinations, [Parameter(HelpMessage = "Show this help message")] [switch]$Help ) # ============================================================ # Now we can have executable code after param() # ============================================================ # Early error handling - don't let script silently fail $ErrorActionPreference = "Continue" $Script:StartupError = $null # Check execution policy try { $execPolicy = Get-ExecutionPolicy -ErrorAction SilentlyContinue if ($execPolicy -eq "Restricted") { Write-Host "WARNING: Execution Policy is Restricted." -ForegroundColor Yellow Write-Host "Run: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser" -ForegroundColor Yellow } } catch { Write-Verbose "Could not get ExecutionPolicy" } # Fix for $PSScriptRoot being empty when run via right-click "Run with PowerShell" $Script:ScriptPath = if ($PSScriptRoot) { $PSScriptRoot } elseif ($PSCommandPath) { Split-Path -Parent $PSCommandPath } else { Split-Path -Parent $MyInvocation.MyCommand.Path } if (-not $Script:ScriptPath) { $Script:ScriptPath = Get-Location } # Pre-load Windows Forms (needed for GUI dialogs) try { Add-Type -AssemblyName System.Windows.Forms -ErrorAction Stop } catch { $errMsg = $_.Exception.Message $Script:StartupError = "Failed to load Windows Forms - $errMsg" } #region Global Variables $Script:Config = $null $Script:LastUsed = $null $Script:ScriptName = "FileOrganizer" $Script:LogFile = $null $Script:IsElevated = $false $Script:PSVersion = $PSVersionTable.PSVersion.Major #endregion # Check for startup errors if ($Script:StartupError) { Write-Host "ERROR: $Script:StartupError" -ForegroundColor Red Write-Host "Press Enter to exit..." -ForegroundColor Yellow Read-Host exit 1 } # Show startup banner Write-Host "" Write-Host "======================================" -ForegroundColor Cyan Write-Host " FileOrganizer v2.0 - Loading..." -ForegroundColor Cyan Write-Host "======================================" -ForegroundColor Cyan Write-Host "Script Path: $Script:ScriptPath" -ForegroundColor Gray Write-Host "PowerShell: $($Script:PSVersion)" -ForegroundColor Gray Write-Host "" #region Comment-Based Help <# .SYNOPSIS Organizes files by type into destination folders. .DESCRIPTION FileOrganizer is a professional-grade Windows utility that scans a source folder, categorizes files by type (Word, PDF, Images, etc.), and moves or copies them to destination folders. Supports both CLI and interactive GUI modes. Features: - CLI and Interactive GUI hybrid operation - Configurable file categories via JSON - Move or Copy operations - Dry-Run mode for safe testing - Modern Out-GridView selection - Remembers last used folders - Comprehensive logging .PARAMETER SourcePath Source folder to scan. If not provided, launches interactive folder browser. .PARAMETER DestPath Destination folder for organized files. If not provided, launches interactive browser. .PARAMETER Depth Scan depth: 0=Main folder only, 1=One level deep, 2=Two levels, 100=All (default). .PARAMETER Action Operation type: "Move" (default) or "Copy". Use Copy for safer testing. .PARAMETER DryRun Simulates the operation without actually moving/copying files. Shows what would happen. .PARAMETER CategoriesFile Path to custom JSON file with file type categories. .PARAMETER NoLoop Runs single operation without prompting for additional tasks. .PARAMETER SeparateDestinations Use separate destination folder per file type (prompts for each). .EXAMPLE .\FileOrganizer.ps1 Launches interactive GUI mode with folder browsers and selections. .EXAMPLE .\FileOrganizer.ps1 -SourcePath "C:\Downloads" -DestPath "D:\Organized" -DryRun Scans Downloads, shows what would be organized to D:\Organized without making changes. .EXAMPLE .\FileOrganizer.ps1 -SourcePath "C:\Files" -Action Copy -Depth 2 Copies files from C:\Files (including subfolders 2 levels deep) with interactive destination. .NOTES Author: FileOrganizer Team Version: 2.0.0 Requires: PowerShell 5.1+ #> #endregion #region Initialization Functions function Initialize-Environment { <# .SYNOPSIS Initializes the script environment - logging, config, paths #> [CmdletBinding()] param() # Set up log file path $logDate = Get-Date -Format "yyyy-MM-dd" $Script:LogFile = Join-Path $Script:ScriptPath "$($Script:ScriptName)_$logDate.log" # Create script path if it doesn't exist (for when running from other locations) if (-not $Script:ScriptPath) { $Script:ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path } # Load configuration Load-Config -CategoriesFile $CategoriesFile # Load last used paths Load-LastUsed # Check PowerShell version for feature compatibility if ($Script:PSVersion -lt 5) { Write-Warning "PowerShell version 5.1 or higher recommended. Current: $($Script:PSVersion)" } Write-Log "INFO" "FileOrganizer started - PS Version: $($Script:PSVersion)" } function Load-Config { <# .SYNOPSIS Loads configuration from JSON file or creates default #> [CmdletBinding()] param( [string]$CategoriesFile ) # Determine config file path if ($CategoriesFile) { $configPath = $CategoriesFile } else { $configPath = Join-Path $Script:ScriptPath "config.json" } # Default configuration $defaultConfig = @{ categories = @{ "Word Files" = @("doc", "docx", "docm", "dotx") "PowerPoint Files" = @("ppt", "pptx", "pptm") "Excel Files" = @("xls", "xlsx", "xlsm", "csv") "PDF Files" = @("pdf") "Images" = @("jpg", "jpeg", "png", "gif", "bmp", "svg", "webp", "tiff") "Videos" = @("mp4", "avi", "mkv", "mov", "wmv", "flv") "Executables" = @("exe", "msi", "bat", "cmd") "Archives" = @("zip", "rar", "7z", "tar", "gz") "Audio" = @("mp3", "wav", "flac", "ogg", "aac", "wma") "Code" = @("ps1", "py", "js", "ts", "cpp", "c", "h", "cs", "java", "rb", "go", "rs") "Text" = @("txt", "md", "log", "rtf", "json", "xml", "yaml", "yml", "ini", "cfg") } excludePatterns = @("Thumbs.db", ".DS_Store", "desktop.ini", "*.tmp", "~$*") createTypeSubfolder = $true defaultAction = "Move" defaultDepth = 100 } # Load existing or create default config if (Test-Path $configPath) { try { $configContent = Get-Content $configPath -Raw -ErrorAction Stop # Handle PS 5.1 vs PS 6+ compatibility if ($Script:PSVersion -ge 6) { $Script:Config = $configContent | ConvertFrom-Json -AsHashtable -ErrorAction Stop } else { $loadedConfig = $configContent | ConvertFrom-Json -ErrorAction Stop # Convert PSObject to Hashtable for PS 5.1 compatibility $Script:Config = @{} $loadedConfig.PSObject.Properties | ForEach-Object { $Script:Config[$_.Name] = $_.Value } } Write-Log "INFO" "Configuration loaded from: $configPath" } catch { $errMsg = $_.Exception.Message Write-Warning "Failed to load config, using defaults - $errMsg" $Script:Config = $defaultConfig Save-Config -Config $Script:Config -Path $configPath } } else { # Create default config file $Script:Config = $defaultConfig try { Save-Config -Config $Script:Config -Path $configPath Write-Host "Created default config: $configPath" -ForegroundColor Cyan } catch { $errMsg = $_.Exception.Message Write-Warning "Could not create config file - $errMsg" } } # Ensure required keys exist if (-not $Script:Config.categories) { $Script:Config.categories = $defaultConfig.categories } if (-not $Script:Config.excludePatterns) { $Script:Config.excludePatterns = $defaultConfig.excludePatterns } if ($null -eq $Script:Config.createTypeSubfolder) { $Script:Config.createTypeSubfolder = $true } } function Save-Config { <# .SYNOPSIS Saves configuration to JSON file #> [CmdletBinding()] param( [hashtable]$Config, [string]$Path ) try { $Config | ConvertTo-Json -Depth 10 | Set-Content $Path -Force -ErrorAction Stop } catch { $errMsg = $_.Exception.Message Write-Warning "Failed to save config - $errMsg" } } function Load-LastUsed { <# .SYNOPSIS Loads last used source/destination paths #> [CmdletBinding()] param() $lastUsedPath = Join-Path $Script:ScriptPath "lastused.json" if (Test-Path $lastUsedPath) { try { $content = Get-Content $lastUsedPath -Raw # Handle PS 5.1 vs PS 6+ compatibility if ($Script:PSVersion -ge 6) { $Script:LastUsed = $content | ConvertFrom-Json -AsHashtable -ErrorAction Stop } else { $loaded = $content | ConvertFrom-Json -ErrorAction Stop # Convert PSObject to Hashtable for PS 5.1 compatibility $Script:LastUsed = @{} $loaded.PSObject.Properties | ForEach-Object { $Script:LastUsed[$_.Name] = $_.Value } } } catch { $Script:LastUsed = @{ sourcePath = ""; destPath = "" } } } else { $Script:LastUsed = @{ sourcePath = ""; destPath = "" } } } function Save-LastUsed { <# .SYNOPSIS Saves last used paths for future sessions #> [CmdletBinding()] param( [string]$SourcePath, [string]$DestPath ) $lastUsedPath = Join-Path $Script:ScriptPath "lastused.json" $Script:LastUsed.sourcePath = $SourcePath $Script:LastUsed.destPath = $DestPath try { $Script:LastUsed | ConvertTo-Json | Set-Content $lastUsedPath -Force } catch { $errMsg = $_.Exception.Message Write-Warning "Could not save last used paths - $errMsg" } } function Write-Log { <# .SYNOPSIS Writes timestamped log entry to file #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Level, [Parameter(Mandatory)] [string]$Message ) $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $logEntry = "[$timestamp] [$Level] $Message" # Write to log file try { Add-Content -Path $Script:LogFile -Value $logEntry -ErrorAction SilentlyContinue } catch { Write-Verbose "Logging silently failed" } # Also output to console based on level switch ($Level) { "ERROR" { Write-Host $logEntry -ForegroundColor Red } "WARN" { Write-Host $logEntry -ForegroundColor Yellow } "INFO" { Write-Verbose $logEntry } "DRYRUN" { Write-Host $logEntry -ForegroundColor Cyan } } } #endregion #region GUI Helper Functions function Test-OutGridViewAvailable { <# .SYNOPSIS Checks if Out-GridView is available (Windows only) #> if ($IsWindows -or (-not $IsLinux -and -not $IsMacOS)) { return $true } return $false } function Select-SourceFolder { <# .SYNOPSIS Shows folder browser dialog for source selection #> [CmdletBinding()] param( [string]$InitialPath ) # Use last used path if available and no initial path provided if (-not $InitialPath -and $Script:LastUsed.sourcePath) { if (Test-Path $Script:LastUsed.sourcePath) { $InitialPath = $Script:LastUsed.sourcePath } } # Use Windows Forms folder browser Add-Type -AssemblyName System.Windows.Forms $dialog = New-Object System.Windows.Forms.FolderBrowserDialog $dialog.Description = "Select Source Folder to Scan" $dialog.ShowNewFolderButton = $false if ($InitialPath -and (Test-Path $InitialPath)) { $dialog.SelectedPath = $InitialPath } $result = $dialog.ShowDialog() if ($result -eq "OK") { Write-Log "INFO" "Source folder selected: $($dialog.SelectedPath)" return $dialog.SelectedPath } return $null } function Select-DestinationFolder { <# .SYNOPSIS Shows folder browser dialog for destination selection #> [CmdletBinding()] param( [string]$Description = "Select Destination Folder", [string]$InitialPath ) # Use last used path if available if (-not $InitialPath -and $Script:LastUsed.destPath) { if (Test-Path $Script:LastUsed.destPath) { $InitialPath = $Script:LastUsed.destPath } } Add-Type -AssemblyName System.Windows.Forms $dialog = New-Object System.Windows.Forms.FolderBrowserDialog $dialog.Description = $Description $dialog.ShowNewFolderButton = $true if ($InitialPath -and (Test-Path $InitialPath)) { $dialog.SelectedPath = $InitialPath } $result = $dialog.ShowDialog() if ($result -eq "OK") { Write-Log "INFO" "Destination folder selected: $($dialog.SelectedPath)" return $dialog.SelectedPath } return $null } function Select-DepthGUI { <# .SYNOPSIS Shows depth selection via modern Windows Forms #> [CmdletBinding()] param() Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing $form = New-Object System.Windows.Forms.Form $form.Text = "FileOrganizer - Scan Depth" $form.Size = New-Object System.Drawing.Size(460, 320) $form.StartPosition = "CenterScreen" $form.FormBorderStyle = "FixedDialog" $form.MaximizeBox = $false $form.MinimizeBox = $false $form.BackColor = [System.Drawing.Color]::FromArgb(245, 245, 250) $form.Font = New-Object System.Drawing.Font("Segoe UI", 10) $headerPanel = New-Object System.Windows.Forms.Panel $headerPanel.Dock = "Top" $headerPanel.Height = 50 $headerPanel.BackColor = [System.Drawing.Color]::FromArgb(55, 71, 133) $headerLabel = New-Object System.Windows.Forms.Label $headerLabel.Text = " Select Scan Depth" $headerLabel.ForeColor = [System.Drawing.Color]::White $headerLabel.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 14) $headerLabel.Dock = "Fill" $headerLabel.TextAlign = "MiddleLeft" $headerPanel.Controls.Add($headerLabel) $radio0 = New-Object System.Windows.Forms.RadioButton $radio0.Location = New-Object System.Drawing.Point(40, 70) $radio0.Size = New-Object System.Drawing.Size(380, 28) $radio0.Text = "Main Folder Only (Current folder only)" $radio0.FlatStyle = "Flat" $radio1 = New-Object System.Windows.Forms.RadioButton $radio1.Location = New-Object System.Drawing.Point(40, 105) $radio1.Size = New-Object System.Drawing.Size(380, 28) $radio1.Text = "Subfolders (1 level deep)" $radio1.FlatStyle = "Flat" $radio2 = New-Object System.Windows.Forms.RadioButton $radio2.Location = New-Object System.Drawing.Point(40, 140) $radio2.Size = New-Object System.Drawing.Size(380, 28) $radio2.Text = "Sub-Subfolders (2 levels deep)" $radio2.FlatStyle = "Flat" $radio100 = New-Object System.Windows.Forms.RadioButton $radio100.Location = New-Object System.Drawing.Point(40, 175) $radio100.Size = New-Object System.Drawing.Size(380, 28) $radio100.Text = "All Subfolders (Recursive)" $radio100.Checked = $true $radio100.FlatStyle = "Flat" $okButton = New-Object System.Windows.Forms.Button $okButton.Location = New-Object System.Drawing.Point(170, 225) $okButton.Size = New-Object System.Drawing.Size(100, 36) $okButton.Text = "OK" $okButton.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 10) $okButton.FlatStyle = "Flat" $okButton.BackColor = [System.Drawing.Color]::FromArgb(55, 71, 133) $okButton.ForeColor = [System.Drawing.Color]::White $okButton.Cursor = "Hand" $okButton.DialogResult = "OK" $form.Controls.AddRange(@($headerPanel, $radio0, $radio1, $radio2, $radio100, $okButton)) $form.AcceptButton = $okButton $result = $form.ShowDialog() $form.Dispose() if ($result -eq "OK") { if ($radio0.Checked) { return 0 } if ($radio1.Checked) { return 1 } if ($radio2.Checked) { return 2 } if ($radio100.Checked) { return 100 } } return 100 # default } function Select-FileType { <# .SYNOPSIS Shows type selection using custom modern Windows Forms CheckedListBox #> [CmdletBinding()] param( [hashtable]$TypeCounts ) Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing $form = New-Object System.Windows.Forms.Form $form.Text = "FileOrganizer - File Types" $form.Size = New-Object System.Drawing.Size(420, 500) $form.StartPosition = "CenterScreen" $form.FormBorderStyle = "FixedDialog" $form.MaximizeBox = $false $form.MinimizeBox = $false $form.BackColor = [System.Drawing.Color]::FromArgb(245, 245, 250) $form.Font = New-Object System.Drawing.Font("Segoe UI", 10) $headerPanel = New-Object System.Windows.Forms.Panel $headerPanel.Dock = "Top" $headerPanel.Height = 50 $headerPanel.BackColor = [System.Drawing.Color]::FromArgb(55, 71, 133) $headerLabel = New-Object System.Windows.Forms.Label $headerLabel.Text = " Select File Types" $headerLabel.ForeColor = [System.Drawing.Color]::White $headerLabel.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 14) $headerLabel.Dock = "Fill" $headerLabel.TextAlign = "MiddleLeft" $headerPanel.Controls.Add($headerLabel) $listBox = New-Object System.Windows.Forms.CheckedListBox $listBox.Location = New-Object System.Drawing.Point(20, 95) $listBox.Size = New-Object System.Drawing.Size(360, 280) $listBox.Font = New-Object System.Drawing.Font("Segoe UI", 10.5) $listBox.CheckOnClick = $true # Store plain string items so we can easily map them back $itemList = @() foreach ($type in ($TypeCounts.Keys | Sort-Object)) { $count = $TypeCounts[$type] $display = "$type ($count files)" $listBox.Items.Add($display, $true) | Out-Null $itemList += [PSCustomObject]@{ Display = $display; Type = $type } } $selectAllBtn = New-Object System.Windows.Forms.Button $selectAllBtn.Location = New-Object System.Drawing.Point(20, 395) $selectAllBtn.Size = New-Object System.Drawing.Size(100, 32) $selectAllBtn.Text = "Select All" $selectAllBtn.FlatStyle = "Flat" $selectAllBtn.BackColor = [System.Drawing.Color]::FromArgb(220, 220, 225) $selectAllBtn.Cursor = "Hand" $selectAllBtn.Add_Click({ for ($i = 0; $i -lt $listBox.Items.Count; $i++) { $listBox.SetItemChecked($i, $true) } }) $clearAllBtn = New-Object System.Windows.Forms.Button $clearAllBtn.Location = New-Object System.Drawing.Point(130, 395) $clearAllBtn.Size = New-Object System.Drawing.Size(100, 32) $clearAllBtn.Text = "Clear All" $clearAllBtn.FlatStyle = "Flat" $clearAllBtn.BackColor = [System.Drawing.Color]::FromArgb(220, 220, 225) $clearAllBtn.Cursor = "Hand" $clearAllBtn.Add_Click({ for ($i = 0; $i -lt $listBox.Items.Count; $i++) { $listBox.SetItemChecked($i, $false) } }) $okButton = New-Object System.Windows.Forms.Button $okButton.Location = New-Object System.Drawing.Point(280, 390) $okButton.Size = New-Object System.Drawing.Size(100, 40) $okButton.Text = "OK" $okButton.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 10.5) $okButton.FlatStyle = "Flat" $okButton.BackColor = [System.Drawing.Color]::FromArgb(55, 71, 133) $okButton.ForeColor = [System.Drawing.Color]::White $okButton.Cursor = "Hand" $okButton.DialogResult = "OK" $form.Controls.AddRange(@($headerPanel, $listBox, $selectAllBtn, $clearAllBtn, $okButton)) $form.AcceptButton = $okButton # Loop until valid selection or cancelled $selectedTypes = @() while ($true) { $result = $form.ShowDialog() if ($result -eq "OK") { foreach ($item in $listBox.CheckedItems) { # Map display back to type name $mappedType = ($itemList | Where-Object { $_.Display -eq $item }).Type $selectedTypes += $mappedType } if ($selectedTypes.Count -gt 0) { break } else { [System.Windows.Forms.MessageBox]::Show("Please select at least one file type.", "No Selection", "OK", "Warning") } } else { break # Cancelled/closed } } $form.Dispose() if ($selectedTypes.Count -eq 0) { return @() } return $selectedTypes } function Select-ActionMode { <# .SYNOPSIS Shows Move/Copy selection dialog (modern styled) #> [CmdletBinding()] param() Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing $form = New-Object System.Windows.Forms.Form $form.Text = "FileOrganizer - Select Operation" $form.Size = New-Object System.Drawing.Size(440, 220) $form.StartPosition = "CenterScreen" $form.FormBorderStyle = "FixedDialog" $form.MaximizeBox = $false $form.MinimizeBox = $false $form.BackColor = [System.Drawing.Color]::FromArgb(245, 245, 250) $form.Font = New-Object System.Drawing.Font("Segoe UI", 10) $headerPanel = New-Object System.Windows.Forms.Panel $headerPanel.Dock = "Top" $headerPanel.Height = 50 $headerPanel.BackColor = [System.Drawing.Color]::FromArgb(55, 71, 133) $headerLabel = New-Object System.Windows.Forms.Label $headerLabel.Text = " Select Operation Mode" $headerLabel.ForeColor = [System.Drawing.Color]::White $headerLabel.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 14) $headerLabel.Dock = "Fill" $headerLabel.TextAlign = "MiddleLeft" $headerPanel.Controls.Add($headerLabel) $moveRadio = New-Object System.Windows.Forms.RadioButton $moveRadio.Location = New-Object System.Drawing.Point(40, 70) $moveRadio.Size = New-Object System.Drawing.Size(160, 30) $moveRadio.Text = "Move (default)" $moveRadio.Font = New-Object System.Drawing.Font("Segoe UI", 10) $moveRadio.Checked = $true $moveRadio.FlatStyle = "Flat" $copyRadio = New-Object System.Windows.Forms.RadioButton $copyRadio.Location = New-Object System.Drawing.Point(230, 70) $copyRadio.Size = New-Object System.Drawing.Size(160, 30) $copyRadio.Text = "Copy (safer)" $copyRadio.Font = New-Object System.Drawing.Font("Segoe UI", 10) $copyRadio.FlatStyle = "Flat" $okButton = New-Object System.Windows.Forms.Button $okButton.Location = New-Object System.Drawing.Point(110, 125) $okButton.Size = New-Object System.Drawing.Size(100, 36) $okButton.Text = "OK" $okButton.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 10) $okButton.FlatStyle = "Flat" $okButton.BackColor = [System.Drawing.Color]::FromArgb(55, 71, 133) $okButton.ForeColor = [System.Drawing.Color]::White $okButton.Cursor = "Hand" $okButton.DialogResult = "OK" $cancelButton = New-Object System.Windows.Forms.Button $cancelButton.Location = New-Object System.Drawing.Point(225, 125) $cancelButton.Size = New-Object System.Drawing.Size(100, 36) $cancelButton.Text = "Cancel" $cancelButton.Font = New-Object System.Drawing.Font("Segoe UI", 10) $cancelButton.FlatStyle = "Flat" $cancelButton.BackColor = [System.Drawing.Color]::FromArgb(220, 220, 225) $cancelButton.Cursor = "Hand" $cancelButton.DialogResult = "Cancel" $form.Controls.AddRange(@($headerPanel, $moveRadio, $copyRadio, $okButton, $cancelButton)) $form.AcceptButton = $okButton $form.CancelButton = $cancelButton $result = $form.ShowDialog() if ($result -eq "OK") { if ($copyRadio.Checked) { return "Copy" } } return "Move" } function Confirm-Summary { <# .SYNOPSIS Shows modern confirmation dialog with operation summary #> [CmdletBinding()] param( [string]$SourcePath, [string[]]$SelectedTypes, [hashtable]$TypeCounts, [string]$Action, [hashtable]$Destinations, [bool]$DryRun ) Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing # Calculate total files $totalFiles = 0 foreach ($type in $SelectedTypes) { $totalFiles += $TypeCounts[$type] } # Build summary lines using ASCII-safe characters $summaryLines = @() $summaryLines += "Source Folder: $SourcePath" $summaryLines += "" $summaryLines += "Selected Types ($($SelectedTypes.Count) categories):" foreach ($type in $SelectedTypes | Sort-Object) { $count = $TypeCounts[$type] $dest = $Destinations[$type] $summaryLines += " - $type : $count files" $summaryLines += " -> $dest" } $dryRunText = if ($DryRun) { " (DRY RUN - No changes)" } else { "" } $summaryLines += "" $summaryLines += "Operation: $Action$dryRunText" $summaryLines += "Total Files: $totalFiles" $summaryText = $summaryLines -join "`r`n" # Create modern styled confirmation form $form = New-Object System.Windows.Forms.Form $form.Text = "FileOrganizer - Confirm Operation" $form.Size = New-Object System.Drawing.Size(520, 420) $form.StartPosition = "CenterScreen" $form.FormBorderStyle = "FixedDialog" $form.MaximizeBox = $false $form.MinimizeBox = $false $form.BackColor = [System.Drawing.Color]::FromArgb(245, 245, 250) $form.Font = New-Object System.Drawing.Font("Segoe UI", 10) # Header panel $headerPanel = New-Object System.Windows.Forms.Panel $headerPanel.Dock = "Top" $headerPanel.Height = 50 $accentColor = if ($DryRun) { [System.Drawing.Color]::FromArgb(0, 120, 180) } else { [System.Drawing.Color]::FromArgb(55, 71, 133) } $headerPanel.BackColor = $accentColor $headerLabel = New-Object System.Windows.Forms.Label $headerText = if ($DryRun) { " Confirm Operation (Dry Run)" } else { " Confirm Operation" } $headerLabel.Text = $headerText $headerLabel.ForeColor = [System.Drawing.Color]::White $headerLabel.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 14) $headerLabel.Dock = "Fill" $headerLabel.TextAlign = "MiddleLeft" $headerPanel.Controls.Add($headerLabel) # Summary text box (read-only) $textBox = New-Object System.Windows.Forms.TextBox $textBox.Location = New-Object System.Drawing.Point(20, 65) $textBox.Size = New-Object System.Drawing.Size(462, 240) $textBox.Multiline = $true $textBox.ReadOnly = $true $textBox.ScrollBars = "Vertical" $textBox.Font = New-Object System.Drawing.Font("Segoe UI", 9.5) $textBox.BackColor = [System.Drawing.Color]::White $textBox.BorderStyle = "FixedSingle" $textBox.Text = $summaryText # Question label $questionLabel = New-Object System.Windows.Forms.Label $questionLabel.Location = New-Object System.Drawing.Point(20, 315) $questionLabel.Size = New-Object System.Drawing.Size(460, 25) $questionLabel.Text = "Proceed with this operation?" $questionLabel.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 10) # Yes button $yesButton = New-Object System.Windows.Forms.Button $yesButton.Location = New-Object System.Drawing.Point(140, 345) $yesButton.Size = New-Object System.Drawing.Size(110, 36) $yesButton.Text = "Yes, Proceed" $yesButton.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 10) $yesButton.FlatStyle = "Flat" $yesButton.BackColor = $accentColor $yesButton.ForeColor = [System.Drawing.Color]::White $yesButton.Cursor = "Hand" $yesButton.DialogResult = "Yes" # No button $noButton = New-Object System.Windows.Forms.Button $noButton.Location = New-Object System.Drawing.Point(265, 345) $noButton.Size = New-Object System.Drawing.Size(110, 36) $noButton.Text = "Cancel" $noButton.Font = New-Object System.Drawing.Font("Segoe UI", 10) $noButton.FlatStyle = "Flat" $noButton.BackColor = [System.Drawing.Color]::FromArgb(220, 220, 225) $noButton.Cursor = "Hand" $noButton.DialogResult = "No" $form.Controls.AddRange(@($headerPanel, $textBox, $questionLabel, $yesButton, $noButton)) $form.AcceptButton = $yesButton $form.CancelButton = $noButton $result = $form.ShowDialog() $form.Dispose() return ($result -eq "Yes") } function Select-DestinationMode { <# .SYNOPSIS Shows destination mode selection (modern styled) #> [CmdletBinding()] param() Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing $form = New-Object System.Windows.Forms.Form $form.Text = "FileOrganizer - Destination Mode" $form.Size = New-Object System.Drawing.Size(480, 240) $form.StartPosition = "CenterScreen" $form.FormBorderStyle = "FixedDialog" $form.MaximizeBox = $false $form.MinimizeBox = $false $form.BackColor = [System.Drawing.Color]::FromArgb(245, 245, 250) $form.Font = New-Object System.Drawing.Font("Segoe UI", 10) $headerPanel = New-Object System.Windows.Forms.Panel $headerPanel.Dock = "Top" $headerPanel.Height = 50 $headerPanel.BackColor = [System.Drawing.Color]::FromArgb(55, 71, 133) $headerLabel = New-Object System.Windows.Forms.Label $headerLabel.Text = " Select Destination Mode" $headerLabel.ForeColor = [System.Drawing.Color]::White $headerLabel.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 14) $headerLabel.Dock = "Fill" $headerLabel.TextAlign = "MiddleLeft" $headerPanel.Controls.Add($headerLabel) $singleRadio = New-Object System.Windows.Forms.RadioButton $singleRadio.Location = New-Object System.Drawing.Point(40, 70) $singleRadio.Size = New-Object System.Drawing.Size(400, 28) $singleRadio.Text = "Single Destination (all types to one folder)" $singleRadio.Font = New-Object System.Drawing.Font("Segoe UI", 10) $singleRadio.Checked = $true $singleRadio.FlatStyle = "Flat" $separateRadio = New-Object System.Windows.Forms.RadioButton $separateRadio.Location = New-Object System.Drawing.Point(40, 105) $separateRadio.Size = New-Object System.Drawing.Size(400, 28) $separateRadio.Text = "Separate Destination (pick folder per file type)" $separateRadio.Font = New-Object System.Drawing.Font("Segoe UI", 10) $separateRadio.FlatStyle = "Flat" $okButton = New-Object System.Windows.Forms.Button $okButton.Location = New-Object System.Drawing.Point(130, 150) $okButton.Size = New-Object System.Drawing.Size(100, 36) $okButton.Text = "OK" $okButton.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 10) $okButton.FlatStyle = "Flat" $okButton.BackColor = [System.Drawing.Color]::FromArgb(55, 71, 133) $okButton.ForeColor = [System.Drawing.Color]::White $okButton.Cursor = "Hand" $okButton.DialogResult = "OK" $cancelButton = New-Object System.Windows.Forms.Button $cancelButton.Location = New-Object System.Drawing.Point(245, 150) $cancelButton.Size = New-Object System.Drawing.Size(100, 36) $cancelButton.Text = "Cancel" $cancelButton.Font = New-Object System.Drawing.Font("Segoe UI", 10) $cancelButton.FlatStyle = "Flat" $cancelButton.BackColor = [System.Drawing.Color]::FromArgb(220, 220, 225) $cancelButton.Cursor = "Hand" $cancelButton.DialogResult = "Cancel" $form.Controls.AddRange(@($headerPanel, $singleRadio, $separateRadio, $okButton, $cancelButton)) $form.AcceptButton = $okButton $form.CancelButton = $cancelButton $result = $form.ShowDialog() $form.Dispose() if ($result -eq "OK") { if ($separateRadio.Checked) { return "Separate" } } return "Single" } #endregion #region Core Processing Functions function Get-FileType { <# .SYNOPSIS Determines file category based on extension #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Extension ) $ext = $Extension.TrimStart('.').ToLower() # Handle PS 5.1 (PSCustomObject) vs PS 6+ (Hashtable) $categories = $Script:Config.categories if ($categories -is [System.Collections.Hashtable]) { $categoryList = $categories.Keys } else { # PS 5.1 - convert PSObject to collection $categoryList = $categories.PSObject.Properties.Name } foreach ($category in $categoryList) { if ($categories -is [System.Collections.Hashtable]) { $extensions = $categories[$category] } else { $extensions = $categories.$category } if ($extensions -contains $ext) { return $category } } return "Others" } function Test-ShouldExclude { <# .SYNOPSIS Checks if file matches any exclude patterns #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$FileName ) # Get exclude patterns - handle PS 5.1 PSObject vs array $patterns = $Script:Config.excludePatterns if ($patterns -is [PSObject] -and $patterns -isnot [Array]) { $patterns = @($patterns) } if (-not $patterns) { return $false } foreach ($pattern in $patterns) { # Handle wildcard patterns if ($pattern.Contains("*")) { if ($FileName -like $pattern) { return $true } } # Handle prefix patterns like ~$ (temporary Office files) elseif ($pattern.StartsWith("~")) { if ($FileName.StartsWith($pattern)) { return $true } } # Exact match else { if ($FileName -eq $pattern) { return $true } } } return $false } function Scan-SourceFiles { <# .SYNOPSIS Scans source folder and returns file list with metadata #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$SourcePath, [Parameter(Mandatory)] [int]$Depth ) Write-Host "`nScanning files..." -ForegroundColor Cyan Write-Log "INFO" "Scanning source: $SourcePath with depth: $Depth" $files = @() try { # PowerShell 7+ has -Depth parameter if ($Script:PSVersion -ge 7) { $files = @(Get-ChildItem -Path $SourcePath -File -Recurse -Depth $Depth -ErrorAction SilentlyContinue) } else { # PowerShell 5.1 - simulate depth limit if ($Depth -ge 100) { $files = @(Get-ChildItem -Path $SourcePath -File -Recurse -ErrorAction SilentlyContinue) } else { # Manual depth filtering for PS 5.1 # Normalize source path to remove trailing backslash for consistent counting $normalizedSource = $SourcePath.TrimEnd([IO.Path]::DirectorySeparatorChar) $sourceDepth = $normalizedSource.Split([IO.Path]::DirectorySeparatorChar).Count $files = @(Get-ChildItem -Path $SourcePath -File -Recurse -ErrorAction SilentlyContinue | Where-Object { $fileDepth = $_.DirectoryName.TrimEnd([IO.Path]::DirectorySeparatorChar).Split([IO.Path]::DirectorySeparatorChar).Count ($fileDepth - $sourceDepth) -le $Depth }) } } } catch { $errMsg = $_.Exception.Message Write-Log "ERROR" "Error scanning source - $errMsg" throw "Failed to scan source folder - $errMsg" } # Filter out excluded files $filteredFiles = @() foreach ($file in $files) { if (-not (Test-ShouldExclude -FileName $file.Name)) { $filteredFiles += $file } } Write-Host "Found $($filteredFiles.Count) files (excluding $($files.Count - $filteredFiles.Count) matches)" -ForegroundColor Green return $filteredFiles } function Get-TypeSummary { <# .SYNOPSIS Counts files by category #> [CmdletBinding()] param( [Parameter(Mandatory)] [array]$Files ) $typeCount = @{} foreach ($file in $Files) { $typeName = Get-FileType -Extension $file.Extension if (-not $typeCount.ContainsKey($typeName)) { $typeCount[$typeName] = 0 } $typeCount[$typeName]++ } return $typeCount } function Get-UniqueFileName { <# .SYNOPSIS Generates unique filename to avoid conflicts #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$DestinationPath, [Parameter(Mandatory)] [string]$FileName ) $fullPath = Join-Path $DestinationPath $FileName if (-not (Test-Path $fullPath)) { return $FileName } # File exists, generate unique name $baseName = [IO.Path]::GetFileNameWithoutExtension($FileName) $extension = [IO.Path]::GetExtension($FileName) $counter = 1 do { $newName = "${baseName}_${counter}${extension}" $fullPath = Join-Path $DestinationPath $newName $counter++ } while (Test-Path $fullPath) return $newName } function Test-Cancel { <# .SYNOPSIS Checks if user pressed Escape to cancel operation #> [CmdletBinding()] param() if ($Script:PSVersion -ge 7 -and [Console]::IsInputRedirected) { return $false } if ([Console]::KeyAvailable) { $key = [Console]::ReadKey($true) if ($key.Key -eq "Escape") { Write-Host "`nOperation Cancelled by user." -ForegroundColor Yellow Write-Log "WARN" "Operation cancelled by user" return $true } } return $false } #endregion #region File Operation Functions function Process-Files { <# .SYNOPSIS Main file processing loop with progress and cancellation #> [CmdletBinding()] param( [Parameter(Mandatory)] [array]$Files, [Parameter(Mandatory)] [string[]]$SelectedTypes, [Parameter(Mandatory)] [hashtable]$TypeDestinationMap, [Parameter(Mandatory)] [string]$Action, [bool]$DryRun, [bool]$CreateTypeSubfolder ) $processedCount = 0 $errorCount = 0 $total = $Files.Count $index = 0 $operationVerb = if ($Action -eq "Move") { "Moving" } else { "Copying" } $logAction = if ($DryRun) { "DRYRUN" } else { "INFO" } Write-Host "`n$operationVerb files..." -ForegroundColor $(if ($DryRun) { "Cyan" }else { "Yellow" }) foreach ($file in $Files) { # Check for cancellation if (Test-Cancel) { Write-Host "`nOperation cancelled. Stopping..." -ForegroundColor Yellow break } $index++ $percent = [math]::Round(($index / $total) * 100) Write-Progress -Activity "$operationVerb Files ($Action)" -Status "$index of $total files" -PercentComplete $percent # Determine file type $typeName = Get-FileType -Extension $file.Extension # Get destination path $baseDest = $TypeDestinationMap[$typeName] # Create type subfolder if enabled if ($CreateTypeSubfolder) { $destPath = Join-Path $baseDest $typeName } else { $destPath = $baseDest } # Create destination if it doesn't exist if (-not (Test-Path $destPath)) { try { New-Item -ItemType Directory -Path $destPath -Force | Out-Null Write-Log "INFO" "Created directory: $destPath" } catch { $errMsg = $_.Exception.Message Write-Log "ERROR" "Failed to create directory $destPath - $errMsg" $errorCount++ continue } } # Get unique filename $newFileName = Get-UniqueFileName -DestinationPath $destPath -FileName $file.Name $newFullPath = Join-Path $destPath $newFileName # Perform the operation if ($DryRun) { # Dry run - just log what would happen Write-Log "DRYRUN" "Would $Action '$($file.FullName)' -> '$newFullPath'" $processedCount++ } else { # Actual move or copy try { if ($Action -eq "Move") { Move-Item -Path $file.FullName -Destination $newFullPath -Force -ErrorAction Stop } else { Copy-Item -Path $file.FullName -Destination $newFullPath -Force -ErrorAction Stop } Write-Log "INFO" "$Action completed: $($file.Name) -> $newFullPath" $processedCount++ } catch { $errMsg = $_.Exception.Message $fileName = $file.Name Write-Log "ERROR" "Failed to $Action '$fileName' - $errMsg" $errorCount++ } } } Write-Progress -Activity "$operationVerb Files" -Completed # Return summary return @{ Processed = $processedCount Errors = $errorCount } } #endregion #region Interactive Mode function Start-InteractiveMode { <# .SYNOPSIS Runs the interactive GUI-based workflow #> [CmdletBinding()] param() Write-Host "`n=== FileOrganizer - Interactive Mode ===" -ForegroundColor Cyan Write-Host "Press ESC anytime to cancel operation`n" -ForegroundColor Gray # Give user a moment to see the startup Start-Sleep -Milliseconds 500 # Step 1: Select source Write-Host "Step 1: Select Source Folder (a dialog will open)" -ForegroundColor Yellow Write-Host ">>> Click Browse and select a folder <<<" -ForegroundColor Magenta $sourceFolder = Select-SourceFolder if (-not $sourceFolder) { Write-Host "No source folder selected. Exiting." -ForegroundColor Yellow return } # Step 2: Select depth Write-Host "`nStep 2: Select Scan Depth" -ForegroundColor Yellow $depth = Select-DepthGUI Write-Host "Using depth: $depth" -ForegroundColor Gray # Step 3: Scan files $files = Scan-SourcePath -SourcePath $sourceFolder -Depth $depth if ($files.Count -eq 0) { Write-Host "No files found in source folder." -ForegroundColor Yellow return } # Step 4: Get type summary $typeCounts = Get-TypeSummary -Files $files # Step 5: Select types Write-Host "`nStep 3: Select File Types" -ForegroundColor Yellow $selectedTypes = Select-FileType -TypeCounts $typeCounts if ($selectedTypes.Count -eq 0) { Write-Host "No file types selected. Exiting." -ForegroundColor Yellow return } # Step 6: Select action (Move/Copy) Write-Host "`nStep 4: Select Operation" -ForegroundColor Yellow $action = Select-ActionMode Write-Host "Operation: $action" -ForegroundColor Gray # Step 7: Destination mode Write-Host "`nStep 5: Select Destination Mode" -ForegroundColor Yellow $destMode = Select-DestinationMode $typeDestinationMap = @{} if ($destMode -eq "Separate") { # Separate destination per type foreach ($type in $selectedTypes) { Write-Host "`nSelect destination for: $type" -ForegroundColor Cyan $dest = Select-DestinationFolder -Description "Select destination for $type" if (-not $dest) { Write-Host "Destination not selected. Using source folder." -ForegroundColor Yellow $dest = $sourceFolder } $typeDestinationMap[$type] = $dest } } else { # Single destination Write-Host "`nSelect single destination folder" -ForegroundColor Cyan $singleDest = Select-DestinationFolder -Description "Select destination for all files" if (-not $singleDest) { Write-Host "No destination selected. Using source folder." -ForegroundColor Yellow $singleDest = $sourceFolder } foreach ($type in $selectedTypes) { $typeDestinationMap[$type] = $singleDest } } # Step 8: DryRun option Write-Host "`nStep 6: Dry Run Mode?" -ForegroundColor Yellow $dryRunChoice = Read-Host "Run as Dry Run (simulate only)? y/n (default: n)" $dryRun = ($dryRunChoice -eq "y") if ($dryRun) { Write-Host "`n*** DRY RUN MODE - No files will be modified ***" -ForegroundColor Cyan } # Step 9: Confirm summary Write-Host "`nStep 7: Confirm Operation" -ForegroundColor Yellow $confirmed = Confirm-Summary -SourcePath $sourceFolder -SelectedTypes $selectedTypes -TypeCounts $typeCounts -Action $action -Destinations $typeDestinationMap -DryRun $dryRun if (-not $confirmed) { Write-Host "Operation cancelled by user." -ForegroundColor Yellow return } # Step 10: Filter files to only selected types, then process $filteredFiles = @($files | Where-Object { $typeName = Get-FileType -Extension $_.Extension @($selectedTypes) -contains $typeName }) Write-Host "`nFiles to process: $($filteredFiles.Count) (of $($files.Count) total scanned)" -ForegroundColor Gray $createSubfolder = [bool]$Script:Config.createTypeSubfolder $result = Process-Files -Files $filteredFiles -SelectedTypes @($selectedTypes) -TypeDestinationMap $typeDestinationMap -Action $action -DryRun $dryRun -CreateTypeSubfolder $createSubfolder # Step 11: Show results Write-Host "`n=== Operation Complete ===" -ForegroundColor Green Write-Host "Files processed: $($result.Processed)" -ForegroundColor White if ($result.Errors -gt 0) { Write-Host "Errors: $($result.Errors)" -ForegroundColor Red } Write-Log "INFO" "Operation completed - Processed: $($result.Processed), Errors: $($result.Errors)" # Save last used paths on success if ($result.Processed -gt 0 -and -not $dryRun) { $firstDest = ($typeDestinationMap.Values | Select-Object -First 1) Save-LastUsed -SourcePath $sourceFolder -DestPath $firstDest } } function Scan-SourcePath { <# .SYNOPSIS Wrapper for scanning with user feedback #> [CmdletBinding()] param( [string]$SourcePath, [int]$Depth ) return Scan-SourceFiles -SourcePath $SourcePath -Depth $Depth } function Start-CLIMode { <# .SYNOPSIS Runs the command-line interface mode with provided parameters #> [CmdletBinding()] param() Write-Host "`n=== FileOrganizer - CLI Mode ===" -ForegroundColor Cyan # Validate source path if (-not $SourcePath) { Write-Error "SourcePath is required in CLI mode. Use -SourcePath parameter." return } if (-not (Test-Path $SourcePath)) { Write-Error "Source path does not exist: $SourcePath" return } # Set default destination if not provided if (-not $DestPath) { $DestPath = $SourcePath Write-Host "No destination specified, using source path: $DestPath" -ForegroundColor Yellow } # Scan files $files = Scan-SourceFiles -SourcePath $SourcePath -Depth $Depth if ($files.Count -eq 0) { Write-Host "No files found." -ForegroundColor Yellow return } # Get type summary $typeCounts = Get-TypeSummary -Files $files # Show summary Write-Host "`nFile Type Summary:" -ForegroundColor Green foreach ($type in ($typeCounts.Keys | Sort-Object)) { Write-Host " $type : $($typeCounts[$type]) files" -ForegroundColor White } # Determine selected types (all by default in CLI) $selectedTypes = $typeCounts.Keys # Set up destination map $typeDestinationMap = @{} if ($SeparateDestinations) { Write-Host "`nSeparate destinations requested but not implemented in CLI mode." -ForegroundColor Yellow Write-Host "Using single destination: $DestPath" -ForegroundColor Yellow } foreach ($type in $selectedTypes) { $typeDestinationMap[$type] = $DestPath } # Show dry run status if ($DryRun) { Write-Host "`n*** DRY RUN MODE - No files will be modified ***" -ForegroundColor Cyan } # Filter files to selected types, then process $filteredFiles = @($files | Where-Object { $typeName = Get-FileType -Extension $_.Extension @($selectedTypes) -contains $typeName }) $createSubfolder = [bool]$Script:Config.createTypeSubfolder $result = Process-Files -Files $filteredFiles -TypeDestinationMap $typeDestinationMap -Action $Action -DryRun $DryRun -CreateTypeSubfolder $createSubfolder # Show results Write-Host "`n=== Operation Complete ===" -ForegroundColor Green Write-Host "Files processed: $($result.Processed)" -ForegroundColor White if ($result.Errors -gt 0) { Write-Host "Errors: $($result.Errors)" -ForegroundColor Red } Write-Log "INFO" "CLI operation completed - Processed: $($result.Processed), Errors: $($result.Errors)" # Save last used paths if ($result.Processed -gt 0 -and -not $DryRun) { Save-LastUsed -SourcePath $SourcePath -DestPath $DestPath } } #endregion #region Main Entry Point function Main { <# .SYNOPSIS Main entry point for the script #> [CmdletBinding()] param() # Initialize environment Initialize-Environment # Check if Help requested if ($Help) { Get-Help $MyInvocation.MyCommand.Path -Detailed return } # Determine mode: CLI or Interactive $isCLIMode = $SourcePath -and $DestPath if ($isCLIMode) { # Run in CLI mode Start-CLIMode } else { # Run in Interactive mode Start-InteractiveMode # Loop for additional tasks unless -NoLoop specified if (-not $NoLoop) { while ($true) { Write-Host "`n" -NoNewline $again = Read-Host "Do another task? y/n" if ($again -ne "y") { break } Start-InteractiveMode } } } Write-Host "`nGoodbye!" -ForegroundColor Cyan Write-Log "INFO" "FileOrganizer ended" # Pause to keep window open Write-Host "`nPress Enter to exit..." -ForegroundColor Gray try { [void](Read-Host) } catch { Start-Sleep -Seconds 5 } } # Run main function try { Main } catch { $errMsg = $_.Exception.Message $stackTrace = $_.ScriptStackTrace Write-Host "`n" -ForegroundColor Red Write-Host "FATAL ERROR: $errMsg" -ForegroundColor Red Write-Host "Stack Trace: $stackTrace" -ForegroundColor Red Write-Host "`nPress Enter to exit..." -ForegroundColor Yellow try { Read-Host } catch { Start-Sleep -Seconds 5 } exit 1 } #endregion