Skip to content

Repository files navigation

iWish — Tcl/Tk (AndroWish) on iOS

iWish is a port of AndroWish's undroidwish — the batteries-included, SDL2-rendered Tcl/Tk wish — to iOS / iPadOS.

It runs a real Tcl 8.6 interpreter and a real Tk 8.6 widget set on an iPad or iPhone, with Tk drawn through AndroWish's SdlTk (an X11-on-SDL2 emulation layer) using the AGG anti-aliased renderer and FreeType for text — no WebView, no native-UIKit-widget bridge, just the actual Tk canvas/widgets composited onto an SDL2 Metal surface.

Part of the Tcl/Tk on modern platforms collection · Project page: https://johnbuckman.github.io/tcltk/iwish.html

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 (~114 bundled packages, up to 64 of them native arm64-apple-ios dylibs), a File ▸ Demos menu of bundled apps, and hardware access over CoreBluetooth. APIs, the build layout, and the bundled extension set are still in flux. See BUGS.md and TODO.md.

Installing the app: see INSTALL.md (Sideloadly / AltStore / from source / EU notarized). AI agents: start at AGENTS.md.

Talk (EuroTcl 2026): a walkthrough of the port — the lineage from undroidwish, the architecture, borg/ble, iOS 9 / armv7 + jailbreak, App Review, and every install path. Read iWish-for-iOS.pdf, or open the editable iWish-for-iOS.pptx.

This repo is a recipe: build scripts, a set of patches against upstream, and the iOS-native glue code. It does not redistribute the AndroWish, SDL2, or Tcl/Tk sources (each has its own license); you fetch those and apply these patches.

A sibling repo, undroidwish-arm64-batteries-included, builds the same stack as a native Apple-Silicon macOS binary; several of the non-UIKit fixes (the size_t/pid_t BLT bug, libpng NEON, TIFF codecs, the arm64 toolchain flags) are shared and documented there.

A second sibling repo, iwish-ios9, is instead a full-source, clone-and-build snapshot for 32-bit armv7 / iOS 9 (old 32-bit devices such as the jailbroken iPad mini 1, A5) — AndroWish's Tcl/Tk plus all its batteries in one checkout, with the armv7 build scripts, NDK+Apple toolchain wrappers, and a bundled iOS-9-patched SDL2 included. Use that if you want the whole source rather than patches to apply.

iWish running on an iPad — the borg iOS device bridge, a Bluetooth LE debugger connected to a Decent scale and a DE1, and an interactive wish console

iWish on an iPad: the borg device bridge, a live BLE debugger, and a wish console.

What works

  • Tcl 8.6.10 + Tk 8.6 widgets on a real iOS device, the simulator, and Mac Catalyst — the Tk canvas, fonts, buttons, scales, checkbuttons, etc. render edge-to-edge and fill the native screen.
  • Built -DTCL_UTF_MAX=6 (UCS-4) so astral-plane characters and arrows render correctly (stock builds are UTF_MAX=3 and garble them). The UTF6 rebuild is verified safe by the full Tcl and Tk regression suites.
  • A large bundled extension set built for arm64-apple-ios (~114 packages, up to 64 native dylibs): tkimg (jpeg/png/tiff), tls (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 (added by launcher/main.tcl) that launches the bundled demo apps — including four iWish-specific ones in demos/: bltgraph (TkBLT plotting), bledemo (a LightBlue-style BLE debugger), borgdemo (the borg iOS bridge), and paint. Extensions that can't exist on iOS appear greyed-out.
  • iOS-native shims (under src-ios/) that re-implement the AndroWish borg and ble commands on Apple frameworks:
    • borgUIScreen brightness, UIDevice/UIScreen info, UIApplication openURL, AVSpeech, a native toast (via scalessec/Toast), plus a platform subcommand (ios/iossimulator/maccatalyst) so Tcl can identify the run target at runtime.
    • ble → CoreBluetooth (scan / connect / read / write / notify), using the peripheral's CB UUID as its "address" since iOS hides the MAC.
    • hardexit (src-ios/hardexit/) → a one-command dylib that calls _exit(). On iOS Tcl's normal exit (which runs Tcl_Finalize) hangs and leaves a blank window; load this and route exit through it for a clean quit. Generic to any iWish app.

How it's built

The runtime is sdl2wish — undroidwish's single binary with Tcl, Tk/SdlTk, SDL2, AGG and FreeType statically linked — compiled for arm64-apple-ios15.0. The scripts in scripts/ drive it:

script builds
build-device.sh the foundation: FreeType, SDL2, AndroWish Tcl, sdl2tk + sdl2wish (all UTF6, arm64-apple-ios)
build-ext-dev.sh the loadable extension stack (tkimg, tls, TclCurl, tksvg, sqlite3, itcl, thread, zint)
build-blt-dev.sh BLT 2.4 (libBLT24)
build-tkblt-dev.sh TkBLT 3.2 — plotting widgets (blt::graph/barchart/vector); see demos/bltgraph.tcl
build-device-batteries.sh stage the full battery set (~114 packages) into a bundle's lib-batteries/
build-utf6.sh rebuilds Tcl/sdl2tk at TCL_UTF_MAX=6

The app's entry point is launcher/main.tcl (auto-run by the patched tkAppInit.c): it boots the Tk console, loads borg, builds the File ▸ Demos menu, and positions the windows. The custom demos it lists live in demos/.

The shims in src-ios/ are compiled to borg1.0/ble1.0/hardexit loadable dylibs (see the comments at the top of each .c/.m for the exact clang line).

Two more helper scripts package the result:

script does
build-icon.sh <image> [<app>] any image → opaque 1024 → actool AppIcon asset catalog (Assets.car), optionally copied into a bundle
relocate-frameworks.sh <app> wrap every native dylib in its own .framework bundle under Frameworks/ + rewrite the Tcl load paths — required so AltStore/SideStore produce an installable bundle. Their ldid re-signs only the executable and nested bundles (Frameworks/*.framework, PlugIns/*.appex), so each dylib must be a real .framework (dir + Info.plist). Run as the final step before signing.
sign-and-install-device.sh <app> <identity> <profile> <udid> [entitlements] sign nested dylibs + the app and devicectl install to a device

The patches

In the macOS sibling repo, SDL2's configure mis-detects an Apple-Silicon Mac as iOS. For actual iOS the work is the opposite: making SDL2's UIKit backend host a Tk app, and giving the Tcl runtime the iOS-sandbox environment it expects.

patch what
patches/androwish-tkAppInit.c.patch on iOS there is no TCL_LIBRARY/TK_LIBRARY env, so init.tcl/tk.tcl abort; set the script-library paths from the bundle and auto-run main.tcl
patches/androwish-SdlTkX.c.patch, …-SdlTkInt.c.patch, …-SdlTkGfx.c.patch pin SdlTk's virtual screen to the native size and scale-to-fill the real drawable; map input back; render + colour (BGRA/ARGB8888) fixes
patches/sdl2/SDL_uikitviewcontroller.m.patch scene/geometry config so the window fills the screen; hide the iOS status bar (prefersStatusBarHidden + a forced setNeedsStatusBarAppearanceUpdate); bounce the keyboard-caret frame update to the main thread
patches/sdl2/SDL_uikitvideo.m.patch make SDL_DisableScreenSaver (idle-timer) safe on iOS (was Catalyst-only); add a resize/move pointer-shape cursor driver
patches/sdl2/SDL_uikitview.m.patch, …window.m.patch, …modes.m.patch, …events.m.patch UIKit window/view/mode integration for the Tk surface; keyboard and indirect-pointer (trackpad) input fixes

Apply them with apply-patches.sh:

./apply-patches.sh /path/to/androwish /path/to/SDL2-2.30.11

(Patches are against AndroWish's bundled jni/sdl2tk and a stock SDL 2.30.11 tree. The iWish changes are also marked inline with iwish: comments.)

Build (outline)

  1. Get an AndroWish checkout and a stock SDL 2.30.11 source tree.
  2. ./apply-patches.sh <androwish> <SDL2-2.30.11>.
  3. Run scripts/build-device.sh (edit the path variables at the top first), then scripts/build-ext-dev.sh and scripts/build-blt-dev.sh.
  4. Compile the src-ios/ shims to dylibs.
  5. Assemble a .app: the sdl2wish binary (renamed), lib/tcl8.6 + lib/tk8.6, your main.tcl + payload, an Info.plist (landscape orientations, status bar hidden, MinimumOSVersion 15.0), and — for the full battery set — a lib-batteries/ of the extension dylibs.
  6. Build the icon: scripts/build-icon.sh <image> <your.app>.
  7. Wrap the dylibs in .framework bundles (required for AltStore/SideStore): scripts/relocate-frameworks.sh <your.app>. This wraps every native extension .dylib in its own Frameworks/libFoo.framework/ (with an Info.plist) and rewrites the Tcl load paths. AltStore/SideStore re-sign with ldid, which only re-signs the executable and nested bundles it recognises (Frameworks/*.framework, PlugIns/*.appex) — a loose .dylib, even inside Frameworks/, is skipped and keeps its original signature, so iOS rejects the install with 0xe8008001. Run this as the last step before signing.
  8. Sign + install: scripts/sign-and-install-device.sh <your.app> <identity> <profile> <udid> [entitlements] — needs an Apple Development cert + a provisioning profile that includes the device UDID.

The app icon master is assets/AppIcon.icns (the iWish apple + Tcl-feather mark); assets/iwish-icon-1024.png is its 1024×1024 opaque export for the build. scripts/build-icon.sh compiles the PNG with actool into an AppIcon asset catalog (Assets.car + AppIcon*.png); reference CFBundleIconName AppIcon in your Info.plist. iOS icons must be a single opaque 1024×1024 image — iOS applies its own rounded-corner mask.

Built-in Unix-style commands

iOS sandboxes fork/exec, so Tcl's exec ls, exec cat, exec cp, ... can never work in a packaged app. scripts/unix-commands.tcl supplies pure-Tcl replacements for the common filesystem/text utilities:

ls cat head tail grep wc cp mv rm mkdir rmdir touch ln chmod du find basename dirname echo

These are runtime built-ins, not console helpers: the runtime sources unix-commands.tcl from lib/tcl8.6/init.tcl, so the commands exist in every interpreter and are available to all Tcl programs, not just the interactive console. They never override a name a program already defines, and they don't re-implement things Tcl already has (pwd, cd, clock, glob, file, ...). Process/volume/network utilities (ps, kill, df, ping, ...) are left undefined — they need exec or privileged syscalls iOS forbids; use Tcl's http/socket/TclCurl for networking.

Wiring it into the runtime (assembly step): copy unix-commands.tcl into the bundle's lib/tcl8.6/ and append one line to that dir's init.tcl:

catch {source [file join $tcl_library unix-commands.tcl]}

A bare runtime smoke-test main.tcl is then just:

package require Tk
wm title . "iWish"
console show

The 0.2-alpha release attaches a prebuilt iWish.ipa — the full batteries-included build (~114 packages) with the Demos menu — for arm64-apple-ios. It is a development build; re-sign it for your own device/team before installing (Sideloadly/AltStore do this automatically). See INSTALL.md for every install path.

AltStore / SideStore source (one-tap install + auto-updates):

https://raw.githubusercontent.com/johnbuckman/iwish/main/altstore.json

Add it in AltStore → Sources, or tap Add to AltStore.

License

The patches, scripts, shims, and documentation here are licensed under the Tcl/Tk license (a BSD-style license; see LICENSE). They are modifications to / instructions for AndroWish, SDL2, and Tcl/Tk; those projects and the third-party libraries they bundle retain their own original licenses.

src-ios/borg-ios/UIView+Toast.{h,m} are from scalessec/Toast and remain under their original MIT license (© Charles Scalesse), used by borg toast.

About

iWish — a port of Tcl/Tk (AndroWish/undroidwish) to iOS/iPadOS

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages