npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

x11-protocol-visualizer

v0.1.0

Published

X11 MITM proxy with live protocol visualization — inspect, link and throttle X11 traffic

Readme

x11vis — X11 protocol visualizer

CI

A man-in-the-middle X11 proxy with a live protocol inspector. It sits between an X11 client and the X server, forwards the byte stream faithfully both ways, and renders every message (request, reply, event, error) as a color-coded, navigable table with per-field byte mapping.

Fittingly, its own UI is a native X11 application built with react-x11 + react-x11-components — the X11 visualizer is itself an X11 client.

x11vis inspecting a session: the message table on the left, the decoded
request on the right, and the selected field's bytes marked in the hex
dump

The window follows the desktop's colour scheme; the same session on a light desktop is docs/img/x11vis-light.png.

Above: a GetProperty is selected, so the right pane shows it as a call with its arguments, a link to the reply that answered it, and every decoded field. Picking the property field marks the four bytes that carry it — 17000000, atom 23, RESOURCE_MANAGER — in the hex. That picture is rendered by npm run screenshot, headlessly, by the real UI: see Screenshots.

See docs/PRD.md for the product spec and docs/decoder-and-state.md for the decoder/state design.

Status — M0–M4 built

Working and tested against live XQuartz:

  • Transparent TCP proxy on a configurable port → upstream $DISPLAY (Unix-socket or TCP target, incl. the XQuartz launchd socket). Byte-exact relay.

  • Per-direction framing: connection setup handshake, then requests / replies+events+errors. Handles byte order, BIG-REQUESTS extended length, GenericEvent (XGE), and KeymapNotify.

  • Sequence linking: replies/errors matched to their request (16→32-bit sequence widening), with round-trip time.

  • Request decoding for common requests (CreateWindow, InternAtom, QueryExtension, GetProperty, CreateGC, …) with per-field byte spans.

  • Reply decoding — a reply carries no opcode, so it is decoded using the opcode of the request it answers: GetGeometry, QueryTree, GetProperty (typed by its atom, incl. ATOM lists and text), GetWindowAttributes, GetInputFocus, QueryPointer, TranslateCoordinates, ListExtensions, ListProperties, ListFonts, AllocColor, QueryBestSize, GetImage, GetKeyboardMapping, and more.

  • Extension registry: learns extension major opcodes from QueryExtension replies and names subsequent extension traffic.

  • Network emulation (PRD FR-39…42): per-direction added latency + throughput cap with Chrome-style presets, changeable live from the UI or --network. Verified: the same 22-message xdpyinfo session takes ~5 ms unthrottled and ~2.9 s on Slow 3G, while remaining byte-exact.

  • Resource tracking (M2): resource-creating requests (CreateWindow, CreatePixmap, CreateGC, RENDER CreatePicture/GlyphSet, …) are recorded, and any later field that references that XID links back to its creator — jump-to-creator in the UI. Server-owned resources (each screen's root window and default colormap) are pre-seeded from the setup reply, so references to the root link to the handshake instead of dangling.

  • Image & glyph previews (PRD FR-37) — PutImage and GetImage payloads are decoded to real pixels and drawn in the detail pane, honouring the connection's image-byte-order, bitmap-bit-order, scanline padding, the depth's bits-per-pixel, and the visual's colour masks. RENDER AddGlyphs renders each glyph from its A1/A4/A8/ARGB32 coverage. Decoding is lazy (only when a message is selected) and size-capped, so a 4 MB PutImage costs nothing until you look at it. Previews also appear in creator tooltips.

  • Parameter decoding — semi-encoded values are made human-readable: value-masks expand to named bits (value-mask=0x800 (event-mask)), enums to names (class=InputOnly, line-style=OnOffDash), event-masks to their event set, pixel and RENDER color values preview as color swatches, and RENDER PICTFORMAT ids resolve to Direct depth24 R8G8B8 (harvested from QueryPictFormats). Applies to CreateWindow/ChangeWindowAttributes, CreateGC/ChangeGC, and RENDER CreatePicture/ChangePicture.

  • Core events — all of codes 2–34 decode to fields with spans and a summary built around what identifies the event: ButtonPress · button=1 @(312,208) event=0x04800001 state=Shift, ConfigureNotify · window=0x0480000a 300×200+10-20, PropertyNotify · window=0x04800001 _NET_WM_NAME NewValue. ClientMessage's data union is read per format, with the words its type says are atoms resolved by name (data=[WM_DELETE_WINDOW, …]); KeymapNotify's bitmap is listed as the keycodes held down; the request that caused a GraphicsExposure is named from its major opcode; and ConfigureRequest reports only the parameters its value-mask asks for.

  • Extensions (a data-driven registry — add one per file, see below):

    • RENDER: ~30 requests with resource links, color swatches, and a QueryVersion reply; RENDER errors.
    • XInputExtension (XInput 2): the full XI2 event set (Motion, Button, Key, Enter/Leave, Focus, Raw*, Touch*, Gesture*, Hierarchy, Property).
    • XFIXES, SHAPE, DAMAGE, RANDR, Composite, SYNC, Present, XTEST, XC-MISC, DPMS, MIT-SCREEN-SAVER, BIG-REQUESTS: request/event/error names, with field decoders for the resource-creating requests (CreateRegion, DAMAGE Create, NameWindowPixmap, …). Full per-field decode of the long tail is the xcbproto-codegen path noted in the PRD.
  • Filtering: mute whole categories (click the toolbar chips), mute an individual message type (right-click a row, or Filter → Hide type), solo one type, or narrow by a name/summary substring. Active filters show as removable chips.

  • react-x11 UI: menu bar (Capture / View / Filter), draggable split panes, network preset dropdown, filter box, virtualized Table packet list, Tree fields + Code call view, a synchronized hex view (select a field to highlight its bytes; double-click a link to jump to the creator), and a console pane for proxy diagnostics. Falls back to a headless console renderer with no display.

  • Capture save/load (.x11cap): record a session and reopen it offline. The file stores raw bytes and replays them through the current decoder, so an old capture gains every decoding improvement made since — and doubles as a regression corpus. --record out.x11cap / --open out.x11cap, or Capture → Save capture… (Ctrl+S) in the UI.

  • Statistics & hotspots (Statistics tab): traffic and byte totals, round trips, blocking round trips (the client waited), RTT mean/p50/max, and ranked request/event/extension breakdowns — plus hotspot analysis aimed at protocol efficiency: blocking stalls priced at the observed RTT, repeated identical queries (cacheable), and per-frame resource churn (create/destroy cycles that could reuse a buffer).

  • Resource lints (Statistics → Resources, and badges on the message): use-after-free, double-free, freeing an id never created, and resources still live at the end of the capture. Severity is deliberate: a running app legitimately holds windows and buffers open, so a live resource is info — only an accumulation of one type is raised to a suspected leak. XID recycling (free then re-create the same id) is understood and not mis-reported.

  • Find usages (FR-34): every message that creates, references or frees a resource id. Double-click a resource field, or Filter → Find usages, and the table filters to that resource's whole lifetime.

  • Cursor previews: CreateCursor composes its source and mask bitmaps — which live in pixmaps, not in the request — by resolving the PutImage messages that filled them, then colouring by the fore/back values.

  • Colormap-aware rendering: AllocColor / AllocNamedColor replies are paired with their request's colormap to build a palette, so indexed (PseudoColor) images render in real colour instead of as intensity.

  • Unix-socket listener (--unix N): also listen on /tmp/.X11-unix/XN so a client attaches with a plain DISPLAY=:N. Stale sockets are cleaned up.

  • Breakpoints & fault injection (--intercept): rules match on name, direction or category and break (hold the message — the client really blocks), drop, or delay it. Opt-in, because it switches forwarding from raw chunks to whole framed messages. Holding a message holds everything behind it in that direction — a breakpoint stops the client, it does not just delay one packet. Drive it from the Intercept menu (break/drop/delay the selected type, Step Ctrl+N, Continue Ctrl+G, toggle or remove rules), or seed rules from the CLI with --break <name> / --drop <name>, or build one in Break on… (Ctrl+B): a tree of the whole protocol catalog — every request, response, event and error — with per-parameter conditions (numbers, strings, resource ids, and an atom picker), or a Script tab for a JavaScript expression. Both forms evaluate against the same match context: kind, msg, request and atom(name). request is what lets a rule on a response reach the request that asked for it, e.g. kind === 'reply' && msg.name.includes('AddGlyphs') && request.f['num-glyphs'] > 100. A debugger toolbar shows Paused/Running with Step / Continue / Skip, the held message, and how many are queued behind it.

  • Headless statistics (--stats a.x11cap, --json): the Statistics tab's numbers without the UI — bytes each way, round trips, blocking round trips, RTT mean/p50/max, the request/event/extension tables and the hotspot list. --json emits the whole CaptureStats object (and works with --diff too), so a CI script can gate on a number the tool computed rather than reimplement it.

  • Diff two captures (--diff a.x11cap b.x11cap): what changed between two runs, compared over aggregates that survive differing XIDs and timing.

  • Generated protocol tables (npm run gen:protocol): names, enums, value-mask bits and field layouts generated from the xcbproto XML corpus (32 files → 652 requests, 102 events, 67 errors, 230 enums). Used as a fallback wherever a hand-written spec doesn't cover a message, which removes the ext139:req4 long tail entirely.

  • Variable-length tails: a request's trailing <list>s are decoded too — lengths that are a field (InternAtom's name), a literal, an arithmetic expression over earlier fields (ChangeProperty's data_len * format / 8), or unstated, which xcbproto means as "the rest of the message" (every drawing request). Text comes out as text, binary as [N bytes], struct arrays as [2 × RECTANGLE] — each with a byte span, so clicking the field highlights exactly its bytes in the hex.

Still partial: <switch> bitcases, <union>s, and lengths needing popcount or sumof — 62 of 652 requests, down from 156. Such a message decodes up to that point and says so rather than guessing past it.

UI layout

  • Menu barCapture (pause/resume Ctrl+P, clear log Ctrl+K, save Ctrl+S, quit), View (console, columns, jump to newest), Filter, and a Network preset picker in the toolbar.
  • Right pane tabsDetail (per-message decode) and Statistics (traffic, round trips, hotspots).
  • Intercept bar — appears only when rules exist or a message is held; the held state is prominent because the client is blocked while it shows.
  • Packet table — one row per message, color-coded by kind.
  • Detail pane (right of the drag handle) — pretty-printed call, field list, and hex; selecting a field highlights exactly the bytes it came from.
  • Console pane (below the table) — proxy diagnostics only: connections opening and closing, decode failures, network-profile changes. It is deliberately not a second copy of the packet table; hide it from View (hidden, the table fills the whole left column).
  • Filtering — toolbar chips mute categories; the filter box narrows by substring; right-click a row to hide that type; Filter menu has Hide (Ctrl+H), Solo, per-category toggles, Find usages, and Clear all. Active filters appear as removable chips under the toolbar.

Requirements

  • Node.js ≥ 22 (developed on 26).
  • For the UI: a running X server (Linux, or XQuartz on macOS). Without one, use --no-ui / --record.

Install

npm install

The proxy/decoder core has no runtime dependencies and always installs. The UI stack (react-x11@^2, @react-x11/components@^0.2) is in optionalDependencies, resolved from npm as ordinary semver ranges.

Both packages used to be tracked from their GitHub default branches, which cost this repo two workarounds — a legacy-peer-deps=true .npmrc to paper over a pre-release peer-version mismatch, and a manual tsc build because the components shipped no dist/. The published releases fixed both: components 0.2.0 declares peer react-x11@^2.0.0 against react-x11 2.0.0, and ships prebuilt. A plain npm install now resolves under strict peer checking, with a single deduped react-x11 copy (npm ls react-x11) — the one thing worth keeping true, since two reconciler instances would break rendering.

If the UI can't load, x11vis runs headless automatically.

Usage

Start the proxy (and UI, if a display is available):

npm start -- --port 6001

Then point a client at it over TCP (localhost:1 → TCP port 6001):

DISPLAY=127.0.0.1:1 xdpyinfo

The proxy forwards to your real $DISPLAY; the UI renders to that same real display, so its own drawing traffic is not self-captured.

On macOS the UI window is drawn by AppKit rather than by XQuartz — react-x11's native Cocoa backend, which also puts x11vis's menus on the system menu bar instead of in the window. That needs @windowkit/appkit, which npm installs for you on a mac; without it, auto falls back to X11 so an XQuartz setup keeps working. --ui-backend x11 forces the old behaviour, and --ui-backend cocoa makes the bridge a requirement rather than a preference:

npm start -- --ui-backend cocoa

The proxy itself is unaffected either way — it is an X11 proxy on every platform, and only the visualizer's own window changes.

Options

-p, --port <n>        TCP port to listen on (default 6001)
-d, --display <s>     Upstream X server DISPLAY (default: $DISPLAY)
    --record <file>   Record the session to <file> (.x11cap)
    --no-ui           Headless: log to console, don't open the UI
    --ui-display <s>  DISPLAY for the UI window (default: $DISPLAY); naming
                      one selects the X11 backend
    --ui-backend <b>  How the UI window is drawn: auto (default), x11, cocoa
-o, --open <file>     Open a saved .x11cap for offline inspection
-u, --unix <n>        Also listen on /tmp/.X11-unix/X<n> (DISPLAY=:<n>)
    --intercept       Enable breakpoints / fault injection
    --break <name>    Hold matching messages (implies --intercept)
    --drop <name>     Never forward matching messages
    --diff <a> <b>    Compare two captures and print what changed
-n, --network <id>    Network emulation preset (also changeable live in the UI):
                      none, local, lan, wifi, fast4g, slow4g, fast3g, slow3g,
                      ssh, transatlantic
-q, --quiet           Suppress per-message console output
-h, --help

Feel how chatty a client is over a WAN link — same traffic, ~600× the wall clock:

npm start -- --network ssh

Headless capture (no X needed), e.g. over SSH:

npm start -- --no-ui --record capture.jsonl

Development

npm install            # core + dev deps; the react-x11 UI stack is optional
npm test               # decoder, framing, rules, capture tests — no X server needed
npm run typecheck
npm run gen:protocol   # regenerate protocol tables from an xcbproto XML corpus
npm run check:generated
npm run screenshot     # regenerate docs/img/*.png headlessly (no X server)
npm run workbench      # the component workshop (needs a display)
npm run workbench:ls   # what it discovered, without a display

The core (src/core) is pure Node + TypeScript with no runtime dependencies and is tested independently of the UI (src/ui), which is excluded from the core typecheck because it depends on the optional react-x11 stack. CI installs with --omit=optional and never needs a display.

src/core/protocol/generated.ts is generated from the xcbproto XML corpus and committed, so nothing needs xcbproto installed in order to run. Regenerate with npm run gen:protocol — it reads /opt/X11/share/xcb by default (XQuartz ships the corpus there) or takes a directory argument.

See AGENTS.md for scope, architecture and the invariants worth not breaking.

Screenshots

npm run screenshot regenerates docs/img/*.png with no $DISPLAY, no xvfb and no XQuartz. scripts/screenshot.tsx mounts the real UI through react-x11/test against node-x11's in-process pure-JS X server, clicks through it with the real event pipeline, and reads the pixels back off the window's own 2d context. The approach is react-x11's (its npm run screenshots), adopted wholesale; it replaces an xwd recipe that needed a live server and broke on a multi-monitor desktop, where GetImage cannot read a root window that is mostly off-screen.

The traffic in the picture is synthesized, not recorded: fixtures/demo-session.ts is hand-built X11 bytes fed through the shipping ConnectionCapture, so every field, span, link and resource shown was decoded by the real decoder — and the workbench stories mount their panels over that same session. A recording would have been both non-deterministic and unpublishable — captures carry window titles, clipboard contents and keystrokes, which is why *.x11cap is gitignored.

Because the PNGs are committed, everything that would otherwise vary per run is pinned: the wall clock, the animation clock (react-x11 transitions the row-selection colour — read the pixels too early and you photograph a fade), the fonts (family resolution otherwise shells out to fc-match), and the desktop's colour scheme — which the app follows, so it is passed per shot rather than read from the machine. That last one is why there are two PNGs: x11vis.png is a dark desktop, x11vis-light.png a light one. Run it on a machine with Arial (macOS) or Liberation/DejaVu (Linux) installed.

The component workshop

The UI's own components have stories, mounted in isolation by @react-x11/workbench — which is what Storybook is for a toolkit with no browser to put an iframe in:

PATH=/opt/X11/bin:$PATH npm run workbench    # needs a display
npm run workbench:ls                         # what it found; --json for the model

stories/*.story.tsx cover both halves of the UI: the control set and the icons (Controls, Icons), and the app's composed panels — the toolbar, the filter bar, the intercept bar, the detail pane with its field↔hex highlighting, the hex block, the statistics and console panes, and the "Break on…" dialog. They mount the real components from src/ui, over the same synthesized session the screenshot uses (fixtures/demo-session.ts), so a story shows what the app shows rather than a re-implementation that drifts from it. Selecting a file previews every state at once; a story with args gets a knobs panel.

Every story sits on the app's own ground and inside the app's own <ThemeProvider> (<Ground> in stories/ground.tsx), so a control is previewed in the colours it actually wears. They declare theme: 'both', which puts the light and the dark rendering side by side — the app follows the desktop, so a panel has two right answers and neither is the one to check.

Layout

src/core/                proxy, framing, state machines, protocol tables (tested core)
src/core/protocol/       core request/reply/event decoders + tables
src/core/protocol/extensions/   one file per extension (render.ts, xinput.ts, …)
src/ui/                  react-x11 application (Table / Tree / Code / hex)
src/cli.ts               entry point: proxy + UI-or-headless
test/                    unit tests
fixtures/                the synthesized session the stories and screenshot share
stories/                 @react-x11/workbench stories for the UI's components
workbench.config.ts      where the workshop looks for them
docs/                    PRD + decoder/state design

Adding an extension

Extensions are data-driven, resolved at runtime from the QueryExtension negotiation. To add one:

  1. Write src/core/protocol/extensions/<name>.ts exporting an ExtensionSpecrequests keyed by minor opcode, xgeEvents by XI-style evtype, events by offset from the extension's first-event, errors by offset from its first-error. Each decoder returns { summary, fields, created? }, where every field carries a byte span and created records a resource for jump-to-creator.
  2. Register it in extensions/index.ts under the exact name QueryExtension returns (e.g. RENDER, XInputExtension).

Nothing in the connection engine changes; RENDER and XInputExtension are the worked examples.

License

ISC