lib/createstyle.ps1
|
# Writes a style made by `tstyles create` into the user styles directory. # Like Save-TunedStyle, the result is a full style, indistinguishable from a # hand-written one. Unlike it, a taken name is REFUSED, never overwritten. function New-CreatedThemeObject { param( [Parameter(Mandatory)][string]$Name, [Parameter(Mandatory)][string]$FontFace, [Parameter(Mandatory)][int]$FontSize, [Parameter(Mandatory)][string]$CursorShape, [Parameter(Mandatory)][ValidateRange(0.0, 1.0)][double]$ImageOpacity, [Parameter(Mandatory)][ValidateSet('none', 'fill', 'uniform', 'uniformToFill')][string]$StretchMode, [Parameter(Mandatory)][string]$Alignment, [Parameter(Mandatory)][string]$TabColor ) [pscustomobject][ordered]@{ colorScheme = $Name tabTitle = $Name.ToUpperInvariant() tabColor = $TabColor cursorShape = $CursorShape useAcrylic = $false opacity = 100 'experimental.retroTerminalEffect' = $false font = [pscustomobject][ordered]@{ face = $FontFace; size = $FontSize; weight = 'normal' } padding = '16' backgroundImage = '{{BACKGROUND_IMAGE}}' backgroundImageOpacity = $ImageOpacity backgroundImageStretchMode = $StretchMode backgroundImageAlignment = $Alignment } } function Test-CreatedStyleNameTaken { # Is $Name taken for a new style? Taken when Get-StyleDir resolves it (a # user or bundled style), or when a folder already sits at its path in the # user styles directory, style or not. The creator's name page and # Save-CreatedStyle both ask here, so they cannot disagree. param([Parameter(Mandatory)][string]$Name) $dest = Join-Path (Join-Path $script:TStylesDataRoot 'styles') $Name [bool]((Get-StyleDir -StyleName $Name) -or (Test-Path -LiteralPath $dest)) } function Save-CreatedStyle { <# .SYNOPSIS Writes a created style to $DataRoot\styles\<Name>, or refuses. .DESCRIPTION Status is saved | taken | invalid | failed. -Prompt is the hashtable New-StylePromptText returns; its text is written verbatim. With no -Description, meta.json says "Made with tstyles create.". -PromptChoice (ConvertTo-CreatedPromptChoice) is kept in meta.json as `created`, the record Test-StylePromptIsGenerated regenerates the prompt from when the style is submitted. The style is built in $DataRoot\.creating\<guid> and renamed into place (a tab closed mid-save leaves that folder; Remove-StaleCreatingWork sweeps it), so no half-written folder is ever visible under styles\ (Get-AvailableStyles enumerates with -Force, so even a dot-prefixed folder there would be listed the moment its scheme.json landed). Both are on the same volume. #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$Name, [Parameter(Mandatory)]$Scheme, [Parameter(Mandatory)]$Theme, [Parameter(Mandatory)][string]$BackgroundPath, [Parameter(Mandatory)][hashtable]$Prompt, [System.Collections.IDictionary]$PromptChoice, [string]$Description = '' ) $result = { param($s, $p, $r) [pscustomobject]@{ Status = $s; Path = $p; Reason = $r } } if (-not (Test-StyleNameValid -Name $Name)) { return (& $result 'invalid' $null "'$Name' is not a usable style name.") } # Only the extensions Get-BackgroundFileIn resolves paint a background; # Get-BackgroundImageSaveExtension is the one rule, the creator's step 0 # asks it too, and it saves a .jfif as background.jpg. $ext = Get-BackgroundImageSaveExtension -Path $BackgroundPath if (-not $ext) { $got = [System.IO.Path]::GetExtension($BackgroundPath) return (& $result 'invalid' $null ("Background image must be a {0} file (got '{1}')." -f (Get-BackgroundImageTypesText), $got)) } $stylesRoot = Join-Path $script:TStylesDataRoot 'styles' $dest = Join-Path $stylesRoot $Name if (Test-CreatedStyleNameTaken -Name $Name) { return (& $result 'taken' $null "A style named '$Name' already exists.") } $workRoot = Join-Path $script:TStylesDataRoot '.creating' $work = Join-Path $workRoot ([guid]::NewGuid().ToString('N')) try { New-Item -ItemType Directory -Path $work -Force -ErrorAction Stop | Out-Null $utf8 = [System.Text.UTF8Encoding]::new($false) if (-not $Description.Trim()) { $Description = 'Made with tstyles create.' } $schemeOut = $Scheme.PSObject.Copy() $schemeOut | Add-Member -NotePropertyName name -NotePropertyValue $Name -Force [System.IO.File]::WriteAllText((Join-Path $work 'scheme.json'), ($schemeOut | ConvertTo-Json -Depth 16), $utf8) [System.IO.File]::WriteAllText((Join-Path $work 'theme.json'), ($Theme | ConvertTo-Json -Depth 16), $utf8) $meta = [ordered]@{ description = $Description } if ($PromptChoice) { $meta.created = [pscustomobject]$PromptChoice } [System.IO.File]::WriteAllText((Join-Path $work 'meta.json'), ([pscustomobject]$meta | ConvertTo-Json -Depth 4), $utf8) [System.IO.File]::WriteAllText((Join-Path $work 'profile.ps1'), [string]$Prompt.Profile, $utf8) [System.IO.File]::WriteAllText((Join-Path $work 'prompt.sh'), (([string]$Prompt.Shell) -replace "`r`n", "`n"), $utf8) Copy-Item -LiteralPath $BackgroundPath -Destination (Join-Path $work "background$ext") -ErrorAction Stop if (-not (Test-Path -LiteralPath $stylesRoot)) { New-Item -ItemType Directory -Path $stylesRoot -Force -ErrorAction Stop | Out-Null } # Re-check just before the rename: another process may have taken the name. if (Test-Path -LiteralPath $dest) { Remove-Item -LiteralPath $work -Recurse -Force -ErrorAction SilentlyContinue return (& $result 'taken' $null "A style named '$Name' already exists.") } Move-Item -LiteralPath $work -Destination $dest -ErrorAction Stop if (-not (Get-ChildItem -LiteralPath $workRoot -Force)) { Remove-Item -LiteralPath $workRoot -Force -ErrorAction SilentlyContinue } return (& $result 'saved' $dest $null) } catch { Remove-Item -LiteralPath $work -Recurse -Force -ErrorAction SilentlyContinue if ((Test-Path -LiteralPath $workRoot) -and -not (Get-ChildItem -LiteralPath $workRoot -Force)) { Remove-Item -LiteralPath $workRoot -Force -ErrorAction SilentlyContinue } return (& $result 'failed' $null $_.Exception.Message) } } function Remove-StaleCreatingWork { <# .SYNOPSIS Remove the half-built styles a killed save left in $DataRoot\.creating. .DESCRIPTION Save-CreatedStyle builds each style in .creating\<guid> and removes the folder on every way out it sees; a tab closed mid-save is not one of them. Only a folder named like Save-CreatedStyle's (32 hex digits) and last written over an hour ago goes -- a save in progress, in this or another tab, is seconds old -- and containment is proved before each recursive delete. The .creating folder itself goes once it is empty. #> [CmdletBinding()] param([Parameter(Mandatory)][string]$Root, [datetime]$NowUtc = [datetime]::UtcNow) if (-not [System.IO.Directory]::Exists($Root)) { return } $cutoff = $NowUtc.AddHours(-1) foreach ($d in [System.IO.Directory]::GetDirectories($Root)) { $leaf = [System.IO.Path]::GetFileName($d) if ($leaf -notmatch '^[0-9a-fA-F]{32}$') { continue } if ([System.IO.Directory]::GetLastWriteTimeUtc($d) -ge $cutoff) { continue } if (-not (Test-PathIsUnderRoot -Path $d -Root $Root)) { continue } Remove-Item -LiteralPath $d -Recurse -Force -ErrorAction SilentlyContinue } if ([System.IO.Directory]::GetFileSystemEntries($Root).Count -eq 0) { Remove-Item -LiteralPath $Root -Force -ErrorAction SilentlyContinue } } |