← Commit history

v2.0.688 (insiders): Smoother taskbar tile text: every tile size is drawn at 4x and averaged down, so letters sit at exact sub-pixel positions (John, 2026-10-03). Every Settings example image shows again: the sample badges and the badge-size, on/off and grouping previews were broken images because the page asked with n= and the routes read name=. pup_uninstall and pup_reap_profiles describe what the Rust exe does. The vault's card test no longer spells whole test card numbers (the wiki flagged the page as sensitive).

John Lauer ·80a3d142e8 ·18d ago ·parent 08a27c3
6 files changed +76−30
rust/Cargo.lock+12−12
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-pup-bridge"-version = "2.0.686"+version = "2.0.688" dependencies = [  "pup-browser",  "pup-cdp",@@ -655,7 +655,7 @@ dependencies = [  [[package]] name = "pup-browser"-version = "2.0.686"+version = "2.0.688" dependencies = [  "pup-core",  "pup-host",@@ -667,7 +667,7 @@ dependencies = [  [[package]] name = "pup-cdp"-version = "2.0.686"+version = "2.0.688" dependencies = [  "serde_json",  "thiserror",@@ -677,7 +677,7 @@ dependencies = [  [[package]] name = "pup-core"-version = "2.0.686"+version = "2.0.688" dependencies = [  "serde",  "serde_json",@@ -687,7 +687,7 @@ dependencies = [  [[package]] name = "pup-dashboard"-version = "2.0.686"+version = "2.0.688" dependencies = [  "pup-cdp",  "pup-core",@@ -700,7 +700,7 @@ dependencies = [  [[package]] name = "pup-host"-version = "2.0.686"+version = "2.0.688" dependencies = [  "serde",  "serde_json",@@ -709,14 +709,14 @@ dependencies = [  [[package]] name = "pup-host-mac"-version = "2.0.686"+version = "2.0.688" dependencies = [  "pup-host", ]  [[package]] name = "pup-host-win"-version = "2.0.686"+version = "2.0.688" dependencies = [  "pup-core",  "pup-host",@@ -726,7 +726,7 @@ dependencies = [  [[package]] name = "pup-identity"-version = "2.0.686"+version = "2.0.688" dependencies = [  "fontdue",  "image",@@ -736,7 +736,7 @@ dependencies = [  [[package]] name = "pup-session"-version = "2.0.686"+version = "2.0.688" dependencies = [  "pup-core",  "serde",@@ -746,7 +746,7 @@ dependencies = [  [[package]] name = "pup-vault"-version = "2.0.686"+version = "2.0.688" dependencies = [  "pup-core",  "pup-host",@@ -757,7 +757,7 @@ dependencies = [  [[package]] name = "pup-verbs"-version = "2.0.686"+version = "2.0.688" dependencies = [  "base64",  "png 0.17.16",
rust/Cargo.toml+1−1
@@ -4,7 +4,7 @@ members = ["core", "host", "host-win", "host-mac", "cdp", "browser", "session",  # One version for the whole bridge, in lockstep with src/BRIDGE_VERSION (tools/ship.sh bumps both). [workspace.package]-version = "2.0.686"+version = "2.0.688" edition = "2021" license = "MIT" 
rust/dashboard/src/lib.rs+4−3
@@ -193,9 +193,10 @@ pub fn route(ctx: &std::sync::Arc<Ctx>, caller: &Caller, method: &str, path: &st         ("GET", "/dashboard/icon.svg") => Some(Reply::Bytes(200, "image/svg+xml".into(), ICON_SVG.as_bytes().to_vec(), Some("public, max-age=3600".into()))),         ("GET", "/favicon.ico") => Some(Reply::Bytes(200, "image/svg+xml".into(), ICON_SVG.as_bytes().to_vec(), Some("public, max-age=3600".into()))),         ("GET", "/dashboard/newbie-panel.js") => Some(Reply::Bytes(200, "application/javascript".into(), assets_gen::get("newbie-panel.js").map(|b| b.to_vec()).unwrap_or_default(), Some("public, max-age=600".into()))),-        ("GET", "/dashboard/example") => { let n = q(query, "name").unwrap_or_default(); assets_gen::get(&format!("icons/settings-ex/{n}.png")).map(|b| Reply::Bytes(200, "image/png".into(), b.to_vec(), Some("no-store".into()))) }-        ("GET", "/dashboard/sample-favicon") => { let n = q(query, "name").unwrap_or_default(); let (file, ct) = match n.as_str() { "wiki" => ("wiki.svg", "image/svg+xml"), "ti" => ("ti.ico", "image/x-icon"), "mouser" => ("mouser.ico", "image/x-icon"), "digikey" => ("digikey.svg", "image/svg+xml"), _ => return None }; assets_gen::get(&format!("icons/settings-samples/{file}")).map(|b| Reply::Bytes(200, ct.into(), b.to_vec(), Some("public, max-age=3600".into()))) }-        ("GET", "/dashboard/sample-cat") => { let n = q(query, "name").unwrap_or_default(); pup_identity::ICONS.iter().find(|(name, _)| *name == n || name.trim_end_matches(".ico") == n.trim_end_matches(".ico")).map(|(_, b)| Reply::Bytes(200, "image/x-icon".into(), b.to_vec(), Some("public, max-age=3600".into()))) }+        ("GET", "/dashboard/example") => { let n = q(query, "n").or_else(|| q(query, "name")).unwrap_or_default(); assets_gen::get(&format!("icons/settings-ex/{n}.png")).map(|b| Reply::Bytes(200, "image/png".into(), b.to_vec(), Some("no-store".into()))) }+        // 2.0.687: the page sends n=, the route read name=, so every Settings sample badge was a broken image+        ("GET", "/dashboard/sample-favicon") => { let n = q(query, "n").or_else(|| q(query, "name")).unwrap_or_default(); let (file, ct) = match n.as_str() { "wiki" => ("wiki.svg", "image/svg+xml"), "ti" => ("ti.ico", "image/x-icon"), "mouser" => ("mouser.ico", "image/x-icon"), "digikey" => ("digikey.svg", "image/svg+xml"), _ => return None }; assets_gen::get(&format!("icons/settings-samples/{file}")).map(|b| Reply::Bytes(200, ct.into(), b.to_vec(), Some("public, max-age=3600".into()))) }+        ("GET", "/dashboard/sample-cat") => { let n = q(query, "n").or_else(|| q(query, "name")).unwrap_or_default(); pup_identity::ICONS.iter().find(|(name, _)| *name == n || name.trim_end_matches(".ico") == n.trim_end_matches(".ico")).map(|(_, b)| Reply::Bytes(200, "image/x-icon".into(), b.to_vec(), Some("public, max-age=3600".into()))) }         ("GET", "/dashboard/stampicon") => { let sid = q(query, "session").unwrap_or_default(); match tile_for(ctx, &sid) { Some(b) => Some(Reply::Bytes(200, "image/x-icon".into(), b, Some("public, max-age=60".into()))), None => Some(Reply::Bytes(200, "image/x-icon".into(), pup_identity::ICONS[1].1.to_vec(), Some("public, max-age=60".into()))) } }         ("GET", "/dashboard/badge") | ("GET", "/dashboard/bestbadge") => { let sid = q(query, "session").unwrap_or_default(); let f = ctx.host.temp_dir().join("pup-badge-debug").join(format!("{}.png", pup_session::sanitize_sid(&sid))); match std::fs::read(f) { Ok(b) => png(b, "public, max-age=30"), Err(_) => png(png_1x1(), "no-store") } }         ("GET", "/dashboard/baseicon") => Some(Reply::Bytes(200, "image/x-icon".into(), pup_identity::ICONS[0].1.to_vec(), Some("public, max-age=3600".into()))),
rust/identity/src/tile.rs+32−3
@@ -116,6 +116,33 @@ pub fn draw(f: &Font, lines: &[String], bf: f32, s: u32) -> Vec<u8> {     px } +/// 2.0.687 (John, 2026-10-03: "the rendered text in your taskbar icon still looks grainy; it used to look smoother").+/// The browser renderer it replaced (pre-2.0.559) placed glyphs at sub-pixel positions and blended in linear light.+/// draw() snaps every glyph to a whole pixel and blends in sRGB, which at 24-32 px makes uneven letter spacing and+/// blotchy stems. Draw the tile at SS x its size, then area-average it down (premultiplied alpha, gamma space, as+/// browsers blend text): glyph positions become exact to 1/SS px and the edges get real coverage. The plate gets the same benefit.+const SS: u32 = 4;+pub fn draw_smooth(f: &Font, lines: &[String], bf: f32, s: u32) -> Vec<u8> {+    let big = draw(f, lines, bf, s * SS);+    let bs = s * SS;+    let mut out = vec![0u8; (s * s * 4) as usize];+    let n = (SS * SS) as f32;+    for y in 0..s { for x in 0..s {+        let (mut r, mut g, mut b, mut a) = (0.0f32, 0.0f32, 0.0f32, 0.0f32);+        for sy in 0..SS { for sx in 0..SS {+            let i = (((y * SS + sy) * bs + (x * SS + sx)) * 4) as usize;+            let al = big[i + 3] as f32 / 255.0;+            // gamma-space average (what browsers do for text): linear-light averaging measurably THINNED the dark+            // strokes on the bright plate (tried first; the letters went faint)+            r += big[i] as f32 / 255.0 * al; g += big[i + 1] as f32 / 255.0 * al; b += big[i + 2] as f32 / 255.0 * al; a += al;+        } }+        let o = ((y * s + x) * 4) as usize;+        if a > 0.0 { out[o] = ((r / a).clamp(0.0, 1.0) * 255.0).round() as u8; out[o + 1] = ((g / a).clamp(0.0, 1.0) * 255.0).round() as u8; out[o + 2] = ((b / a).clamp(0.0, 1.0) * 255.0).round() as u8; }+        out[o + 3] = ((a / n) * 255.0).round() as u8;+    } }+    out+}+ /// Blit a line of text (left-aligned, alphabetic baseline) onto an RGBA buffer, glyphs rasterised by /// fontdue at exactly `fpx`, coverage-blended into RGB. Shared by the tile and the annotate caption bar. pub fn draw_text(f: &Font, px: &mut [u8], w: u32, h: u32, text: &str, fpx: f32, x0: f32, baseline: f32, ink: [u8; 3]) {@@ -142,7 +169,7 @@ pub fn render_ico(name: &str) -> Option<Vec<u8>> {     let (lines, bf) = layout(&f, name);     let mut raw: Vec<(u32, Vec<u8>)> = Vec::new(); let mut png256: Vec<u8> = Vec::new();     for &sz in crate::TILE_SIZES.iter() {-        let px = draw(&f, &lines, bf, sz);+        let px = if sz >= 256 { draw(&f, &lines, bf, sz) } else { draw_smooth(&f, &lines, bf, sz) };         if sz >= 256 { png256 = crate::png_encode_rgba(&px, sz, sz); } else { raw.push((sz, px)); }     }     Some(crate::ico_from_tiles(&raw, &png256))@@ -158,9 +185,11 @@ mod tests {         for name in ["pup-bridge", "pup-ab-smoke", "kicad-bridge-esc-routing", "bt556", "sxsw", "Logic gate molecule family"] {             let (lines, bf) = layout(&f, name);             assert!(!lines.is_empty() && lines.len() <= 2 && bf > 0.0, "{name}: {lines:?} {bf}");-            for sz in [256u32, 48, 32, 16] {+            for sz in [256u32, 48, 32, 24, 16] {                 let px = draw(&f, &lines, bf, sz);-                std::fs::write(dir.join(format!("{}-{sz}.png", crate::thread_slug(name))), crate::png_encode_rgba(&px, sz, sz)).unwrap();+                std::fs::write(dir.join(format!("{}-{sz}-old.png", crate::thread_slug(name))), crate::png_encode_rgba(&px, sz, sz)).unwrap();+                let px2 = draw_smooth(&f, &lines, bf, sz);+                std::fs::write(dir.join(format!("{}-{sz}.png", crate::thread_slug(name))), crate::png_encode_rgba(&px2, sz, sz)).unwrap();             }             let ico = render_ico(name).expect("ico");             assert!(ico.len() > 2000, "{name}: ico {} bytes", ico.len());
rust/verbs/assets/verb-catalog.json+9−11
@@ -1890,7 +1890,10 @@    },    "hint": "Delete LEAKED isolated profile directories \u2014 the iso-* throwaway jars minted by isolated:true opens (and by wikiView:public before 2.0.132, when the public view moved to the server-side ?view=public param) whose window is long gone (they used to pile up, 100s per machine). Only removes an iso-* dir that no live session or session file owns, that no Chrome is holding, and that is >15 min old; durable jars (adom-you, named profiles) are NEVER touched. Runs automatically every 15 min too. dryRun:true previews the list without deleting.",    "input": {-    "dryRun": "optional bool \u2014 preview which iso-* profiles would be removed, delete nothing"+    "dryRun": "optional bool: list, delete nothing",+    "staleDays": "optional int, default 14",+    "isoOnly": "optional bool: only throwaway iso-* profiles",+    "all": "optional bool: ignore the idle age (still never a protected, open or needed profile)"    },    "longRunning": false,    "name": "pup_reap_profiles",@@ -1900,7 +1903,7 @@    "pitfalls": [],    "related": [],    "statusVerb": null,-   "summary": "Delete LEAKED isolated profile directories \u2014 the iso-* throwaway jars minted by isolated:true opens (and by wikiView:public before 2.0.132, when the public view moved to the server-side ?view=public param) whose window is long gone (they used to pile up, 100s per machine). Only removes an iso-* dir that no live session or session file owns, that no Chrome is holding, and that is >15 min old; durable jars (adom-you, named profiles) are NEVER touched. Runs automatically every 15 min too. dryRun:true previews the list without deleting.",+   "summary": "Delete pup profiles nobody uses: by default every profile idle for staleDays (14) that is not protected (adom-you, adom-wiki-authed, pup-dashboard, _recorder), not open in a browser, and not needed to restore a saved window; with isoOnly:true only leaked throwaway iso-* profiles. dryRun:true lists without deleting. pup's own boot pass runs the isoOnly form.",    "timeoutSeconds": 20   },   {@@ -1940,16 +1943,11 @@   },   {    "example": {-    "dryRun": true+    "confirm": true    },-   "hint": "DESTRUCTIVE \u2014 cleanly remove pup's WHOLE footprint from this machine: the HKCU AppUserModelId Adom.Pup registry keys, all staged extensions, ALL pup profiles + sessions, the OS-keychain credential vault, pup-settings.json, the badge-debug temp dir, and the jump-list launcher. ALWAYS preview first with dryRun:true (removes nothing, returns the plan). To actually remove, pass confirm:\"uninstall\". keepCredentials:true preserves the saved-login vault; keepProfiles:true preserves profiles/sessions. Chrome-for-Testing + firewall rules are SHARED and left alone unless removeChrome:true / removeFirewallRules:true. Closes every window + quits every browser first to release profile locks. (The bridge itself is managed by Adom Bridge \u2014 remove pup from Bridge to stop it re-seeding.)",+   "hint": "There is no dry run: list what you would lose first with pup_status (profiles, windows) and pup_credentials.",    "input": {-    "confirm": "required str \"uninstall\" to actually remove",-    "dryRun": "optional bool \u2014 preview the removal plan without removing anything (do this first)",-    "keepCredentials": "optional bool \u2014 keep the saved-login vault",-    "keepProfiles": "optional bool \u2014 keep pup profiles + sessions",-    "removeChrome": "optional bool \u2014 also delete the Chrome-for-Testing install (shared; default keep)",-    "removeFirewallRules": "optional bool \u2014 also remove the CfT firewall rules (needs admin)"+    "confirm": "required bool true: actually remove"    },    "longRunning": false,    "name": "pup_uninstall",@@ -1959,7 +1957,7 @@    "pitfalls": [],    "related": [],    "statusVerb": null,-   "summary": "DESTRUCTIVE \u2014 cleanly remove pup's WHOLE footprint from this machine: the HKCU AppUserModelId Adom.Pup registry keys, all staged extensions, ALL pup profiles + sessions, the OS-keychain credential vault, pup-settings.json, the badge-debug temp dir, and the jump-list launcher. ALWAYS preview first with dryRun:true (removes nothing, returns the plan). To actually remove, pass confirm:\"uninstall\". keepCredentials:true preserves the saved-login vault; keepProfiles:true preserves profiles/sessions. Chrome-for-Testing + firewall rules are SHARED and left alone unless removeChrome:true / removeFirewallRules:true. Closes every window + quits every browser first to release profile locks. (The bridge itself is managed by Adom Bridge \u2014 remove pup from Bridge to stop it re-seeding.)",+   "summary": "DESTRUCTIVE: remove pup's on-disk footprint from this machine: every pup profile and saved window, the staged extensions, the taskbar icons and tiles, and every taskbar identity pup registered. Pass confirm:true. The saved-login vault is kept (credential_delete removes logins one by one), and Adom Bridge's bridge_uninstall removes the pup program itself.",    "timeoutSeconds": 60   },   {
tools/gen-assets.py+18
@@ -150,6 +150,24 @@ _STUBS = {     'credential_delete': lambda: _entry('credential_delete', "Remove a saved login from pup's vault.", {'host': 'str', 'username': 'optional str: only this account'}, {'host': 'example.com'}, 'Removes the saved password from the vault only; a session the profile is already signed into stays signed in (pup_clear_cookies ends that).', ['pup_credentials', 'credential_set']),     'pup_launch': lambda: dict(_entry('pup_launch', '', {}, {}, '', ['pup_open_window'], like='pup_open_window'), summary='Alias of pup_open_window (url defaults to about:blank).'), }+# 2.0.687: entries whose text came from the Node build but whose Rust behavior differs. An AI reads these as the+# contract, so they must describe what the exe does (pup_uninstall took confirm:"uninstall" and offered a dry run in+# Node; the Rust verb takes confirm:true and has no dry run).+_OVERRIDES = {+    'pup_uninstall': {+        'summary': "DESTRUCTIVE: remove pup's on-disk footprint from this machine: every pup profile and saved window, the staged extensions, the taskbar icons and tiles, and every taskbar identity pup registered. Pass confirm:true. The saved-login vault is kept (credential_delete removes logins one by one), and Adom Bridge's bridge_uninstall removes the pup program itself.",+        'input': {'confirm': 'required bool true: actually remove'},+        'example': {'confirm': True},+        'hint': 'There is no dry run: list what you would lose first with pup_status (profiles, windows) and pup_credentials.',+    },+    'pup_reap_profiles': {+        'summary': "Delete pup profiles nobody uses: by default every profile idle for staleDays (14) that is not protected (adom-you, adom-wiki-authed, pup-dashboard, _recorder), not open in a browser, and not needed to restore a saved window; with isoOnly:true only leaked throwaway iso-* profiles. dryRun:true lists without deleting. pup's own boot pass runs the isoOnly form.",+        'input': {'dryRun': 'optional bool: list, delete nothing', 'staleDays': 'optional int, default 14', 'isoOnly': 'optional bool: only throwaway iso-* profiles', 'all': 'optional bool: ignore the idle age (still never a protected, open or needed profile)'},+        'example': {'dryRun': True},+    },+}+for _v in _verbs:+    if _v['name'] in _OVERRIDES: _v.update(_OVERRIDES[_v['name']]) _missing = [] for _n in _routed:     if _n in _have: continue