Repository navigation
v0.8.25
Fixed
-
opening the picker on a style that ships no
theme.jsonstripped every comment out ofsettings.json, wrote none of the style, and reported success.Get-StyleSettingsPayloaddecides what a style actually contributes tosettings.json, and it had exactly three references repo-wide -- none of them in the picker, and none intests/. Sotstyles aaa-schemeonlycorrectly printed "ships no theme.json, so nothing was written to settings.json" and left the file byte-identical, whiletstylesand 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, andtstyles currentthen 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 lines4 -> 1,"schemes": []still empty. With the fix the file comes back byte-identical at978aca48, 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.jsonmirroring 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-initcould leave a zsh user's zsh completely unstyled, print two green "added" lines, and invent a~/.bash_profilethey 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
~/.bashrcand no~/.zshrc-- a pre-Catalina Mac, or any home an nvm, pyenv or conda installer has dropped a.bashrcinto, and macOS zsh has no newuser hook so it never writes a~/.zshrcfor you -- got the loader in bash files, a~/.bash_profilecreated for them, "source ~/.bashrc" as the closing advice, and a new zsh tab with nothing in it. Measured across rc layouts with$env:SHELLset 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 <- afterThe 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.
~/.profileis 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:SHELLis 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.ps1asks "can the loader run?" twice and asked two different questions. The PRE-check readsGet-ExecutionPolicywith no scope -- the EFFECTIVE policy, which is what actually decides whether a script runs. The POST-check readGet-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 CurrentUserSUCCEEDS -- it writes HKCU and only warns that the setting is overridden -- the scope reads backRemoteSigned, 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-PolicyResolvedacceptedUndefinedas "scripts are allowed".Undefinedis what a single SCOPE reads back when nothing was ever written there -- it means "no answer", not "allowed" -- and an effectiveGet-ExecutionPolicynever returns it, since an all-Undefined machine resolves to the platform default,Restrictedon 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
defaultsprofile deleted every comment insettings.json, wrote nothing of the style, and printed "Style applied" in green.Resolve-WTProfileTargetreturnedOk = $truefordefaultsunconditionally, 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 unlessprofilesis a JSON object -- and a hand-minimisedsettings.jsonmay have noprofileskey,"profiles": nullparses to nothing, and a truncated write leaves a zero-byte file. SoApply-StyleDirect's guard passed, the rolling.bakwas spent, and the merge threw "You cannot call a method on a null-valued expression".A method call on
$nullaborts the STATEMENT, not the script, and$ErrorActionPreferenceisContinueinteractively -- so$settingskept 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, andtstyles currentnamed it anyway. This is precisely the failure the-Targetguard 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-bytesettings.jsonthe backup step overwrites the.bakwith the empty file while printing "Backed up settings to: ...".The legacy flat form
"profiles": [ ... ]fails differently and silently:Add-MemberUNROLLS 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 notGet-WTProfileShapenow answers "what isprofiles, and what may a target name" once, and the four readers that had each assumed the modern shape consult it: the resolver forOkandAvailable,Merge-StyleIntoSettingsandSet-ProfileFontfor 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'sstillUsedcomputation in the same change -- reachable profiles it could not previously see would otherwise have let a reset delete a scheme still in use.Apply-StyleDirectalso 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 andapply.ps1all resolved one silently.Ambiguoushad 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" whenWT_PROFILE_IDnames 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.0both 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-ForEachrather 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 at7.0.0on all three install sites, the Windows PowerShell leg gains the matching-MaximumVersionso 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.22andv0.8.23were both committed, tagged and released on GitHub and neither ever reached PSGallery, so every installed copy stayed on 0.8.21 for weeks while atstyles resetthat 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.ymlasks 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.ymlis 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