diff --git a/src/powershell-extended/Install-NerdFont.ps1 b/src/powershell-extended/Install-NerdFont.ps1 index f8eb201..4106011 100644 --- a/src/powershell-extended/Install-NerdFont.ps1 +++ b/src/powershell-extended/Install-NerdFont.ps1 @@ -21,6 +21,12 @@ .SYNOPSIS Install Nerd Fonts on Windows, macOS, or Linux. + You may also run this script directly from the web using the following command: + + ```powershell + & ([scriptblock]::Create((iwr 'https://bit.ly/ps-install-nerdfont'))) + ``` + .DESCRIPTION This PowerShell script installs Nerd Fonts on Windows, macOS, or Linux. Nerd Fonts is a project that patches developer targeted fonts with a high number of glyphs (icons). @@ -41,10 +47,10 @@ Parameters may be passed just like any other PowerShell script. For example: ```powershell - & ([scriptblock]::Create((iwr 'https://bit.ly/ps-install-nerdfont'))) -FontName 'Cascadia' + & ([scriptblock]::Create((iwr 'https://bit.ly/ps-install-nerdfont'))) -Name 'Cascadia' ``` -.PARAMETER FontName +.PARAMETER Name The name of the Nerd Font to install. Multiple font names can be specified as an array of strings. If no font name is specified, the script provides an interactive menu to select the font to install @@ -54,559 +60,828 @@ Install all available Nerd Fonts. .EXAMPLE - Install-NerdFont -FontName 'Cascadia' - Install the Cascadia fonts from the Microsoft repository. This includes the Cascadia Code and Cascadia Mono fonts. - -.EXAMPLE - Install-NerdFont -FontName 'CascadiaCode' + Install-NerdFont -Name cascadia-code Install the Cascadia Code fonts from the Microsoft repository. .EXAMPLE - Install-NerdFont -FontName 'CascadiaMono' + Install-NerdFont -Name cascadia-mono Install the Cascadia Mono fonts from the Microsoft repository. .NOTES This script must be run on your local machine, not in a container. #> -[CmdletBinding(DefaultParameterSetName = 'ByFontName', SupportsShouldProcess, ConfirmImpact = 'High')] +[CmdletBinding(DefaultParameterSetName = 'ByName', SupportsShouldProcess, ConfirmImpact = 'High')] param( - [Parameter(Mandatory = $false, ParameterSetName = 'ByFontName')] - [ArgumentCompleter( - { - param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) - @( - '0xProto' - '3270' - 'Agave' - 'AnonymicePro' - 'AnonymousPro' - 'Arimo' - 'AurulentSansMono' - 'BigBlueTerminal' - 'BitstreamVeraSansMono' - 'Cascadia' - 'CascadiaCode' - 'CascadiaMono' - 'CaskaydiaCove' - 'CaskaydiaMono' - 'CodeNewRoman' - 'ComicShannsMono' - 'CommitMono' - 'Cousine' - 'D2Coding' - 'DaddyTimeMono' - 'DejaVuSansMono' - 'DroidSansMono' - 'EnvyCodeR' - 'FantasqueSansMono' - 'FiraCode' - 'FiraMono' - 'GeistMono' - 'Go-Mono' - 'Gohu' - 'Hack' - 'Hasklig' - 'Hasklug' - 'HeavyData' - 'Hermit' - 'Hurmit' - 'iA-Writer' - 'IBMPlexMono' - 'iMWriting' - 'Inconsolata' - 'InconsolataGo' - 'InconsolataLGC' - 'IntelOneMono' - 'IntoneMono' - 'Iosevka' - 'IosevkaTerm' - 'IosevkaTermSlab' - 'JetBrainsMono' - 'Lekton' - 'LiberationMono' - 'Lilex' - 'LiterationMono' - 'MartianMono' - 'Meslo' - 'Monaspace' - 'Monaspice' - 'Monofur' - 'Monoid' - 'Mononoki' - 'MPlus' - 'NerdFontsSymbolsOnly' - 'Noto' - 'OpenDyslexic' - 'Overpass' - 'ProFont' - 'ProggyClean' - 'Recursive' - 'RobotoMono' - 'SauceCodePro' - 'ShareTechMono' - 'SourceCodePro' - 'SpaceMono' - 'SureTechMono' - 'Terminess' - 'Terminus' - 'Tinos' - 'Ubuntu' - 'UbuntuMono' - 'UbuntuSans' - 'VictorMono' - 'ZedMono' - ) | - Where-Object { $_ -like "$wordToComplete*" } | - ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } - } - )] - [string[]]$FontName, - [Parameter(Mandatory = $true, ParameterSetName = 'ByAll')] - [switch]$All -) - -# Abort if running in a container -if ( - $null -ne $env:REMOTE_CONTAINERS -or - $null -ne $env:CODESPACES -or - $null -ne $env:WSL_INTEROP -) { - Write-Host 'This script most be run on your local machine, not in a container. Exiting...' -ForegroundColor Yellow - return -} + [switch]$All, -$AllNerdFonts = @( - '0xProto' - '3270' - 'Agave' - 'AnonymicePro' - 'AnonymousPro' - 'Arimo' - 'AurulentSansMono' - 'BigBlueTerminal' - 'BitstreamVeraSansMono' - 'Cascadia' - 'CascadiaCode' - 'CascadiaMono' - 'CaskaydiaCove' - 'CaskaydiaMono' - 'CodeNewRoman' - 'ComicShannsMono' - 'CommitMono' - 'Cousine' - 'D2Coding' - 'DaddyTimeMono' - 'DejaVuSansMono' - 'DroidSansMono' - 'EnvyCodeR' - 'FantasqueSansMono' - 'FiraCode' - 'FiraMono' - 'GeistMono' - 'Go-Mono' - 'Gohu' - 'Hack' - 'Hasklig' - 'Hasklug' - 'HeavyData' - 'Hermit' - 'Hurmit' - 'iA-Writer' - 'IBMPlexMono' - 'iMWriting' - 'Inconsolata' - 'InconsolataGo' - 'InconsolataLGC' - 'IntelOneMono' - 'IntoneMono' - 'Iosevka' - 'IosevkaTerm' - 'IosevkaTermSlab' - 'JetBrainsMono' - 'Lekton' - 'LiberationMono' - 'Lilex' - 'LiterationMono' - 'MartianMono' - 'Meslo' - 'Monaspace' - 'Monaspice' - 'Monofur' - 'Monoid' - 'Mononoki' - 'MPlus' - 'NerdFontsSymbolsOnly' - 'Noto' - 'OpenDyslexic' - 'Overpass' - 'ProFont' - 'ProggyClean' - 'Recursive' - 'RobotoMono' - 'SauceCodePro' - 'ShareTechMono' - 'SourceCodePro' - 'SpaceMono' - 'SureTechMono' - 'Terminess' - 'Terminus' - 'Tinos' - 'Ubuntu' - 'UbuntuMono' - 'UbuntuSans' - 'VictorMono' - 'ZedMono' -) | Sort-Object - -function Show-Menu { - param ( - $Options - ) - Clear-Host - - # Function to draw the menu - function Draw-Menu { - param ( - $Options, - $terminalWidth - ) + [Parameter(Mandatory = $false, ParameterSetName = 'ByAll', HelpMessage = 'In which scope do you want to install the Nerd Font, AllUsers or CurrentUser?')] + [Parameter(Mandatory = $false, ParameterSetName = 'ByName', HelpMessage = 'In which scope do you want to install the Nerd Font, AllUsers or CurrentUser?')] + [ValidateSet('AllUsers', 'CurrentUser')] + [string]$Scope = 'CurrentUser' +) - # Print the centered and bold title - if ($IsCoreCLR) { - $title = "`u{1F913} $($PSStyle.Bold)Nerd Fonts Installation$($PSStyle.BoldOff)" - } else { - $title = "Nerd Fonts Installation" +dynamicparam { + # Define the URL and cache file path + $url = 'https://raw.githubusercontent.com/ryanoasis/nerd-fonts/master/bin/scripts/lib/fonts.json' + $cacheFilePath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'github-nerd-fonts.json') + $cacheDuration = [TimeSpan]::FromMinutes(2) + + #region Functions ========================================================== + function Get-FontsListFromWeb { + <# + .SYNOPSIS + Fetch fonts list from the web server. + + .DESCRIPTION + This function fetches the fonts list from the specified web server URL. + It also adds a release URL property to each font object. + #> + try { + $fonts = (Invoke-RestMethod -Uri $url -ErrorAction Stop -Verbose:$false -Debug:$false).fonts + $releaseUrl = "https://api.github.com/repos/ryanoasis/nerd-fonts/releases/latest" + foreach ($font in $fonts) { + $font.PSObject.Properties.Add([PSNoteProperty]::new("releaseUrl", $releaseUrl)) + } + return $fonts } - $padding = [math]::Max(0, ($terminalWidth - $title.Length) / 2) - Write-Host (' ' * $padding + $title) -ForegroundColor Cyan -NoNewline - Write-Host -ForegroundColor Cyan - Write-Host (("=" * $terminalWidth) + "`n") -ForegroundColor Cyan - - # Calculate the maximum width of each column - $maxOptionLength = ($Options | Measure-Object -Maximum Length).Maximum - $maxIndexLength = ($Options.Length).ToString().Length - $columnWidth = $maxIndexLength + $maxOptionLength + 4 # 4 for padding and ": " - - # Calculate the number of columns that can fit in the terminal width - $numColumns = [math]::Floor($terminalWidth / $columnWidth) - - # Calculate the number of rows - $numRows = [math]::Ceiling($Options.Length / $numColumns) - - # Print the options in rows - for ($row = 0; $row -lt $numRows; $row++) { - for ($col = 0; $col -lt $numColumns; $col++) { - $index = $row + $col * $numRows - if ($index -lt $Options.Length) { - $number = $index + 1 - $fontName = $Options[$index] - $numberText = ("{0," + $maxIndexLength + "}") -f $number - $fontText = ("{0,-" + $maxOptionLength + "}") -f $fontName - Write-Host -NoNewline -ForegroundColor DarkYellow $numberText - Write-Host -NoNewline -ForegroundColor Yellow ": " - Write-Host -NoNewline -ForegroundColor White "$($PSStyle.Bold)$fontText$($PSStyle.BoldOff)" - } + catch { + $PSCmdlet.ThrowTerminatingError($_) + } + } + + function Get-FontsListFromCache { + <# + .SYNOPSIS + Load fonts list from cache. + + .DESCRIPTION + This function loads the fonts list from a cache file if it exists and is not expired. + #> + if (Test-Path $cacheFilePath) { + $cacheFile = Get-Item $cacheFilePath + if ((Get-Date) -lt $cacheFile.LastWriteTime.Add($cacheDuration)) { + return Get-Content $cacheFilePath | ConvertFrom-Json } - Write-Host } + return $null } - # Initial terminal width - $initialWidth = [console]::WindowWidth + function Save-FontsListToCache($fonts) { + <# + .SYNOPSIS + Save fonts list to cache. - # Draw the initial menu - Draw-Menu -Options $Options -terminalWidth $initialWidth + .DESCRIPTION + This function saves the fonts list to a cache file in JSON format. + #> + $fonts | ConvertTo-Json | Set-Content $cacheFilePath + } - Write-Host "`nEnter 'q' to quit." -ForegroundColor Cyan + function Add-CustomEntries($fonts) { + <# + .SYNOPSIS + Add custom entries to the fonts list. + + .DESCRIPTION + This function adds custom font entries to the provided fonts list and sorts them by folder name. + #> + $customEntries = @( + [PSCustomObject]@{ + unpatchedName = 'Cascadia Code' + licenseId = 'OFL-1.1-RFN' + RFN = $true + version = 'latest' + patchedName = 'CascadiaCode' + folderName = 'CascadiaCode' + imagePreviewFont = 'Cascadia Code' + imagePreviewFontSource = $null + linkPreviewFont = $null + caskName = 'cascadia-code' + repoRelease = $false + description = 'Cascadia Code is a monospaced font designed to work well with the new Windows Terminal.' + releaseUrl = 'https://api.github.com/repos/microsoft/cascadia-code/releases/latest' + }, + [PSCustomObject]@{ + unpatchedName = 'Cascadia Mono' + licenseId = 'OFL-1.1-RFN' + RFN = $true + version = 'latest' + patchedName = 'CascadiaMono' + folderName = 'CascadiaMono' + imagePreviewFont = 'Cascadia Mono' + imagePreviewFontSource = $null + linkPreviewFont = $null + caskName = 'cascadia-mono' + repoRelease = $false + description = 'Cascadia Mono is a monospaced font designed to work well with the new Windows Terminal.' + releaseUrl = 'https://api.github.com/repos/microsoft/cascadia-code/releases/latest' + } + ) - # Loop to handle user input and terminal resizing - while ($true) { - $currentWidth = [console]::WindowWidth - if ($currentWidth -ne $initialWidth) { - Clear-Host - Draw-Menu -Options $Options -terminalWidth $currentWidth - Write-Host "`nEnter 'q' to quit." -ForegroundColor Cyan - $initialWidth = $currentWidth - } + # Combine the original fonts with custom entries and sort by folderName + $allFonts = $fonts + $customEntries + $sortedFonts = $allFonts | Sort-Object -Property caskName - $selection = Read-Host "`nSelect a number" - if ($selection -eq 'q') { - return 'quit' - } - elseif ($selection -match '^\d+$') { - $selection = [int]$selection - if ($selection -ge 1 -and $selection -le $Options.Length) { - return $Options[$selection - 1] - } - } - Write-Host "Invalid selection. Please enter a number between 1 and $($Options.Length) or 'q' to quit." -ForegroundColor Red + return $sortedFonts } -} + #endregion Functions ------------------------------------------------------- -if (-not $FontName -and -not $All) { - # Provide interactive selection if no font name is specified - do { - $FontName = Show-Menu -Options $AllNerdFonts - if ($FontName -eq 'quit') { - Write-Host "Selection process canceled." - return - } - } while (-not $FontName) + # Try to load fonts list from cache + $allNerdFonts = Get-FontsListFromCache - if ($FontName) { - Write-Host "`nYour selected font: $FontName`n" -ForegroundColor Yellow - # Proceed with the installation of the selected font + # If cache is not valid, fetch from web, add custom entries, and update cache + if (-not $allNerdFonts) { + $allNerdFonts = Get-FontsListFromWeb + $allNerdFonts = Add-CustomEntries $allNerdFonts + Save-FontsListToCache $allNerdFonts } - else { - Write-Host "No font selected." - return - } -} -$FontAliasNames = @{ - 'AnonymicePro' = 'AnonymousPro' - 'BitstromWera' = 'BitstreamVeraSansMono' - 'BlexMono' = 'IBMPlexMono' - 'CaskaydiaCove' = 'CascadiaCode' - 'CaskaydiaMono' = 'CascadiaMono' - 'Hasklug' = 'Hasklig' - 'Hurmit' = 'Hermit' - 'iMWriting' = 'iA-Writer' - 'IntoneMono' = 'IntelOneMono' - 'LiterationMono' = 'LiberationMono' - 'Monaspice' = 'Monaspace' - 'SureTechMono' = 'ShareTechMono' - 'SauceCodePro' = 'SourceCodePro' - 'Terminess' = 'Terminus' -} + # Extract caskName values for auto-completion + $caskNames = [string[]]@($allNerdFonts | ForEach-Object { $_.caskName }) + + # Define the name and type of the dynamic parameter + $paramName = 'Name' + $paramType = [string[]] + + # Create a collection to hold the attributes for the dynamic parameter + $attributes = [System.Collections.ObjectModel.Collection[System.Attribute]]::new() -if ($All) { - $FontName = $AllNerdFonts | Where-Object { $_ -notin $FontAliasNames.Keys } + # Convert the caskNames array to a string representation + $caskNamesString = $caskNames -join "', '" + $caskNamesString = "@('$caskNamesString')" + + # Create an ArgumentCompleter attribute using the caskName values for auto-completion and add it to the collection + $argumentCompleterScript = [scriptblock]::Create(@" +param(`$commandName, `$parameterName, `$wordToComplete, `$commandAst, `$fakeBoundParameter) +# Static array of cask names for auto-completion +`$caskNames = $caskNamesString + +# Filter and return matching cask names +`$caskNames | Where-Object { `$_ -like "`$wordToComplete*" } | ForEach-Object { + [System.Management.Automation.CompletionResult]::new(`$_, `$_, 'ParameterValue', `$_) } +"@) -$resetFontCache = $null + $argumentCompleterAttribute = [System.Management.Automation.ArgumentCompleterAttribute]::new($argumentCompleterScript) + $attributes.Add($argumentCompleterAttribute) -$releaseInfo = @{} + # Create a Parameter attribute and add it to the collection + $paramAttribute = [System.Management.Automation.ParameterAttribute]::new() + $paramAttribute.Mandatory = $( + # Make the parameter mandatory if the script is not running interactively + if ( + $null -ne ([System.Environment]::GetCommandLineArgs() | Where-Object { $_ -match '^-NonI.*' }) -or + ( + $null -ne ($__PSProfileEnvCommandLineArgs | Where-Object { $_ -match '^-C.*' }) -and + $null -eq ($__PSProfileEnvCommandLineArgs | Where-Object { $_ -match '^-NoE.*' }) + ) + ) { + $true + } + elseif ($Host.UI.RawUI.KeyAvailable -or [System.Environment]::UserInteractive) { + $false + } + else { + $true + } + ) + $paramAttribute.Position = 0 + $paramAttribute.ParameterSetName = 'ByName' + $paramAttribute.HelpMessage = 'Which Nerd Font do you want to install?' + "`n" + "Available values: $($caskNames -join ', ')" + $paramAttribute.ValueFromPipeline = $true + $paramAttribute.ValueFromPipelineByPropertyName = $true + $attributes.Add($paramAttribute) + + # Create the dynamic parameter + $runtimeParam = [System.Management.Automation.RuntimeDefinedParameter]::new($paramName, $paramType, $attributes) + + # Create a dictionary to hold the dynamic parameters + $paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new() + $paramDictionary.Add($paramName, $runtimeParam) + + # Return the dictionary + return $paramDictionary +} -try { +begin { if ( - ( - $FontName.Contains('CascadiaCode') -and - $FontName.Contains('CascadiaMono') - ) -or - ( - $FontName.Contains('Cascadia') -and - $FontName.Contains('CascadiaCode') - ) -or - ( - $FontName.Contains('Cascadia') -and - $FontName.Contains('CascadiaMono') - ) + $null -ne $env:REMOTE_CONTAINERS -or + $null -ne $env:CODESPACES -or + $null -ne $env:WSL_INTEROP ) { - Write-Verbose "CascadiaCode and CascadiaMono fonts are in the same package as Cascadia: Removing duplicates." - $FontName = $FontName | Where-Object { $_ -ne 'Cascadia' -and $_ -ne 'CascadiaCode' -and $_ -ne 'CascadiaMono' } - $FontName += 'Cascadia' + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + [System.Exception]::new('This script must be run on your local machine, not in a container.'), + 'NotLocalMachine', + [System.Management.Automation.ErrorCategory]::InvalidOperation, + $null + ) + ) } if ( - ( - $FontName.Contains('CaskaydiaCove') -or - $FontName.Contains('CaskaydiaMono') - ) -and - ( - $FontName.Contains('Cascadia') -or - $FontName.Contains('CascadiaCode') -or - $FontName.Contains('CascadiaMono') - ) + $Scope -eq 'AllUsers' -and + $PSVersionTable.Platform -ne 'Unix' -and + -not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator') ) { - Write-Host "CaskaydiaCove and CaskaydiaMono are clones, giving priority to Cascadia original fonts." -ForegroundColor Magenta - $FontName = $FontName | Where-Object { $_ -ne 'CaskaydiaCove' -and $_ -ne 'CaskaydiaMono' } + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + [System.Exception]::new('Elevated permissions are required to install fonts for all users. Alternatively, you can install fonts for the current user using the -Scope parameter with the CurrentUser value.'), + 'InsufficientPermissions', + [System.Management.Automation.ErrorCategory]::InvalidOperation, + $null + ) + ) } - $FontName | Sort-Object | ForEach-Object { - if (@('Cascadia', 'CascadiaCode', 'CascadiaMono') -contains $_.Trim() ) { - $FontName = $_.Trim() - $sourceName = 'GitHub.com/Microsoft' - $releaseUrl = 'https://api.github.com/repos/microsoft/cascadia-code/releases/latest' - if (-not $releaseInfo[$releaseUrl]) { - $releaseInfo[$releaseUrl] = Invoke-RestMethod -Uri $releaseUrl -ErrorAction Stop + #region Functions ========================================================== + function Show-Menu { + <# + .SYNOPSIS + Displays a menu for selecting fonts. + + .DESCRIPTION + This function clears the host and displays a menu with options for selecting fonts. + It handles user input and terminal resizing to dynamically adjust the menu display. + #> + param ( + $Options + ) + Clear-Host + + function Show-MenuOptions { + <# + .SYNOPSIS + Draws the menu options. + + .DESCRIPTION + This function prints the menu options in a formatted manner. + It calculates the number of columns and rows based on the terminal width and displays the options accordingly. + #> + param ( + $Options, + $terminalWidth + ) + + # Print the centered and bold title + if ($IsCoreCLR) { + $title = "`u{1F913} $($PSStyle.Bold)Nerd Fonts Installation$($PSStyle.BoldOff)" + } + else { + $title = 'Nerd Fonts Installation' + } + $padding = [math]::Max(0, ($terminalWidth - $title.Length) / 2) + Write-Host (' ' * $padding + $title) -ForegroundColor Cyan -NoNewline + Write-Host -ForegroundColor Cyan + Write-Host (('=' * $terminalWidth) + "`n") -ForegroundColor Cyan + + # Add the 'All Nerd Fonts' option at the top + $Options = @([pscustomobject]@{ imagePreviewFont = 'All Nerd Fonts'; unpatchedName = 'All'; caskName = 'All' }) + $Options + + # Calculate the maximum width of each column + $maxOptionLength = ($Options | ForEach-Object { $_.imagePreviewFont.Length } | Measure-Object -Maximum).Maximum + $maxIndexLength = ($Options.Length).ToString().Length + $columnWidth = $maxIndexLength + $maxOptionLength + 4 # 4 for padding and ': ' + + # Calculate the number of columns that can fit in the terminal width + $numColumns = [math]::Floor($terminalWidth / $columnWidth) + + # Calculate the number of rows + $numRows = [math]::Ceiling($Options.Length / $numColumns) + + # Print the options in rows + for ($row = 0; $row -lt $numRows; $row++) { + for ($col = 0; $col -lt $numColumns; $col++) { + $index = $row + $col * $numRows + if ($index -lt $Options.Length) { + $number = $index + $fontName = $Options[$index].imagePreviewFont + $numberText = ('{0,' + $maxIndexLength + '}') -f $number + $fontText = ('{0,-' + $maxOptionLength + '}') -f $fontName + + if ($index -eq 0) { + # Special formatting for 'All Nerd Fonts' + Write-Host -NoNewline -ForegroundColor Magenta $numberText + Write-Host -NoNewline -ForegroundColor Magenta ': ' + Write-Host -NoNewline -ForegroundColor Magenta "$($PSStyle.Italic)$fontText$($PSStyle.ItalicOff)" + } + else { + Write-Host -NoNewline -ForegroundColor DarkYellow $numberText + Write-Host -NoNewline -ForegroundColor Yellow ': ' + Write-Host -NoNewline -ForegroundColor White "$($PSStyle.Bold)$fontText$($PSStyle.BoldOff)" + } + } + } + Write-Host } - $assetUrl = $releaseInfo[$releaseUrl].assets | Where-Object { $_.name -like '*.zip' } | Select-Object -ExpandProperty browser_download_url - $sha256Url = $null } - else { - if ($FontAliasNames.ContainsKey($_.Trim())) { - Write-Host "Font alias found: $FontName ➜ $($FontAliasNames[$_])" -ForegroundColor Yellow - $FontName = $FontAliasNames.$_ + + # Initial terminal width + $initialWidth = [console]::WindowWidth + + # Draw the initial menu + Show-MenuOptions -Options $Options -terminalWidth $initialWidth + + Write-Host "`nEnter 'q' to quit." -ForegroundColor Cyan + + # Loop to handle user input and terminal resizing + while ($true) { + $currentWidth = [console]::WindowWidth + if ($currentWidth -ne $initialWidth) { + Clear-Host + Show-MenuOptions -Options $Options -terminalWidth $currentWidth + Write-Host "`nEnter 'q' to quit." -ForegroundColor Cyan + $initialWidth = $currentWidth + } + + $selection = Read-Host "`nSelect one or more numbers separated by commas" + if ($selection -eq 'q') { + return 'quit' + } + + # Remove spaces and split the input by commas + $selection = $selection -replace '\s', '' + $numbers = $selection -split ',' | Select-Object -Unique + + # Validate each number + $validSelections = @() + $invalidSelections = @() + foreach ($number in $numbers) { + if ($number -match '^\d+$') { + $number = [int]$number + if ($number -eq 0) { + return 'All' + } + elseif ($number -ge 1 -and $number -le $Options.Length) { + $validSelections += $Options[$number - 1] + } + else { + $invalidSelections += $number - 1 + } + } + else { + $invalidSelections += $number - 1 + } + } + + if ($invalidSelections.Count -eq 0) { + # Check for conflicting fonts + $conflictingFonts = $validSelections | Group-Object -Property unpatchedName | Where-Object { $_.Count -gt 1 } + if ($conflictingFonts.Count -eq 0) { + return $validSelections.caskName + } + else { + foreach ($conflict in $conflictingFonts) { + $conflictNames = $conflict.Group | ForEach-Object { $_.imagePreviewFont } + Write-Host "Conflicting selection(s): $($conflictNames -join ', '). These fonts cannot be installed together because they share the same base font name." -ForegroundColor Red + } + } } else { - $FontName = $_.Trim() + Write-Host "Invalid selection(s): $($invalidSelections -join ', '). Please enter valid numbers between 0 and $($Options.Length - 1) or 'q' to quit." -ForegroundColor Red } + } + } + + function Invoke-GitHubApiRequest { + <# + .SYNOPSIS + Makes anonymous requests to GitHub API and handles rate limiting. + + .DESCRIPTION + This function sends a request to the specified GitHub API URI and handles rate limiting by retrying the request + up to a maximum number of retries. It also converts JSON responses to PowerShell objects. + #> + param ( + [string]$Uri + ) + $maxRetries = 5 + $retryCount = 0 + $baseWaitTime = 15 + + while ($retryCount -lt $maxRetries) { + try { + $headers = @{} + $parsedUri = [System.Uri]$Uri + if ($parsedUri.Host -eq "api.github.com") { + $headers["Accept"] = "application/vnd.github.v3+json" + } - $sourceName = 'GitHub.com/ryanoasis' - $releaseUrl = 'https://api.github.com/repos/ryanoasis/nerd-fonts/releases/latest' - if (-not $releaseInfo[$releaseUrl]) { - $releaseInfo[$releaseUrl] = Invoke-RestMethod -Uri $releaseUrl -ErrorAction Stop + $response = Invoke-RestMethod -Uri $Uri -Headers $headers -ErrorAction Stop -Verbose:$false -Debug:$false + + return [PSCustomObject]@{ + Headers = $response.PSObject.Properties["Headers"].Value + Content = $response + } + } + catch { + if ($_.Exception.Response.StatusCode -eq 403 -or $_.Exception.Response.StatusCode -eq 429) { + $retryAfter = $null + $rateLimitReset = $null + $waitTime = 0 + + if ($_.Exception.Response.Headers -and $_.Exception.Response.Headers["Retry-After"]) { + $retryAfter = $_.Exception.Response.Headers["Retry-After"] + } + if ($_.Exception.Response.Headers -and $_.Exception.Response.Headers["X-RateLimit-Reset"]) { + $rateLimitReset = $_.Exception.Response.Headers["X-RateLimit-Reset"] + } + + if ($retryAfter) { + $waitTime = [int]$retryAfter + } + elseif ($rateLimitReset) { + $resetTime = [DateTimeOffset]::FromUnixTimeSeconds([int]$rateLimitReset).LocalDateTime + $waitTime = ($resetTime - (Get-Date)).TotalSeconds + } + + if ($waitTime -gt 0 -and $waitTime -le 60) { + Write-Host "Rate limit exceeded. Waiting for $waitTime seconds." + Start-Sleep -Seconds $waitTime + } + else { + $exponentialWait = $baseWaitTime * [math]::Pow(2, $retryCount) + Write-Host "Rate limit exceeded. Waiting for $exponentialWait seconds." + Start-Sleep -Seconds $exponentialWait + } + $retryCount++ + } + else { + $PSCmdlet.ThrowTerminatingError($_) + } + } + } + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + [System.Exception]::new('Max retries exceeded. Please try again later.'), + 'MaxRetriesExceeded', + [System.Management.Automation.ErrorCategory]::ResourceUnavailable, + $null + ) + ) + } + + function Invoke-GitHubApiPaginatedRequest { + <# + .SYNOPSIS + Fetches all pages of a paginated response if the host is api.github.com. + + .DESCRIPTION + This function sends requests to the specified GitHub API URI and handles pagination by following the 'next' links + in the response headers. It collects all pages of data and returns them as a single array. + #> + param ( + [string]$Uri + ) + $allData = @() + $parsedUri = [System.Uri]$Uri + + if ($parsedUri.Host -eq "api.github.com") { + while ($true) { + $response = Invoke-GitHubApiRequest -Uri $Uri + if ($null -eq $response) { + break + } + $data = $response.Content + $allData += $data + $linkHeader = $null + if ($response.Headers -and $response.Headers["Link"]) { + $linkHeader = $response.Headers["Link"] + } + if ($linkHeader -notmatch 'rel="next"') { + break + } + $nextLink = ($linkHeader -split ',') | Where-Object { $_ -match 'rel="next"' } | ForEach-Object { ($_ -split ';')[0].Trim('<> ') } + $Uri = $nextLink } - $assetUrl = $releaseInfo[$releaseUrl].assets | Where-Object { $_.name -like "$FontName.zip" } | Select-Object -ExpandProperty browser_download_url - $sha256Url = $releaseInfo[$releaseUrl].assets | Where-Object { $_.name -eq 'SHA-256.txt' } | Select-Object -ExpandProperty browser_download_url } + else { + $response = Invoke-GitHubApiRequest -Uri $Uri + $allData = $response.Content + } + return $allData + } + #endregion Functions ------------------------------------------------------- - Write-Verbose "Font '$FontName' asset URL: $assetUrl" - Write-Verbose "Font '$FontName' SHA-256 URL: $sha256Url" + # Provide interactive selection if no font name is specified + if (-not $PSBoundParameters.Name -and -not $PSBoundParameters.All) { + do { + $Name = Show-Menu -Options $allNerdFonts + if ($Name -eq 'quit') { + Write-Host "Selection process canceled." + exit + } + } while (-not $Name) - if (-not $assetUrl) { - if ($WhatIfPreference -eq $true) { - Write-Warning "Font '$FontName' not found." + if ($Name) { + if ($Name -eq 'All') { + Write-Host "`nYou selected all Nerd Fonts.`n" -ForegroundColor Yellow + # Proceed with the installation of all fonts } else { - Write-Error "Font '$FontName' not found." + Write-Host "`nYour selected font(s): $($Name -join ', ')`n" -ForegroundColor Yellow + # Proceed with the installation of the selected font(s) } - return } - if ($assetUrl -notlike '*.zip') { - if ($WhatIfPreference -eq $true) { - Write-Warning "Font '$FontName' archive format is not supported." + else { + Write-Host 'No font selected.' + exit + } + } + elseif ($PSBoundParameters.Name) { + $Name = $PSBoundParameters.Name + } + + $nerdFontsToInstall = @() + + if ($PSBoundParameters.All -or $Name -eq 'All') { + # Group fonts by unpatchedName to identify conflicts + $groupedFonts = $allNerdFonts | Group-Object -Property unpatchedName + + # Resolve conflicts by giving precedence to fonts with imagePreviewFontSource = $null + $resolvedFonts = @() + foreach ($group in $groupedFonts) { + $fonts = $group.Group + $preferredFont = $fonts | Where-Object { $_.imagePreviewFontSource -eq $null } + if ($preferredFont) { + $resolvedFonts += $preferredFont } else { - Write-Error "Font '$FontName' archive format is not supported." + $resolvedFonts += $fonts[0] # If no preferred font, take the first one } - return } - # Define the local paths - $zipPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "NerdFont_$FontName.zip") - $extractPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "NerdFont_$FontName") + $nerdFontsToInstall = $resolvedFonts + } + else { + # Remove duplicates and collect selected fonts + $uniqueNames = [System.Collections.Generic.HashSet[string]]::new() + $selectedFonts = @() + $conflictCheck = @{} + + foreach ($fontName in $Name) { + if ($uniqueNames.Add($fontName)) { + $matchingFonts = $allNerdFonts | Where-Object { $_.caskName -eq $fontName -or $_.folderName -eq $fontName } + foreach ($font in $matchingFonts) { + $selectedFonts += $font + if ($conflictCheck.ContainsKey($font.unpatchedName)) { + $conflictCheck[$font.unpatchedName] += $font.imagePreviewFont + } + else { + $conflictCheck[$font.unpatchedName] = @($font.imagePreviewFont) + } + } + } + } - Write-Verbose "Zip download path: $zipPath" - Write-Verbose "Extract path: $extractPath" + # Check for conflicting fonts + $conflictMessages = @() + foreach ($key in $conflictCheck.Keys) { + if ($conflictCheck[$key].Count -gt 1) { + $conflictMessages += "Conflicting fonts: $($conflictCheck[$key] -join ', '). These fonts cannot be installed together because they share the same base font name." + } + } - if ( - $PSCmdlet.ShouldProcess( - "Install the font '$FontName' from $sourceName", - "Do you confirm to install the font '$FontName' from $sourceName ?", - "Nerd Fonts Installation" + if ($conflictMessages.Count -gt 0) { + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + [System.Exception]::new($conflictMessages -join "`n"), + 'ConflictMessagesPresent', + [System.Management.Automation.ErrorCategory]::ResourceUnavailable, + $null + ) ) - ) { - # Download the zip file if not already downloaded - if (Test-Path -Path $zipPath) { - Write-Verbose "Font '$FontName' already downloaded." - } - else { - Write-Verbose "Downloading font '$FontName' from $assetUrl ..." - Invoke-WebRequest -Uri $assetUrl -OutFile $zipPath -ErrorAction Stop + } + + # Add unique and non-conflicting fonts to the installation list + $nerdFontsToInstall = $selectedFonts + } + + # Fetch releases for each unique URL + $fontReleases = @{} + foreach ($url in $nerdFontsToInstall.releaseUrl | Sort-Object -Unique) { + Write-Verbose "Fetching release data for $url" + $release = Invoke-GitHubApiPaginatedRequest -Uri $url + $fontReleases[$url] = @{ + ReleaseData = $release + Sha256Data = @{} + } + + # Check if the release contains a SHA-256.txt asset + $shaAsset = $release.assets | Where-Object { $_.name -eq 'SHA-256.txt' } + if ($shaAsset) { + $shaUrl = $shaAsset.browser_download_url + Write-Verbose "Fetching SHA-256.txt content from $shaUrl" + $shaContent = Invoke-WebRequest -Uri $shaUrl -ErrorAction Stop -Verbose:$false -Debug:$false + + # Convert the binary content to a string + $shaContentString = [System.Text.Encoding]::UTF8.GetString($shaContent.Content) + + # Parse the SHA-256.txt content + $shaLines = $shaContentString -split "`n" + foreach ($line in $shaLines) { + if ($line -match '^\s*([a-fA-F0-9]{64})\s+(.+)$') { + $sha256 = $matches[1] + $fileName = $matches[2].Trim() + $fontReleases[$url].Sha256Data[$fileName] = $sha256 + Write-Debug "SHA-256: $sha256, File: $fileName" + } } + } + } - # Verify the SHA-256 hash - if ($sha256Url) { - Write-Verbose "Verifying SHA-256 hash ..." - $sha256Path = "$zipPath.sha256" - $zipName = (Get-Item $zipPath).Name -replace '^NerdFont_', '' + # Generate a unique temporary directory to store the font files + $tempFile = [System.IO.Path]::GetTempFileName() + $tempPath = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($tempFile), [System.IO.Path]::GetFileNameWithoutExtension($tempFile)) + $null = [System.IO.Directory]::CreateDirectory($tempPath) + [System.IO.File]::Delete($tempFile) + Write-Verbose "Using temporary directory: $tempPath" - if (Test-Path -Path $sha256Path) { - Write-Verbose 'SHA-256 hash file already downloaded.' + $resetFontCache = $null +} + +process { + try { + Write-Verbose "Installing $($nerdFontsToInstall.Count) Nerd Fonts to $Scope scope." + + foreach ($nerdFont in $nerdFontsToInstall) { + $sourceName = $nerdFont.releaseUrl -replace '^https?://(?:[^/]+\.)*([^/]+\.[^/]+)/repos/([^/]+)/([^/]+).*', '$1/$2/$3' + Write-Verbose "Processing font: $($nerdFont.folderName) [$($nerdFont.caskName)] ($($nerdFont.imagePreviewFont)) from $sourceName" + + if ( + $PSCmdlet.ShouldProcess( + "Install the font '$($nerdFont.imagePreviewFont)' from $sourceName", + "Do you confirm to install the font '$($nerdFont.imagePreviewFont)' from $sourceName ?", + "Nerd Fonts Installation" + ) + ) { + if ($null -eq $nerdFont.imagePreviewFontSource) { + $assetUrl = $fontReleases[$nerdFont.releaseUrl].ReleaseData.assets | Where-Object { $_.name -match "\.zip$" } | Select-Object -ExpandProperty browser_download_url } else { - Write-Verbose "Downloading SHA-256 hash file from $sha256Url ..." - Invoke-WebRequest -Uri $sha256Url -OutFile $sha256Path -ErrorAction Stop + $assetUrl = $fontReleases[$nerdFont.releaseUrl].ReleaseData.assets | Where-Object { $_.name -match "^$($nerdFont.folderName)\.zip$" } | Select-Object -ExpandProperty browser_download_url + } + if ([string]::IsNullOrEmpty($assetUrl)) { + if ($WhatIfPreference -eq $true) { + Write-Warning "Nerd Font '$($nerdFont.folderName)' not found." + } + else { + Write-Error "Nerd Font '$($nerdFont.folderName)' not found." + } + continue + } + if ($assetUrl -notmatch '\.zip$') { + if ($WhatIfPreference -eq $true) { + Write-Warning "Nerd Font '$($nerdFont.folderName)' archive format is not supported." + } + else { + Write-Error "Nerd Font '$($nerdFont.folderName)' archive format is not supported." + } + continue } - $hash = Get-FileHash -Path $zipPath -Algorithm SHA256 - $expectedHashes = Get-Content -Path $sha256Path - - $hashVerified = $false - - Write-Verbose "Searching for hash $($hash.Hash) of $zipName in SHA-256 file ..." - foreach ($line in $expectedHashes) { - if ($line -match '^\s*([a-fA-F0-9]{64})\s+(.+)$') { - # Line contains a hash and a filename - $expectedHash = $matches[1] - $fileName = $matches[2] - if ($fileName -eq $zipName -and $hash.Hash -eq $expectedHash) { - Write-Verbose "Found hash $expectedHash for $fileName." - $hashVerified = $true - break - } + Write-Verbose "Font archive URL: $assetUrl" + + # Download the zip file if not already downloaded + $zipPath = [System.IO.Path]::Combine($tempPath, [System.IO.Path]::GetFileName(([System.Uri]::new($assetUrl)).LocalPath)) + if (Test-Path -Path $zipPath) { + Write-Verbose "Font archive already downloaded: $zipPath" + } + else { + Write-Verbose "Downloading font archive from $assetUrl to $zipPath" + Invoke-WebRequest -Uri $assetUrl -OutFile $zipPath -ErrorAction Stop -Verbose:$false -Debug:$false + } + + # Verify the SHA-256 hash if available + if ($fontReleases[$nerdFont.releaseUrl].Sha256Data.Count -gt 0) { + if (-not $fontReleases[$nerdFont.releaseUrl].Sha256Data.ContainsKey("$($nerdFont.folderName).zip")) { + Write-Warning "SHA-256 Hash not found for $($nerdFont.folderName).zip. Skipping installation." + continue } - elseif ($line -match '^\s*([a-fA-F0-9]{64})\s*$') { - # Line contains only a hash - $expectedHash = $matches[1] - if ($hash.Hash -eq $expectedHash) { - Write-Verbose "Found hash $expectedHash." - $hashVerified = $true - break - } + + $expectedSha256 = $fontReleases[$nerdFont.releaseUrl].Sha256Data["$($nerdFont.folderName).zip"] + $actualSha256 = Get-FileHash -Path $zipPath -Algorithm SHA256 | Select-Object -ExpandProperty Hash + if ($expectedSha256 -ne $actualSha256) { + Write-Error "SHA-256 Hash mismatch for $($nerdFont.folderName).zip. Skipping installation." + continue } + Write-Verbose "SHA-256 Hash verified for $($nerdFont.folderName).zip" } - if (-not $hashVerified) { - throw 'SHA-256 hash mismatch.' + # Extract the font files if not already extracted + $extractPath = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($zipPath), [System.IO.Path]::GetFileNameWithoutExtension($zipPath)) + if (Test-Path -Path $extractPath) { + Write-Verbose "Font files already extracted to $extractPath" + } + else { + Write-Verbose "Extracting font files to $extractPath" + $null = [System.IO.Directory]::CreateDirectory($extractPath) + Add-Type -AssemblyName 'System.IO.Compression.FileSystem' + [System.IO.Compression.ZipFile]::ExtractToDirectory($zipPath, $extractPath) + } + + # Install the fonts + $fileCounter = 0 + if ($null -eq $nerdFont.imagePreviewFontSource) { + $filter = "$($nerdFont.folderName)*.ttf" + } + else { + $filter = "*.ttf" } - } - # Extract the zip file - Expand-Archive -Path $zipPath -DestinationPath $extractPath -Force - - # Install the fonts - if ($IsMacOS) { - $Destination = "${HOME}/Library/Fonts" - $null = New-Item -Path $Destination -ItemType Directory -Force - Write-Host "`nInstalling font files to $Destination" -ForegroundColor White - Get-ChildItem -Path $extractPath -Filter "$FontName*.ttf" -Recurse | ForEach-Object { - if ($_.FullName -like '*static*') { - Write-Verbose "Skipping static font file: $($_.Name)" - return + if ($IsMacOS) { + $Destination = "${HOME}/Library/Fonts" + $null = [System.IO.Directory]::CreateDirectory($Destination) + Write-Host "`nInstalling font files to $Destination" -ForegroundColor White + Write-Verbose "Searching for $filter font files in $extractPath" + Get-ChildItem -Path $extractPath -Filter $filter -Recurse | ForEach-Object { + if ($null -eq $nerdFont.imagePreviewFontSource -and $_.FullName -like '*static*') { + Write-Verbose "Skipping static font file: $($_.Name)" + return + } + Write-Host " $($_.Name)" + Copy-Item -Path $_.FullName -Destination $Destination -Force -Confirm:$false -Verbose:$(if ($VerbosePreference -eq 'Continue') { $true } else { $false }) + $fileCounter++ } - Write-Host " $($_.Name)" - Copy-Item -Path $_.FullName -Destination $Destination -Force -Confirm:$false -Verbose:$(if ($VerbosePreference -eq 'Continue') { $true } else { $false }) } - } - elseif ($IsLinux) { - $Destination = "${HOME}/.local/share/fonts" - $null = New-Item -Path $Destination -ItemType Directory -Force - Write-Host "`nInstalling font files to $Destination" -ForegroundColor White - Get-ChildItem -Path $extractPath -Filter "$FontName*.ttf" -Recurse | ForEach-Object { - if ($_.FullName -like '*static*') { - Write-Verbose "Skipping static font file: $($_.Name)" - return + elseif ($IsLinux) { + $Destination = "${HOME}/.local/share/fonts" + $null = [System.IO.Directory]::CreateDirectory($Destination) + Write-Host "`nInstalling font files to $Destination" -ForegroundColor White + Write-Verbose "Searching for $filter font files in $extractPath" + Get-ChildItem -Path $extractPath -Filter $filter -Recurse | ForEach-Object { + if ($null -eq $nerdFont.imagePreviewFontSource -and $_.FullName -like '*static*') { + Write-Verbose "Skipping static font file: $($_.Name)" + return + } + Write-Host " $($_.Name)" + Copy-Item -Path $_.FullName -Destination $Destination -Force -Confirm:$false -Verbose:$(if ($VerbosePreference -eq 'Continue') { $true } else { $false }) + $fileCounter++ + } + + $Script:resetFontCache = $Destination + } + elseif ($Scope -eq 'AllUsers') { + $Destination = "${env:windir}\Fonts" + Write-Host "`nInstalling font files to All Users Font Directory" -ForegroundColor White + Write-Verbose "Searching for $filter font files in $extractPath" + Get-ChildItem -Path $extractPath -Filter $filter -Recurse | ForEach-Object { + if ($null -eq $nerdFont.imagePreviewFontSource -and $_.FullName -like '*static*') { + Write-Verbose "Skipping static font file: $($_.Name)" + return + } + Write-Host " $($_.Name)" + Copy-Item -Path $_.FullName -Destination $Destination -Force -Confirm:$false -Verbose:$(if ($VerbosePreference -eq 'Continue') { $true } else { $false }) + $fileCounter++ + } + } + else { + $Destination = (New-Object -ComObject Shell.Application).Namespace(0x14) + Write-Host "`nInstalling font files to Current User Font Directory" -ForegroundColor White + Write-Verbose "Searching for $filter font files in $extractPath" + Get-ChildItem -Path $extractPath -Filter $filter -Recurse | ForEach-Object { + if ($null -eq $nerdFont.imagePreviewFontSource -and $_.FullName -like '*static*') { + Write-Verbose "Skipping static font file: $($_.Name)" + return + } + Write-Host " $($_.Name)" + $Destination.CopyHere($_.FullName, 0x10) + $fileCounter++ } - Write-Host " $($_.Name)" - Copy-Item -Path $_.FullName -Destination $Destination -Force -Confirm:$false -Verbose:$(if ($VerbosePreference -eq 'Continue') { $true } else { $false }) } - $Script:resetFontCache = $Destination + if ($fileCounter -eq 0) { + Write-Error "No TTF font files found for $($nerdFont.folderName)." + } + else { + Write-Host "'$($nerdFont.imagePreviewFont)' font installed successfully.`n" -ForegroundColor Green + } } else { - $Destination = (New-Object -ComObject Shell.Application).Namespace(0x14) - Write-Host "`nInstalling font files to Current User Font Directory" -ForegroundColor White - Get-ChildItem -Path $extractPath -Filter "$FontName*.ttf" -Recurse | ForEach-Object { - if ($_.FullName -like '*static*') { - Write-Verbose "Skipping static font file: $($_.Name)" - return - } - Write-Host " $($_.Name)" - $Destination.CopyHere($_.FullName, 0x10) - } + Write-Verbose "Skipping font: $($nerdFont.folderName) [$($nerdFont.caskName)] ($($nerdFont.imagePreviewFont))" } - - Write-Host "$FontName font installed successfully.`n" -ForegroundColor Green } - else { - return + } + catch { + if ([System.IO.Directory]::Exists($tempPath)) { + Write-Verbose "Removing temporary directory: $tempPath" + [System.IO.Directory]::Delete($tempPath, $true) } + $PSCmdlet.ThrowTerminatingError($_) } } -catch { - Write-Error "Failed to install font:`n$_" -} -finally { - Remove-Item -Path $([System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'NerdFont_*')) -Force -Recurse -Confirm:$false -} -# Refresh the font cache -if ($resetFontCache -and (Get-Command -Name fc-cache -ErrorAction Ignore)) { - Write-Host "Resetting font cache in $resetFontCache ..." -ForegroundColor Yellow - fc-cache -f $resetFontCache +end { + if ([System.IO.Directory]::Exists($tempPath)) { + Write-Verbose "Removing temporary directory: $tempPath" + [System.IO.Directory]::Delete($tempPath, $true) + } + + # Refresh the font cache + if ($resetFontCache -and (Get-Command -Name fc-cache -ErrorAction Ignore)) { + Write-Host "Resetting font cache in $resetFontCache ..." -ForegroundColor Yellow + fc-cache -f $resetFontCache + } }