Skip to content

v0.8.25

Choose a tag to compare

@fcreme fcreme released this 11 Sep 22:11
· 215 commits to main since this release

Fixed

  • opening the picker on a style that ships no theme.json stripped every comment out of settings.json, wrote none of the style, and reported success. Get-StyleSettingsPayload decides what a style actually contributes to settings.json, and it had exactly three references repo-wide -- none of them in the picker, and none in tests/. So tstyles aaa-schemeonly correctly printed "ships no theme.json, so nothing was written to settings.json" and left the file byte-identical, while tstyles and Enter on the SAME style rewrote it: re-serialising the parsed object drops every JSONC comment the user had, nothing of the style was written, "Style applied" printed in green, and tstyles current then named a style Windows Terminal was never told about.

    The load-bearing site is the FIRST PREVIEW, which fires the instant the picker opens, before a key is pressed. Measured end to end through the real picker on a real pty, with one Enter: sha 978aca48 -> c1b9877e, comment lines 4 -> 1, "schemes": [] still empty. With the fix the file comes back byte-identical at 978aca48, all four comments intact, and the picker prints the direct path's own sentence.

    The three merge sites are now one helper. Pasting the guard at each was the obvious fix and is how the rule came to be missing from all three; it would also have left the decision inside a function no test can drive, so the only possible regression test would have been a source-text assertion -- the shape this project has been bitten by twice.

    Two things fell out of gating it that a naive guard would have broken. The picker relies on settings.json mirroring the highlighted row, so arrowing from a themed style onto a payload-less one now restores the user's original bytes rather than leaving the previous style's preview on disk under the new name. And Esc no longer writes at all when the picker never wrote: restoring over an untouched file is not free -- it drops a BOM the user had, bumps the mtime, and Windows Terminal watches the file and reloads.

  • tstyles shell-init could leave a zsh user's zsh completely unstyled, print two green "added" lines, and invent a ~/.bash_profile they never had. Which shell the user actually logs in to was computed INSIDE the "nothing existed at all" fallback, which fires only when not one rc file was found. On every other path the three decisions that follow -- create a login file, register in ~/.profile, name a file to source in the closing hint -- were taken with no idea which shell the user runs, and all three assumed bash.

    So a zsh user with a leftover ~/.bashrc and no ~/.zshrc -- a pre-Catalina Mac, or any home an nvm, pyenv or conda installer has dropped a .bashrc into, and macOS zsh has no newuser hook so it never writes a ~/.zshrc for you -- got the loader in bash files, a ~/.bash_profile created for them, "source ~/.bashrc" as the closing advice, and a new zsh tab with nothing in it. Measured across rc layouts with $env:SHELL set to each shell:

    SHELL   home starts with   files carrying the block after shell-init
    zsh     .bashrc only       .bash_profile, .bashrc      <- before
    zsh     .bashrc only       .bashrc, .zshrc             <- after
    

    The login-shell decision is hoisted to where the three consumers can see it, and the branch that CREATES a login file is gated on it. Registering in rc files that already exist stays unconditional -- someone who uses zsh interactively and keeps a bash rc for scripts should get both, and it costs nothing. ~/.profile is now recorded as the login shell's file rather than as sh's: it is sh's by ownership, but the only reason it is ever written is that it is what this login shell reads, and that is the question the closing hint asks of the list. $env:SHELL is unset on Windows and empty in some daemons, and bash was the answer in both cases before, so the default leaves them as they were.

  • the installer's execution-policy fix verified the wrong scope, so it reported success on exactly the machines where it had failed. install.ps1 asks "can the loader run?" twice and asked two different questions. The PRE-check reads Get-ExecutionPolicy with no scope -- the EFFECTIVE policy, which is what actually decides whether a script runs. The POST-check read Get-ExecutionPolicy -Scope CurrentUser: the value it had just written. Precedence is MachinePolicy > UserPolicy > Process > CurrentUser > LocalMachine, so the scoped read-back cannot detect the failure the pre-check was asking about. On a machine with a Group Policy pinning the policy, Set-ExecutionPolicy -Scope CurrentUser SUCCEEDS -- it writes HKCU and only warns that the setting is overridden -- the scope reads back RemoteSigned, and the installer printed a green "Done." while the loader still could not run. The write keeps its scope, because CurrentUser is the one that needs no elevation; only the verification changed.

  • Test-PolicyResolved accepted Undefined as "scripts are allowed". Undefined is what a single SCOPE reads back when nothing was ever written there -- it means "no answer", not "allowed" -- and an effective Get-ExecutionPolicy never returns it, since an all-Undefined machine resolves to the platform default, Restricted on Windows clients. So rejecting it costs nothing and stops a scoped value that reaches the function by mistake from reading as resolved, which is precisely what the post-check above was doing: a write that never landed announced in green as "policy is now Undefined".

  • the advice printed when the policy fix fails named the wrong culprit. It blamed "LocalMachine / GPO", and LocalMachine LOSES to CurrentUser -- it is the lowest-precedence scope of the five, so it can never be what overrode the write. Only the two Group Policy scopes and a Process-scope policy outrank it. The message now names those, distinguishes "the engine answered and the answer still blocks scripts" from "the engine printed nothing at all", and asks the user to read what the manual command reports rather than promising elevation will help.

  • applying a style to the defaults profile deleted every comment in settings.json, wrote nothing of the style, and printed "Style applied" in green. Resolve-WTProfileTarget returned Ok = $true for defaults unconditionally, on the reasoning that the block does not have to exist yet because an apply creates it lazily. It creates it with $Settings.profiles | Add-Member, which has nothing to add to unless profiles is a JSON object -- and a hand-minimised settings.json may have no profiles key, "profiles": null parses to nothing, and a truncated write leaves a zero-byte file. So Apply-StyleDirect's guard passed, the rolling .bak was spent, and the merge threw "You cannot call a method on a null-valued expression".

    A method call on $null aborts the STATEMENT, not the script, and $ErrorActionPreference is Continue interactively -- so $settings kept the parsed-but-unmerged object and the very next line wrote it out. The user's comments were gone, the style was in no profile, and tstyles current named it anyway. This is precisely the failure the -Target guard was added to prevent -- "a misspelled profile name silently and irreversibly deleted their JSONC comments" -- re-entered through the one target the resolver declared always addressable. Worse on the second run: run 1 leaves the good file in the .bak, run 2 copies the stripped one over it. On a zero-byte settings.json the backup step overwrites the .bak with the empty file while printing "Backed up settings to: ...".

    The legacy flat form "profiles": [ ... ] fails differently and silently: Add-Member UNROLLS an array, so the whole theme was grafted onto the first profile object, where Windows Terminal ignores it, and the resolver advertised "Available: defaults" while naming none of the user's real profiles. Measured against the shipped function:

    shape              main                              fixed
    no profiles key    throws -> comments deleted        returns, writes nothing
    legacy array       grafts defaults onto a profile    does not
    

    Get-WTProfileShape now answers "what is profiles, and what may a target name" once, and the four readers that had each assumed the modern shape consult it: the resolver for Ok and Available, Merge-StyleIntoSettings and Set-ProfileFont for their lazy creation, and reset's orphan-scheme sweep. The slot test is structural rather than a type name, so it answers the same on both engines. Named targets in the array form now resolve and merge correctly, which meant fixing reset's stillUsed computation in the same change -- reachable profiles it could not previously see would otherwise have let a reset delete a scheme still in use.

    Apply-StyleDirect also no longer writes when the merge did not come back. That guard is what separates an ugly error from silent comment loss, and it will do the same for the next throw inside the merge.

  • only tstyles <style> warned that a profile name was duplicated; the picker, reset, font, tune and apply.ps1 all resolved one silently. Ambiguous had two producers and exactly one consumer, across eleven call sites. The note is now written in one place that every caller reads, and the resolver records HOW it broke the tie rather than letting the note re-derive it -- so it no longer claims "the one this session is running in" when WT_PROFILE_ID names a third profile and file order actually decided.

Changed

  • CI installed whatever major version of Pester PSGallery was serving. Every install line was unbounded above -- -Version '[5.0.0,)' and -MinimumVersion 5.0.0 both mean "newest" -- so steps named "Install Pester 5" have been running Pester 6. That is not cosmetic: Pester 6 discovers and executes each file in turn rather than discovering all of them first, which silently made the guard that watches for tests writing to the operator's own files photograph the state AFTER the damage and compare it against itself; and it fails DISCOVERY on an empty -ForEach rather than producing no tests, which took both Windows legs down for a file about POSIX shells. Both were found by hand. The range is bounded at 7.0.0 on all three install sites, the Windows PowerShell leg gains the matching -MaximumVersion so the two engines cannot drift onto different majors, and the steps are named for what they do.

  • a release that is tagged but never published now says so. v0.8.22 and v0.8.23 were both committed, tagged and released on GitHub and neither ever reached PSGallery, so every installed copy stayed on 0.8.21 for weeks while a tstyles reset that deleted hand-made profile settings and a trash sweep that erased styles seconds after promising seven days sat fixed in git and shipped to nobody. Nothing anywhere said so. release-drift.yml asks PSGallery for its published version -- which is public, so this needs no API key and nothing that rotates -- compares it with the newest tag, and fails on the tag push and again every Monday for as long as they differ. An unreachable PSGallery warns and exits 0: a network failure is not a release problem and must not read as one. publish.yml is there for the case where a key is current, and is dispatch-only so a rotated one cannot redden every release.

Full changelog: v0.8.24...v0.8.25