Skip to main content

max / quasi

Make a generated app build again: current pins, and Step::Copy Found by generating outside ~/Code, which is the only place the template's own pins are what resolves -- under the [patch] block every in-tree build reads the working copy and the manifests say nothing. A generated app did not compile. makeover-build was pinned at 0.52.0, which wants makeover-webview ^0.59, while quasi wants 0.70; that crate carries links = "makeover-webview", so two versions in one graph is a resolver error rather than a warning. makeover was 3.0 against 3.2.0. Behind that, quasi-tui's Step grew Copy and the template's match never learned it. It is handled the way Open is, because the clipboard is the host's for the same reason opening a URL is, and with OSC 52: it needs no dependency and works over ssh, which a system clipboard library does not. Base64 is written out for the same reason -- twelve lines, one call site, and a scaffold that takes a crate for it teaches the wrong lesson. Generated and tested outside the tree against the pushed 0.82.0: 30 tests, clippy clean, fmt clean.
Author: Max Johnson <me@maxj.phd> · 2026-08-30 19:29 UTC
Signed with PGP, not checked
Commit: d8550fc0ec06f75b2d2c10c8b70dda1c7f5d3388
Parent: 4580a96
3 files changed, +44 insertions, -4 deletions
@@ -41,8 +41,8 @@
41 41 # so a stale pin here is not one app a release behind: it is every app anyone
42 42 # scaffolds from now on. 2.4.1 and 0.13.0 sat here once, and 2.5 and 0.28.0
43 43 # after that, while the suite reached 3.0.0 and 0.47.0.
44 - makeover = "3.0"
45 - makeover-build = "0.52.0"
44 + makeover = "3.2"
45 + makeover-build = "0.63.0"
46 46 # The renderer's own scripts, written into `static/` beside the stylesheets.
47 47 # A build dependency and not a runtime one: this crate names no host, and
48 48 # reading three constants out of the webview renderer at build time does not
@@ -24,5 +24,5 @@
24 24 # theirs as CSS from `build.rs`; a terminal reads the same theme file and
25 25 # resolves it to sixteen colours or to truecolor depending on what the terminal
26 26 # admits to.
27 - makeover = "3.0"
27 + makeover = "3.2"
28 28 makeover-tui = { version = "0.41.0", features = ["theme"] }
@@ -34,7 +34,7 @@
34 34 //! whatever the machine's user already is, the same as the desktop binary: a
35 35 //! terminal has no session and no cookie to put one in.
36 36
37 - use std::io;
37 + use std::io::{self, Write as _};
38 38 use std::path::PathBuf;
39 39 use std::sync::Arc;
40 40 use std::time::Instant;
@@ -236,6 +236,21 @@
236 236 runtime.say(format!("this goes outside the app: {address}"));
237 237 return None;
238 238 }
239 + // The clipboard is the host's, the same way opening a URL is:
240 + // `quasi-tui` turns keys into requests and owns no I/O. OSC 52 is
241 + // the terminal's own answer and needs no dependency, and it works
242 + // over ssh, which a system clipboard library does not.
243 + //
244 + // A terminal that does not support the sequence ignores it, so the
245 + // failure mode is nothing happening rather than an error. That is
246 + // why the message is said either way.
247 + Step::Copy(text) => {
248 + let encoded = base64(text.as_bytes());
249 + let _ = write!(io::stdout(), "\x1b]52;c;{encoded}\x07");
250 + let _ = io::stdout().flush();
251 + runtime.say("copied");
252 + return None;
253 + }
239 254 Step::Call(request) => match router.handle(state, request.clone()) {
240 255 // A response that says to go somewhere else is a second
241 256 // request, performed the same way; anything else is done.
@@ -252,6 +267,31 @@
252 267 }
253 268 }
254 269
270 + /// Base64, because OSC 52 carries its payload that way.
271 + ///
272 + /// Written out rather than taken as a dependency: it is twelve lines, it is the
273 + /// only encoding this binary does, and a scaffold that pulls a crate in for it
274 + /// is teaching the wrong lesson about what a dependency is for.
275 + fn base64(bytes: &[u8]) -> String {
276 + const ALPHABET: &[u8; 64] =
277 + b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
278 + let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
279 + for chunk in bytes.chunks(3) {
280 + let mut block = [0u8; 3];
281 + block[..chunk.len()].copy_from_slice(chunk);
282 + let packed = u32::from(block[0]) << 16 | u32::from(block[1]) << 8 | u32::from(block[2]);
283 + for shift in [18, 12, 6, 0] {
284 + out.push(ALPHABET[(packed >> shift) as usize & 0x3f] as char);
285 + }
286 + }
287 + // The last group is padded to four characters whatever it held, so the
288 + // characters standing for bytes that were not there become `=`.
289 + let pad = (3 - bytes.len() % 3) % 3;
290 + out.truncate(out.len() - pad);
291 + out.push_str(&"=".repeat(pad));
292 + out
293 + }
294 +
255 295 /// The renderer, in this terminal's colours.
256 296 ///
257 297 /// Constructed here rather than in `{{name}}-core`, the same way the other two