NerdFonts.psm1
|
[CmdletBinding()] param() $baseName = [System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath) $script:PSModuleInfo = Import-PowerShellDataFile -Path "$PSScriptRoot\$baseName.psd1" $script:PSModuleInfo | Format-List | Out-String -Stream | ForEach-Object { Write-Debug $_ } $scriptName = $script:PSModuleInfo.Name Write-Debug "[$scriptName] - Importing module" #region [functions] - [public] Write-Debug "[$scriptName] - [functions] - [public] - Processing folder" #region [functions] - [public] - [completers] Write-Debug "[$scriptName] - [functions] - [public] - [completers] - Importing" Register-ArgumentCompleter -CommandName Get-NerdFont, Install-NerdFont -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter Get-NerdFont -Verbose:$false | Select-Object -ExpandProperty Name | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new("'$_'", $_, 'ParameterValue', $_) } } Write-Debug "[$scriptName] - [functions] - [public] - [completers] - Done" #endregion [functions] - [public] - [completers] #region [functions] - [public] - [Get-NerdFont] Write-Debug "[$scriptName] - [functions] - [public] - [Get-NerdFont] - Importing" function Get-NerdFont { <# .SYNOPSIS Get NerdFonts list .DESCRIPTION Get NerdFonts list, filtered by name, from the latest release. .EXAMPLE Get-NerdFonts Get all the NerdFonts. .EXAMPLE Get-NerdFonts -Name 'FiraCode' Get the NerdFont with the name 'FiraCode'. .EXAMPLE Get-NerdFonts -Name '*Code' Get the NerdFont with the name ending with 'Code'. .LINK https://psmodule.io/NerdFonts/Functions/Get-NerdFont .NOTES More information about the NerdFonts can be found at: [NerdFonts](https://www.nerdfonts.com/) | [GitHub](https://github.com/ryanoasis/nerd-fonts) #> [Alias('Get-NerdFonts')] [OutputType([System.Object[]])] [CmdletBinding()] param ( # Name of the NerdFont to get [Parameter()] [SupportsWildcards()] [string] $Name = '*' ) Write-Verbose 'Selecting assets by:' Write-Verbose "Name: [$Name]" $script:NerdFonts | Where-Object { $_.Name -like $Name } } Write-Debug "[$scriptName] - [functions] - [public] - [Get-NerdFont] - Done" #endregion [functions] - [public] - [Get-NerdFont] #region [functions] - [public] - [Install-NerdFont] Write-Debug "[$scriptName] - [functions] - [public] - [Install-NerdFont] - Importing" #Requires -Modules @{ ModuleName = 'Fonts'; RequiredVersion = '1.1.21' } #Requires -Modules @{ ModuleName = 'Admin'; RequiredVersion = '1.1.6' } function Install-NerdFont { <# .SYNOPSIS Installs Nerd Fonts to the system. .DESCRIPTION Installs Nerd Fonts to the system. .EXAMPLE Install-NerdFont -Name 'Fira Code' Installs the font 'Fira Code' to the current user. .EXAMPLE Install-NerdFont -Name 'Ubuntu*' Installs all fonts that match the pattern 'Ubuntu*' to the current user. .EXAMPLE Install-NerdFont -Name 'Fira Code' -Scope AllUsers Installs the font 'Fira Code' to all users. This requires to be run as administrator. .EXAMPLE Install-NerdFont -All Installs all Nerd Fonts to the current user. .LINK https://psmodule.io/NerdFonts/Functions/Install-NerdFont .NOTES More information about the NerdFonts can be found at: [NerdFonts](https://www.nerdfonts.com/) | [GitHub](https://github.com/ryanoasis/nerd-fonts) #> [CmdletBinding( DefaultParameterSetName = 'ByName', SupportsShouldProcess )] [Alias('Install-NerdFonts')] param( # Specify the name of the NerdFont(s) to install. [Parameter( ParameterSetName = 'ByName', Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName )] [SupportsWildcards()] [string[]] $Name, # Specify to install all NerdFont(s). [Parameter( ParameterSetName = 'All', Mandatory )] [switch] $All, [Parameter()] [ValidateSet('CurrentUser', 'AllUsers')] [string] $Scope = 'CurrentUser', # Select which variant(s) to install from each archive. Default 'All' preserves current behavior. [Parameter()] [ValidateSet('All', 'Standard', 'Mono', 'Propo')] [string] $Variant = 'All', # Force will overwrite existing fonts [Parameter()] [switch] $Force ) begin { if ($Scope -eq 'AllUsers' -and -not (IsAdmin)) { $errorMessage = @' Administrator rights are required to install fonts. Please run the command again with elevated rights (Run as Administrator) or provide '-Scope CurrentUser' to your command." '@ throw $errorMessage } $nerdFontsToInstall = [System.Collections.Generic.List[object]]::new() $seenNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $guid = (New-Guid).Guid $tempPath = Join-Path -Path $HOME -ChildPath "NerdFonts-$guid" if (-not (Test-Path -Path $tempPath -PathType Container)) { Write-Verbose "Create folder [$tempPath]" $null = New-Item -Path $tempPath -ItemType Directory } } process { if ($All) { foreach ($font in $script:NerdFonts) { if ($seenNames.Add($font.Name)) { $nerdFontsToInstall.Add($font) } } } else { foreach ($fontName in $Name) { foreach ($font in $script:NerdFonts) { if ($font.Name -like $fontName -and $seenNames.Add($font.Name)) { $nerdFontsToInstall.Add($font) } } } } } end { Write-Verbose "[$Scope] - Installing [$($nerdFontsToInstall.Count)] fonts" $cacheRoot = if ($IsWindows) { Join-Path -Path ([Environment]::GetFolderPath('LocalApplicationData')) -ChildPath 'PSModule/NerdFonts/cache' } else { Join-Path -Path $HOME -ChildPath '.cache/PSModule/NerdFonts' } if (-not (Test-Path -LiteralPath $cacheRoot)) { $null = New-Item -ItemType Directory -Path $cacheRoot -Force } $installedFamilies = $null if (-not $Force) { $installedNames = @(Get-Font -Scope $Scope -ErrorAction SilentlyContinue | ForEach-Object { $_.Name } | Where-Object { $_ }) $installedFamilies = [System.Collections.Generic.HashSet[string]]::new( [string[]]$installedNames, [System.StringComparer]::OrdinalIgnoreCase ) } $toProcess = [System.Collections.Generic.List[object]]::new() foreach ($nerdFont in $nerdFontsToInstall) { $fontName = $nerdFont.Name if (-not $Force -and $installedFamilies) { $alreadyInstalled = $false foreach ($family in $installedFamilies) { if ($family -like "$fontName*") { $alreadyInstalled = $true; break } } if ($alreadyInstalled) { Write-Verbose "[$fontName] - already installed, skipping" continue } } $toProcess.Add($nerdFont) } Add-Type -AssemblyName System.Net.Http -ErrorAction SilentlyContinue $httpClient = [System.Net.Http.HttpClient]::new() $httpClient.Timeout = [TimeSpan]::FromMinutes(5) $pending = [System.Collections.Generic.List[object]]::new() $throttle = 8 try { foreach ($nerdFont in $toProcess) { $URL = $nerdFont.URL $fontName = $nerdFont.Name $downloadFileName = Split-Path -Path $URL -Leaf $downloadPath = Join-Path -Path $tempPath -ChildPath $downloadFileName $cacheTag = if ($URL -match '/releases/download/([^/]+)/') { $Matches[1] } else { 'unknown' } $cacheTagDir = Join-Path -Path $cacheRoot -ChildPath $cacheTag $cachedFile = Join-Path -Path $cacheTagDir -ChildPath $downloadFileName if ((Test-Path -LiteralPath $cachedFile) -and -not $Force) { Write-Verbose "[$fontName] - Cache hit at [$cachedFile]" Copy-Item -LiteralPath $cachedFile -Destination $downloadPath -Force $pending.Add([pscustomobject]@{ Name = $fontName URL = $URL DownloadPath = $downloadPath CachedFile = $cachedFile CacheTagDir = $cacheTagDir FromCache = $true }) } else { Write-Verbose "[$fontName] - Queue download to [$downloadPath]" $pending.Add([pscustomobject]@{ Name = $fontName URL = $URL DownloadPath = $downloadPath CachedFile = $cachedFile CacheTagDir = $cacheTagDir FromCache = $false }) } } $toDownload = @($pending | Where-Object { -not $_.FromCache }) for ($i = 0; $i -lt $toDownload.Count; $i += $throttle) { $end = [Math]::Min($i + $throttle - 1, $toDownload.Count - 1) $chunk = $toDownload[$i..$end] $tasks = @() foreach ($q in $chunk) { $tasks += [pscustomobject]@{ Q = $q; Task = $httpClient.GetByteArrayAsync($q.URL) } } foreach ($t in $tasks) { $bytes = $t.Task.GetAwaiter().GetResult() [System.IO.File]::WriteAllBytes($t.Q.DownloadPath, $bytes) if (-not (Test-Path -LiteralPath $t.Q.CacheTagDir)) { $null = New-Item -ItemType Directory -Path $t.Q.CacheTagDir -Force } [System.IO.File]::WriteAllBytes($t.Q.CachedFile, $bytes) } } } finally { $httpClient.Dispose() } foreach ($p in $pending) { $fontName = $p.Name $downloadPath = $p.DownloadPath $extractPath = Join-Path -Path $tempPath -ChildPath $fontName Write-Verbose "[$fontName] - Extract to [$extractPath]" if ($PSCmdlet.ShouldProcess("[$fontName] to [$extractPath]", 'Extract')) { if (-not (Test-Path -LiteralPath $extractPath)) { $null = New-Item -ItemType Directory -Path $extractPath } [System.IO.Compression.ZipFile]::ExtractToDirectory($downloadPath, $extractPath, $true) Remove-Item -Path $downloadPath -Force } if ($Variant -ne 'All') { $allFiles = Get-ChildItem -Path $extractPath -Recurse -File -Include '*.ttf', '*.otf' $keep = switch ($Variant) { 'Mono' { $allFiles | Where-Object { $_.Name -like '*NerdFontMono*' } } 'Propo' { $allFiles | Where-Object { $_.Name -like '*NerdFontPropo*' } } 'Standard' { $allFiles | Where-Object { $_.Name -like '*NerdFont*' -and $_.Name -notlike '*NerdFontMono*' -and $_.Name -notlike '*NerdFontPropo*' } } } $keepNames = [string[]]@($keep.FullName) $keepSet = [System.Collections.Generic.HashSet[string]]::new( $keepNames, [System.StringComparer]::OrdinalIgnoreCase ) $removed = 0 foreach ($f in $allFiles) { if (-not $keepSet.Contains($f.FullName)) { Remove-Item -LiteralPath $f.FullName -Force -ErrorAction SilentlyContinue $removed++ } } Write-Verbose "[$fontName] - Variant '$Variant' kept $($keep.Count) files, removed $removed" } Write-Verbose "[$fontName] - Install to [$Scope]" if ($PSCmdlet.ShouldProcess("[$fontName] to [$Scope]", 'Install font')) { Install-Font -Path $extractPath -Scope $Scope -Force:$Force Remove-Item -Path $extractPath -Force -Recurse } } Write-Verbose "Remove folder [$tempPath]" } clean { Remove-Item -Path $tempPath -Force } } Write-Debug "[$scriptName] - [functions] - [public] - [Install-NerdFont] - Done" #endregion [functions] - [public] - [Install-NerdFont] Write-Debug "[$scriptName] - [functions] - [public] - Done" #endregion [functions] - [public] #region [variables] - [private] Write-Debug "[$scriptName] - [variables] - [private] - Processing folder" #region [variables] - [private] - [NerdFonts] Write-Debug "[$scriptName] - [variables] - [private] - [NerdFonts] - Importing" $script:NerdFonts = Get-Content -Path (Join-Path -Path $PSScriptRoot -ChildPath 'FontsData.json') | ConvertFrom-Json Write-Debug "[$scriptName] - [variables] - [private] - [NerdFonts] - Done" #endregion [variables] - [private] - [NerdFonts] Write-Debug "[$scriptName] - [variables] - [private] - Done" #endregion [variables] - [private] #region Member exporter $exports = @{ Alias = '*' Cmdlet = '' Function = @( 'Get-NerdFont' 'Install-NerdFont' ) Variable = '' } Export-ModuleMember @exports #endregion Member exporter |