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
Draft
Conversation
…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>
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 +iproxyreach it and the daemon dials the device. Compiled in only whenPOCKET_SVC_WIREis defined, so every other legacy Apple build keeps its op table byte-identical.locationInView:already reports touches rotated.framework/src/manifest/resolve.tsadmits a native viewport that fills the panel transposed and reports the transposed physical size.POCKET_KEEP_AWAKE—setIdleTimerDisabled:for an app that must not auto-lock, andsvc=in the device's acceptance record sostatussays which transport is up (up,up-usb,absent).bake-fontgainsfallbackTtfs, 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.tsreads an app'sfonts.json, andtools/font-subset.tscuts 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.tstakes an external app —POCKETJS_IPODTOUCH4_APP_FILEnames 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-rootpointed 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).drawable 960x640, daemon link up (iPod touch 4 at usb:…: allowed).Note for the reviewer: the product repository pins a
shell-submoduleintegration branch that carries this PR and #352 together, and will move tomainonce both land.🤖 Generated with Claude Code