Skip to content

feat(ipodtouch4): the runtime behind Pocket Shell on the iPod touch — a network transport, landscape on a portrait panel, and an external app - #353

Draft
doodlewind wants to merge 13 commits into
mainfrom
feat/pocket-remote-ipod
Draft

feat(ipodtouch4): the runtime behind Pocket Shell on the iPod touch — a network transport, landscape on a portrait panel, and an external app#353
doodlewind wants to merge 13 commits into
mainfrom
feat/pocket-remote-ipod

Conversation

@doodlewind

@doodlewind doodlewind commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

The runtime work behind Pocket Shell on the iPod touch 4 — an Omarchy companion that mirrors the desktop over USB and drives it. The app itself now lives in pocket-stack/pocket-shell under ipod/, beside the 3DS shell; this PR is what the runtime needed to carry it.

What lands here

  • hosts/iphone2g/svcwire.c — the legacy Apple hosts' SVC WIRE (PKNT) transport: spec ops 30–32 over non-blocking BSD sockets pumped once per guest frame, no threads. It dials a beacon like the other companions and also listens on 8624, so when the iPod is plugged into the Omarchy machine usbmuxd + iproxy reach it and the daemon dials the device. Compiled in only when POCKET_SVC_WIRE is defined, so every other legacy Apple build keeps its op table byte-identical.
  • Landscape on a portrait panel — the host keeps the window at the panel's own 320x480, rotates the content view a quarter turn and lets the drawable follow, so GL renders 960x640 and locationInView: already reports touches rotated. framework/src/manifest/resolve.ts admits a native viewport that fills the panel transposed and reports the transposed physical size.
  • POCKET_KEEP_AWAKEsetIdleTimerDisabled: for an app that must not auto-lock, and svc= in the device's acceptance record so status says which transport is up (up, up-usb, absent).
  • Fontsbake-font gains fallbackTtfs, tried per codepoint the slot's own face does not map (so an icon font can share the atlas with the text it labels), tools/build.ts reads an app's fonts.json, and tools/font-subset.ts cuts a face down to what an app spells — normalising each glyph's advance to its ink, because a monospaced Nerd Font patch draws double-width icons across two cells while advancing one.
  • tools/ipodtouch4.ts takes an external appPOCKETJS_IPODTOUCH4_APP_FILE names a descriptor beside the app's own sources (bundle, executable, URL scheme, receipt slug, project root, whether it wants the wire) and the guest builds with --project-root pointed there. The toolchain, the sysroot, the linker and the deployment transaction stay here; the app does not have to. Pocket Clear's own entry is untouched and its bundle stays byte-identical.

Checks

  • bun tools/test.ts — the iPod/iPhone profile suites and the renderer tests are unchanged; the app's own tests moved with it (91 checks in the product repository, both guests bundled).
  • Hardware: deployed from the product repository through the Omarchy machine over the cable, drawable 960x640, daemon link up (iPod touch 4 at usb:…: allowed).

Note for the reviewer: the product repository pins a shell-submodule integration branch that carries this PR and #352 together, and will move to main once both land.

🤖 Generated with Claude Code

doodlewind and others added 6 commits September 1, 2026 16:43
…pe, over the svc wire

A second app for the iPod touch 4: a 480x320 landscape remote for an
Omarchy desktop. The iPod mirrors Hyprland — workspaces on a strip, the
focused monitor as a live miniature of tiles, volume and brightness on two
rails, the theme's palette on every node — and drives it: tap a tab to
switch, tap a tile to focus, hold to close, drag onto a tile to swap or
onto a tab to move, rails follow the thumb relatively, a dock of nine, a
pad with the long tail grouped like Omarchy's menu, a keyboard that types
into the focused window. Every command travels as an id from one table
both ends read; the daemon runs exactly what Omarchy's own keybinding runs.

Host side:
- hosts/iphone2g/svcwire.{c,h}: the legacy Apple hosts' first network
  path, a frame-pumped non-blocking port of the 3DS PKNT transport (spec
  ops 30..32), compiled in only under POCKET_SVC_WIRE.
- Landscape on the portrait panel: the window stays 320x480, the content
  view turns a quarter turn about its centre; touches and the GL drawable
  follow. The resolver admits a native viewport that fills the panel
  transposed and reports the transposed physical size.
- tools/ipodtouch4.ts knows two apps (POCKETJS_IPODTOUCH4_APP): own bundle,
  executable, URL scheme and receipt paths, so Clear and Remote coexist;
  the receipt carries the plan's drawable; a keep-awake flag disables the
  idle timer for the remote.

Daemon (apps/pocket-remote/host, Node on the Omarchy machine as a systemd
user unit): Hyprland over its two sockets, debounced snapshots under the
8 KiB poll cap, levels via Omarchy's own scripts, theme from colors.toml,
approval of new devices through hyprland-dialog. tools/pocket-remote.ts
deploys it, tails it, and runs a relay (ssh tunnel + beacon) for a laptop
whose ufw drops the wire port.

Verified: full gate 13/13, sim drives hello/theme/snapshot/tap, the iPod
found the relay's beacon and connected to the daemon; hardware capture at
960x640 shows the desk mirroring the laptop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… allowed devices, fit the dock captions

Hyprland's request socket now evaluates `dispatch <text>` as
`hl.dispatch(<text>)` — `dispatch workspace 1` fails with a Lua parse error,
from hyprctl too. Every dispatcher in actions.ts is a constructor call
(hl.dsp.focus({ workspace = "1" }), hl.dsp.window.close({ window = … })),
the daemon builds window and workspace targets only from validated pieces,
and a batch is one `eval` chunk. Verified against the laptop: the remote's
workspace switch now moves the desktop and the iPod mirrors it.

Also: an unanswered approval dialog no longer counts as a refusal, allowed
connections are logged, and the dock speaks in five-letter captions so the
labels stop overlapping in their 44 px slots (seen on the device capture).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…with chords

Round two of the remote's UX, from the first hands-on:

- The rails are gone. Brightness and volume were low-frequency and their
  caps were easy to hit by accident, so they live in one levels card now —
  the control-centre control: tap Levels to open it sticky (tap outside
  closes), or hold Levels and slide: the row under the finger follows the
  finger relatively, release lingers a second, then the card puts itself
  away. The speaker glyph is a stepped horn instead of the rotated square
  that read as broken.
- With the rails gone the strip, stage and dock span the full 480 px and
  the dock has eleven 43 px slots, so "more" no longer sits under a rail.
- Hold Menu and a cascade rises above the dock: six routes (Apps, Capture,
  Toggle, Style, System, Learn) with a staggered entrance over a veil;
  slide onto a route and its leaves fan out beside it; release on a leaf to
  run it, on a route to open it on the desktop, elsewhere to cancel.
- The keyboard takes the whole screen: five rows of 48 px keys, esc and
  digits on top, arrows, tab, a hide chevron in the caption bar, and two
  ways to make chords — sticky ctrl/alt that arm for one key, and
  hold-and-slide variants (hold x → ^X ⌥X, hold 1 → F1 ^1) as light chips.
  The wire carries `mods` on a key; the daemon builds `wtype -M … -k … -m …`
  from an allow-list.
- Geometry and the key table moved to keyboard-layout.ts so tests import
  them bare; the sim now drives a real hold-and-slide through the touch path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…aller in the daemon, deploys via a jump host

The iPod now sits on the Omarchy machine's USB, so the wire learns to run
down the cable instead of through the air:

- hosts/iphone2g/svcwire.c listens on port 8624 as well as discovering by
  beacon. An accepted connection is the same wire with connect() reversed —
  the device still speaks the hello first — and it beats any outbound
  attempt in flight. The acceptance record reports the transport
  (`svc=discover|connecting|hello|up|up-usb|backoff`).
- The daemon (default `--usb`, `--no-usb` to disable) polls `idevice_id -l`,
  forwards a host port per device with iproxy, dials the listener, and
  trusts a device on the cable without a dialog: physical possession is the
  pairing. A missing usbmuxd is reported once and the cable path stays off.
- tools/ipodtouch4.ts takes POCKETJS_IPODTOUCH4_VIA=<ssh host>: device
  discovery and the iproxy tunnel run on the machine the iPod is plugged
  into, and every ssh/scp to the device jumps through it (ProxyJump), so
  the deployment key and the pinned host key stay on this Mac.

Not yet exercised on the wire: the machine has libimobiledevice but not the
usbmuxd package, which needs root to install. Also: the cascade's veil is
denser and its hint shorter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ection replaces a WiFi one

The wrapper is compiled without the guest runtime's POCKET_SVC_WIRE switch,
so the acceptance record said svc=absent while the daemon was talking to
the device over usbmuxd. The wrapper now gets the same switch. On the
device, an inbound (cable) connection arriving while a WiFi one is up
replaces it instead of being refused; a second inbound is still refused.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…u as a sheet, a control centre, a virtual pointer

The third pass over the iPod touch remote after hands-on use. The dock is
gone; everything it held has a better place.

- Strip: tabs, the layout badge, a Stage | Deck mode switch and the control
  centre's button. Stage is the desktop miniature (now 292 px tall); Deck is
  the laptop's C surface — five compact rows of keys over a trackpad.
- Stage: hold a tile and a classic popup (Float/Tile, Full screen, Close)
  opens at the finger with a caret; drag a floating window and it moves on
  the laptop under the finger (`win place`, monitor-relative; the daemon
  restores the monitor origin and dispatches hl.dsp.window.move). An empty
  workspace offers Terminal, Browser and Files.
- Deck: comma, period and apostrophe on the letter layers, backtick on the
  symbol layer; a pressed key rises, glows and shows its character in a
  bubble; hold-and-slide variants stay. The trackpad is a relative pointer
  with acceleration: tap clicks, two fingers scroll, two-finger tap is the
  right button, a hold drags.
- The ball: an AssistiveTouch-style handle that snaps to a side edge; tapping
  it opens Omarchy's whole menu as a centred two-column scrolling sheet with
  the shell's own rows, order, titles and glyphs. menu.ts is generated from
  omarchy-menu.jsonc (`bun tools/pocket-remote.ts menu x1nano`); the daemon
  parses the same file live, runs rows by id under `bash -lc` as the shell
  does, evaluates every `when`/`checked` in one bash run and sends the hidden
  and checked ids. The wire still carries no command strings.
- Control centre: hangs from its button — Wi-Fi (nmcli), screenshot,
  nightlight, MPRIS now-playing with transport (busctl), brightness and
  volume sliders; hold the button and slide to adjust relatively.
- Icons are Nerd Font glyphs: fonts.json names a 68 KiB subset
  (tools/font-subset.ts) and bake-font gains `fallbackTtfs`, tried per
  codepoint the slot's face lacks and scaled through the source face's upm.
- host/pointer/pocket-pointer.c: a zwlr_virtual_pointer_v1 client fed one
  command per line, built on the Omarchy machine at deploy with
  wayland-scanner and cc (protocol XML vendored). Verified against Hyprland.
- Protocol 2: host `cc` and `menu` lines; device `win place/float/full`,
  `ptr`, `click`, `scroll`, `drag`, `wifi`, `menu`.
- `bun tools/pocket-remote.ts shots <dir>` renders every screen in the
  headless sim; the README shows them. Tests cover the new geometry, the
  JSONC parser and baked table, the readers, and the sim flows over a fake
  svc channel (46 tests).
- Licensed GPL-3.0-or-later (LICENSE, SPDX headers).

Gotchas kept: Node's type stripping rejects constructor parameter
properties; a View's default cross-axis alignment is centre, so fixed-width
labels take items-start.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@doodlewind doodlewind changed the title feat(ipodtouch4): Pocket Remote — an Omarchy touch companion for the iPod touch 4 feat(pocket-remote): Pocket Remote — an Omarchy touch companion for the iPod touch 4 Sep 2, 2026
doodlewind and others added 7 commits September 2, 2026 02:10
…e, a popup that answers the finger that opened it

Everything here came off the hardware.

- Layout: a View's main axis is horizontal and its cross-axis default is
  stretch, so every fixed-size label box needed `items-center` rather than
  the `justify-center` I had reached for. Menu rows, the now-playing lines,
  the launch chips, the popup's rows and the trackpad's caption were all
  painting their text against the top edge.
- The menu sheet is one column of 40 px rows, centred, scrolling — two
  columns of eleven-character labels made the eye jump, and Omarchy's own
  menu is a list. Its header bar carries the card's own top radius (an
  overflow clip is a rectangular scissor, so a child cannot inherit rounded
  corners), squared off below by a plain rect.
- Apps now lists the machine's applications: the daemon reads the XDG desktop
  entries (Type/NoDisplay/Hidden respected, the user's directory first) and
  pages them forty at a time, because the whole list does not fit one 8 KiB
  poll batch; the row opens that list on the device and a tap launches
  through `uwsm-app`. 56 entries on x1nano.
- A held tile's popup lost its caret (a triangle that small cannot meet a
  rounded, bordered body without a seam) and gained the gesture it should
  have had: the finger that opened it slides onto a row and releases — one
  gesture — and lifting without sliding leaves it up for a tap. Both paths
  are the same code because the row now runs on `onUp`: that edge precedes
  `onTap` for one release, and clearing the highlight there was why a tap on
  a row did nothing on hardware.
- The control centre lost its caret too, for the same reason.
- The key bubble follows the key: `Show` keeps one instance while the value
  behind it changes, so a `style` object read once froze the bubble on the
  first letter typed. Its geometry — and a key's own, which moves when the
  symbol layer gives a row one more column — is written through the mirror
  from an effect now.
- The ball's mark is a glyph instead of two bordered circles: a stroked
  circle is rasterised at logical size and looked soft on a 2x panel.
- Icon centring: the subset's advances are normalised to each glyph's ink.
  A monospaced Nerd Font patch gives every glyph one cell of advance while
  drawing the double-width icons across two, so a centred icon sat right of
  centre — the camera most visibly.
- Protocol: host `apps` pages, device `launch`.
- Tests: 52 across the two files, including the popup's two paths, the
  single-column sheet with a fling, the applications list, and a pixel check
  that the bubble moves. `bun tools/pocket-remote.ts shots` grew a menu-apps
  screen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…he bottom, the mode switch centred

- The strip showed "2 3" on a machine whose desks are 1..5: Hyprland
  destroys an empty workspace, so a tab list built from its snapshot shrinks
  to whatever has windows. The tabs are now a fixed 1..N — at least five (up
  to eight, which is what fits beside the centred switch) — because the
  numbers Omarchy binds with SUPER+1..0 are addressable regardless, and a
  tab for a workspace that does not exist yet is how you get there. Standing
  on a workspace above the cap, the last tab carries its number so the strip
  always shows where you are.
- Terminal, Browser and Files are back in a fixed bar across the bottom of
  the stage, three wide cells; an empty workspace no longer needs launchers
  of its own. The deck keeps its full height: its bottom half is the
  trackpad. The stage is 260 px, and the ball can no longer sit on the bar.
- The mode switch is centred on the strip; the layout badge moved right of
  it, the control centre stays at the end.
- The menu sheet no longer flashes a row when a finger lands: a press is
  ARMED on the down edge and only shown after the finger has stayed put for
  seven frames, so landing to fling highlights nothing. A release still
  commits, so nothing about picking a row changed.

Tests: the fixed tab set including the above-cap case, the bar's cells, the
ball clearing the bar, and in the sim the bar launching from a stage with
windows up, the armed highlight not appearing under a fling, and appearing
under a finger that stays.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…a resize corner, and a laptop's arrow cluster

- design.ts and ui.tsx: the spacing scale, radii, row heights, icon box and
  one `rowMetrics` arithmetic, plus the drawn primitives (Card, Row,
  PressTint) and the class literals that must spell a radius out. Every list
  — a held tile's popup, the menu sheet, the launch bar — is assembled from
  them, so the paddings that had drifted per surface now agree by
  construction; a test asserts the popup's metrics and the sheet's match.
- A tile leads with its window's TITLE and names the program underneath:
  four terminals all read "foot" when the class came first. names.ts
  prettifies a class (org.gnome.Nautilus → Files, dev.zed.Zed → Zed) and the
  daemon strips the program's own name off a title's tail ("Omarchy Manual -
  Chromium" → "Omarchy Manual"). Titles carry 34 code points now.
- Floating windows stack above tiled ones, most recently focused on top —
  the compositor's own order, written as a paint order (zIndex) instead of
  left to whichever pool slot a window happened to take.
- Drag a tile's bottom-right corner to resize the window: relative monitor
  px through the dispatcher SUPER+arrow uses, so a tiled window moves its
  split and a floating one changes size. The corner is drawn on tiles big
  enough to carry it and wins over the ball where they overlap.
- The popup gained "Open another": the daemon resolves the window's class
  against the machine's desktop entries (StartupWMClass, then the entry id,
  then the executable) and launches it. It also no longer runs a row when
  the finger lifts where it started — on a 260 px stage a four-row popup
  often covers its own anchor, and the last row closes the window.
- Deck: the character bubble is gone (it covered the row above, and on the
  top row it had to open downwards), the arrows are a laptop's cluster (←,
  ↑ over ↓ in one column, →) at the bottom right, return closes the home
  row, and SUPER joins ctrl and alt as a sticky modifier so Omarchy's own
  grammar is reachable from the deck.
- The ball hides on the deck (it sat on the trackpad) and starts on the left
  edge, where no tile keeps its resize corner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…direction

Four arrow keys sharing the width of two were each too small to hit on a
480 px panel. There is one arrow key now: pressing it fans a compass out
over the trackpad — the deck's only empty place, so nothing that matters is
covered — and the direction comes from the SLIDE, not from touching a chip,
so there is nothing to aim at.

- A flick sends exactly one press. Holding a direction repeats it after
  0.37 s at about seven a second, which is what an arrow key is for and as
  fast as one key press per repeat over the wire should go.
- Sliding through a direction on the way to another sends nothing: a
  compass that fired on the way would move the cursor where nobody asked.
- A sticky modifier rides along for the whole hold, so ctrl then the
  compass is ctrl+Left and stays ctrl+Left while it repeats.
- The pad's caption stands down while the legend is up, and the hint moves
  to the free half of the pad.
- The key's face is a glyph (KeyDef.glyph), which the letter keys' Text path
  could not carry at 12 px.

Space took the width back (3.75 units), and the half-height stacked-key
support the old cluster needed is gone with it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…narrower pad, d-pad — and typing that lands where it aims

The deck is the C surface read literally now. The trackpad no longer runs
edge to edge; what a laptop leaves as palm rest carries the two things a
hand wants beside a pad.

- Left rest: Omarchy's menu key (the deck's way in, since the ball belongs
  to the stage) with the CLICK BUTTON under it. Holding the click button
  keeps the laptop's left button down, so the other finger's travel on the
  pad drags a selection — the last of the common interactions with no model
  here, and a laptop's own two-handed gesture rather than an invented one.
- Right rest: a d-pad cross of four discrete keys. They fire on the press
  and repeat while held. The slide-a-direction compass is gone: a cross
  needs no gesture and nothing aimed at inside it.
- Typing accuracy, two corrections rather than bigger keys: the hit regions
  TILE the keyboard (nearest key by edge distance, so the gaps belong to
  their neighbours instead of swallowing a press, which is what lets the
  keys be drawn with 6 px of air instead of 4), and the hit test moves a
  touch UP a few pixels before assigning it, because a press on a
  capacitive panel lands below where the eye aimed. A test walks every point
  over the rows and asserts none resolves to nothing.
- ctrl-click works: a virtual pointer carries no modifier state, so the
  daemon has wtype hold the modifier for a moment and puts the pointer press
  inside that window. Same for a ctrl-drag with the click button.
- The resize corner keeps its 18 px mark but answers a 34 px target that
  reaches past the tile's edge, lights up while it is dragged (with the
  tile's own border and face), and is left off tiles under 72 px.
- The bottom keyboard row took the compass's width back as space and the two
  punctuation keys a terminal reaches for.

Found on the way, and now written down: the engine draws a rounded box with
a coloured border by filling the whole rounded rect with the BORDER colour
and insetting the background over it, so a translucent fill inside a border
reads as opaque. The held click button is an opaque accent key because of it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…iPod companion out to pocket-shell

The app is a product, not a runtime demo, so it lives in the product
repository with its own history — the same move apps/pocket-shell and
apps/term made. What stays here is the runtime work it needed:

- hosts/iphone2g/svcwire.c, the legacy Apple hosts' PKNT transport (dialler
  plus a listener for the cable), behind POCKET_SVC_WIRE.
- The landscape presentation for a portrait panel, and the resolver's
  transposed native fill.
- bake-font's fallbackTtfs and tools/build.ts reading an app's fonts.json,
  so an icon font can share the atlas with the text it labels; plus
  tools/font-subset.ts to cut one down to what an app spells.
- tools/ipodtouch4.ts gains an EXTERNAL app mode: POCKETJS_IPODTOUCH4_APP_FILE
  names a descriptor beside the app's own sources (bundle, executable, URL
  scheme, receipt slug, whether it wants the wire) and the guest builds with
  --project-root pointed there. The toolchain, the sysroot, the linker and
  the deployment transaction stay in this repository; the app does not have
  to.

Pocket Clear's own entry is untouched and its bundle stays byte-identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t its descriptor's directory

The manifest's own entry is written relative to the product repository (the
3DS pipeline's convention), so the descriptor names a projectRoot and the
guest builds against that.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@doodlewind doodlewind changed the title feat(pocket-remote): Pocket Remote — an Omarchy touch companion for the iPod touch 4 feat(ipodtouch4): the runtime behind Pocket Shell on the iPod touch — a network transport, landscape on a portrait panel, and an external app Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant