Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
4976e7d
feat(3ds): Pocket Shell — a tiling window shell for the console
doodlewind Sep 1, 2026
7738aa4
fix(3ds): give the JS stack room for ordinary UI depth, and open ever…
doodlewind Sep 1, 2026
beb745c
fix(3ds): derive the clock from the epoch, draw the icons, answer a p…
doodlewind Sep 1, 2026
4ea6170
feat(ipodtouch4): Pocket Remote — an Omarchy touch companion, landsca…
doodlewind Sep 1, 2026
9881bf0
fix(pocket-remote): speak Hyprland 0.56's Lua dispatcher grammar, log…
doodlewind Sep 1, 2026
dd577e6
feat(pocket-remote): levels card, menu cascade, full-screen keyboard …
doodlewind Sep 2, 2026
31c0091
feat(pocket-remote): the cable — a device-side listener, a usbmuxd di…
doodlewind Sep 2, 2026
1b99d8a
fix(ipodtouch4): report the svc transport in the record; a cable conn…
doodlewind Sep 2, 2026
c9a9db8
refactor(3ds): move Pocket Shell to its own repository, keep the runt…
doodlewind Sep 2, 2026
582cc1c
feat(pocket-remote): stage and deck modes, the ball and Omarchy's men…
doodlewind Sep 2, 2026
a792b78
fix(pocket-remote): one column for the menu, labels on the middle lin…
doodlewind Sep 2, 2026
ea79fc1
fix(pocket-remote): workspace tabs are a fixed set, a launch bar at t…
doodlewind Sep 2, 2026
220df88
feat(pocket-remote): a design system, windows named by their titles, …
doodlewind Sep 2, 2026
ab2d082
feat(pocket-remote): the arrows are one key, and the slide picks the …
doodlewind Sep 2, 2026
2cd2f3a
feat(pocket-remote): a laptop's palm rests — menu key, click button, …
doodlewind Sep 2, 2026
63238d0
chore: integration branch for the pocket-shell submodule (3DS runtime…
doodlewind Sep 2, 2026
8063f67
refactor(ipodtouch4): take an app from another project, and move the …
doodlewind Sep 2, 2026
d5103e1
Merge branch 'feat/pocket-remote-ipod' into shell-submodule
doodlewind Sep 2, 2026
f4021ea
fix(ipodtouch4): an external app's project root is its repository, no…
doodlewind Sep 2, 2026
c68390a
Merge branch 'feat/pocket-remote-ipod' into shell-submodule
doodlewind Sep 2, 2026
ef1a72e
feat(design): add classic Pocket icon studies
doodlewind Sep 5, 2026
4f549b9
feat(icons): ship the quiet arcade artwork on classic iOS and 3DS
doodlewind Sep 5, 2026
b4429fb
test(icons): distinguish the selected classic hardware palette
doodlewind Sep 5, 2026
25106f7
build(ipodtouch4): integrate current Shell runtime with the latest ho…
doodlewind Sep 5, 2026
6cc54ae
test(icons): distinguish the selected classic hardware palette
doodlewind Sep 5, 2026
f8edade
build(ipodtouch4): record the validated icon branch dependency
doodlewind Sep 5, 2026
b8c19c4
fix(ios): pre-mask classic icon corners without dark fringes
doodlewind Sep 5, 2026
c7de326
fix(ipodtouch4): include transparent v6 icon masks
doodlewind Sep 5, 2026
40af188
fix(ipodtouch4): install removable user applications
doodlewind Sep 5, 2026
6c37623
fix(ipodtouch4): apply user installation to external app builds
doodlewind Sep 5, 2026
901edf9
fix(ios): keep companion transport bounded through peer disconnects
doodlewind Sep 5, 2026
3efd6ae
build(ipodtouch4): validate user apps against current main
doodlewind Sep 5, 2026
95a55f5
Merge remote-tracking branch 'origin/main' into merge/ipod-runtime
doodlewind Sep 5, 2026
b3add48
fix(ios): let SpringBoard mask user application icons
doodlewind Sep 5, 2026
1467cd1
build(ios): integrate current native harness and icon base
doodlewind Sep 5, 2026
34f772c
fix(renderer): retain main self-insertion validation
doodlewind Sep 5, 2026
5aa48cf
Merge remote-tracking branch 'origin/build/ipod-quiet-arcade' into fi…
doodlewind Sep 5, 2026
5bb1e53
refactor(ipodtouch4): reuse deployment shell quoting
doodlewind Sep 5, 2026
0fa362c
build(ios): rebase installation delivery on merged runtime
doodlewind Sep 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 40 additions & 7 deletions docs/IPODTOUCH4.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ iOS 6.1.6 is untethered). The completed bootstrap provides:
- OpenSSH on device port 22;
- a dedicated RSA client key and pinned device host key;
- `PasswordAuthentication no` after public-key login succeeds;
- `ldid`, `uicache`, and `uiopen` for application deployment.
- `ldid`, `uicache`, and `uiopen` for application deployment;
- **AppSync Unified and its Cydia Substrate dependencies** for local self-signed
User applications. Install the `iphoneos-arm` package from the
[upstream release](https://github.com/akemin-dayo/AppSync/releases), then
reboot once to activate it. `doctor` checks the installed package. The
deployment command reports an installation failure if the signing support
is inactive; it does not fall back to a System application.

The default local files are:

Expand Down Expand Up @@ -72,6 +78,7 @@ bun ipodtouch4 deploy
bun ipodtouch4 launch
bun ipodtouch4 status [--require-action]
bun ipodtouch4 capture
bun ipodtouch4 uninstall # removes the app and its data
```

`build` resolves `apps/clear/pocket.json` against the `ipodtouch4-dev`
Expand All @@ -81,16 +88,42 @@ embedded as `__pocket_js` / `__pocket_pak` sections. The build id hashes the
plan, the guest artifacts, every native object, the sysroot stubs, and the
baked artwork.

`deploy` copies the bundle over the USB SSH tunnel, verifies every file's
SHA-256 on the device against the local receipt, and installs under a leased
transactional lock with rollback — the same protocol as the iPhone 4S, with
device paths under `pocketjs-ipodtouch4`.

`status` reads `/private/var/tmp/pocketjs-ipodtouch4.status` twice and
**`build` also produces `dist/ipodtouch4/PocketJSiPodTouch4.ipa`.** `deploy`
transfers that IPA over the pinned USB SSH tunnel and calls iOS 6
`MobileInstallationInstall` with `ApplicationType=User`. **iOS creates the
UUID container under `/var/mobile/Applications`, owns updates, and preserves
`Documents` and `Library` on update.** Every installed bundle file, including
the build receipt, must match its local SHA-256. A kernel file lock serializes
installation and CLI removal; process exit releases the lock.

The first deployment migrates the former `/Applications/PocketJSiPodTouch4.app`
installation. It checks the bundle identifier, retains the old bundle in a
root-owned migration journal, and refreshes its System registration before
installing the User app. The migration restarts `installd` to reload its
in-memory System map; SpringBoard is not restarted. A failed installation restores the old bundle; the
next deployment reconciles an interrupted migration. The journal is removed
after User registration and installed byte verification pass. The app's old
bundle-specific preferences are copied into its new container when present;
other files in the shared mobile home are not treated as app-owned data.

**Long-pressing the User app on SpringBoard exposes the native delete badge.**
Deleting there, or running `bun ipodtouch4 uninstall`, uses iOS's uninstall
service and removes the application container, including its data. A later
`deploy` installs a fresh container. The CLI verifies that the registration
and container are gone. The privileged installer bridge stays under
`/var/root/Library/PocketJS`; its install/uninstall entitlement is never added
to the application binary.

`launch`, `status`, and `capture` look up the current container from the iOS
installation record. The runtime resolves `NSTemporaryDirectory()` and keeps
its receipts and captures inside that container. **`status` reads
`<container>/tmp/pocketjs.status` twice** and
requires the running build id, an advancing frame counter and heartbeat, and
the GLES1 640×960 density-2 drawable. With `--require-action` it additionally
requires at least one completed touch sequence and a reported `clear_gesture`
action — a receipt that a gesture interaction completed on the hardware.

`capture` asks the running app for a raw RGBA frame and converts it to
`dist/ipodtouch4/device-frame.png`.

User application icons use **opaque 57×57 and 114×114 artwork**. SpringBoard applies the rounded mask and shadow; `UIPrerenderedIcon` suppresses the stock gloss. The System application path uses a precomposed transparent mask instead. Baking that mask into a User icon adds an inset rim under the native mask. Icon filenames include the artwork revision so an update selects a fresh SpringBoard cache entry.
12 changes: 9 additions & 3 deletions hosts/ios-legacy/svcwire.c
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@ static int set_nonblocking(int fd) {
return flags >= 0 && fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0;
}

static int disable_sigpipe(int fd) {
int enabled = 1;
return setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &enabled, sizeof enabled) == 0;
}

static void enter_state(SvcState next) {
state = next;
state_since_ms = now_ms();
Expand Down Expand Up @@ -232,7 +237,7 @@ static int poll_listener(int adopt) {
if (listen_fd < 0) return 0;
fd = accept(listen_fd, (struct sockaddr *)&peer, &peer_length);
if (fd < 0) return 0;
if (!adopt || !set_nonblocking(fd)) {
if (!adopt || !set_nonblocking(fd) || !disable_sigpipe(fd)) {
close(fd);
return 0;
}
Expand Down Expand Up @@ -336,7 +341,7 @@ static void start_connect(void) {
enter_state(SVC_STATE_BACKOFF);
return;
}
if (!set_nonblocking(fd)) {
if (!set_nonblocking(fd) || !disable_sigpipe(fd)) {
close(fd);
enter_state(SVC_STATE_BACKOFF);
return;
Expand Down Expand Up @@ -466,7 +471,8 @@ static void handle_frame(uint8_t type, const uint8_t *payload, uint32_t length)
}

static void pump_rx(void) {
for (;;) {
/* A continuously readable peer must still yield to the next UI frame. */
for (unsigned reads = 0; reads < 4; ++reads) {
int progressed;
/* An oversized (or unwanted-bulk) payload is drained without buffering. */
if (skip_remaining > 0) {
Expand Down
6 changes: 6 additions & 0 deletions hosts/iphone4s/Icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions hosts/ipodtouch4/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
<key>CFBundleName</key>
<string>PocketJS iPod touch 4</string>
<key>CFBundleIconFile</key>
<string>PocketClassic-v6.png</string>
<string>PocketClassic-User-v7.png</string>
<key>CFBundleIconFiles</key>
<array>
<string>PocketClassic-v6</string>
<string>PocketClassic-User-v7</string>
</array>
<key>CFBundlePackageType</key>
<string>APPL</string>
Expand Down
9 changes: 7 additions & 2 deletions hosts/ipodtouch4/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,10 @@ for the legacy Apple hosts.
SpringBoard artwork reuses the classic PocketJS icon pipeline
(`tools/iphone-classic-icon.ts`).

Use `bun ipodtouch4 doctor`, then the build, deploy, launch, status, and
capture commands documented in `docs/IPODTOUCH4.md`.
Deployment produces an IPA and uses iOS 6 MobileInstallation to register a
**sandboxed User application with native SpringBoard deletion**. AppSync
Unified is required for these local self-signed builds. Updates retain app
data; uninstall removes the container and its runtime receipt files.

Use `bun ipodtouch4 doctor`, then the build, deploy, launch, status, capture,
and uninstall commands documented in `docs/IPODTOUCH4.md`.
12 changes: 12 additions & 0 deletions hosts/ipodtouch4/installer-entitlements.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.private.mobileinstall.allowedSPI</key>
<array>
<string>Install</string>
<string>Uninstall</string>
<string>Lookup</string>
</array>
</dict>
</plist>
145 changes: 145 additions & 0 deletions hosts/ipodtouch4/installer.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/* iOS 6 MobileInstallation bridge. Runs as root over the pinned USB SSH
* transport; application binaries remain ordinary, sandboxed User apps.
* Like the legacy host, this uses runtime messaging to avoid modern ObjC
* metadata in the historical ARMv7 linker. No installation cache is edited. */
#include <dlfcn.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <unistd.h>

typedef void *id;
typedef void *SEL;
extern id objc_getClass(const char *name);
extern SEL sel_registerName(const char *name);
extern void *objc_msgSend(void);

static id send0(id object, const char *selector) {
return ((id (*)(id, SEL))objc_msgSend)(object, sel_registerName(selector));
}
static id send1(id object, const char *selector, id value) {
return ((id (*)(id, SEL, id))objc_msgSend)(object, sel_registerName(selector), value);
}
static id string(const char *value) {
return send1(objc_getClass("NSString"), "stringWithUTF8String:", (id)value);
}
static const char *utf8(id value) {
return value ? (const char *)send0(value, "UTF8String") : "";
}
static id get(id object, const char *key) {
return send1(object, "objectForKey:", string(key));
}
static int identifier_valid(const char *value) {
if (!value[0] || value[0] == '.' || strlen(value) > 200) return 0;
for (const char *c = value; *c; ++c)
if (!((*c >= 'a' && *c <= 'z') || (*c >= 'A' && *c <= 'Z') ||
(*c >= '0' && *c <= '9') || *c == '.' || *c == '-')) return 0;
return strchr(value, '.') != NULL && strstr(value, "..") == NULL;
}
static id lookup(const char *identifier, const char *type) {
id cache = send1(objc_getClass("NSDictionary"), "dictionaryWithContentsOfFile:",
string("/var/mobile/Library/Caches/com.apple.mobile.installation.plist"));
return get(get(cache, type), identifier);
}
static id user_record(const char *identifier) {
id record = lookup(identifier, "User");
const char *path = utf8(get(record, "Path"));
const char *container = utf8(get(record, "Container"));
const char *prefix = "/private/var/mobile/Applications/";
if (strncmp(container, prefix, strlen(prefix)) != 0) prefix = "/var/mobile/Applications/";
if (strcmp(utf8(get(record, "ApplicationType")), "User") ||
strcmp(utf8(get(record, "CFBundleIdentifier")), identifier) ||
strncmp(container, prefix, strlen(prefix)) ||
strlen(container) != strlen(prefix) + 36 || strstr(path, "/../") ||
strncmp(path, container, strlen(container)) || path[strlen(container)] != '/' ||
!path[strlen(container) + 1] || strchr(path + strlen(container) + 1, '/')) return NULL;
const char *uuid = container + strlen(prefix);
for (unsigned i = 0; i < 36; ++i) {
if (i == 8 || i == 13 || i == 18 || i == 23) { if (uuid[i] != '-') return NULL; }
else if (!((uuid[i] >= '0' && uuid[i] <= '9') || (uuid[i] >= 'a' && uuid[i] <= 'f') ||
(uuid[i] >= 'A' && uuid[i] <= 'F'))) return NULL;
}
return record;
}
static void progress(id info, void *context) {
(void)context;
if (get(info, "Error")) fprintf(stderr, "%s\n", utf8(send0(info, "description")));
}
/* Migration refreshes the global System map, so all apps share one lock. */
static int acquire_lock(void) {
int fd = open("/var/root/Library/PocketJS/deployment.lock", O_CREAT | O_RDWR | O_NOFOLLOW, 0600);
if (fd < 0 || flock(fd, LOCK_EX | LOCK_NB)) {
perror("deployment busy (or lock unavailable)"); return -1;
}
return fd;
}
int main(int argc, char **argv) {
if (argc < 3 || getuid() != 0) {
fprintf(stderr, "usage (root): installer lookup|user-path|install|uninstall|bundle-id <value>; lock <id> <script>\n");
return 2;
}
if (!strcmp(argv[1], "lock")) {
if (argc != 4 || !identifier_valid(argv[2])) return 2;
if (acquire_lock() < 0) return 73;
/* No CLOEXEC: the shell keeps the kernel lock through install/readback.
* Process exit releases it, including transport loss; no stale lease. */
execl("/bin/sh", "sh", argv[3], (char *)NULL);
perror("exec deployment"); return 1;
}
if (!strcmp(argv[1], "uninstall")) {
if (!identifier_valid(argv[2])) return 2;
if (acquire_lock() < 0) return 73;
}
id pool = send0(send0(objc_getClass("NSAutoreleasePool"), "alloc"), "init");
int result = 1;
if (!strcmp(argv[1], "ready")) {
void *library = dlopen("/System/Library/PrivateFrameworks/MobileInstallation.framework/MobileInstallation", RTLD_NOW);
id (*lookup_apps)(id) = library ? dlsym(library, "MobileInstallationLookup") : NULL;
result = !lookup_apps || !lookup_apps(NULL);
} else if (!strcmp(argv[1], "bundle-id")) {
char path[4096];
if (snprintf(path, sizeof(path), "%s/Info.plist", argv[2]) >= (int)sizeof(path)) return 2;
id info = send1(objc_getClass("NSDictionary"), "dictionaryWithContentsOfFile:", string(path));
const char *identifier = utf8(get(info, "CFBundleIdentifier"));
if (*identifier) { puts(identifier); result = 0; }
} else if (!strcmp(argv[1], "lookup") && identifier_valid(argv[2])) {
id record = lookup(argv[2], "User");
if (!record) record = lookup(argv[2], "System");
if (!record) { puts("null"); result = 0; }
else {
id summary = send0(objc_getClass("NSMutableDictionary"), "dictionary");
const char *keys[] = {"CFBundleIdentifier", "ApplicationType", "Path", "Container"};
for (unsigned i = 0; i < 4; ++i) {
id value = get(record, keys[i]);
if (value) ((void (*)(id, SEL, id, id))objc_msgSend)(summary, sel_registerName("setObject:forKey:"), value, string(keys[i]));
}
id data = ((id (*)(id, SEL, id, unsigned long, id))objc_msgSend)(
objc_getClass("NSJSONSerialization"), sel_registerName("dataWithJSONObject:options:error:"), summary, 0, NULL);
if (data) {
unsigned long length = ((unsigned long (*)(id, SEL))objc_msgSend)(data, sel_registerName("length"));
fwrite(send0(data, "bytes"), 1, length, stdout); putchar('\n'); result = 0;
}
}
} else if (!strcmp(argv[1], "user-path") && identifier_valid(argv[2])) {
id record = user_record(argv[2]);
if (record) { puts(utf8(get(record, "Path"))); result = 0; }
} else if (!strcmp(argv[1], "install") || !strcmp(argv[1], "uninstall")) {
void *library = dlopen("/System/Library/PrivateFrameworks/MobileInstallation.framework/MobileInstallation", RTLD_NOW);
if (library) {
id options = ((id (*)(id, SEL, id, id))objc_msgSend)(objc_getClass("NSDictionary"),
sel_registerName("dictionaryWithObject:forKey:"), string("User"), string("ApplicationType"));
if (!strcmp(argv[1], "install")) {
int (*install)(id, id, void (*)(id, void *), id) = dlsym(library, "MobileInstallationInstall");
if (install) result = install(string(argv[2]), options, progress, string(argv[2])) != 0;
} else if (identifier_valid(argv[2]) && user_record(argv[2])) {
int (*uninstall)(id, id, void (*)(id, void *)) = dlsym(library, "MobileInstallationUninstall");
if (uninstall) result = uninstall(string(argv[2]), options, progress) != 0;
}
}
}
if (result) fprintf(stderr, "MobileInstallation %s failed\n", argv[1]);
send0(pool, "drain");
return result;
}
50 changes: 22 additions & 28 deletions hosts/ipodtouch4/runtime.c
Original file line number Diff line number Diff line change
@@ -1,32 +1,26 @@
/*
* Device-local receipt paths. Pocket Clear's are the defaults; a second app
* on the same iPod (tools/ipodtouch4.ts selects it by POCKETJS_IPODTOUCH4_APP)
* gets its own set through -D flags so two installed apps never share a
* status file.
*/
#ifndef POCKET_ACCEPTANCE_PATH
#define POCKET_ACCEPTANCE_PATH "/private/var/tmp/pocketjs-ipodtouch4.status"
#endif
#ifndef POCKET_ACCEPTANCE_TEMP
#define POCKET_ACCEPTANCE_TEMP "/private/var/tmp/pocketjs-ipodtouch4.status.new"
#endif
#ifndef POCKET_CAPTURE_REQUEST_PATH
#define POCKET_CAPTURE_REQUEST_PATH "/private/var/tmp/pocketjs-ipodtouch4.capture"
#endif
#ifndef POCKET_CAPTURE_OUTPUT_PATH
#define POCKET_CAPTURE_OUTPUT_PATH "/private/var/tmp/pocketjs-ipodtouch4.frame.rgba"
#endif
#ifndef POCKET_PREFER_GL_PATH
#define POCKET_PREFER_GL_PATH "/private/var/tmp/pocketjs-ipodtouch4.gles1"
#endif
#include <stdio.h>

/* Every User app owns its tmp directory. Resolve it at runtime because iOS
* chooses a container UUID at installation and may change it on update. */
extern void *NSTemporaryDirectory(void);
extern void *sel_registerName(const char *name);
extern void *objc_msgSend(void);
static const char *pocket_ipod_receipt_path(unsigned index, const char *suffix) {
static char paths[5][4096];
if (!paths[index][0]) {
const char *tmp = ((const char *(*)(void *, void *))objc_msgSend)(
NSTemporaryDirectory(), sel_registerName("UTF8String"));
snprintf(paths[index], sizeof(paths[index]), "%spocketjs.%s", tmp, suffix);
}
return paths[index];
}
#define POCKET_ACCEPTANCE_PATH pocket_ipod_receipt_path(0, "status")
#define POCKET_ACCEPTANCE_TEMP pocket_ipod_receipt_path(1, "status.new")
#define POCKET_CAPTURE_REQUEST_PATH pocket_ipod_receipt_path(2, "capture")
#define POCKET_CAPTURE_OUTPUT_PATH pocket_ipod_receipt_path(3, "frame.rgba")
#define POCKET_PREFER_GL_PATH pocket_ipod_receipt_path(4, "gles1")
#define POCKET_GL_DEFAULT 1
#define POCKET_REQUIRE_GL 1

/*
* The iPod touch 4 (iPod4,1, iOS 6.1.6) shares the iPhone 4S display tuple —
* 320x480 logical at density 2 on a PowerVR SGX535 — and runs the same
* runtime-registered UIKit host. Keeping one implementation keeps touch slot
* tracking, hit facts, damage accounting, and hardware receipts identical
* across legacy Apple devices; this wrapper only scopes device-local paths.
*/
/* The iPod touch 4 shares the iPhone 4S legacy UIKit implementation. */
#include "../ios-legacy/runtime.c"
Loading