← writing featured
Glazier Release: v0.2.4
Last night, I minted and tagged Glazier v0.2.4. This release focussed on addressing 80 bugs and stability issues I’ve collated over the last few months. I initially thought I’d take care of a dozen or so HIGH and MEDIUM ones, but ended up spending the better part of the last week working through all of them.
I might’ve gone a little overboard.
Yes, you read that right. Dozens of bugs covering the full spectrum of compatibility, stability, security and just plain broken functionality. Here’s the list in its entirety:
| Bug | Problem |
|---|---|
B01 | tmux matched session names by prefix, so glaze down on a session named project could kill project-long. |
B02 | A window with four or more panes failed with “no space for new pane”. |
B03 | Panes and windows ignored the starting_directory of their window and session. |
B04 | A non-final command ending in &, ; or a comment hung up. |
B05 | The layout overwrote size and adjust, so neither had any effect. |
B06 | Session commands hung or failed with some session names. |
B07 | Logs and diagnostics went to stdout, so save --stdout > file wrote an invalid profile. |
B08 | A failed glaze up left a partial session behind a subsequent glaze up would accept. |
B09 | The first window index was base-index + 1. |
B10 | A semicolon or a tab in a name broke the parsing of tmux output. |
B11 | save overwrote an existing .glaze without asking. |
B12 | save outside tmux picked a session without a warning. |
B13 | Validation accepted some values that up could not apply. |
B14 | Diagnostics contained ANSI colour codes when the output was not a terminal. |
B15 | Diagnostics for a syntax error showed “(source code not available)”. |
B16 | The README and --help claimed that --var-file reads JSON; it reads HCL only. |
B17 | The README example for the session-created hook could never fire. |
B18 | A ~ in --profile-path or ${GLAZE_PATH} was not expanded. |
B19 | ls exited 1 when no tmux server was running. |
B20 | attach passed the socket flag twice. |
B21 | tmux expanded format sequences such as #{session_name} inside names. |
B22 | save wrote directories that no longer existed. |
B23 | The README did not say that reverse only accepts a string. |
B24 | tmux quietly changed . and : in a session name. |
B25 | With tmux 3.4, a $ in a name gained an extra backslash on every read. |
B26 | up --clear from inside the target session killed its own shell and left nothing behind. |
B27 | ~/ in starting_directory passed validation but reached tmux unexpanded. |
B28 | ${TMUX} was trusted without a socket check, so up could switch a client on another server. |
B29 | format and save truncated the file before writing, so a failed write destroyed the only copy. |
B30 | --debug printed every envs value, secrets included. |
B31 | down panicked when the session name evaluated to a number, a bool or null. |
B32 | A session without a name worked with up, but down rejected the same profile. |
B33 | down rolled random() in the session name again and killed the wrong session. |
B34 | down, ls and save reported “not running” when tmux was merely unreachable. |
B35 | tmux split an argument ending in ;, which broke up with an odd error. |
B36 | A ! in a command triggered history expansion in bash and zsh. Oops. |
B37 | Readline completion ate a tab in a command. |
B38 | The wait-for signal relied on tmux from ${PATH} and ${TMUX}, so a changed environment hung up. |
B39 | A pane whose command exits at once broke the next split. |
B40 | A session option declared on a window leaked to every later window. |
B41 | A window or pane option declared on the session landed on a throwaway window and was lost. |
B42 | A base-index set in the background by tmux.conf made glaze kill the first declared window. |
B43 | -v after a subcommand was ignored, so glaze up -v built a session. |
B44 | Under LANG=C, non-ASCII names read back as _ and broke a second up and save. |
B45 | A small chain of locals could exhaust memory, even in down and format --validate. |
B46 | Deeply nested brackets (about 100,000 levels) crashed glaze with a stack overflow. |
B47 | A size with only x or only y passed validation but was never applied. |
B48 | A percentage adjust.amount passed validation, then failed halfway through up. |
B49 | make install on Apple Silicon built an amd64 binary outside ${PATH}. |
B50 | A null element in a list or map panicked up and format --validate. |
B51 | A command starting with - was read as send-keys flags. |
B52 | A final command equal to a tmux key name, such as Enter, was sent as that key. |
B53 | The shell’s line editor cut long command lines, which hung up. |
B54 | SIGINT or SIGTERM during a wait left an orphaned tmux client and a partial session. |
B55 | A bare ~ was rejected, and ~user gave a misleading error. |
B56 | glaze failed from a deleted working directory, even with an absolute profile path. |
B57 | A ${GLAZE_PATH} naming a file, or a .glaze directory, gave “profile not found” with no hint. |
B58 | format hung on a FIFO and dropped its output on /dev/stdin. |
B59 | format rewrote an unchanged file and bumped its modification time. |
B60 | down without a required --var reported “Unsupported attribute”. |
B61 | A cycle between locals showed up as three misleading errors. |
B62 | random() on a map put a Go stack dump in its diagnostic. |
B63 | save of a session that glaze did not create named every pane after the host and every window after the shell. |
B64 | Two up runs at the same time left the loser with an opaque error. |
B65 | glaze read base-index over and over, so ls on 80 windows spawned 83 processes. |
B66 | A tmux.conf hook could rename every declared window in silence. |
B67 | A raw layout with the wrong number of panes was applied in part without a word. |
B68 | Unknown hook names passed validation and failed halfway through up. |
B69 | A bool variable accepted 1 and 0, which the README never mentioned. |
B70 | A binary from go install reported Version: dev. |
B71 | --help showed the author as a Go struct. |
B72 | save warned “EXPERIMENTAL” on every run. |
B73 | ${NO_COLOR} was ignored in a pipe. |
B74 | tmux 3.9 prints layouts as JSON, which save wrote and glaze then rejected. |
B75 | tmux read a window name starting with - as a flag, which failed up. |
B76 | up without a terminal exited 1 but left the session running. |
B77 | A failed write in format exited 3, the code for an invalid profile. |
B78 | format --validate printed its errors in a different order on each run. |
B79 | A signal during a wait was blamed on the pane’s shell. |
B80 | Errors in a --var-file showed “(source code not available)”. |
Some of these were just plain embarrassing, but I’m glad I was able to work through them all. Surely, there are many more lurking in the code somewhere, but each one is a learning opportunity. On the bright side, Glazier is now more stable than ever!
We now have proper package management!
I also took the time to reduce the friction involved in actually installing the tool. I looked at something like GoReleaser, which would definitely have simplified distribution, but it would’ve come at the cost of the current release pipeline in GitHub. Definitely doable, but I’d have to bend the pipeline to fit the tool rather than the other way around. Besides, a handful of files was enough to get the following platforms sorted.
Homebrew
This is probably the one that makes the most sense for a lot of fellow developers. The most tedious part was standing up an additional repository to get a custom tap working. But now, installing Glazier on your Mac is as easy as:
brew install wilhelm-murdoch/tap/glazierAPK, DEB and RPM packages
These were actually easier to implement, as each one is rendered by a simple Bash script.
.deb
curl -LO https://github.com/wilhelm-murdoch/glazier/releases/latest/download/glazier-linux-amd64.deb
sudo apt install ./glazier-linux-amd64.deb.rpm
curl -LO https://github.com/wilhelm-murdoch/glazier/releases/latest/download/glazier-linux-amd64.rpm
sudo dnf install ./glazier-linux-amd64.rpm.apk
curl -LO https://github.com/wilhelm-murdoch/glazier/releases/latest/download/glazier-linux-amd64.apk
apk add --allow-untrusted ./glazier-linux-amd64.apk.apk package isn’t signed, so you’ll have to use the --allow-untrusted flag for this to work. You can always verify it yourself against the release’s attestation. Alternatively, if enough people ask, I’ll go through the trouble of getting it properly signed.What about AUR support?
This was one of the last platforms I wanted to support. However, the AUR currently has a registration freeze in place due to a massive influx of requests. So, I’m putting this one on pause and I’ll check back later.
E2E testing gets a brand-new harness!
Most of these bugs were found by an end-to-end harness that drives the real binary against real tmux servers. It began life as a bunch’a Bash scripts. For this release, I rebuilt it as an extensible Go test module that lives in the repository under e2e/.
Every case starts its own tmux server in its own temporary directory, runs the glaze binary exactly as you would and reads the result back from tmux itself. The harness never imports glaze, so a bug cannot hide behind glaze’s own code.
A few details:
- It currently has over
200cases and more than1,000checks, with profiles kept as real.glazefixture files. - It covers six tmux versions from
3.2ato3.7c, Alpine (musl) included, onamd64andarm64, plus macOS. - Every pull request runs the full matrix and compares the result of each check against the
mainbranch. - A weekly canary runs against the development branch of tmux. It caught the JSON layouts of tmux 3.9 before any tmux release shipped them.
- Releases are now gated behind the harness, so a release tag publishes only when every case passes against it.
- Known failures live in a baseline that fails the run the moment a fix makes an entry stale.
The last big release, v0.1.6, failed 88 checks on every target. At the time of this writing, all checks pass on all targets! This was arguably the biggest win for future releases because we now have near-full integration coverage on top of the 700+ unit tests… probably a bit excessive for this kind of tool. I’m nothing if not thorough.
The downside to this new harness is that it’s one more thing I need to maintain alongside the rest of the code. We’ll see how much of a headache that becomes.
What’s next?
Now that all known bugs have been squashed, I can focus on some interesting new features. Specifically, something like glaze diff ... that compares a given session with the specified .glaze profile. Think of terraform plan, but for your tmux session. I’m also considering a registry, or pool, of named profiles that can be called from anywhere regardless of directory context. I’ve also been thinking about lifecycle hooks outside of tmux, and about a way to import your existing profiles from other tools like tmuxinator, tmuxp and smug.
Then there’s the repository’s ever-growing README.md. It isn’t exactly the most intuitive thing to peruse as a newcomer, and the SPEC.md file is a bit daunting. So, a dedicated documentation site is something I’m considering as a next step.
If there’s one thing I’ve learned while building this tool it’s that I never want to build functionality around an other well-known tool ever again. But, I probably will anyway.
To the two other people using this, enjoy!
1 revision since publication
- e7b4aba +2 −0 promote glazier release article to featured and pinned
- 4a2397e +175 −0 published
Comments