What it is
iWish is a port of AndroWish's undroidwish — the batteries-included, SDL2-rendered Tcl/Tk wish — to iOS and iPadOS (with a Mac Catalyst build alongside).
It runs a real Tcl 8.6 interpreter and the real Tk 8.6 widget set on an iPhone or iPad. Tk is drawn through AndroWish's SdlTk — an X11-on-SDL2 emulation layer — using the AGG anti-aliased renderer for graphics and FreeType for text, composited onto an SDL2 Metal surface. There is no WebView and no native-UIKit-widget bridge: it is the actual Tk canvas and widgets, running on the device.
The whole stack — Tcl, Tk/SdlTk, SDL2, AGG, FreeType — is statically linked into a single sdl2wish binary, compiled for arm64-apple-ios.
Status
0.2 — alpha. The runtime runs a large third-party Tk application end-to-end on a physical iPad: full GUI, the batteries-included extension set, a bundled demo menu, and hardware access over CoreBluetooth. APIs, the build layout, and the bundled extension set are still in flux.
- The core port renders a Tk GUI on the iOS Simulator, Mac Catalyst, and a physical iPad, all screenshot-verified.
- Built with
TCL_UTF_MAX=6(UCS-4) so astral-plane characters and arrows render correctly. Stock builds (UTF_MAX=3) garble them; the UTF6 rebuild is verified by the full Tcl and Tk regression suites. - A Tcl/Tk espresso application (Decent's
de1app) has been booted on a physical iPad and connected to a DE1 espresso machine over CoreBluetooth, on-device rather than in the simulator. - Published as v0.2-alpha, then v0.2.2 with the AltStore/SideStore
.frameworkpackaging fix.
What's included (batteries)
Every build (device and Catalyst) bundles the AndroWish extension set plus built-in demo apps; the bare runtime is not shipped separately.
- ~114–118 bundled packages, of which 56–64 are native
arm64-apple-iosdylibs, totaling roughly 80 MB. - Included: tkimg (jpeg/png/tiff), tls (over LibreSSL), TclCurl, sqlite3, itcl, itk, thread, tdom, Tktable, tktreectrl, zint, Img, tkpath, tkvnc, BLT 2.4, TkBLT (scientific plotting —
blt::graph/barchart/vector), Tix, vectcl, tksvg, and more. - A File ▸ Demos menu in the console launches the bundled demo apps. Four are iWish-specific:
bltgraph(live TkBLT plotting + PostScript export),bledemo(a LightBlue-style BLE debugger — scan, connect, browse services/characteristics, read/subscribe/write),borgdemo(theborgiOS device bridge), andpaint. Extensions that can't exist on iOS (Windows-only, Linux/Android hardware, fork/exec, huge external native libs) appear greyed-out. - iOS-native shims re-implement AndroWish's device commands on Apple frameworks:
borg→UIScreenbrightness, device/screen info, URL opening, AVSpeech text-to-speech, a native toast, plus aplatformsubcommand (ios/iossimulator/maccatalyst) so Tcl can identify the run target at runtime.ble→ CoreBluetooth (scan / connect / read / write / notify), using the peripheral's CoreBluetooth UUID as its address since iOS hides the hardware MAC.hardexit→ a one-command dylib that calls_exit(). On iOS, Tcl's normalexit(which runsTcl_Finalize) hangs and leaves a blank window; routingexitthroughhardexitgives a clean quit.
- Because iOS sandboxes
fork/exec,exec ls/cat/cp/… can never work in a packaged app. iWish supplies pure-Tcl replacements (ls cat head tail grep wc cp mv rm mkdir touch find echo…) wired intoinit.tclso they exist in every interpreter, not just the console. They never shadow a name a program already defines.
Building & signing (high level)
The repo is a recipe — build scripts, a set of patches against upstream, and the iOS-native glue code. It does not redistribute AndroWish, SDL2, or Tcl/Tk; you fetch those and apply the patches.
Rough shape of a build:
- Get an AndroWish checkout and a stock SDL 2.30.11 source tree.
- Apply the iWish patches (
apply-patches.sh). - Build the foundation — FreeType, SDL2, AndroWish Tcl, sdl2tk and
sdl2wish— all forarm64-apple-ios, atTCL_UTF_MAX=6. - Build the loadable extension stack and stage the full battery set into the bundle's
lib-batteries/. - Compile the
src-ios/shims (borg/ble/hardexit) to dylibs. - Assemble the
.app: the renamedsdl2wishbinary,lib/tcl8.6+lib/tk8.6,main.tcl+ payload, anInfo.plist(landscape orientations, status bar hidden,MinimumOSVersion 15.0), and thelib-batteries/. - Build the app icon (any image → opaque 1024×1024 → asset catalog via
actool). - Wrap every native dylib in its own
.frameworkbundle underFrameworks/(see Implementation notes) — required for AltStore/SideStore. - Sign and install to a device.
Signing uses a standard Apple Development certificate plus a provisioning profile that includes the target device's UDID. Distribution is via TestFlight / sideload / EU notarization — App Store policy 2.5.2 forbids interpreters that load downloaded code. (This page omits the project's private signing identities, team IDs, device UDIDs, and local paths.)
Implementation notes
The port reconciles a threaded X11-style toolkit with UIKit's main-thread rules and pixel model. Fixes:
Keyboard input. SDL2's UIKit backend registers a GameController keyboard and then skips delivering normal key presses, expecting a handler that never fires for ordinary typing — so keys vanished. The fix disables the GameController keyboard path so keys flow through the standard press handlers. Related device fixes eliminated double-typed characters (hardware key + hidden text-field both firing) and made Shift produce shifted symbols (
Shift+=→+) via an ANSI shift map gated on Shift, not CapsLock.Mouse, drag, and cursor. On Catalyst and iPad the pointer arrives as an indirect (trackpad) touch, not a classic mouse. Fixes route it through the touch handlers, add drag-motion reporting, correct a button-release bug (the pressed bit is already cleared at release time, so releases were lost), and add a cursor driver that morphs the pointer into resize/move shapes — using AppKit's
NSCursorvia the Objective-C runtime on Catalyst, and a custom bezier double-arrowUIPointerStyleon the device.Keyboard-caret crash (SIGABRT). undroidwish runs Tcl/Tk on a secondary thread. Tk's caret update eventually set a
UIViewframe off the main thread, and UIKit's safe-area recalculation throws when that happens off-main → abort. The fix bounces the frame update to the main thread and skips it when it's a no-op. A recurring lesson across the port: neverdispatch_syncfrom the Tcl thread to the main queue (the main thread is busy in the SDL/Tk loop and not servicing the main queue → deadlock) — alwaysdispatch_async.Screen auto-size and fill. On the real device iWish adopts the native screen size automatically; the fixed-resolution pins are all Catalyst-only. Pinning SdlTk's virtual screen to the native size, using
SDL_RenderSetLogicalSizeto scale the present, and ignoring spurious window-resize events made the Tk surface fill the display and map input back correctly.Colour correctness on Catalyst. The whole UI came up red/blue-swapped.
SDL_GetWindowPixelFormatreportedABGR8888, but the actual Metal drawable is BGRA; SDL presented without converting. Forcing the surface and textures toARGB8888(== BGRA) on Catalyst fixed every colour at the final present, and AGG adapts to match.Catalyst beachball. SdlTk's event loop blocks forever on the main thread with
SDL_WaitEvent, but on Catalyst AppKit owns the main run loop, so never returning to it reads as "not responding". The fix returns the main thread to AppKit and drives Tk from a per-frameCADisplayLinkcallback instead.AltStore/SideStore packaging. iWish bundles ~64 native dylibs. AltStore/SideStore re-sign with
ldid, which only re-signs the executable and nested bundles (Frameworks/*.framework,PlugIns/*.appex) — a loose.dylib, even insideFrameworks/, keeps its original signature and iOS rejects the install (0xe8008001). The fix wraps every dylib in its own.frameworkbundle and rewrites the Tclloadpaths. (A deeper root cause —ldidemitting a legacy SHA-1-primary code directory that iOS 26 rejects — is why a modern, codesign-equivalent signer is ultimately needed for reliable free-account sideloading.)
Repository
Source, build scripts, and patches: https://github.com/johnbuckman/iwish
Part of the Tcl/Tk on modern platforms collection. Sibling projects build the same stack as a native Apple-Silicon macOS binary and as a 32-bit armv7 / iOS 9 full-source snapshot for old jailbroken devices.