MartinsProfile.psm1
function AddDevShell { <# .Synopsis Add Visual Studio Developer Powershell Prompt functionality to the current session. #> [CmdletBinding()] param () $vsWhere = "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" if (!(Test-Path $vsWhere)) { throw "Visual Studio Installer not found. Cannot determine VS location." } $installPath = &$vsWhere -version 16.0 -property installationpath Import-Module (Join-Path $installPath "Common7\Tools\Microsoft.VisualStudio.DevShell.dll") Enter-VsDevShell -VsInstallPath $installPath -SkipAutomaticLocation } function GetParentProcess { <# .Synopsis Get the Process of the current process' parent. #> [CmdletBinding()] param () Get-Process -Id (GetParentProcessId) } function GetParentProcessId { <# .Synopsis Get the Process Id of the current process' parent. #> [CmdletBinding()] param () if ($PSVersionTable.PSVersion.Major -lt 6) { # PS 5 isn't as smart... ((Get-WmiObject win32_process | Where-Object processid -eq $pid).parentprocessid) } else { (Get-Process -Id $pid).Parent.Id } } function GetPhysicalPsDrive { [CmdletBinding()] param () process { Get-PSDrive | Where-Object Name -in @( Get-Partition | Where-Object Type -eq Basic | Where-Object { -not [string]::IsNullOrWhitespace($_) } | Select-Object -ExpandProperty DriveLetter) } } function ImportHumanizer { <# .SYNOPSIS Import Humanizer formats and type updates. .EXAMPLE PS C:\> Import-Humanizer Import Humanizer formats and type updates. .NOTES Does not include localization libraries, so only Invariant Culture. #> [CmdletBinding()] param () $path = "$PSScriptRoot/Dlls/Humanizer.dll" WriteDebug "Importing Humanizer: $path" Add-Type -Path $path -Verbose:([boolean]$env:MartinsProfileDebugMode) -Debug:([boolean]$env:MartinsProfileDebugMode) } function InstallEverythingCli { [CmdletBinding()] param () WriteDebug "Checking on Windows" if (-not $IsWindows) { return } # Test for cli WriteDebug "Checking for existing es.exe" if (Test-Path "$env:LOCALAPPDATA\Everything\es.exe") { $p = @{ SearchPath = @("$env:LOCALAPPDATA\Everything") ToolName = 'es.exe' AliasName = 'es' } WriteDebug "Creating Alias 'es'" NewToolAlias @p return } WriteDebug "Checking for winget" if (-not (Test-Command -Name winget)) { Write-Warning "Winget not available, reduced functionality likely" } WriteDebug "Checking for voidtools.everything" $response = winget list voidtools.everything if ($response -match "No installed package found matching input criteria") { WriteDebug "Installing voidtools.everything" Write-Warning "Everything is not installed, installing" winget install voidtools.everything -r } WriteDebug "Checking for AppData\Everything" if (-not (Test-Path "$env:LOCALAPPDATA\Everything")) { New-Item -Path $env:LOCALAPPDATA\Everything -ItemType Directory } WriteDebug "Checking for es.exe" if (-not (Test-Path "$env:LOCALAPPDATA\Everything\es.exe")) { Write-Warning "Everything CLI is not installed, installing" $esZipPath = "$env:LOCALAPPDATA\Everything\ES-1.1.0.23.zip" Invoke-WebRequest -Uri https://www.voidtools.com/ES-1.1.0.23.zip -OutFile $esZipPath Unblock-File $esZipPath Expand-Archive -Path $esZipPath -DestinationPath "$env:LOCALAPPDATA\Everything" } WriteDebug "Creating Alias 'es'" $p = @{ SearchPath = @("$env:LOCALAPPDATA\Everything") ToolName = 'es.exe' AliasName = 'es' } NewToolAlias @p } function NewToolAlias { <# .SYNOPSIS Add an alias for a CLI tool #> param( # Paths to search for the tool (if not in env:PATH) # Do not include tool/exe name # e.g. c:\windows\system32 [String[]]$SearchPath, # Tool/Exe name e.g. notepad.exe [String]$ToolName, # Alias for tool/exe [String]$AliasName ) $s = $PSStyle.Foreground.Yellow $f = $PSStyle.Reset if (-not [String]::IsNullOrWhiteSpace((Get-DropboxFolder -ErrorAction SilentlyContinue))) { # First portable/dropbox $dropboxPortable = Join-Path -Path (Get-DropboxFolder) 'Software' -AdditionalChildPath 'portable' $portable = Get-ChildItem -Path $dropboxPortable -Recurse -Filter $ToolName | Select-Object -First 1 if (-not [String]::IsNullOrWhiteSpace($portable)) { WriteDebug "Set Tool Alias ${s}${AliasName}${f} -> ${s}${portable}${f}" New-Alias -Name $AliasName -Value $portable -Scope Global return } } else { WriteDebug "Dropbox not found/installed" } # Then Check in path if (Test-Command -Name $ToolName) { WriteDebug "Set Tool Alias From env:PATH: ${s}${AliasName}${f} -> ${s}${ToolName}${f}" return } # Then check search paths $found = $SearchPath | Where-Object { Test-Path (Join-Path $_ $ToolName) } | Select-Object -First 1 if ([String]::IsNullOrWhiteSpace($found)) { WriteDebug "Tool ${s}$ToolName${f} not found" return } $path = (Join-Path $found $ToolName) WriteDebug "Set Tool Alias ${s}${AliasName}${f} -> ${s}${path}${f}" New-Alias -Name $AliasName -Value $path -Scope Global } # Copied from Environment Module ((c) 2016,2018 Joel Bennett. All rights reserved. MIT License) function SelectPrfUniquePath { [CmdletBinding()] param( # If non-full, split path by the delimiter. Defaults to '[IO.Path]::PathSeparator' so you can use this on $Env:Path [Parameter(Mandatory=$False)] [AllowNull()] [string]$Delimiter = [IO.Path]::PathSeparator, # Paths to folders [Parameter(Position=1,Mandatory=$true,ValueFromRemainingArguments=$true)] [AllowEmptyCollection()] [AllowEmptyString()] [string[]]$Path ) begin { Write-Information "Select-PrfUniquePath $Delimiter $Path" -Tags "Trace", "Enter" [string[]]$Output = @() } process { $Output += $( # Split and trim trailing slashes to normalize $oldPaths = $Path -split $Delimiter -replace '[\\\/]$' -gt "" # Injecting wildcards causes Windows to figure out the actual case of the path $folders = $oldPaths -replace '(?<!(?::|\\\\))(\\|/)', '*$1' -replace '$', '*' $newPaths = Get-Item $folders -Force | Convert-Path # Make sure we didn't add anything that wasn't already there $newPaths | Where-Object { $_ -iin $oldPaths } ) } end { if($Delimiter) { [System.Linq.Enumerable]::Distinct($Output) -join $Delimiter } else { [System.Linq.Enumerable]::Distinct($Output) } Write-Information "Select-PrfUniquePath $Delimiter $Path" -Tags "Trace", "Exit" } } function UpdatePSReadLine { <# .Synopsis Preferred PSReadline Settings. #> } function UpdateToolPath { <# .Synopsis Add useful tool aliases #> param() VerboseBlock "docker" { $p = @{ SearchPath = @() ToolName = 'docker.exe' AliasName = 'd' } NewToolAlias @p } VerboseBlock "docker-compose" { $p = @{ SearchPath = @() ToolName = 'docker-compose.exe' AliasName = 'dc' } NewToolAlias @p } VerboseBlock "Sublime Merge" { $p = @{ SearchPath = @("$env:ProgramFiles\Sublime Merge") ToolName = 'smerge.exe' AliasName = 'sm' } NewToolAlias @p } VerboseBlock "Notepad++" { $p = @{ SearchPath = @("$env:ProgramFiles\Notepad++") ToolName = 'notepad++.exe' AliasName = 'npp' } NewToolAlias @p } VerboseBlock "FAR filemanager" { $p = @{ SearchPath = @() ToolName = 'Far.exe' AliasName = 'far' } NewToolAlias @p } VerboseBlock "RegexBuddy" { $p = @{ SearchPath = @() ToolName = 'RegexBuddy4.exe' AliasName = 'regbuddy' } NewToolAlias @p } } $VerboseDepth = 0 $VerboseBlockName = [System.Collections.Stack]::new() Function VerboseBlock { param( [Parameter(Mandatory = $true)] $Name, [Parameter(Mandatory = $true)] [scriptblock]$Scriptblock ) try { if ($ProfileDebugMode) { WriteDebug "🔽 '$($PSStyle.Foreground.BrightYellow)$Name$($PSStyle.Reset)'" } $sw = [System.Diagnostics.Stopwatch]::StartNew() $script:VerboseDepth++ $script:VerboseBlockName.Push($Name) $Scriptblock.Invoke() $sw.Stop $nameArray = $script:VerboseBlockName.ToArray() [array]::Reverse($nameArray) $fullName = [string]::Join( ' ', $nameArray) $script:timers += [PSCustomObject]@{ Name = $fullName; Timer = $sw.ElapsedMilliseconds } $null = $script:VerboseBlockName.Pop() $script:VerboseDepth-- if ($ProfileDebugMode) { WriteDebug "🔼 '$($PSStyle.Foreground.BrightYellow)$Name$($PSStyle.Reset)' $($sw.ElapsedMilliseconds)ms" } } catch { WriteError "Unhandled Error in VerboseBlock '$Name': $_" throw } } function WriteDebug() { [CmdletBinding()] param( [Parameter(ValueFromPipeline=$true)] [string]$Message ) if ($ProfileDebugMode) { $space = " " * $VerboseDepth Write-Information "${messagePrefix}$($PSStyle.Foreground.Cyan)DEBUG$($PSStyle.Reset): ${space}${Message}" -Tags @('MartinsProfile', 'Debug') -InformationAction "Continue" } } function WriteError() { [CmdletBinding()] param( [Parameter(ValueFromPipeline=$true)] [string]$Message ) $space = " " * $script:VerboseDepth Write-Information "${messagePrefix}$($PSStyle.Foreground.Red)ERROR$($PSStyle.Reset): ${space}${Message}" -Tags @('MyPro', 'Error') -InformationAction "Continue" } function Add-ExtensionMethod { [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = 'Path to assembly to process.')] [Alias('PSPath')] [Alias('Path')] [ValidateNotNullOrEmpty()] [string] $DllPath ) Process { $assembly = [System.Reflection.Assembly]::LoadFrom($DllPath) $types = $assembly.GetExportedTypes() $extensionMethods = @() foreach($type in $types) { if($type.IsSealed -and $type.IsAbstract) { $methods = $type.GetMethods() | Where-Object { $_.IsStatic } | Where-Object { -not $_.IsGenericMethod } foreach($method in $methods) { if([System.Runtime.CompilerServices.ExtensionAttribute]::IsDefined($method, [System.Runtime.CompilerServices.ExtensionAttribute])) { $parameters = $method.GetParameters() # simple extension methods first if ($parameters.Count -eq 1) { $extensionMethods += $method } } } } } $extensionMethods | ForEach-Object { $targetType = $_.GetParameters()[0].ParameterType $methodName = $_.Name Write-Debug "Found: TypeName: $targetType, MemberName: $methodName" $command = @" [OutputType([$($_.ReturnType)])] param() [$($_.DeclaringType)]::$methodName(`$this) "@ $scriptblock = [Scriptblock]::Create($command) Write-Debug "--- Generated Command ---" Write-Debug $command Write-Debug "-------------------------" Update-TypeData -TypeName $targetType -MemberType ScriptProperty -MemberName $methodName -Value $scriptblock } } } function Add-TerminalConfig { [CmdletBinding()] param () $InformationPreference = 'Continue' if (-not $IsWindows) { Write-Error 'Only supported on Windows' return } $file = 'default.json' $dir = Join-Path $env:LOCALAPPDATA 'Microsoft\Windows Terminal\Fragments\MartinsProfile' $path = Join-Path $dir $file if (-not (Test-Path $dir)) { Write-Information "Creating Fragments Directory '$dir'" $null = New-Item -ItemType Directory -Path $dir -Force } if (Test-Path $path) { Write-Information "File '$path' already exists, removing" $null = Remove-Item -Path $path -Force } @{ profiles = @( @{ name = "Martin's Profile" commandline = 'pwsh -NoLogo -NoProfile -NoExit -Command "Import-Module MartinsProfile"' icon = 'ms-appx:///ProfileIcons/pwsh.png' }, @{ name = 'pwsh (NoProfile)' commandline = 'pwsh -NoLogo -NoProfile' icon = 'ms-appx:///ProfileIcons/pwsh.png' } ) schemes = @( @{ name = "Tokyo Night" black = "#15161e" red = "#f7768e" green = "#9ece6a" yellow = "#e0af68" blue = "#7aa2f7" purple = "#bb9af7" cyan = "#7dcfff" white = "#a9b1d6" brightBlack = "#414868" brightRed = "#f7768e" brightGreen = "#9ece6a" brightYellow = "#e0af68" brightBlue = "#7aa2f7" brightPurple = "#bb9af7" brightCyan = "#7dcfff" brightWhite = "#c0caf5" background = "#1a1b26" foreground = "#c0caf5" selectionBackground = "#33467c" cursorColor = "#c0caf5" }, @{ name = 'Dracula' black = '#000000' red = '#ff5555' green = '#50fa7b' yellow = '#f1fa8c' blue = '#bd93f9' purple = '#ff79c6' cyan = '#8be9fd' white = '#bbbbbb' brightBlack = '#555555' brightRed = '#ff5555' brightGreen = '#50fa7b' brightYellow = '#f1fa8c' brightBlue = '#bd93f9' brightPurple = '#ff79c6' brightCyan = '#8be9fd' brightWhite = '#ffffff' background = '#1e1f29' foreground = '#f8f8f2' selectionBackground = '#44475a' cursorColor = '#bbbbbb' }, @{ name = 'GitHub Dark' black = '#000000' red = '#f78166' green = '#56d364' yellow = '#e3b341' blue = '#6ca4f8' purple = '#db61a2' cyan = '#2b7489' white = '#ffffff' brightBlack = '#4d4d4d' brightRed = '#f78166' brightGreen = '#56d364' brightYellow = '#e3b341' brightBlue = '#6ca4f8' brightPurple = '#db61a2' brightCyan = '#2b7489' brightWhite = '#ffffff' background = '#101216' foreground = '#8b949e' selectionBackground = '#3b5070' cursorColor = '#c9d1d9' } ) } | ConvertTo-Json | Set-Content -Path $path -Encoding UTF8 Write-Information "Saved '$path'" Write-Warning "You need to restart Windows Terminal to take effect." } function Clear-Space { [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(ParameterSetName = 'All')] [switch]$All, [Parameter(ParameterSetName = 'Individual')] [switch]$Folders, [Parameter(ParameterSetName = 'Individual')] [switch]$Chocolatey, [Parameter(ParameterSetName = 'Individual')] [switch]$Scoop, [Parameter(ParameterSetName = 'Individual')] [switch]$NuGet, [Parameter(ParameterSetName = 'Individual')] [switch]$Info, [Parameter(ParameterSetName = 'Individual')] [switch]$RecycleBin, [Parameter(ParameterSetName = 'Individual')] [switch]$Docker, [Parameter(ParameterSetName = 'Individual')] [switch]$DockerVhdx, [Parameter(ParameterSetName = 'Individual')] [switch]$Yarn, [Parameter(ParameterSetName = 'Help')] [switch]$Help ) if (-not $IsWindows) { Write-Error 'Only supported on Windows OS' return } if ($Help) { Get-Help Clear-Space return } if (-Not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')) { Write-Error 'Must Run as Administrator.' return } class SpaceInfo { $Before $After $Info $Command } function DirectorySize() { param ( $Path ) (du -c -nobanner -l 1000 $Path | ConvertFrom-Csv | Measure-Object -AllStats -Property DirectorySize).Sum.bytes } $cHeader = '{0}{1}' -f $PSStyle.Foreground.Black, $PSStyle.Background.LightYellow $cInfoStart = $PSStyle.Foreground.LightYellow $cText = $PSStyle.Foreground.Yellow $cName = $PSStyle.Foreground.Cyan $cFolder = $PSStyle.Foreground.Magenta $cReset = $PSStyle.Reset $cSuccess = $PSStyle.Foreground.LightGreen $drives = Get-PhysicalPsDrive | ForEach-Object { @{ Name = $_.Name ; StartFree = $_.Free } } $results = @() $startFree = (Get-PSDrive c).Free Write-Host Write-Host "$cHeader ---- Clean ---- $cReset" Write-Host if ($All -or $Folders) { @( @{ Path = $env:TEMP; Name = '$env:TEMP' }, @{ Path = $env:TMP; Name = '$env:TMP' }, @{ Path = 'c:\temp'; Name = 'c:\temp' }) | ForEach-Object { Write-Host "$cText $cName$($_.Name) $cText($cFolder$($_.Path)$cText) folder$cReset" $sizeBefore = DirectorySize -Path $_.Path if ($PSCmdlet.ShouldProcess($_, 'Clean')) { Get-ChildItem $_.Path | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue } $sizeAfter = DirectorySize -Path $_.Path $results += [SpaceInfo]@{ Command = 'Folders'; Info = "Directory $($_.Name)"; Before = $sizeBefore; After = $sizeAfter } Write-Host } } if ($All -or $Docker) { if ($null -eq (Get-Command -ErrorAction SilentlyContinue docker)) { Write-Warning 'Docker not in PATH. Skipping.' } else { Write-Host "$cText Docker" -ForegroundColor Cyan if ($PSCmdlet.ShouldProcess('Docker', 'Prune')) { docker system prune --force } if ($All -or $DockerVhdx) { if ($null -eq (Get-Command -ErrorAction SilentlyContinue wsl)) { Write-Warning 'WSL not in PATH. Skipping.' } else { Write-Host "$cText Docker VHXD" -ForegroundColor Cyan if ($PSCmdlet.ShouldProcess('Docker', 'Shrink VXHD')) { wsl --shutdown Optimize-VHD -Path $env:LOCALAPPDATA\Docker\wsl\Data\ext4.vhdx, $env:LOCALAPPDATA\Packages\CanonicalGroupLimited.*\LocalState\ext4.vhdx -Mode full Write-Warning 'You Will Need to Restart Docker Desktop.' } } } Write-Host } } if ($All -or $Chocolatey) { if ($null -eq (Get-Command -ErrorAction SilentlyContinue choco)) { Write-Warning 'Choco not in PATH. Skipping.' } else { Write-Host "$cText🍫 Chocolatey$cReset" if ($PSCmdlet.ShouldProcess('Chocolatey', 'Cleaner')) { cinst choco-cleaner -y choco-cleaner } Write-Host } } if ($All -or $Scoop) { if ($null -eq (Get-Command -ErrorAction SilentlyContinue scoop)) { Write-Warning 'Scoop not in PATH. Skipping.' } else { Write-Host "$cText🍧 Scoop$cReset" if ($PSCmdlet.ShouldProcess('Scoop', 'Cleanup, Empty Cache')) { scoop cleanup * scoop cache rm * } Write-Host } } if ($All -or $RecycleBin) { Write-Host "$cText Recycle Bin$cReset" if ($PSCmdlet.ShouldProcess('Recycle Bin', 'Clear')) { Clear-RecycleBin -Force } Write-Host } if ($All -or $NuGet) { if ($null -eq (Get-Command -ErrorAction SilentlyContinue dotnet)) { Write-Warning 'dotnet cli not in PATH. Skipping.' } else { Write-Host "$cText NuGet$cReset" if ($PSCmdlet.ShouldProcess('NuGet', 'Clear Caches')) { dotnet nuget locals all --clear } Write-Host } } if ($All -or $Yarn) { if ($null -eq (Get-Command -ErrorAction SilentlyContinue yarn)) { Write-Warning 'yarn not in path. Skipping.' } else { if ($null -eq (Get-Command -ErrorAction SilentlyContinue node)) { Write-Warning 'node not in path. Skipping.' } else { Write-Host "$cText Yarn$cReset" if ($PSCmdlet.ShouldProcess('Yarn', 'Clear Caches')) { yarn cache clean --emoji true } Write-Host } } } $endFree = (Get-PSDrive c).Free $spaceRecovered = ($endFree - $startFree).bytes function InfoTable() { param ( $DataTable, $Name ) $tableFormat = @(@{ Expression = { $v = ([int]$_.Size).bytes ; "$cName{0:n1} {1}$cReset" -f $v.LargestWholeNumberValue, $v.LargestWholeNumberSymbol } Label = 'Size' } , 'Filename') Write-Host Write-Host "$cInfoStart === ${cText}$Name$cReset" $DataTable | Select-Object -First 10 | Format-Table -Property $tableFormat $stats = $DataTable | Measure-Object -Sum -Property Size Write-Host "Total Items: $($stats.Count)" Write-Host ("Total Size: $cName{0:n1} {1}$cReset" -f $stats.Sum.bytes.LargestWholeNumberValue, $stats.Sum.bytes.LargestWholeNumberSymbol) } if ($All -or $Info) { if ($null -eq (Get-Command -ErrorAction SilentlyContinue es)) { Write-Warning 'es [1] not in PATH. Skipping. [1] voidtools everything command line' } else { Write-Host "$cHeader ---- Info ---- $cReset" InfoTable -DataTable (es -csv -sort-size -size wholefilename:cache attrib:D | ConvertFrom-Csv) -Name 'Caches' InfoTable -DataTable (es -csv -sort-size -size wholefilename:temp attrib:D | ConvertFrom-Csv) -Name 'Temps' InfoTable -DataTable (es -csv -sort-size -size ext:log attrib:F | ConvertFrom-Csv) -Name 'Logs' InfoTable -DataTable (es -csv -sort-size -size (Resolve-Path ~\downloads) | ConvertFrom-Csv) -Name 'Downloads' } } $drives = $drives | ForEach-Object { $_.EndFree = (Get-PSDrive -Name $_.Name).Free ; $_.Recovered = $_.EndFree - $_.StartFree; $_ } | ForEach-Object { [PSCustomObject]$_ } Write-Host $drives | Format-Table -Property @{ Label = "💾 ${cText}Name$cReset"; Expression = { "💾 ${cText}$($_.Name)$cReset" } }, @{ Label = "▶ ${cName}Start$cReset"; Expression = { "▶ ${cName}{0,6:n1} {1,-2:s}$cReset" -f $_.StartFree.bytes.LargestWholeNumberValue, $_.StartFree.bytes.LargestWholeNumberSymbol } }, @{ Label = "🏁 ${cFolder}End$cReset"; Expression = { "🏁 ${cFolder}{0,6:n1} {1,-2:s}$cReset" -f $_.EndFree.bytes.LargestWholeNumberValue, $_.EndFree.bytes.LargestWholeNumberSymbol } }, @{ Label = "✔ ${cSuccess}Freed$cReset"; Expression = { "✔ ${cSuccess}{0,6:n1} {1,-2:s}$cReset" -f $_.Recovered.bytes.LargestWholeNumberValue, $_.Recovered.bytes.LargestWholeNumberSymbol } } Write-Host Write-Host "${cSuccess}Done. Bye. 🙋$cReset" } function ConvertTo-EncodedCommand { <# .Description Encode a command, as required by pwsh -EncodedCommand #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [string] # Command (string) to encode. $Command ) process { $bytes = [System.Text.Encoding]::Unicode.GetBytes($Command) $encodedCommand = [Convert]::ToBase64String($bytes) $encodedCommand } } function script:Find-VisualStudio { [CmdletBinding()] param () if (-not $IsWindows) { Write-Error 'Only supported on Windows.' return } $vsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" if (-not (Test-Path $vsWhere)) { Write-Error "'vswhere.exe' not found in default location: '$vsWhere'. Is Visual Studio installed?" } &$vsWhere -format json | ConvertFrom-Json | ForEach-Object { $_.PSObject.TypeNames.Insert(0, 'VsWhereOutput'); $_ } } function Get-DropboxFolder { <# .SYNOPSIS Find the location where Dropbox stores files for the current user. #> [CmdletBinding()] param () $info = $null $testPaths = @() if ($IsWindows) { $testPaths = @( (Join-Path "$env:APPDATA" 'dropbox\info.json') (Join-Path "$env:LOCALAPPDATA" 'dropbox\info.json') ) } else { WriteDebug "Dropbox not supported on $env:OS" return $null } if ($testPaths.Count -le 0) { WriteDebug 'Dropbox not installed' return $null } $path = $testPaths | Where-Object { Test-Path $_ } | Select-Object -First 1 if ($null -eq $path) { WriteDebug 'Dropbox is not found' return $null } $info = Get-Content $path | ConvertFrom-Json if ($null -eq $info) { Write-Warning 'Dropbox info.json invalid.' return $null } $path = $info.personal.path Write-Verbose "Found Path: $path" $path } function Get-OneDriveFolder() { [CmdletBinding()] param( [switch]$FullInfo ) if (-not $IsWindows) { Write-Error 'Only supported on Windows' return } $folders = Get-ChildItem HKCU:\Software\Microsoft\OneDrive\Accounts\ | ForEach-Object { [pscustomobject]@{ Name = Split-Path $_.Name -Leaf Folder = $_.GetValue('UserFolder') UserEmail = $_.GetValue('UserEmail') } } if ($FullInfo) { $folders } else { $folders.Folder | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } } } <# .SYNOPSIS Get the location of various profile related files. .DESCRIPTION Get the location of various profile related files. .EXAMPLE Get-ProfileLocation Get the location of various profile related files. .OUTPUTS [PSCustomObject] A custom object with the properties: - Name: The name of the profile related file. - Location: The location of the profile related file. - Exists: A string indicating whether the file exists or not, colored green for "YES" and red for "NO". .PARAMETER None .NOTES This function is only available in PowerShell v3 and later, due to the use of the $PSStyle automatic variable. .LINK https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/about/about_automatic_variables?view=powershell-7#psstyle #> function Get-ProfileLocation { [CmdletBinding()] param() $stYes = $PSStyle.Background.Green + $PSStyle.Foreground.BrightWhite $stNo = $PSStyle.Background.Red + $PSStyle.Foreground.BrightWhite $r = $PSStyle.Reset $Profile | Get-Member -MemberType NoteProperty | ForEach-Object { [PSCustomObject]@{ Name = $_.Name Exists = (Test-Path $Profile.$($_.Name)) ? "$stYes YES $r" : "$stNo NO $r" Location = $Profile.$($_.Name) } } } function Get-Quote { [CmdletBinding()][Alias('gq')] param( [Parameter(ParameterSetName = 'File', Mandatory = $true)] [string]$Path, [Parameter(ParameterSetName = 'Known', Mandatory = $true)] [ValidateSet('ChuckNorris', 'Quotes')] [string]$Source, [int]$Count = 1, [switch]$Pretty ) if ($PSCmdlet.ParameterSetName -eq 'Path') { if (!(Test-Path $Path) ) { throw "File not found: '$Path'" } } if ($PSCmdLet.ParameterSetName -eq 'Known') { switch ($Source) { 'Quotes' { $Path = (Resolve-Path "$PSScriptRoot/config/Quotes/attributed-quotes.txt") } 'ChuckNorris' { $Path = (Resolve-Path "$PSScriptRoot/config/Quotes/chuck-norris.txt") } Default { throw "Unknown Source '$Source'" } } } $quotes = Get-Content $Path | Where-Object { $_ } | Get-Random -Count $Count if ($Pretty) { $quotes | Show-MyProQuote } else { $quotes } } function Get-StringHash { <# .Synopsis Get a hash/checksum for a string. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] # String to encode [string] $String, [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5')] # Hashing algorithm to use. Default is SHA1 [string]$Algorithm = 'SHA1' ) $stream = [IO.MemoryStream]::new([Text.Encoding]::UTF8.GetBytes($String)) Get-FileHash -InputStream $stream -Algorithm $Algorithm } enum Terminals { Unknown VsCode JetBrainsJediTerm WindowsTerminal WindowsConsole Console2Z ConEmu FluentTerminal AzureCloudShell } function Get-Terminal { [OutputType([Terminals])] [CmdletBinding()] param ( [switch]$ListKnown ) if ($ListKnown) { return [Terminals].GetEnumNames() | Select-Object -Skip 1 } # VS Code if ($env:TERM_PROGRAM -eq 'vscode' ) { return [Terminals]::VsCode } # IntelliJ if ($env:TERMINAL_EMULATOR -eq 'JetBrains-JediTerm') { return [Terminals]::JetBrainsJediTerm } # AzureCloudShell if ($null -ne $env:ACC_CLOUD) { return [Terminals]::AzureCloudShell } # Resort to Process Name $processName = (Get-Process -Id $PID).Parent.Name switch ($processName) { 'code' { [Terminals]::VsCode } 'idea64' { [Terminals]::JetBrainsJediTerm } 'rider64' { [Terminals]::JetBrainsJediTerm } 'WindowsTerminal' { [Terminals]::WindowsTerminal } { $PSItem -in 'explorer', 'conhost' } { [Terminals]::WindowsConsole } 'Console' { [Terminals]::Console2Z } 'ConEmuC64' { [Terminals]::ConEmu } 'FluentTerminal.SystemTray' { [Terminals]::FluentTerminal } Default { [Terminals]::Unknown } } } Function New-DynamicParam { <# .SYNOPSIS Helper function to simplify creating dynamic parameters .DESCRIPTION Helper function to simplify creating dynamic parameters Example use cases: Include parameters only if your environment dictates it Include parameters depending on the value of a user-specified parameter Provide tab completion and intellisense for parameters, depending on the environment Please keep in mind that all dynamic parameters you create will not have corresponding variables created. One of the examples illustrates a generic method for populating appropriate variables from dynamic parameters Alternatively, manually reference $PSBoundParameters for the dynamic parameter value .NOTES Source: https://github.com/RamblingCookieMonster/PowerShell/blob/master/New-MyProDynamicParam.ps1 Credit to http://jrich523.wordpress.com/2013/05/30/powershell-simple-way-to-add-dynamic-parameters-to-advanced-function/ Added logic to make option set optional Added logic to add RuntimeDefinedParameter to existing DPDictionary Added a little comment based help Credit to BM for alias and type parameters and their handling .PARAMETER Name Name of the dynamic parameter .PARAMETER Type Type for the dynamic parameter. Default is string .PARAMETER Alias If specified, one or more aliases to assign to the dynamic parameter .PARAMETER ValidateSet If specified, set the ValidateSet attribute of this dynamic parameter .PARAMETER Mandatory If specified, set the Mandatory attribute for this dynamic parameter .PARAMETER ParameterSetName If specified, set the ParameterSet attribute for this dynamic parameter .PARAMETER Position If specified, set the Position attribute for this dynamic parameter .PARAMETER ValueFromPipelineByPropertyName If specified, set the ValueFromPipelineByPropertyName attribute for this dynamic parameter .PARAMETER HelpMessage If specified, set the HelpMessage for this dynamic parameter .PARAMETER DPDictionary If specified, add resulting RuntimeDefinedParameter to an existing RuntimeDefinedParameterDictionary (appropriate for multiple dynamic parameters) If not specified, create and return a RuntimeDefinedParameterDictionary (appropriate for a single dynamic parameter) See final example for illustration .EXAMPLE function Show-Free { [CmdletBinding()] Param() DynamicParam { $options = @( gwmi win32_volume | %{$_.driveletter} | sort ) New-MyProDynamicParam -Name Drive -ValidateSet $options -Position 0 -Mandatory } begin{ #have to manually populate $drive = $PSBoundParameters.drive } process{ $vol = gwmi win32_volume -Filter "driveletter='$drive'" "{0:N2}% free on {1}" -f ($vol.Capacity / $vol.FreeSpace),$drive } } #Show-Free Show-Free -Drive <tab> # This example illustrates the use of New-MyProDynamicParam to create a single dynamic parameter # The Drive parameter ValidateSet populates with all available volumes on the computer for handy tab completion / intellisense .EXAMPLE # I found many cases where I needed to add more than one dynamic parameter # The DPDictionary parameter lets you specify an existing dictionary # The block of code in the Begin block loops through bound parameters and defines variables if they don't exist Function Test-DynPar{ [cmdletbinding()] param( [string[]]$x = $Null ) DynamicParam { #Create the RuntimeDefinedParameterDictionary $Dictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary New-MyProDynamicParam -Name AlwaysParam -ValidateSet @( gwmi win32_volume | %{$_.driveletter} | sort ) -DPDictionary $Dictionary #Add dynamic parameters to $dictionary if($x -eq 1) { New-MyProDynamicParam -Name X1Param1 -ValidateSet 1,2 -mandatory -DPDictionary $Dictionary New-MyProDynamicParam -Name X1Param2 -DPDictionary $Dictionary New-MyProDynamicParam -Name X3Param3 -DPDictionary $Dictionary -Type DateTime } else { New-MyProDynamicParam -Name OtherParam1 -Mandatory -DPDictionary $Dictionary New-MyProDynamicParam -Name OtherParam2 -DPDictionary $Dictionary New-MyProDynamicParam -Name OtherParam3 -DPDictionary $Dictionary -Type DateTime } #return RuntimeDefinedParameterDictionary $Dictionary } Begin { #This standard block of code loops through bound parameters... #If no corresponding variable exists, one is created #Get common parameters, pick out bound parameters not in that set Function _temp { [cmdletbinding()] param() } $BoundKeys = $PSBoundParameters.keys | Where-Object { (get-command _temp | select -ExpandProperty parameters).Keys -notcontains $_} foreach($param in $BoundKeys) { if (-not ( Get-Variable -name $param -scope 0 -ErrorAction SilentlyContinue ) ) { New-Variable -Name $Param -Value $PSBoundParameters.$param Write-Verbose "Adding variable for dynamic parameter '$param' with value '$($PSBoundParameters.$param)'" } } #Appropriate variables should now be defined and accessible Get-Variable -scope 0 } } # This example illustrates the creation of many dynamic parameters using New-MyProDynamicParam # You must create a RuntimeDefinedParameterDictionary object ($dictionary here) # To each New-MyProDynamicParam call, add the -DPDictionary parameter pointing to this RuntimeDefinedParameterDictionary # At the end of the DynamicParam block, return the RuntimeDefinedParameterDictionary # Initialize all bound parameters using the provided block or similar code .FUNCTIONALITY PowerShell Language #> param( [string] $Name, [System.Type] $Type = [string], [string[]] $Alias = @(), [string[]] $ValidateSet, [switch] $Mandatory, [string] $ParameterSetName = '__AllParameterSets', [int] $Position, [switch] $ValueFromPipelineByPropertyName, [string] $HelpMessage, [validatescript({ if (-not ( $_ -is [System.Management.Automation.RuntimeDefinedParameterDictionary] -or -not $_) ) { Throw 'DPDictionary must be a System.Management.Automation.RuntimeDefinedParameterDictionary object, or not exist' } $True })] $DPDictionary = $false ) #Create attribute object, add attributes, add to collection $ParamAttr = New-Object System.Management.Automation.ParameterAttribute $ParamAttr.ParameterSetName = $ParameterSetName if ($mandatory) { $ParamAttr.Mandatory = $True } if ($Position -ne $null) { $ParamAttr.Position = $Position } if ($ValueFromPipelineByPropertyName) { $ParamAttr.ValueFromPipelineByPropertyName = $True } if ($HelpMessage) { $ParamAttr.HelpMessage = $HelpMessage } $AttributeCollection = New-Object 'Collections.ObjectModel.Collection[System.Attribute]' $AttributeCollection.Add($ParamAttr) #param validation set if specified if ($ValidateSet) { $ParamOptions = New-Object System.Management.Automation.ValidateSetAttribute -ArgumentList $ValidateSet $AttributeCollection.Add($ParamOptions) } #Aliases if specified if ($Alias.count -gt 0) { $ParamAlias = New-Object System.Management.Automation.AliasAttribute -ArgumentList $Alias $AttributeCollection.Add($ParamAlias) } #Create the dynamic parameter $Parameter = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter -ArgumentList @($Name, $Type, $AttributeCollection) #Add the dynamic parameter to an existing dynamic parameter dictionary, or create the dictionary and add it if ($DPDictionary) { $DPDictionary.Add($Name, $Parameter) } else { $Dictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary $Dictionary.Add($Name, $Parameter) $Dictionary } } function New-TemporaryFolder { $tempPath = [System.IO.Path]::GetTempPath() + [System.IO.Path]::GetFileNameWithoutExtension([System.IO.Path]::GetRandomFileName()) New-Item -ItemType Directory -Path $tempPath } function Resolve-Command { <# .SYNOPSIS Equivalent to unix "which" command. .DESCRIPTION Equivalent to unix "which" command. Gets the first available command, ie. the command that will will be used if you invoke it from the CLI. NOTE: for Functions et.al. it will return the DLL name, or module name. .EXAMPLE PS C:\> Resolve-Command pwsh.exe Test if pwsh.exe exists in available in path. .EXAMPLE PS C:\> Resolve-Command Get-ChildItem #> [CmdletBinding()] param ( [OutputType([string])] [Parameter(Mandatory = $true)] [string] $Name ) process { if ($IsWindows) { $path = where.exe $Name 2>&1 if ($?) { return $path | Select-Object -First 1 } } else { $path = which $Name 2>&1 if ($?) { return $path } } $alias = Get-Alias -Name $Name -ErrorAction SilentlyContinue if ($?) { return $alias; } Get-Command $Name -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Source } } function Show-AzBuild() { [CmdletBinding()] param () $raw = az pipelines build list | ConvertFrom-Json $raw | Sort-Object -Property startTime -Descending | Format-Table -Property @( @{Label = 'R'; Expression = { switch ($_.result) { 'succeeded' { '💚' } 'failed' { '🔴' } 'canceled' { '🚫' } Default { $_.result } } } } @{Label = 'DefName'; Expression = { $_.definition.name } } @{Label = 'DefId'; Expression = { $_.definition.id } } @{Label = 'BuildId' ; Expression = { $_.id } } @{Label = 'BuildNumber' ; Expression = { $PSStyle.FormatHyperlink($_.buildNumber, $_.url) } } @{Label = 'StartTime' ; Expression = { $_.startTime } } @{Label = 'Status' ; Expression = { $_.status } } ) } function Show-CustomViews() { $formats = @( @{ File = (Get-Item "$PSScriptRoot/../Formats/FileSystemTypes.formats.ps1xml" ); Example = { Get-ChildItem "$PSScriptRoot/.." | Format-Table } } @{ File = (Get-Item "$PSScriptRoot/../Formats/CommandInfo.formats.ps1xml" ); Example = { Get-Command -Name gi, sm } } @{ File = (Get-Item "$PSScriptRoot/../Formats/HistoryInfo.formats.ps1xml" ); Example = { Get-History -Count 2 | Format-Table } } @{ File = (Get-Item "$PSScriptRoot/../Formats/Runtime.formats.ps1xml" ); Example = { ''.GetType() | Format-Table } } @{ File = (Get-Item "$PSScriptRoot/../Formats/TimeSpan.formats.ps1xml" ); Example = { New-TimeSpan -Seconds 4 -Minutes 2 -Hours 3 } } ) $types = @( @{ File = (Get-Item "$PSScriptRoot/../Types/CommandInfo.formats.ps1xml" ); Example = { Get-Command -Name gi, sm } } ) foreach ($item in @($formats + $types)) { $header = $item.File.BaseName Write-Host "$($PSStyle.Foreground.BrightBlue)====== $($PSStyle.Foreground.White)${header} $($PSStyle.Foreground.BrightBlue)======" $item.Example.Invoke() Write-Host '' Write-Host "$($PSStyle.Foreground.BrightBlue)======" Write-Host '' } } function Show-Quote { <# .Synopsis Pretty print a quote. .Notes The quote must be a single-line string with the quote attribution after the last '--'. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [string] # The text to format. $Text, [Int] # Width of the displayed qoute. $Width = 50, [string]$BackgroundColor = $PSStyle.Background.Black, [string]$FrameColor = $PSStyle.Foreground.BrightWhite, [string]$QuoteColor = $PSStyle.Foreground.BrightYellow, [string]$AttribColor = $PSStyle.Foreground.BrightCyan ) $quoteMatch = ([Regex]'^(?<text>.+)\s*--(?!.*--)\s*(?<attrib>.+)$').Match($Text.Trim()) if ($quoteMatch.Success) { $quote = $quoteMatch.Groups['text'].Value $attrib = $quoteMatch.Groups['attrib'].Value } else { $quote = $Text.Trim() $attrib = $null } $myStack = New-Object System.Collections.Queue $quote -split ' ' | ForEach-Object { $myStack.Enqueue($_) } $leftIndent = ' ' $rightOutdent = " $($PSStyle.Reset)" $tl = "${FrameColor}┌" $tr = "${FrameColor}┐" $bl = "${FrameColor}└" $br = "${FrameColor}┘" $vr = "${FrameColor}│" $hr = "${FrameColor}─" $hd = "${FrameColor}┬" $blankLine = "$BackgroundColor" + (' ' * ($Width + 6)) Write-Host $blankLine Write-Host -Object ($BackgroundColor + $leftIndent + $tl + ($hr * $Width) + $tr + $rightOutdent) $count = 0 $curLine = '' while ($myStack.Count -gt 0) { if (($curLine + ' ' + $myStack.Peek()).Length -gt ($Width - 2)) { Write-Host -Object ("$BackgroundColor$leftIndent{0}$QuoteColor{1,-$($Width)}{0}$rightOutdent" -f $vr, $curLine) $curLine = ' ' + $myStack.Dequeue() } else { $count++ $curLine += ' ' + $myStack.Dequeue() } } Write-Host -Object ("$BackgroundColor$leftIndent{0}$QuoteColor{1,-$Width}{0}$RightOutdent" -f $vr, $curLine) if ($null -ne $attrib) { Write-Host -Object ("$BackgroundColor$leftIndent{0}{1}{1}{2}{3}{4}$RightOutdent" -f $bl, $hr, $hd, ($hr * ($Width - 3)), $br) Write-Host -Object ("$BackgroundColor$leftIndent {0} $AttribColor{1,-$($Width - 3)}$RightOutdent" -f $bl, $attrib) } else { Write-Host -Object ("$BackgroundColor$leftIndent{0}{1}{1}{2}{3}{4}$RightOutdent" -f $bl, $hr, $hr, ($hr * ($Width - 3)), $br) } Write-Host $blankLine } function Switch-DarkMode { <# .SYNOPSIS Switch Windows Dark Mode #> [CmdletBinding()] param ( [Parameter(HelpMessage = 'Only show current state, do not switch')] [switch] $OnlyShowState ) if (!$IsWindows) { Write-Error 'Only supported on Windows' return } $personalizePath = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize' $appState = Get-ItemProperty -Path $personalizePath -Name 'AppsUseLightTheme' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty 'AppsUseLightTheme' $osState = Get-ItemProperty -Path $personalizePath -Name 'SystemUsesLightTheme' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty 'SystemUsesLightTheme' if (!$OnlyShowState.IsPresent) { Set-ItemProperty -Path $personalizePath -Name 'AppsUseLightTheme' -Value ($appState -eq 0 ? 1 : 0) Set-ItemProperty -Path $personalizePath -Name 'SystemUsesLightTheme' -Value ($osState -eq 0 ? 1 : 0) } $appState = Get-ItemProperty -Path $personalizePath -Name 'AppsUseLightTheme' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty 'AppsUseLightTheme' $osState = Get-ItemProperty -Path $personalizePath -Name 'SystemUsesLightTheme' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty 'SystemUsesLightTheme' $light = "[" + $PSStyle.Background.White + $PSStyle.Foreground.Black + " LIGHT " + $PSStyle.Reset + "]" $dark = "[" +$PSStyle.Background.Black + $PSStyle.Foreground.White + " DARK " + $PSStyle.Reset + "]" Write-Output "Apps: $($appState -eq 0 ? $dark : $light) OS: $($osState -eq 0 ? $dark : $light)" } function Test-Command { <# .SYNOPSIS Test if a command/exe exists in memory or on path. .EXAMPLE PS C:\> Test-Command pwsh.exe Test if pwsh.exe exists in available in path. .EXAMPLE PS C:\> Test-Command Get-ChildItem Test if Get-ChildItem cmdlet is available. .OUTPUTS $true / $false #> [CmdletBinding()] param ( [OutputType([boolean])] [Parameter(Mandatory = $true)] [string] $Name ) process { $null -ne (Resolve-Command $Name -ErrorAction SilentlyContinue) } } $moduleSw = [System.Diagnostics.Stopwatch]::StartNew() $script:timers = @() ######## # Debug Mode ######## if (('Desktop' -eq $PSVersionTable.PSEdition) -or ($PSVersionTable.PSVersion.Major -ge 7)) { # Detect Shift Key try { # Check SHIFT state ASAP at startup so I can use that to control verbosity :) Add-Type -Assembly PresentationCore, WindowsBase if ([System.Windows.Input.Keyboard]::IsKeyDown([System.Windows.Input.Key]::LeftShift) -OR [System.Windows.Input.Keyboard]::IsKeyDown([System.Windows.Input.Key]::RightShift)) { $ProfileDebugMode = 'true' } } catch { # If that didn't work ... oh well. } } if ($env:MartinsProfileDebugMode) { $ProfileDebugMode = 'true' } ######## # Critical Variables ######## $messagePrefix = "$($PSStyle.Foreground.BrightBlack)${moduleNamespace}$($PSStyle.Reset): " if ($ProfileDebugMode) { WriteDebug '>>> MyProfile Runing in DEBUG Mode <<<' } $VerboseDepth = 0 $VerboseBlockName = [System.Collections.Stack]::new() ######## # Setup Profile ######## VerboseBlock 'Config Blocks' { $configs = @(Get-ChildItem -Path $PSScriptRoot\config\init.d\*.ps1 -ErrorAction SilentlyContinue ) | Sort-Object -Property Name foreach ($item in $configs) { WriteDebug "⚙️ $($item.BaseName)" . $item.FullName } } $moduleSw.Stop() $script:timers += @{ Name = 'Total'; Timer = $moduleSw.ElapsedMilliseconds } if ($ProfileDebugMode) { $longest = ($script:timers | Sort-Object -Property @{ Expression = { $_.Name.Length } } -Descending | Select-Object -First 1).Name.Length $script:timers | ForEach-Object { WriteDebug ("⌛ {0,-$longest} {1,5} ms" -f $_.Name, $_.Timer) } } $formats = Get-ChildItem -Path $PSScriptRoot/Formats -Filter '*.ps1xml' Update-FormatData -PrependPath $formats $types = Get-ChildItem -Path $PSScriptRoot/Types -Filter '*.ps1xml' Update-TypeData -PrependPath $types Write-Information "⏲️ $($PSStyle.Foreground.BrightYellow)MartinsProfile$($PSStyle.Foreground.BrightBlue) processed in $($PSStyle.Foreground.BrightYellow)$($moduleSw.ElapsedMilliseconds)$($PSStyle.Foreground.BrightBlue) ms.$($PSStyle.Reset)" -InformationAction 'Continue' |