find your extension ID at chrome://extensions (Developer mode), then:
September 1, 2026 · View on GitHub
_____________ _______ _____ _ _ _____ ____ __ __ ______
|___ / __ \ \ / / __ \ / ____| | | | __ \ / __ \| \/ | ____|
/ /| |__) \ \ /\ / /| |__) | | | |__| | |__) | | | | \ / | |__
/ / | ___/ \ \/ \/ / | _ /| | | __ | _ /| | | | |\/| | __|
/ /__| | \ /\ / | | \ \| |____| | | | | \ \| |__| | | | | |____
/_____|_| \/ \/ |_| \_\\_____|_| |_|_| \_\\____/|_| |_|______|
[THE BROWSER POWER-TOOL — PASS · DOWNLOADS · TABS · HISTORY · USERSCRIPTS]
"UNIX
passin the browser. Segmented download manager that owns the default. JetBrains-style tab switcher. fzf history. Tampermonkey-equivalent userscripts.""One extension, 55 commands, zero compromises."
[CYBERPUNK HUD]
A Chrome MV3 extension that bundles every daily-driver browser tool into one toolbar icon: a browserpass-compatible UNIX pass integration (fill / copy / OTP / auto-submit / basic-auth injection / full-page CRUD manager / profile + credit-card autofill), a segmented multi-connection download manager that intercepts every browser GET download by default (non-GET downloads such as SharePoint folder-zip POSTs stay with Chrome, since the request body cannot be replayed; HEAD probe + parallel Range GETs via a vendored Rust host), a JetBrains-style tab switcher with cross-window MRU + named scenes + opener-tree + minimap, an fzf-fuzzy search over up to 5000 browser-history entries, a Tampermonkey-equivalent userscript engine, a full-page screenshot capture that scrolls the active tab and stitches the tiles into one PNG, a Wappalyzer-compatible technology detector that fingerprints the active page against a vendored 3,993-tech corpus, a cyberpunk page-theme injector that paints arbitrary pages with the strykelang HUD palette, a Turn Off the Lights cinema dimmer that lifts <video> above a dark overlay, an auto-detected JSON viewer + a sibling XML viewer (covers application/xml, +xml vendor types, SVG, RSS, Atom, plist, KML, GPX, …), a User-Agent switcher backed by chrome.declarativeNetRequest dynamic rules, a ModHeader-style request/response header + URL redirect manager (multi-profile, also via chrome.declarativeNetRequest), and a find-in-all-tabs full-text search. 51 commands bindable to keyboard shortcuts. Built by MenkeTechnologies, Manifest V3, zero JS runtime dependencies.
Live Site · Source · Theme
Table of Contents
- [0x00] Overview
- [0x01] Install
- [0x02] Keyboard Commands
- [0x03] Popup UI
- [0x04] Tab Switcher
- [0x05] Companion Theme
- [0x06] Architecture
- [0x07] Capability Surface
- [0x08] Files
- [0x09] Tests
- [0x0A] CI
- [0x0B] Regenerating Docs
- [0xFF] License
[0x00] OVERVIEW
zpwrchrome is a Chrome MV3 extension that collapses a browser power-user's toolchain into one toolbar icon. The headline capabilities: UNIX pass integration (with browser-side GPG unlock, full-page CRUD manager, and profile / credit-card autofill), a segmented download manager that takes over Chrome's default, a JetBrains-style tab switcher with fzf history search, a Tampermonkey-equivalent userscript engine, full-page screenshot capture, and a Wappalyzer-compatible technology detector — the rest are enumerated in Capability Surface. 55 keyboard commands, a cyberpunk HUD popup, and a matching browser theme. Highlights:
- MRU stack — cross-window most-recently-used tracking via
chrome.storage.session, survives service-worker restarts - Alt+T popup — the cyberpunk HUD with 12 categories (All / Current Window / Pinned / Audible / Muted / Recently Closed / Scenes / Tree / Minimap / History / Pass / Tech), Cmd+1–0 jumps for the first ten, Cmd+P → Pass, Cmd+K → Tech, fzf scoring on every row
- Cmd+E / Ctrl+E MRU switch —
switch-previous-tabjumps straight to the previously active tab, across windows, with no intermediate UI. The handler re-pushes the real active tab and drops ids that no longer resolve, so a service-worker suspend that swallowedtabs.onActivated/onRemovedcan't leave the shortcut pointing at a dead tab. (The JetBrains-style shadow-DOM overlay that used to own this key is retired — see Tab Switcher) - Cmd+Y / Ctrl+Y history — replaces Chrome’s built-in chrome://history page with an fzf-fuzzy search over up to 5000 entries, Backspace deletes the highlighted URL from history
- UNIX
passintegration — replaces browserpass via a vendored Rust native-messaging host that walks~/.password-storewith eTLD+1 + multi-label PSL matching, shells topass show/pass otp, returns credentials over a length-prefixed JSON port. PASS popup category with fill / user / pw / otp buttons. Hotkeys:pass-fillautofills the active tab via injectedHTMLInputElement.valuesetter (React/Vue safe) + input/change dispatch;pass-copy-{pw,user,otp}write to clipboard with 45 s auto-clear matchingpass -c - Unlock the store from the browser — the GPG passphrase is entered in zpwrchrome, not in a terminal. Every decrypt the host performs runs
gpg --batch, which forbids gpg from prompting, and a browser-spawned host has no controlling TTY for a pinentry to draw on — so until now the store had to be primed by runningpass showin a shell first. Thepass.unlockaction takes the passphrase, decrypts one probe entry with--pinentry-mode loopback --passphrase-fd 0(passphrase on stdin, never in argv, never on disk), and gpg hands it to gpg-agent, which caches it for the agent'sdefault-cache-ttl— every later fetch / otp / save /pass showthen succeeds through the unmodified code path. Surfaces:🔓 unlock storein the popup's PASS category, and🔓 unlock/🔒 lockin the pass manager's toolbar. Both surfaces carry a live lock-state chip (unlocked/locked/state unknown) fed bypass.status, which resolves the store's recipients from.gpg-idto their encryption keygrips and asks gpg-agent (keyinfo --list) which of them it currently holds — a store whose keys or agent cannot be resolved readsstate unknownrather than claiming a state it did not verify. Any op that fails on a locked store auto-opens the prompt and retries itself once unlocked; the unlock is performed against the entry that tripped the lock, so the retry can't hit a second locked key.pass.lockflushes the agent cache (gpg-connect-agent reloadagent) to re-lock on demand. The extension never stores the passphrase — it is read off the input at submit, forwarded, and cleared; it is excluded from the diagnostic ring, and the host wipes its copy with volatile writes. Loopback pinentry is allowed by default (man gpg-agent: "Allow is the default"), so no gpg-agent configuration is normally needed; if an agent has been started with--no-allow-loopback-pinentry, the error names the exactgpg-agent.confline to add - Full-page pass manager —
scripts-manager/pass.html(toolbar right-click → "Open pass manager" or popup →pass ▸). Two-pane CRUD on~/.password-store: store tree (left, filter + ↑↓/Enter) + entry editor (right) with show/hide password, password generator, copy buttons per row, OTP-code copy via host, fill-active-tab, k/v list for non-synonym fields, free-form notes, delete with confirm scrim.⚙ rawtoggle drops to a verbatim file-bytes textarea — escape hatch for entries with non-standard schemas. URL row auto-derives from the first path segment (adobe.com/jmenke@wccnet.edu→adobe.com) when no expliciturl:key is present - Profile + credit-card autofill from
pass— two new commands:pass-fill-profilefills name / address / email / phone / etc. on the active tab fromprofile/<name>entries;pass-fill-ccfills card-number / exp / csc / cardholder fromcreditcard/<name>entries. Entry keys can be either the WHATWG HTML autocomplete tokens (given-name,street-address,postal-code,cc-exp-month, …) or friendly synonyms (first-name,address,city,state,zipcode,cvv, …) — both resolve to the same field. Field recognition:<input autocomplete=…>wins outright (composite forms likeshipping street-addresssupported), then an ordered regex ruleset (FIELD_RULE_SOURCES, ported from the Firefox HeuristicsRegExp + Chromium autofill patterns — the de-facto spec for real-world field-name spellings) matched across name+id+label+placeholder, then<input type="email|tel">. Names are canonicalized first (camelCase split, then every non-alphanumeric run — spaces,_,.,[,]— folded to-), so nested framework names resolve too:address.line1,billing[postcode],checkout[shipping_address][zip],billing_address_1,addressLine1all match. Rule order is precedence (specific beats general): cc-exp-month/year before cc-exp, the leaf address tokens (city/state/zip/country) before the catch-all street-address so a nested ZIP field is never swallowed as an address. Collision guards ride Chrome's lookbehind/lookahead: state ≠ "united states",\bcity\b≠ "capacity"/"electricity", county → state,unit(?!e), email-address → email. Alias chains backfill:cc-exp← month/year,name← given+family, given/family ← split ofname,street-address← line1+line2+line3. Widget-aware fill drives more than plain<input>s: native<select>via tolerant option-matching (value or visible text), custom dropdowns / comboboxes (react-select, MUI, Ant, Downshift, Headless UI, Radix, react-aria — anything exposingrole="combobox"/aria-haspopup="listbox"+role="option") by opening, type-to-filter, polling for the option (handles async-loaded menus like geocoded city fields) and clicking it, andintl-tel-inputphone fields (a full+E.164number auto-selects the country).country: US/state: CAresolve to the dropdown's display name ("United States" / "California") via embedded ISO-3166 + USPS + CA-province tables (lib/geo-data.js).autocomplete="off"no longer suppresses recognition (custom widgets set it on their inner inputs). The field scan (andpass-fill's login scan) pierces open shadow roots — web-component design systems (SmartRecruiters spl-input, Salesforce LWC, Vaadin, …) render the real<input>inside a shadow root wheredocument.querySelectorAllnever looks; the injectors recursively descend every open root (labels resolve viagetRootNode()too), so those forms fill like plain HTML. Card number / CVC / expiry rendered in a processor's cross-origin iframe (Stripe Elements, Braintree, Adyen, Square, Checkout.com, Recurly, Spreedly, Shopify) are physically unfillable by any extension — those are detected and reported so you know to enter the card manually rather than left silently blank. Multi-entry stores get an in-tab shadow-DOM picker (filter input, last-used cached per host). A third commandpass-fill-identitydoes everything in one keystroke — login (username/password) + profile + credit-card. It pre-scans the page and only invokes a picker for categories actually present; login uses the exact same path as the standalonepass-fill— host-matching apassentry with the same eTLD+1 matcher and the sharedfillLoginForm()injector, so a 2-step username-first login (step 1 with no visible password field) fills just as it does underpass-fill. Gated on host-match alone (no password-field requirement);pickLoginEntryreturns nothing when the host has no stored login, so non-login pages are untouched. Auto-submit is forced off so it never abandons the address/card fields it just wrote. Exampleprofile/personal.gpgbody — first line is a free-form label, the rest are friendly key:value pairs:
personal
given-name: Jane
family-name: Doe
email: jane.doe@example.com
phone: +15551234
address: 123 Main St
city: Springfield
state: IL
zipcode: 62701
country: US
And creditcard/visa.gpg:
visa
cc-name: Jane Doe
cc-number: 4111 1111 1111 1111
cc-exp-month: 09
cc-exp-year: 2031
cvv: 123
- Segmented download accelerator — same Rust host vendors a multi-connection download accelerator (IDM / aria2 / axel class): a HEAD probe sizes the file, then it is split into N byte-range segments fetched over N concurrent
Rangeconnections (default 6, pre-allocated dest file) to pull more throughput than Chrome's single-stream download. Segmented mode engages only when the server advertisesAccept-Rangesand the file clears a minimum size; otherwise it falls back to a single stream. Cookie + User-Agent forwarded fromchrome.cookies.getAllso logged-in downloads work; transient errors retry with 200 ms × 3ⁿ backoff and resume viaRangefrom the segment-local offset. A truncated response — a CDN closing the connection beforeContent-Lengthbytes arrive — is detected (premature EOF on an incomplete segment is treated as resumable, not success) and a final byte-count gate refuses to stamp a jobdonewhen fewer thanContent-Lengthbytes landed on disk, so a partial file never reports as complete; forward progress on a resume doesn't count against the retry budget so a repeatedly-truncating server still finishes. Per-rowrestartre-downloads from byte zero (discards the partial/old file, respawns a fresh worker — distinct fromresume, which continues from the current offset). When a server answers HEAD without aContent-Length(streamed downloads behind X-Accel-Redirect / X-Sendfile), the total is recovered from theRangeprobe'sContent-Rangeand a previously-known size survives a sizeless re-probe, so the UI shows a real total instead of?. Queue mirrored tochrome.storage.localso the UI paints instantly across service-worker restarts. Right-clickDownload with zpwrchromeon links / images / video / audio;dl-paste-urlreads the clipboard via injectednavigator.clipboard.readText. Live queue UI atscripts-manager/downloads.htmlsubscribes to host push events. Filename collisions auto-renamefoo.zip→foo (1).zip. Pure-Rust, vendorable TLS (ureq+rustls), noaria2or other runtime binary - Full-page screenshot —
screenshot-full-pagecommand (or right-click toolbar icon → "Full-page screenshot (this tab)") captures the active tab edge-to-edge, including parts off-screen. Strategy: scroll the page in viewport-sized steps with a 200 px overlap, capture each viewport viachrome.tabs.captureVisibleTab(Chrome's hard ~2 Hz quota → 600 ms gap + exponential-backoff retry: 1.1 s → 2.5 s → 5 s), pin everyposition: fixed/stickyelement tostaticduring capture so stickies don't appear N times, stitch tiles on anOffscreenCanvasin the SW, stream the PNG to the host in 512 KiB base64 chunks viadl.writeFileChunk(Chrome's host → ext native-messaging cap is 1 MiB), then rename the upload.partfile to the chosen filename in your downloads dir. Hard caps: 60 tiles, ~16k × 16k output pixels. Nochrome.debuggerpermission required (so no permanent yellow "DevTools attached" banner) - Wappalyzer-compatible technology detection —
lib/wappalyzer/engine.jsruns the vendored 3,993-fingerprint HTTPArchive/wappalyzer corpus (lib/wappalyzer/data/technologies.json, GPL-3 isolated underLICENSE-WAPPALYZER; engine code stays MIT). On every main_frame navigation:webRequest.onCompletedcaptures response headers per tabId;webNavigation.onCompletedinjectsscrapeSignalsto harvest HTML / scripts / meta / cookies / window globals + pre-flights all 1,045 unique dom-selector rules in one pass;detect()runs the merged signals against the compiled corpus, implementing every matcher group (html / scripts / scriptSrc / text / url / meta / headers / cookies / js / dom — exists, text, attributes, properties) + implies/requires/excludes graph rewrites +\\;version:\\1backref resolution. The match count shares one toolbar badge with downloads + pass viaapplyMultiplexedBadge(see below); Cmd+K from the popup jumps to the 12thTechcategory;⤓ Exportships the detected stack as JSON (filenametech-<host>-<iso>.json).scripts/vendor-wappalyzer.shre-runs the corpus merge from a fresh upstream clone - ModHeader-style HTTP header manager — multi-profile request/response header editor + URL redirect manager. Each profile owns N rules; only the active profile's enabled rules are projected into
chrome.declarativeNetRequestdynamic rules (id range 2000-2999, sibling to the UA switcher's 1001). Rule kinds: request header (set / append / remove), response header (set / append / remove), URL redirect (urlFilter-scoped). Each rule carries its ownurlFilterso a single profile can have per-site overrides. Settings UI atscripts-manager/modheader.html— sidebar profile list, click-to-activate, inline rule editor with name / value / op / filter fields; storage underchrome.storage.local["modheader.state"]. Survives SW suspension (rules re-applied on boot) - Three-channel toolbar badge — single Chrome action badge multiplexes downloads (cyan), tech detection (orange), and pass matches (magenta). The visible NUMBER is the dominant counter by priority (downloads → tech → pass) and the COLOR follows it. Trailing letter tags spell out which other counters are coexisting:
t= tech also detected,l= login (pass) also matching. So a tab with 10 active downloads + 5 tech + 2 pass renders10tl; 5 tech + 2 pass renders5l; 2 pass alone renders2. Tooltip spells out the plain-English breakdown for any state.refreshActiveTabBadgeis the one orchestrator that repaints onchrome.tabs.onActivated+chrome.tabs.onUpdated - 51 user-bindable commands — Chrome caps default-suggested at 4; everything else binds at
chrome://extensions/shortcuts(single-tab ops, batch ops, numeric jumps, clipboard utilities, pass-* + dl-*) - Sub-popup live filter — type to filter open + closed tabs;
↑/↓/Enter/Delete/Escnav - Reader mode —
modal/reader-mode.jsstrips the active page to its main article and renders it in a fixed-position overlay with the strykelang HUD palette. Heuristic extraction (largest<article>→<main>/[role="main"]→ densest paragraph cluster outside noise containers); cloned + sanitized DOM (scripts, iframes, nav, forms, inline event handlers stripped); A−/A+ font controls + Esc to close. Settings UI atscripts-manager/reader-mode.html— four themes (cyberpunk / classic-dark / classic-light / sepia), three font families (mono / serif / sans), font-size / line-width / line-height sliders, optional CRT scanlines. Original page DOM untouched - Post-download commands — per-rule glob → argv-style command, fired by the SW the moment a download flips
active → done. Rules are matched top-to-bottom first-wins against the finished file's basename (*.zip,*.tar.gz, case-insensitive). Argv is parsed shlex-style on the JS side and shipped as an array to the host'srun.spawnaction —std::process::Command::new(argv[0]).args(argv[1..])with no shell invocation anywhere on the path, so{path}substitution can't introduce a quoting or injection surface. Per-ruleconfirmflag pops a Chrome notification with Run / Skip buttons (pending argv survives SW suspends viachrome.storage.session). Placeholders:{path},{dir},{name},{base},{ext}. Output stdout/stderr are captured (64 KiB cap each), exit code is reported, and a 30s default timeout (max 5 min) kills runaways with code 124. Pipes / redirects / && require wrapping inbash -c '…'explicitly. Settings UI atscripts-manager/dl-postcommands.html - Turn off the lights (cinema mode) —
modal/lights-off.jsinjects a full-viewport near-black overlay over the active tab and lifts every visible<video>element (with its entire ancestor chain) above the overlay viaz-index. Trigger via thelights-offcommand, the toolbar context menuTurn off the lights (this tab), or popup →lights ▸. Click the overlay or press Esc to undim. Settings UI atscripts-manager/lights-off.html(opacity 0–1, fade duration ms, overlay color, per-host blocklist/allowlist). Port of the Turn Off the Lights Chrome extension - Color schemes (8 + custom) — pick one of Cyberpunk / Midnight / Matrix / Ember / Arctic / Crimson / Toxic / Vapor from the theme page (
scripts-manager/theme-injector.html). The palettes are vendored from the app-shell source of truth inlib/color-schemes.js; the choice recolors zpwrchrome's OWN pages (popup + every dashboard) at runtime vialib/ui-scheme.js(overriding each page's CSS:rootdefault throughdocumentElement.style, persisted underchrome.storage.local["ui.scheme"], broadcast live overstorage.onChanged) AND sets the page-theme injector palette below. One pick, both surfaces. The picker also lists every custom scheme saved anywhere in the zwire fleet —background.jssubscribes to the hostschemestopic (the shared saved-scheme library in~/.zwire/global.toml, mirrored tochrome.storage.local["ui.schemes"]); picking one writes its resolvedui.paletteand pushes it back to the host so the whole fleet repaints in step. A custom-scheme editor (a<input type=color>swatch per base token; glow/dim/bg tints auto-derive viabuildCustomScheme) plus a saved-scheme library toolbar (name · Save · Update · Delete-all · per-chip delete) let you build/edit/name schemes right in zpwrchrome; every save rewrites the shared library through the host, so a scheme created here shows up on every zwire surface - Cyberpunk page-theme injector —
modal/cyber-theme.jsruns atdocument_starton every http(s) tab and paints arbitrary pages with the chosen scheme's palette (the strykelang HUD palette by default). Settings UI atscripts-manager/theme-injector.html(toolbar right-click → "Open theme injector" or popup →theme ▸). Knobs: intensity (subtle = links + headings + scrollbars only / medium = + body bg + form fields + code blocks / full = + tables, cards, dimmed images), dark mode (smart overlay —color-scheme: dark+ targeted overrides for common white-card patterns: AUI.a-box/.order-header/.delivery-box/.bia-content, generic[class*="card|panel|widget"], ARIA dialogs, inlinebackground: white|#fff|rgb(255,…); deliberately NOTfilter: invert()so already-dark pages stay dark), forceMono (Share Tech Mono everywhere — exempts icon-font carriers<i>/<svg>/[class*="icon|fa-|material-icons|material-symbols|lucide|phosphor|glyphicon"]/[data-icon|data-lucide|data-cds="Icon"|data-radix-icon]so Anthropicons, Material Symbols, Lucide, etc. keep their glyphs instead of rendering as PUA tofu), and scanlines (CRT overlay viabody::after). Per-host blocklist / allowlist via the textarea; settings broadcast to every tab overchrome.storage.onChanged - Save to zcite (web connector) — page right-click →
Save page to zcite (reference)extracts the active page's bibliographic metadata into CSL-JSON (lib/zcite-extract.js: Highwirecitation_*tags + Dublin Core + Open Graph + schema.org JSON-LD), then the native host'szcite.saveaction drops it into zcite's inbox (<data_dir>/zcite/inbox/) for zcite'sinbox.importto pull in. The Zotero-Connector role for the zcite reference manager; the handoff is a plain CSL-JSON file, so the MIT extension/host never link the proprietary zcite engine - Companion Chrome theme —
theme/paints frame/toolbar/omnibox/NTP with the strykelang HUD palette - Strykelang HUD aesthetic — palette and animations sourced from
strykelang/docs/hud-static.css(--cyan #05d9e8,--accent #ff2a6d,--magenta #d300c5, CRT scanlines, neon-border-glow card frames) - Pure-helper test surface — MRU stack semantics, hostname parsing, jump-index resolution in
lib/util.js+ pass match/parse + dl filename/collision helpers inzpwrchrome-host/src/{ported,extensions}/all unit-tested without a Chrome runtime - Single source of truth —
README.md,docs/index.html, and command counts are all generated frommanifest.jsonbyscripts/gen.sh; CI guards against drift - Zero JS runtime dependencies — no bundler, no transpiler, no npm modules at runtime; pure ES module service worker. The native host adds
serde/serde_json/ureq(foundational pure-Rust crates) and ships as a single static binary
[0x01] INSTALL
git clone https://github.com/MenkeTechnologies/zpwrchrome.git
Extension
- Open
chrome://extensions - Enable Developer mode (top-right)
- Click Load unpacked, pick the cloned directory
- Open
chrome://extensions/shortcutsto bind any of the 51 user-configurable commands
Native messaging host (required for pass, downloads, screenshots)
- Install GPG +
passif you want the password-store integration (brew install passon macOS,apt install passon Debian/Ubuntu); make surepass showdecrypts an entry from your shell first. (Skip if you only want downloads + screenshots.) - Install the host binary from crates.io and register it for this extension's ID:
cargo install zpwrchrome-host
# find your extension ID at chrome://extensions (Developer mode), then:
zpwrchrome-host --install <ext-id>
The installer writes com.menketechnologies.zpwrchrome.json into every detected Chromium-family browser config dir — Chrome, Chromium, Brave and Edge on macOS + Linux, each written only when that browser's config root already exists, so no manifest is littered for a browser you don't have. It also registers into the zwire profile's own NativeMessagingHosts/ (zwire is a Chromium fork run against its own --user-data-dir, and registration happens before its first launch, so that directory is created unconditionally; $ZWIRE_STATE overrides the location). allowed_origins is populated with chrome-extension://<ext-id>/ so the browser will only spawn the host for this extension. Reload the extension at chrome://extensions after running it.
To upgrade later: cargo install zpwrchrome-host --force — the NM manifest already points at $CARGO_HOME/bin/zpwrchrome-host so no re-install is needed.
Theme
- Load unpacked the
theme/subdirectory (separate Chrome extension — themes cannot be bundled with action extensions) chrome://settings/appearance→ Reset to default to remove
[0x02] KEYBOARD COMMANDS
Chrome’s MV3 manifest allows at most 4 commands with default-suggested keys; the rest are bound by the user at chrome://extensions/shortcuts. zpwrchrome ships 4 default-keyed and 51 user-bindable, for 55 total — covering pass actions, download manager, tab switcher, history search, and userscript management.
| Command | Default | Description |
|---|---|---|
_execute_action | Alt+T | Open zpwrchrome popup |
switch-previous-tab | Ctrl+E | Switch to the previously active tab (MRU) |
restore-last-closed | (user-set in chrome://extensions/shortcuts) | Restore the most recently closed tab |
recent-modal | (user-set in chrome://extensions/shortcuts) | Open the recent-tabs popup (same as Alt+T — user-bindable) |
open-history | Ctrl+Y | Open the popup focused on the History category (fzf-search browsing history) |
search-tabs | (user-set in chrome://extensions/shortcuts) | Open popup focused on the tab search box |
mru-next | (user-set in chrome://extensions/shortcuts) | Cycle forward through MRU stack |
mru-prev | (user-set in chrome://extensions/shortcuts) | Cycle backward through MRU stack |
jump-to-1 | (user-set in chrome://extensions/shortcuts) | Jump to tab #1 in current window |
jump-to-2 | (user-set in chrome://extensions/shortcuts) | Jump to tab #2 in current window |
jump-to-3 | (user-set in chrome://extensions/shortcuts) | Jump to tab #3 in current window |
jump-to-4 | (user-set in chrome://extensions/shortcuts) | Jump to tab #4 in current window |
jump-to-5 | (user-set in chrome://extensions/shortcuts) | Jump to tab #5 in current window |
jump-to-6 | (user-set in chrome://extensions/shortcuts) | Jump to tab #6 in current window |
jump-to-7 | (user-set in chrome://extensions/shortcuts) | Jump to tab #7 in current window |
jump-to-8 | (user-set in chrome://extensions/shortcuts) | Jump to tab #8 in current window |
jump-to-9 | (user-set in chrome://extensions/shortcuts) | Jump to last tab in current window |
duplicate-tab | (user-set in chrome://extensions/shortcuts) | Duplicate the active tab |
pin-tab | (user-set in chrome://extensions/shortcuts) | Toggle pin on the active tab |
mute-tab | (user-set in chrome://extensions/shortcuts) | Toggle mute on the active tab |
move-to-new-window | (user-set in chrome://extensions/shortcuts) | Detach active tab to a new window |
close-others | (user-set in chrome://extensions/shortcuts) | Close all other tabs in current window |
close-right | (user-set in chrome://extensions/shortcuts) | Close all tabs to the right of the active tab |
close-duplicates | (user-set in chrome://extensions/shortcuts) | Close tabs with duplicate URLs (keeps leftmost) |
reload-all | (user-set in chrome://extensions/shortcuts) | Reload every tab in current window |
sort-by-url | (user-set in chrome://extensions/shortcuts) | Sort tabs in current window by URL |
group-by-domain | (user-set in chrome://extensions/shortcuts) | Group tabs in current window by domain (Chrome tab groups) |
copy-url | (user-set in chrome://extensions/shortcuts) | Copy active tab URL to clipboard |
copy-title-md | (user-set in chrome://extensions/shortcuts) | Copy active tab as Markdown link |
bookmark-tab | (user-set in chrome://extensions/shortcuts) | Bookmark active tab to Other Bookmarks |
open-dashboard | (user-set in chrome://extensions/shortcuts) | Open the zpwrchrome dashboard — a searchable tile grid of every tool, settings page and info screen |
manage-scripts | (user-set in chrome://extensions/shortcuts) | Open the userscript manager (Tampermonkey-style) |
save-scene-prompt | (user-set in chrome://extensions/shortcuts) | Open popup focused on save-scene input |
restore-scene-1 | (user-set in chrome://extensions/shortcuts) | Restore scene #1 (newest) — opens a new window with saved tabs |
restore-scene-2 | (user-set in chrome://extensions/shortcuts) | Restore scene #2 |
restore-scene-3 | (user-set in chrome://extensions/shortcuts) | Restore scene #3 |
restore-scene-4 | (user-set in chrome://extensions/shortcuts) | Restore scene #4 |
restore-scene-5 | (user-set in chrome://extensions/shortcuts) | Restore scene #5 |
pass-open-popup | (user-set in chrome://extensions/shortcuts) | Open popup focused on the PASS category (matches credentials for the active tab from ~/.password-store via the zpwrchrome native host) |
pass-fill | Ctrl+Shift+L | Autofill the best-matching pass credential into the active tab's login form (requires the native host) — customize at chrome://extensions/shortcuts |
pass-copy-pw | (user-set in chrome://extensions/shortcuts) | Copy the best-matching pass password for the active tab to the clipboard (auto-clears after 45 s) |
pass-copy-user | (user-set in chrome://extensions/shortcuts) | Copy the best-matching pass username for the active tab to the clipboard |
pass-copy-otp | (user-set in chrome://extensions/shortcuts) | Copy the TOTP code for the best-matching pass entry to the clipboard |
pass-open-url | (user-set in chrome://extensions/shortcuts) | Navigate the active tab to the URL stored in the best-matching pass entry (parses url/link/website/web/site keys) |
pass-fill-identity | (user-set in chrome://extensions/shortcuts) | Fill whatever identity fields the active tab has — login (username/password), profile (name/address/email/phone/…) AND credit-card (cc-number/cc-exp/cc-csc/cardholder/…) in one keystroke. Picks the best-matching profile/* and creditcard/* entries plus a host-matched login entry (picker if multiple, last-used cached per host). |
pass-fill-profile | (user-set in chrome://extensions/shortcuts) | Fill profile fields only (name, address, email, phone, …) on the active tab from a profile/* entry in pass. Quick-pick overlay when multiple profiles exist. |
pass-fill-cc | (user-set in chrome://extensions/shortcuts) | Fill credit-card fields only (cc-number, cc-exp, cc-csc, cardholder, …) on the active tab from a creditcard/* entry in pass. Quick-pick overlay when multiple cards exist. |
find-in-all-tabs | (user-set in chrome://extensions/shortcuts) | Full-text search across every open tab — opens a search UI, scrapes innerText from every http(s) tab in parallel, fuzzy-filters as you type, Enter activates the chosen tab and scrolls to the match (no DevTools required) |
lights-off | (user-set in chrome://extensions/shortcuts) | Turn off the lights — dim the entire active page with a near-black overlay while lifting any video elements above it (cinema-mode for YouTube and beyond). Click the overlay or press Esc to undim |
reader-mode | (user-set in chrome://extensions/shortcuts) | Reader mode — strip the active page to its main article and render it in a strykelang HUD overlay with adjustable typography. Click × in the top bar or press Esc to close |
screenshot-full-page | (user-set in chrome://extensions/shortcuts) | Full-page screenshot — scrolls the active tab in viewport-sized steps and stitches into one PNG (no extra permissions required; customize at chrome://extensions/shortcuts) |
dl-paste-url | (user-set in chrome://extensions/shortcuts) | Download the URL currently on the clipboard via the zpwrchrome segmented downloader |
dl-show-queue | (user-set in chrome://extensions/shortcuts) | Open the zpwrchrome download manager queue view |
dl-pause-all | (user-set in chrome://extensions/shortcuts) | Pause every active download in the zpwrchrome download manager |
dl-resume-all | (user-set in chrome://extensions/shortcuts) | Resume every paused download in the zpwrchrome download manager |
[0x03] POPUP UI
The popup (popup.html / popup.css / `popup.js$) \text{is} \text{a} 520 \times 600 \text{cyberpunk} \text{HUD} \text{with} \text{two} \text{stacked} \text{lists}:
- \text{Open} // \text{MRU} — \text{every} \text{open} \text{tab}, \text{in} \text{most}-\text{recently}-\text{used} \text{order}
- \text{Recently} \text{Closed} — \text{last} 25 \text{closed} \text{tabs}/\text{windows} \text{via} $chrome.sessions`
Keyboard nav inside the popup:
| Key | Action |
|---|---|
| any character | live-filter by title / URL / hostname |
↑ / ↓ | move selection |
Enter | switch to open tab, or restore closed tab |
Delete / Shift+Backspace | close highlighted open tab |
Esc | clear filter, or close popup |
Click any row to activate it. Hover reveals a × icon to close.
[0x04] TAB SWITCHER
Switching is split between one no-UI keystroke and the popup. Cmd+E / Ctrl+E is bound to switch-previous-tab: it activates the previously used tab directly, across windows, and focuses that tab's window. Everything with a list attached goes through the popup.
| Key | Command | Action |
|---|---|---|
Cmd+E / Ctrl+E | switch-previous-tab | switch to the previously active tab (MRU step, no UI) |
Alt+T | _execute_action | open the popup |
Cmd+Y / Ctrl+Y | open-history | open the popup on the History category |
| (user-set) | mru-next / mru-prev | step forward / backward through the MRU stack |
| (user-set) | recent-modal / search-tabs | open the popup (same surface as Alt+T) |
Inside the popup: Cmd+1…Cmd+0 jump to the first ten categories (Cmd+0 = History), Cmd+P → Pass, Cmd+K → Tech, ↑/↓ move the selection, Enter activates or restores it, Delete closes the highlighted tab (or deletes the highlighted history URL), Esc clears a non-empty filter and otherwise closes the popup, and any letter live-filters by title / URL / hostname.
MRU durability: the switch-previous-tab handler re-pushes the genuinely-active tab before reading the stack, then walks down it skipping ids that no longer resolve and dropping them as it goes. A service-worker suspend that swallowed tabs.onActivated / onRemoved therefore can't leave the shortcut aimed at a stale id.
The in-page modal is retired. JetBrains IDEs have a Recent Files modal (Cmd+E on Mac) and zpwrchrome originally shipped that UX as a full-page closed-shadow-root overlay in modal/content.js. It landed center-of-viewport while every other command anchors top-right at the toolbar icon, so recent-modal was rewired to chrome.action.openPopup() and Cmd+E was handed to the MRU primitive. The content script is still registered on <all_urls> and still opens on an open-modal message, but nothing sends one — tests/dispatch-popup.test.js and tests/protocol.test.js pin that the service worker must not.
[0x05] COMPANION THEME
The theme/ directory ships a separate Chrome theme. Same strykelang palette as the popup, applied to the browser frame, toolbar, omnibox, and new-tab page.
| Theme image | Resolution | Purpose |
|---|---|---|
| `theme_ntp_background.png$ | 3840 \times 2400 | \text{New}-\text{tab}-\text{page} \text{background} (4\text{K}-\text{ready}) — \text{grid} + \text{radial} \text{gradients} + \text{HUD} \text{corner} \text{brackets} |
| 1920 \times 120 | \text{Window}-\text{frame} \text{strip} — \text{gradient} + \text{cyan}→\text{accent} \text{seam} | |
| 1920 \times 80 | \text{Toolbar} \text{background} |
\text{Color} \text{anchors} (\text{RGB} \text{triplets} \text{in} $theme/manifest.json`):
| Slot | Hex | RGB | Strykelang variable |
|---|---|---|---|
frame / ntp_background | #05050a | [5, 5, 10] | --bg-primary |
toolbar / omnibox_background | #0a0a14 | [10, 10, 20] | --bg-secondary |
bookmark_text / ntp_link | #05d9e8 | [5, 217, 232] | --cyan |
ntp_header | #ff2a6d | [255, 42, 109] | --accent |
tab_text / ntp_text | #e0f0ff | [224, 240, 255] | --text |
[0x06] ARCHITECTURE
chrome.tabs ┌──────────────────────────┐ chrome.storage
onActivated ──────▶│ background.js (sw) │◀──── .session (MRU)
onRemoved │ ────────────────────── │ .local (scenes,
onReplaced │ pushMru / dropFromMru │ userscripts,
│ command dispatcher │ dl.snapshot)
chrome.commands ──▶│ message API │
│ nmCall / nmPort │
│ contextMenus / cookies │
└────────────┬─────────────┘
│
┌────────────────┼─────────────────┐
│ │ │
│ runtime. │ connectNative │ scripting.
│ sendMessage │ (NM port) │ executeScript
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ popup.{js,css, │ │ zpwr-chrome-host │ │ active tab: │
│ html} │ │ (Rust binary) │ │ • pass-fill │
│ ──────────────── │ │ ──────────────── │ │ injector │
│ MRU + 10 tab cat │ │ frame.rs (LE32+ │ │ (native value │
│ + PASS category │ │ JSON, ≤1 MiB) │ │ setter, R/V/L │
│ Cmd+1–0 jumps │ │ proto.rs (id/ │ │ safe) │
│ fzf scoring │ │ kind/op/args) │ │ • dl-paste-url: │
└──────────────────┘ │ dispatch.rs │ │ clipboard read │
│ → pass: list/ │ │ • writeClipboard │
┌──────────────────┐ │ match/fetch/ │ │ (copy hotkeys) │
│ scripts-manager/ │ │ otp │ └──────────────────┘
│ downloads.{html, │ │ → dl: add/ │
│ css,js} │◀│ list/pause/ │ ┌────────────┐
│ ──────────────── │ │ resume/ │ │ ureq+rustls│
│ live queue UI │ │ cancel │─────▶│ HEAD + N× │
│ subs to push │ │ push events │ │ Range GET │
│ events (id=0) │ │ id=0 / 200ms │ │ +retry/200 │
│ rehydrates from │ └──────────────────┘ │ ×3ⁿ backoff│
│ dl.snapshot │ │ └────────────┘
└──────────────────┘ │ │
│ shells to │ writes
▼ ▼
┌─────────────────────┐ ┌──────────────────┐
│ pass / gpg / pinentry│ │ ~/Downloads/ │
│ ~/.password-store/ │ │ zpwrchrome/ │
└─────────────────────┘ │ (pre-allocated │
│ N-segment file) │
└──────────────────┘
The service worker holds no globals — MRU lives in chrome.storage.session. Pure helpers in lib/util.js (JS) and zpwrchrome-host/src/{ported,extensions}/ (Rust) carry no Chrome / Process references and are unit-tested in plain Node / cargo test. The native host is a 1:1 Rust port of browserpass-native v3.1.2: zpwrchrome-host/src/ported/** mirrors the upstream Go source file-for-file (per-fn // go:NN citations, Go comments carried over verbatim — see zpwrchrome-host/docs/port_report.html), while src/extensions/** layers on seven Rust-only tool families upstream never had. One binary serves both; each request is one process spawn (BP process model), with download workers detaching to keep state under $XDG_CACHE_HOME/zpwrchrome/dl/.
Additive-action multiplexing over the browserpass wire
Every request is one native-messaging frame on stdin. src/bin/zpwrchrome_host.rs reads it once via frame::read_msg, then parses it twice: first as a raw serde_json::Value to read the "action" string, and — only for upstream actions — again into the ported request struct, so the strict-port struct never grows extension fields. Dispatch is additive-first: the extension action names are matched before control ever reaches the ported switch. Because browserpass-extension never emits any additive name, a stock browserpass client talking to this binary gets byte-identical upstream behavior.
dl.*(prefix) →extensions::dl::dispatch_dlotp/search→extensions::otp/extensions::searchrun.spawn→extensions::run_commandhost.crawl/host.exec→extensions::hostpass.unlock/pass.lock/pass.status→extensions::gpg_unlockzcite.save→extensions::zcite- anything else →
process_dispatch— the byte-for-byte mirror of upstreamrequest/process.go:configure/list/tree/fetch/save/delete/echo
The seven additive tool families (src/extensions/)
| Module | Wire action(s) | What it does |
|---|---|---|
dl | dl.add / list / pause / resume / cancel / remove / clear, dl.openDir, dl.openFile, dl.writeFile, dl.writeFileChunk | Segmented multi-connection download manager (HEAD probe → N concurrent Range GETs → pre-allocated dest file); also the chunked file sink the full-page screenshot streams PNG tiles into |
otp | otp | Shells pass otp <path> for the current TOTP code — browserpass v3 dropped OTP, so it is re-added host-side |
search | search | Whole-store fuzzy search over every configured store's .gpg paths (substring outranks subsequence), so large stores don't round-trip every path per keystroke |
host | host.crawl / host.exec | Filesystem crawl + program exec, delegated to zwire_host::api (see below) |
gpg_unlock | pass.unlock / pass.lock / pass.status | Enter the GPG passphrase from the browser instead of a terminal: decrypts one probe entry with --pinentry-mode loopback --passphrase-fd 0, which primes gpg-agent for its cache TTL so every later --batch decrypt works. pass.lock flushes the cache via gpg-connect-agent reloadagent; pass.status resolves the store's recipients from .gpg-id to encryption keygrips and asks the agent (keyinfo --list) which of them are cached, so the UI can show a lock state instead of discovering it by failing an op. Both locate gpg-connect-agent beside the resolved gpg (and through its symlink target) rather than on $PATH, which a browser-spawned host inherits empty of GnuPG |
run_command | run.spawn | Post-download argv execution via std::process::Command (no shell); stdout/stderr capped 64 KiB each, 30 s default / 5 min max timeout |
zcite | zcite.save | Writes extracted CSL-JSON into zcite's inbox, path resolved with dirs::data_dir() to match zcite-core exactly |
Identity / credit-card autofill (profile/*, creditcard/*) is not a separate wire action — it rides the ported fetch, then lib/identity-tokens.js + the page-injected fillIdentityForm() map the decrypted key:value body onto WHATWG autocomplete tokens client-side. The combined pass-fill-identity additionally reuses the login path: it host-matches an entry and injects the shared fillLoginForm() (username/password) on any host-match — the same gate as pass-fill, so 2-step username-first logins fill too — giving one keystroke over login + address + card.
Transport
src/frame.rs is the whole wire: a little-endian u32 length prefix, then that many JSON bytes, capped at MAX_MSG = 1 MiB (Chrome's host↔extension ceiling). read_msg / write_msg are the only I/O primitives; every handler emits its envelope through ported::response (SendOk / SendErrorAndExit / SendRaw). The download family's HTTP is ureq 2.10 with default-features = false + the tls (rustls) and native-certs features — pure-Rust TLS, no OpenSSL, no OS TLS, so it cross-compiles to macOS / Linux / future arches without a C dependency. serde / serde_json / dirs round out the crate.
zwire-host reuse — one native agent, two front-ends
host.crawl / host.exec do not re-implement a recursive walk or a capture-stdout exec: they call zwire_host::api::walk and zwire_host::api::exec, the same native capability library the zwire browser agent runs. zpwrchrome-host/Cargo.toml pins it from crates.io (zwire-host = { version = "0.3", default-features = false }) so only the light filesystem/exec half compiles in — the heavy portable-pty / sysinfo deps stay out of this tree. The zwire browser and this NM host share the exact crawl/exec code path.
[0x07] CAPABILITY SURFACE
Each row names a capability and what it replaces / supersedes in the typical browser power-user stack.
| Capability | Replaces / supersedes | Implementation |
|---|---|---|
UNIX pass integration (fill / copy / OTP / open URL / basic-auth injection) | browserpass-extension | client-side eTLD+1 + multi-label PSL match, server-side via the zpwrchrome-host Rust crate (PROTOCOL.md v3.1.2 compatible — drop-in for the Go binary) |
Full-page pass manager (CRUD on ~/.password-store) | upstream browserpass-extension's options page · the standalone pass TUI · 1Password / Bitwarden vault UIs (for the GPG-backed flow) | scripts-manager/pass.{html,css,js} — store tree (left) + form editor (right) talking to the BP list / fetch / save / delete actions over NM. Versioned alongside the extension; no separate install |
GPG passphrase entry from the browser (pass.unlock / pass.lock) + a live lock-state readout (pass.status) | browserpass (no unlock path — the store must be primed from a terminal) · GUI pinentries (pinentry-mac, pinentry-gnome3), which need a desktop session the NM host may not have | The host decrypts a probe entry with --pinentry-mode loopback --passphrase-fd 0, priming gpg-agent for its cache TTL so the unmodified --batch decrypt path works afterwards. Passphrase on stdin only — never argv, never disk, never chrome.storage, excluded from the diag ring, wiped with volatile writes host-side |
Profile + credit-card autofill from pass (profile/* + creditcard/* entries) | Chrome's built-in autofill profiles · 1Password / Bitwarden card filler | lib/identity-tokens.js + page-injected fillIdentityForm(). Entry keys use WHATWG HTML autocomplete tokens directly — the store IS the schema. Recognition via an ordered regex ruleset ported from the Firefox + Chromium autofill heuristics, over camelCase/dotted/bracketed-flattened names (nested framework fields like billing[postcode] / address.line1 resolve), with lookbehind collision guards; alias chains backfill missing tokens; React/Vue-safe native value-setter pattern across all frames; widget-aware fill drives native <select>, custom comboboxes (react-select / MUI / Ant / Radix / react-aria via role=option clicking) and intl-tel-input phones; field + option scans pierce open shadow roots (web-component forms: SmartRecruiters spl-input, LWC, Vaadin); ISO country / US-state / CA-province code→name resolution (lib/geo-data.js); cross-origin hosted-card fields (Stripe / Braintree / Adyen / …) detected + reported as unfillable; in-tab shadow-DOM picker with last-used cache per host |
| Segmented download accelerator (multi-connection, default-handler takeover) | Chrome's built-in single-stream download UI · IDM / FDM / DAP · aria2c · axel | A real download accelerator: HEAD probe → split the file into N byte ranges → fetch them over N concurrent Range connections to saturate bandwidth a single stream can't, then reassemble in a pre-allocated dest file. Cookies + User-Agent forwarded, retry with backoff, file-state worker model, full sidebar-nav queue page |
| JetBrains-style tab switcher (MRU + scenes + opener-tree + minimap) | Recent Tabs by Jason Savard · OneTab · Workona | cross-window MRU via chrome.storage.session, Alt+T popup with 12 categories, Cmd+E one-keystroke MRU switch, fzf scoring on every row, batch tab ops + clipboard utilities |
| Wappalyzer-compatible technology detection | Wappalyzer · BuiltWith · Stack Inspector | lib/wappalyzer/engine.js runs the vendored 3,993-fingerprint HTTPArchive/wappalyzer corpus (lib/wappalyzer/data/technologies.json, GPL-3 isolated under LICENSE-WAPPALYZER). Every matcher type implemented: html / scripts / scriptSrc / text / url / meta / headers / cookies / js / dom (exists + text + attributes + properties). Implies / requires / excludes graph rewrites. Cmd+K from the popup, ⤓ Export to JSON, three-channel toolbar badge with letter tags (10tl = 10 downloads + tech + login matches) |
| fzf history search | Chrome's chrome://history page · the omnibox | re-ranks chrome.history.search results by frecency, up to 5000 entries, Backspace deletes inline |
| Tampermonkey-equivalent userscript engine | Tampermonkey · Greasemonkey · Violentmonkey | @metadata block parser, @match pattern compilation, full GM_* shim (getValue/setValue/openInTab/setClipboard/notification), fire-log ring buffer |
| Full-page screenshot (off-screen content included, no debugger banner) | GoFullPage · FireShot · Awesome Screenshot | lib/screenshot.js — scroll + viewport-capture + OffscreenCanvas stitch in the SW. Sticky/fixed elements pinned to position: static during capture. PNG streamed to the host via chunked dl.writeFileChunk (Chrome's 1 MiB host → ext NM cap), lands in your configured downloads dir |
Counts & invariants
| Total chrome.commands | 55 (manifest cap on default keys is 4 — this ext ships 4; the other 51 are user-bindable at chrome://extensions/shortcuts) |
| Manifest | MV3 |
| License | MIT |
| Test suite | 3084 node:test cases (JS) + 134 cargo test cases (Rust) |
| Generator + doc-drift CI | Yes — README + landing page regenerated from manifest.json by scripts/gen.sh; CI fails on drift |
| Runtime deps | Zero on the JS side (pure ES-module SW). The Rust host adds serde / serde_json / ureq (foundational pure-Rust crates) and ships as a single static binary |
[0x08] FILES
| Path | Purpose |
|---|---|
manifest.json | MV3 manifest, command registry (the only source of truth) |
background.js | Service worker — MRU tracker, command dispatcher, popup message API, NM port (nmCall/nmAddEventListener), pass-fill injector, clipboard auto-clear, context-menu Download with zpwrchrome, enrichDownloadArgs (cookie + UA forwarding), dl.snapshot mirror, passFillIdentityActive profile/CC dispatcher, in-tab shadow-DOM picker, fillIdentityForm token-driven page injector |
lib/util.js | Pure helpers — mruPush/mruDrop/mruStep/mruPrevious/hostnameOf/resolveJumpIndex |
lib/bp-pass.js | Pure pass helpers — parseEntry / fallbackUsernameFromPath / fallbackUrlFromPath / matchIn / eTLD+1 candidates |
lib/pass-entry.js | Pure pass-entry serializer — formatEntry (inverse of parseEntry), validatePassPath, buildTree |
lib/identity-tokens.js | Profile + credit-card autofill — PROFILE_TOKENS / CC_TOKENS (WHATWG HTML autocomplete vocabulary), FIELD_RULE_SOURCES (ordered regex ruleset ported from Firefox + Chromium autofill heuristics; recognizeField / matchFieldRules / compileFieldRules over camelCase/dotted/bracketed-flattened names with lookbehind collision guards), TOKEN_SYNONYMS (entry-key → value expansion), expandFieldValue (alias chains: cc-exp ↔ month/year, name ↔ given/family, street-address ↔ line1/2/3) |
lib/geo-data.js | ISO-3166 country code→name + USPS US-state + CA-province tables, used by the combobox driver to turn a stored country: US / state: CA into the "United States" / "California" text a custom dropdown lists |
popup.html / popup.css / popup.js | Cyberpunk HUD popup with 12 categories including PASS (fill/user/pw/otp buttons + 🔓 unlock store for GPG passphrase entry) + TECH (Wappalyzer detection) + pass ▸ link to the full-page pass manager |
lib/wappalyzer/engine.js | Pure-JS Wappalyzer-compatible detection engine — pattern compilation, every signal-group matcher, \\;version:\\1 backref resolution, implies/requires/excludes graph rewrites, page-side scrapeSignals injection |
lib/wappalyzer/data/technologies.json + categories.json | Vendored 3,993-fingerprint upstream corpus (HTTPArchive/wappalyzer, GPL-3 — see LICENSE-WAPPALYZER adjacent). scripts/vendor-wappalyzer.sh re-runs the merge from a fresh upstream clone |
scripts-manager/pass.{html,css,js} | Full-page pass manager — store tree (left) + entry editor (right); CRUD via the BP list / fetch / save / delete actions; raw-bytes textarea toggle; 🔓 unlock / 🔒 lock toolbar buttons either side of a live lock-state chip fed by pass.status (GPG passphrase entry via pass.unlock, auto-prompted on any locked-store failure); URL row auto-derives from the first path segment when no url: key is present |
modal/content.js | JetBrains-style Recent Tabs modal — content script, shadow DOM, 2-column layout |
scripts-manager/manager.{html,css,js} | Userscript engine dashboard (Tampermonkey-equivalent) |
scripts-manager/downloads.{html,css,js} | Live download queue UI — push-event subscription + cached snapshot rehydration |
scripts-manager/theme-injector.{html,css,js} + lib/color-schemes.js + lib/ui-scheme.js + lib/cyber-theme-css.js + modal/cyber-theme.js | Color scheme + cyberpunk page-theme injector — pick one of 8 schemes (Cyberpunk / Midnight / Matrix / Ember / Arctic / Crimson / Toxic / Vapor, vendored from the app-shell palette in lib/color-schemes.js). The choice recolors zpwrchrome's own pages (popup + dashboards, applied at runtime by lib/ui-scheme.js, persisted under chrome.storage.local["ui.scheme"]) AND drives the page-theme injector palette. Page-theme knobs: color-scheme: dark + targeted overrides for white-card patterns + intensity / forceMono / scanlines; settings persisted under chrome.storage.local["theme.injector"] and broadcast to every tab via storage.onChanged |
scripts-manager/lights-off.{html,css,js} + lib/lights-off-css.js + modal/lights-off.js | Turn-off-the-lights cinema dimmer — full-viewport overlay + <video> lifted above via z-index: 2147483647. Click overlay or Esc to undim; per-host block/allowlist; settings under chrome.storage.local["lights.off"] |
scripts-manager/reader-mode.{html,css,js} + lib/reader-mode-css.js + modal/reader-mode.js | Reader mode — extract article via heuristic (<article> → <main> → densest paragraph cluster), sanitize (strip scripts/iframes/nav/forms/inline-handlers), render in a fixed overlay with the strykelang palette. Four themes × three font families; A−/A+ live bumpers in the top bar; settings under chrome.storage.local["reader.mode"] |
scripts-manager/dl-postcommands.{html,css,js} + lib/dl-postcommands.js + zpwrchrome-host/src/extensions/run_command.rs | Post-download custom commands — basename-glob → argv (first-match-wins); per-rule confirm notification with Run / Skip buttons survives SW suspends via chrome.storage.session. Host run.spawn action spawns via std::process::Command (no shell), captures stdout/stderr capped at 64 KiB each, 30s default / 5min max timeout (kill → code 124). Placeholders: {path} {dir} {name} {base} {ext}; settings under chrome.storage.local["dl.postCommands"] |
scripts-manager/ua-switcher.{html,css,js} + lib/ua-presets.js | User-Agent switcher — 16 vendor-shipped presets across 6 families plus a custom UA field. Backed by a single chrome.declarativeNetRequest dynamic rule (id 1001) that rewrites the User-Agent request header |
scripts-manager/modheader.{html,css,js} | ModHeader-style HTTP header manager — multi-profile, per-rule set / append / remove on request OR response headers plus URL-filter-scoped redirects. Backed by chrome.declarativeNetRequest dynamic rules in id range 2000-2999 (UA switcher owns 1001). Only the active profile's enabled rules project into DNR; storage under chrome.storage.local["modheader.state"] |
scripts-manager/find-all.{html,css,js} + lib/find-snippet.js | Find-in-all-tabs — fzf-fuzzy search across every open tab's innerText (parallel scrape capped at 200 KB / tab). Enter activates the chosen tab and scrolls to the match via window.find() |
modal/json-viewer.js + lib/json-format.js | Auto-detects JSON-served pages and replaces <pre> with a collapsible tree (RFC 6901 pointer copy, prettyPrint / minify toggles, clipboard with execCommand fallback for non-secure contexts) |
modal/xml-viewer.js + lib/xml-format.js | Auto-detects XML/SVG/RSS/Atom/plist/KML/GPX served pages and replaces <pre> with a DOMParser-driven collapsible tree. Attribute coloring, CDATA / comment / PI rendering, XPath copy per node, live filter, prettyPrint / minify / raw toggles, http(s) auto-linkify in text + attribute values |
zpwrchrome-host/Cargo.toml / zpwrchrome-host/src/{lib,frame}.rs + src/ported/** + src/extensions/** + src/bin/zpwrchrome_host.rs | Rust port of browserpass-native v3.1.2 + seven additive extension modules over length-prefixed JSON on stdio: dl (dl.* segmented downloads), otp, search, host (host.crawl / host.exec via zwire_host::api, the zwire agent's capability crate, pulled from crates.io), run_command (run.spawn), gpg_unlock (pass.unlock / pass.lock — GPG passphrase entry from the browser), zcite (zcite.save). Additive actions dispatched before the ported upstream switch, so the browserpass wire stays byte-compatible. Strict 1:1 port discipline (per-fn citations, Go comment carry-over) — see zpwrchrome-host/docs/port_report.html |
zpwrchrome-host --install <ext-id> (CLI flag on the binary, not a separate script) | Writes com.menketechnologies.zpwrchrome.json into every detected Chromium-family browser config dir on macOS / Linux. allowed_origins is set to chrome-extension://<ext-id>/ so the browser will only spawn the host for this extension |
lib/zpc-palette.js + lib/palette-cmds.js + lib/cmd-defaults.js + lib/zgui/{util,fzf,command-palette}.js | zwire command palette (⌘K) on every dashboard page — the SAME palette as the HUD (hud-internal/zpalette.js) and the New Tab (newtab/palette.js), sharing one item source (palette-cmds.js, vendored identically into all three) so the surfaces can't drift: zpwrchrome's own tool pages, chrome:// destinations + settings, keyword web-search, shared custom commands (host/shell/stryke bridged to hud-internal), inline compute, history + open-tab rows. Opened only by hud-internal's ⌘K router (chrome.commands → cross-ext relay → this extension's SW → the focused page); no local ⌘K, so standalone (outside zwire) it stays inert. The HUD + New Tab palettes list zpwrchrome's pages too, gated on a zwirePing so they drop when zpwrchrome is disabled |
scripts-manager/host.{html,css,js} | Host console — interactive REPL to the zpwrchrome-host native binary over one-shot chrome.runtime.sendNativeMessage. Grouped catalog of the full {action:…} surface (browserpass configure / list / tree / fetch / save / delete / echo + otp / search / pass.lock / pass.status / host.crawl / host.exec / run.spawn / zcite.save / dl.* — pass.unlock is deliberately absent, since every request typed here lands in the exportable transcript and that one carries a passphrase); click-to-load JSON templates, editable editor (⌘/Ctrl-Enter to send), collapsible JSON-tree responses, transcript filter + Export. STATUS pane echo-probes the host for protocol version + round-trip latency; COMMAND LOG tails the service-worker diag ring (diag.read) so every native round-trip from any surface is visible. Adapted from the zwire HUD host console (hud-internal/pages/host.js) to this extension's browserpass-model host |
zpwrchrome-host/tests/ported_*.rs + extensions_*.rs | cargo test suite — per-fn pins for the port + extensions, end-to-end binary spawn tests, segmented download against a local HTTP fixture |
docs/index.html | GitHub-Pages landing page (regenerated from manifest) |
docs/report.html | Strykelang-style engineering report (regenerated from repo stats) |
theme/ | Companion Chrome theme — separate unpacked extension |
icons/icon.svg + icon{16,32,48,128}.png | Extension icons; PNGs rasterized via rsvg-convert |
scripts/gen.sh + scripts/gen.mjs | Regenerate README.md and docs/index.html from manifest.json |
tests/ | node:test suite — pure logic + static invariants + theme + protocol + pass / dl integration |
.github/workflows/ci.yml | GitHub Actions — npm test (Node 20 + 22) + cargo test --locked for the host crate, on push/PR |
package.json | npm test script |
[0x09] TESTS
npm test
Stock Node ≥ 20, no external dependencies. 3084 tests across 192 files. Covers:
- Pure logic (
tests/logic*.test.js,tests/util-*.test.js) — MRU stack semantics (prepend, dedup, cap, wrap, no-mutate, large-|delta| double-mod), hostname parse, jump-index resolution, scene CRUD, opener-tree forest (iterative flatten — handles 50k-deep chains without stack overflow), domain hue distribution, frecency formula - fzf scoring (
tests/fzf*.test.js) — match algorithm correctness, scoring constants (BOUNDARY ≥ NON_WORD ≥ CAMEL > CONSECUTIVE > 0), highlight integration (indices spell needle case-insensitively, HTML escape preserved inside marks), ranking stability over realistic filter passes - Userscript parser (
tests/userscript*.test.js,tests/parseMetadata-*.test.js,tests/matchPatternToRegex-*.test.js) — Tampermonkey/Greasemonkey metadata block parsing, match-pattern compilation per Chrome's spec (file/ftp/* scheme handling), validate→register→matchUrl pipeline roundtrip - GM_/GM. shim (
tests/gm-shim*.test.js,tests/gm-background.test.js) — every GM_* alias and gm:* message wiring against the background.js dispatcher - Fuzz (
tests/fuzz-*.test.js) — deterministic-PRNG sweeps over fzfMatch, util helpers, parseMetadata; adversarial inputs (regex metachars, nested markers, 100k-char values, pathological *.host patterns); Monte Carlo scene CRUD against a Map+order-list oracle - Stress (
tests/stress-*.test.js) — time budgets for keystroke-hot paths (10k fzfMatch < 1s, 100k mruPush < 2s, 500-item filter pipeline < 200ms); scale (1M mruPush, 10k-deep tree, 50k buildScene churn, 10k-pattern matchUrl); pathological inputs (all-same-char haystacks, 50k-char fzf, 60k-char rejection in O(haystack)) - Static manifest invariants (
tests/static.test.js) — MV3, ≤4 suggested keys (Chrome ceiling), no macOS/Chrome-reserved defaults, no key collisions, kebab-case command names, every manifest command has abackground.jshandler and vice versa, every referenced file exists with correct PNG dimensions, popup HTML has no inline event handlers or inline<script>(MV3 CSP), strykelang palette intact in popup.css and docs/index.html, every declared permission is actually used in code, README + docs/index.html stay byte-identical after re-runningscripts/gen.sh - Theme invariants (
tests/theme.test.js) — MV3 +themeblock, noaction/background(Chrome rejects mixed manifests), all theme images are PNGs at declared dimensions, every color is a 0–255 integer triplet, strykelang palette anchors pinned, version conforms to Chrome's 1–4-part 0–65535 rule - Popup ↔ background protocol (
tests/protocol.test.js) — every messagekindsent bypopup.jsis handled bybackground.jsand vice versa, no orphans on either side - Build pipeline (
tests/build.test.js,tests/gen-pipeline.test.js) — UTIL_INLINE/FZF_INLINE markers present + balanced, build-modal.mjs stripsexportcorrectly, generated banner pinned, gen.sh counts tests dynamically
[0x0A] CI
.github/workflows/ci.yml runs npm test on every push and pull-request. Matrix: Node 20 + 22 on ubuntu-latest. The Node 22 leg also runs cargo test --locked against the zpwrchrome-host crate (the Rust download/pass host), so JS and Rust regressions both block merge. The doc-drift test (re-run scripts/gen.sh and compare) catches stale README / landing page in the same job.
on: [push, pull_request]
jobs:
test:
strategy:
matrix:
node: [20, 22]
CI badge at the top of this README.
[0x0B] REGENERATING DOCS
README.md and docs/index.html are derived from manifest.json. Refresh both with:
scripts/gen.sh
CI re-runs the same generator and fails the build if either file is not byte-identical to what gen.sh emits, so stale docs can never land on main.
[0xFF] LICENSE
MIT — see LICENSE.
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░ >>> TRACK MRU. SWITCH FAST. CYBERPUNK HUD. OWN YOUR BROWSER. <<< ░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░