app
Pup - the AI's own browser
Public Made by Adomby adom
pup gives your AI a real browser on your desktop that it drives completely, in the background, signed in as you: windows that never jump in front of your work, one shared profile that learns your logins, and every window labeled on the taskbar with its AI thread's name.
master
John Lauer
v2.0.689 (insiders): Every existing thread's taskbar tile is redrawn with the smoother text from 2.0.688 (the tile cach…
d310442
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
//! pup-identity: taskbar identity POLICY and ART, no OS calls. AUMID naming (identical to the Node
//! build so pinned buttons survive the port), URL categories, the thread-tile and overlay-badge
//! canvas scripts (rendered by the browser pup already drives: no font rasterizer, no image
//! library), a PNG-in-ICO packer, the embedded icon set and font, and the jump-list launcher.
//! The verbs crate applies all of it through ab's identity verbs.
use serde_json::json;
use std::path::{Path, PathBuf};
pub const APP_ID: &str = "Adom.Pup";
pub const DISPLAY_NAME: &str = "Adom Pup";
pub const TILE_GEN: &str = "t11"; // 2.0.689: tiles redrawn smooth (4x supersampled); t10 skipped, older builds left t10 folders behind
pub const ICON_GEN: &str = "9";
pub const TITLE_LABEL: &str = "ai-thread";
pub mod tile;
pub mod badge;
pub static ICONS: &[(&str, &[u8])] = &[
("adom-pup.ico", include_bytes!("../../../src/icons/adom-pup.ico")),
("pup-cat-default9.ico", include_bytes!("../../../src/icons/pup-cat-default9.ico")),
("pup-cat-wiki9.ico", include_bytes!("../../../src/icons/pup-cat-wiki9.ico")),
("pup-cat-wiki-in9.ico", include_bytes!("../../../src/icons/pup-cat-wiki-in9.ico")),
("pup-cat-web9.ico", include_bytes!("../../../src/icons/pup-cat-web9.ico")),
("pup-cat-app9.ico", include_bytes!("../../../src/icons/pup-cat-app9.ico")),
("pup-cat-mixed9.ico", include_bytes!("../../../src/icons/pup-cat-mixed9.ico")),
];
/// Write the embedded icons to a folder (skipped when the bytes already match).
pub fn write_icons(dir: &Path) -> std::io::Result<()> {
std::fs::create_dir_all(dir)?;
for (name, bytes) in ICONS {
let p = dir.join(name);
let same = std::fs::metadata(&p).map(|m| m.len() == bytes.len() as u64).unwrap_or(false);
if !same { std::fs::write(&p, bytes)?; }
}
Ok(())
}
// ── naming (Node: threadSlug / threadAumid / classifyUrl) ──────────────────────────────
pub fn thread_slug(name: &str) -> String {
let base = name.to_lowercase();
let mut s = String::new(); let mut last_dash = false;
for ch in base.chars() { if ch.is_ascii_alphanumeric() { s.push(ch); last_dash = false; } else if !last_dash { s.push('-'); last_dash = true; } }
let s: String = s.trim_matches('-').chars().take(24).collect();
let s = if s.is_empty() { "thread".to_string() } else { s };
format!("{s}-{}", &sha1_hex(name.as_bytes())[..6])
}
/// The tile token folds the tile file's content signature into the AUMID (Windows caches a
/// button's icon per AUMID, so only a virgin AUMID can change art). Same scheme as Node.
pub fn tile_token(ico_path: &Path) -> String {
match std::fs::metadata(ico_path) {
Ok(m) => { let mtime = m.modified().ok().and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()).map(|d| d.as_millis()).unwrap_or(0); format!("i{}", &sha1_hex(format!("{}-{}", mtime, m.len()).as_bytes())[..6]) }
Err(_) => String::new(),
}
}
pub fn thread_aumid(name: &str, token: &str) -> String { format!("{APP_ID}.thread.{}.{TILE_GEN}{token}", thread_slug(name)) }
pub fn session_aumid(session_id: &str) -> String { format!("{APP_ID}.w.{}", session_id.chars().map(|c| if c.is_ascii_alphanumeric() { c } else { '-' }).collect::<String>()) }
pub fn category_aumid(cat: &str) -> String { format!("{APP_ID}.cat.{cat}{ICON_GEN}") }
pub fn is_adom_url(u: &str) -> bool { u.contains("wiki.adom.inc") || u.contains("adom.inc/") }
/// wiki | app | web | None (browser-internal pages)
pub fn classify_url(u: &str) -> Option<&'static str> {
if u.is_empty() { return None; }
if is_adom_url(u) { return Some("wiki"); }
let lower = u.to_ascii_lowercase();
if lower.starts_with("about:") || lower.starts_with("chrome:") || lower.starts_with("edge:") || lower.starts_with("devtools:") || lower.starts_with("file:") { return None; }
let host = lower.split("//").nth(1).unwrap_or("").split('/').next().unwrap_or("").split(':').next().unwrap_or("");
if host == "localhost" || host == "127.0.0.1" || host == "[::1]" || host.ends_with(".adom.cloud") { return Some("app"); }
Some("web")
}
pub fn category_icon(cat: Option<&str>, wiki_authed: bool) -> &'static str {
match cat { Some("wiki") => if wiki_authed { "pup-cat-wiki-in9.ico" } else { "pup-cat-wiki9.ico" }, Some("app") => "pup-cat-app9.ico", Some("web") => "pup-cat-web9.ico", Some("mixed") => "pup-cat-mixed9.ico", _ => "pup-cat-default9.ico" }
}
// ── art, rendered by the browser (Node: renderThreadTileIco / dualBadgeB64 / applyAppOverlay) ──
/// The thread-tile canvas script (verbatim port of the Node build's, v2.0.172 sizing rules):
/// resolves to a PNG data URL of a 256x256 teal tile with the name bottom-left.
/// The thread-tile canvas script (verbatim port of the Node build's, v2.0.172 sizing rules):
/// resolves to a PNG data URL of a 256x256 teal tile with the name bottom-left.
pub const TILE_SIZES: [u32; 8] = [16, 20, 24, 32, 40, 48, 64, 256];
/// The tile is rendered ONCE at this size (8x the largest DIB entry, per the brand pipeline) and every
/// ICO entry is produced from it by `brand_downscale`.
/// The thread-tile canvas script: Node v2.0.172 sizing rules on the Adom rounded plate (corner radius =
/// size / 5.5, the pup icon family's own spec), TWO lines at most (three read too small, John
/// 2026-09-06), names with separators wrapped on whole words when that fits at the floor, else Node's
/// character split; never an ellipsis (every character on a tile is identity). Resolves to base64 of the
/// A multi-entry ICO: small sizes as 32-bit DIBs (BGRA bottom-up + an all-opaque AND mask, Node
/// v2.0.160), the 256 entry as PNG. `raw` = (size, RGBA bytes), `png256` = the PNG bytes.
pub fn ico_from_tiles(raw: &[(u32, Vec<u8>)], png256: &[u8]) -> Vec<u8> {
let mut entries: Vec<(u32, Vec<u8>)> = Vec::new();
for (s, rgba) in raw {
let s = *s; let n = (s * s) as usize;
if rgba.len() < n * 4 { continue; }
let mut d = Vec::with_capacity(40 + n * 4 + (((s + 31) / 32) * 4 * s) as usize);
d.extend_from_slice(&40u32.to_le_bytes()); d.extend_from_slice(&(s as i32).to_le_bytes()); d.extend_from_slice(&((s * 2) as i32).to_le_bytes());
d.extend_from_slice(&1u16.to_le_bytes()); d.extend_from_slice(&32u16.to_le_bytes()); d.extend_from_slice(&[0u8; 24]);
for y in 0..s { let sy = s - 1 - y; for x in 0..s { let i = ((sy * s + x) * 4) as usize; d.extend_from_slice(&[rgba[i + 2], rgba[i + 1], rgba[i], rgba[i + 3]]); } }
let and_row = ((s + 31) / 32) * 4; d.extend(std::iter::repeat(0u8).take((and_row * s) as usize));
entries.push((s, d));
}
entries.push((256, png256.to_vec()));
let n = entries.len() as u16;
let mut out = Vec::new();
out.extend_from_slice(&[0, 0, 1, 0]); out.extend_from_slice(&n.to_le_bytes());
let mut offset = 6 + 16 * entries.len() as u32;
for (s, d) in &entries {
out.push(if *s >= 256 { 0 } else { *s as u8 }); out.push(if *s >= 256 { 0 } else { *s as u8 });
out.extend_from_slice(&[0, 0, 1, 0, 32, 0]);
out.extend_from_slice(&(d.len() as u32).to_le_bytes()); out.extend_from_slice(&offset.to_le_bytes());
offset += d.len() as u32;
}
for (_, d) in &entries { out.extend_from_slice(d); }
out
}
// ── the brand raster pipeline, dependency-free (gallia/brand/logos/SKILL.md, the May 2026 shootout) ──
// resvg at 8x -> PIL Lanczos -> ImageMagick unsharp 0.5x0.5+1.5+0.02 -> alpha < 25 clamped to 0.
// Here the 8x render is the canvas; the three raster steps are reproduced below so the bridge needs no
// image library (John: "embed all that into your rust code so you have no dependencies").
fn lanczos3(x: f64) -> f64 { if x == 0.0 { return 1.0; } let a = 3.0; if x.abs() >= a { return 0.0; } let px = std::f64::consts::PI * x; (a * px.sin() * (px / a).sin()) / (px * px) }
/// PIL's LANCZOS: separable, support 3, the kernel widened by the scale factor when shrinking.
pub fn lanczos_resize(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
fn weights(src_len: u32, dst_len: u32) -> Vec<(usize, Vec<f64>)> {
let scale = src_len as f64 / dst_len as f64; let support = 3.0 * scale.max(1.0); let ss = 1.0 / scale.max(1.0);
(0..dst_len).map(|d| {
let center = (d as f64 + 0.5) * scale; let lo = ((center - support).floor().max(0.0)) as usize; let hi = ((center + support).ceil().min(src_len as f64)) as usize;
let mut w: Vec<f64> = (lo..hi).map(|s| lanczos3((s as f64 + 0.5 - center) * ss)).collect(); let sum: f64 = w.iter().sum(); if sum != 0.0 { for v in w.iter_mut() { *v /= sum; } }
(lo, w)
}).collect()
}
let wx = weights(sw, dw); let wy = weights(sh, dh);
// horizontal pass -> f64 rows of width dw
let mut tmp = vec![0f64; (dw * sh * 4) as usize];
for y in 0..sh as usize { for (dx, (lo, w)) in wx.iter().enumerate() { for ch in 0..4 { let mut acc = 0.0; for (k, wv) in w.iter().enumerate() { acc += *wv * src[(y * sw as usize + lo + k) * 4 + ch] as f64; } tmp[(y * dw as usize + dx) * 4 + ch] = acc; } } }
let mut out = vec![0u8; (dw * dh * 4) as usize];
for (dy, (lo, w)) in wy.iter().enumerate() { for x in 0..dw as usize { for ch in 0..4 { let mut acc = 0.0; for (k, wv) in w.iter().enumerate() { acc += *wv * tmp[((lo + k) * dw as usize + x) * 4 + ch]; } out[(dy * dw as usize + x) * 4 + ch] = acc.round().clamp(0.0, 255.0) as u8; } } }
out
}
/// ImageMagick `-unsharp 0.5x0.5+1.5+0.02` on the colour channels: gaussian blur (sigma 0.5, radius 0.5,
/// which IM widens to a 3x3 kernel), then pixel + 1.5 * (pixel - blur) wherever |pixel - blur| exceeds
/// 2% of full scale.
pub fn unsharp_im(px: &mut [u8], w: u32, h: u32) {
let (w, h) = (w as usize, h as usize);
let sigma = 0.5f64; let g = |d: f64| (-(d * d) / (2.0 * sigma * sigma)).exp();
let k = [g(1.0), g(0.0), g(1.0)]; let ksum: f64 = k.iter().sum();
let src = px.to_vec();
let at = |x: isize, y: isize, ch: usize| -> f64 { let xx = x.clamp(0, w as isize - 1) as usize; let yy = y.clamp(0, h as isize - 1) as usize; src[(yy * w + xx) * 4 + ch] as f64 };
let threshold = 0.02 * 255.0; let gain = 1.5;
for y in 0..h { for x in 0..w { for ch in 0..3 {
let mut blur = 0.0; for (j, ky) in k.iter().enumerate() { for (i, kx) in k.iter().enumerate() { blur += ky * kx * at(x as isize + i as isize - 1, y as isize + j as isize - 1, ch); } }
blur /= ksum * ksum; let orig = src[(y * w + x) * 4 + ch] as f64; let diff = orig - blur;
if diff.abs() > threshold { px[(y * w + x) * 4 + ch] = (orig + gain * diff).round().clamp(0.0, 255.0) as u8; }
} } }
}
/// The alpha clamp: alpha below 25 becomes fully transparent (kills the dark fringe on rounded corners).
pub fn alpha_clamp(px: &mut [u8]) { for p in px.chunks_mut(4) { if p[3] < 25 { p[0] = 0; p[1] = 0; p[2] = 0; p[3] = 0; } } }
/// One ICO entry's pixels from the 8x render: Lanczos, unsharp, alpha clamp.
pub fn brand_downscale(src: &[u8], sw: u32, size: u32) -> Vec<u8> { let mut px = lanczos_resize(src, sw, sw, size, size); unsharp_im(&mut px, size, size); alpha_clamp(&mut px); px }
/// Minimal PNG encoder (RGBA, stored deflate blocks) for the 256 entry: no image library needed.
pub fn png_encode_rgba(px: &[u8], w: u32, h: u32) -> Vec<u8> {
fn crc32(data: &[u8]) -> u32 { let mut c = 0xFFFF_FFFFu32; for &b in data { c ^= b as u32; for _ in 0..8 { c = if c & 1 != 0 { 0xEDB8_8320 ^ (c >> 1) } else { c >> 1 }; } } !c }
fn chunk(out: &mut Vec<u8>, kind: &[u8], data: &[u8]) { out.extend_from_slice(&(data.len() as u32).to_be_bytes()); let mut cd = kind.to_vec(); cd.extend_from_slice(data); out.extend_from_slice(&cd); out.extend_from_slice(&crc32(&cd).to_be_bytes()); }
let mut raw = Vec::with_capacity((w * h * 4 + h) as usize); for y in 0..h as usize { raw.push(0); raw.extend_from_slice(&px[y * w as usize * 4..(y + 1) * w as usize * 4]); }
// zlib stream of stored (uncompressed) deflate blocks
let mut z = vec![0x78, 0x01]; let mut i = 0; while i < raw.len() { let n = (raw.len() - i).min(65535); let last = i + n >= raw.len(); z.push(if last { 1 } else { 0 }); z.extend_from_slice(&(n as u16).to_le_bytes()); z.extend_from_slice(&(!(n as u16)).to_le_bytes()); z.extend_from_slice(&raw[i..i + n]); i += n; }
let (mut a, mut b) = (1u32, 0u32); for &byte in &raw { a = (a + byte as u32) % 65521; b = (b + a) % 65521; } z.extend_from_slice(&((b << 16) | a).to_be_bytes());
let mut out = b"\x89PNG\r\n\x1a\n".to_vec();
let mut ihdr = Vec::new(); ihdr.extend_from_slice(&w.to_be_bytes()); ihdr.extend_from_slice(&h.to_be_bytes()); ihdr.extend_from_slice(&[8, 6, 0, 0, 0]);
chunk(&mut out, b"IHDR", &ihdr); chunk(&mut out, b"IDAT", &z); chunk(&mut out, b"IEND", &[]); out
}
/// The whole tile ICO from the 8x render: DIB entries for every small size, PNG for 256.
pub fn ico_from_render(rgba: &[u8], render: u32) -> Vec<u8> {
let mut raw = Vec::new();
for sz in TILE_SIZES { if sz >= 256 { continue; } raw.push((sz, brand_downscale(rgba, render, sz))); }
let big = brand_downscale(rgba, render, 256);
ico_from_tiles(&raw, &png_encode_rgba(&big, 256, 256))
}
/// A single-image ICO wrapping PNG bytes (Vista+ format; Windows AUMID registry icons accept it).
pub fn png_to_ico(png: &[u8], width: u32, height: u32) -> Vec<u8> {
let mut out = Vec::with_capacity(png.len() + 22);
out.extend_from_slice(&[0, 0, 1, 0, 1, 0]);
out.push(if width >= 256 { 0 } else { width as u8 }); out.push(if height >= 256 { 0 } else { height as u8 });
out.extend_from_slice(&[0, 0, 1, 0, 32, 0]);
out.extend_from_slice(&(png.len() as u32).to_le_bytes());
out.extend_from_slice(&22u32.to_le_bytes());
out.extend_from_slice(png);
out
}
pub fn png_size(png: &[u8]) -> Option<(u32, u32)> {
if png.len() < 24 || &png[..8] != b"\x89PNG\r\n\x1a\n" { return None; }
Some((u32::from_be_bytes([png[16], png[17], png[18], png[19]]), u32::from_be_bytes([png[20], png[21], png[22], png[23]])))
}
// ── the jump-list launcher (Node: ensurePupJumplistLauncher / pupTask / pupRelaunchCommand) ──
pub const LAUNCHER_VERSION: &str = "rs1";
pub fn launcher_script() -> String {
format!(r#"# Adom Pup jump-list callback (v{LAUNCHER_VERSION}) — pup talks to itself; the CLI wakes it if asleep.
param([Parameter(Mandatory=$true)][string]$Verb, [string]$B64='')
$ErrorActionPreference='Stop'
$json = if ($B64) {{ [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($B64)) }} else {{ '{{}}' }}
try {{ $a = $json | ConvertFrom-Json }} catch {{ $a = @{{}} }}
try {{ $a | Add-Member -NotePropertyName caller -NotePropertyValue (@{{aiThread='user-taskbar-menu'}}) -Force }} catch {{}}
$body = (@{{ command=$Verb; args=$a }} | ConvertTo-Json -Depth 12 -Compress)
$portFile = Join-Path $env:LOCALAPPDATA 'Adom Bridge\pup-bridge-port.json'
$ok=$false
try {{
$port = (Get-Content $portFile -Raw -ErrorAction Stop | ConvertFrom-Json).port
Invoke-RestMethod -Uri ("http://127.0.0.1:{{0}}/command" -f $port) -Method Post -ContentType 'application/json' -Headers @{{'x-adom-caller-thread'='user-taskbar-menu';'x-adom-caller-reason'='jump-list click'}} -Body $body -TimeoutSec 60 | Out-Null
$ok=$true
}} catch {{ $ok=$false }}
if (-not $ok) {{
$cli = Join-Path $env:LOCALAPPDATA 'Adom Bridge\adom-bridge.exe'
if (-not (Test-Path $cli)) {{ $cli = Join-Path $env:LOCALAPPDATA 'Adom Bridge\adom-bridge-cli.exe' }}
if (Test-Path $cli) {{ & $cli $Verb $json | Out-Null }}
}}
"#)
}
pub fn launcher_path(local_app_data: &Path) -> PathBuf { local_app_data.join("Adom Bridge").join("pup-jumplist-call.ps1") }
pub fn port_file_path(local_app_data: &Path) -> PathBuf { local_app_data.join("Adom Bridge").join("pup-bridge-port.json") }
/// `launch(tail, quote_for_relaunch)` -> (target, args): the host's hidden script runner (HostWindows::script_launch).
pub fn task(launch: &dyn Fn(&str, bool) -> (String, String), verb: &str, args: serde_json::Value, title: &str, description: &str, icon: Option<&Path>) -> serde_json::Value {
let b64 = base64_encode(args.to_string().as_bytes());
let (target, argv) = launch(&format!("{verb} {b64}"), false);
json!({ "title": title, "description": description, "iconPath": icon.map(|p| p.to_string_lossy().to_string()), "target": target, "args": argv })
}
pub fn relaunch_command(launch: &dyn Fn(&str, bool) -> (String, String), session_id: &str) -> String {
let b64 = base64_encode(json!({ "sessionId": session_id, "foregroundReason": "the user clicked this window's taskbar entry" }).to_string().as_bytes());
let (target, argv) = launch(&format!("pup_focus_window {b64}"), true);
format!("{target} {argv}")
}
// ── tiny hashing + base64 (no dependencies) ────────────────────────────────────────────
pub fn base64_encode(bytes: &[u8]) -> String {
const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity((bytes.len() + 2) / 3 * 4);
for chunk in bytes.chunks(3) {
let b = [chunk[0], *chunk.get(1).unwrap_or(&0), *chunk.get(2).unwrap_or(&0)];
let n = ((b[0] as u32) << 16) | ((b[1] as u32) << 8) | b[2] as u32;
out.push(T[(n >> 18) as usize & 63] as char); out.push(T[(n >> 12) as usize & 63] as char);
out.push(if chunk.len() > 1 { T[(n >> 6) as usize & 63] as char } else { '=' });
out.push(if chunk.len() > 2 { T[n as usize & 63] as char } else { '=' });
}
out
}
pub fn sha1_hex(data: &[u8]) -> String {
let mut h: [u32; 5] = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0];
let mut msg = data.to_vec(); let bit_len = (data.len() as u64) * 8;
msg.push(0x80); while msg.len() % 64 != 56 { msg.push(0); } msg.extend_from_slice(&bit_len.to_be_bytes());
for block in msg.chunks(64) {
let mut w = [0u32; 80];
for i in 0..16 { w[i] = u32::from_be_bytes([block[i * 4], block[i * 4 + 1], block[i * 4 + 2], block[i * 4 + 3]]); }
for i in 16..80 { w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]).rotate_left(1); }
let (mut a, mut b, mut c, mut d, mut e) = (h[0], h[1], h[2], h[3], h[4]);
for (i, wi) in w.iter().enumerate() {
let (f, k) = match i { 0..=19 => ((b & c) | (!b & d), 0x5A827999), 20..=39 => (b ^ c ^ d, 0x6ED9EBA1), 40..=59 => ((b & c) | (b & d) | (c & d), 0x8F1BBCDC), _ => (b ^ c ^ d, 0xCA62C1D6) };
let t = a.rotate_left(5).wrapping_add(f).wrapping_add(e).wrapping_add(k).wrapping_add(*wi);
e = d; d = c; c = b.rotate_left(30); b = a; a = t;
}
h[0] = h[0].wrapping_add(a); h[1] = h[1].wrapping_add(b); h[2] = h[2].wrapping_add(c); h[3] = h[3].wrapping_add(d); h[4] = h[4].wrapping_add(e);
}
h.iter().map(|x| format!("{x:08x}")).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test] fn sha1_known() { assert_eq!(sha1_hex(b"abc"), "a9993e364706816aba3e25717850c26c9cd0d89d"); assert_eq!(sha1_hex(b""), "da39a3ee5e6b4b0d3255bfef95601890afd80709"); }
#[test] fn b64() { assert_eq!(base64_encode(b"hello"), "aGVsbG8="); assert_eq!(base64_encode(b"hi"), "aGk="); assert_eq!(base64_encode(b""), ""); }
#[test] fn slugs_match_node() {
// Node: threadSlug('pup-bridge') = 'pup-bridge-' + sha1('pup-bridge')[..6]
assert_eq!(thread_slug("pup-bridge"), format!("pup-bridge-{}", &sha1_hex(b"pup-bridge")[..6]));
assert!(thread_slug("My Thread / x").starts_with("my-thread-x-"));
assert_eq!(thread_aumid("t", "iabc123"), format!("Adom.Pup.thread.t-{}.{}iabc123", &sha1_hex(b"t")[..6], TILE_GEN));
}
#[test] fn classify() { assert_eq!(classify_url("https://wiki.adom.inc/adom/x"), Some("wiki")); assert_eq!(classify_url("http://localhost:3000/"), Some("app")); assert_eq!(classify_url("https://x.adom.cloud/proxy/1/"), Some("app")); assert_eq!(classify_url("https://example.com"), Some("web")); assert_eq!(classify_url("about:blank"), None); }
#[test] fn ico_wraps_png() { let png = [0x89, b'P', b'N', b'G', b'\r', b'\n', 0x1a, b'\n', 0,0,0,13, b'I',b'H',b'D',b'R', 0,0,1,0, 0,0,1,0, 8,6,0,0,0]; assert_eq!(png_size(&png), Some((256, 256))); let ico = png_to_ico(&png, 256, 256); assert_eq!(&ico[..4], &[0,0,1,0]); assert_eq!(ico.len(), png.len() + 22); }
}