Skip to content

atkirtland/runpy-android

Repository files navigation

RunPy

An Android app that runs a .py file you pick, with a live console: stdout/stderr streamed to the screen, a stdin box for input(), and Ctrl+C / Ctrl+D buttons. Python execution is provided by Chaquopy (bundled CPython 3.11), so no server or network connection is needed to run a script.

How it works

  • MainActivity — UI: pick a file (ACTION_OPEN_DOCUMENT) or reopen one from a recent-scripts list, then Run/Stop and drive the console.
  • PyWorkerService — hosts the Chaquopy interpreter in its own process (android:process=":pyworker"), so a stuck script (e.g. http.server.serve_forever()) can't freeze the UI, and can be killed by killing just that process. Runs as a foreground service with a notification while a script is active, so it survives the app being backgrounded or swiped from Recents.
  • runner.py (app/src/main/python/runner.py) — the harness that actually loads and executes the chosen script via runpy.run_path, with sys.stdout/stderr/stdin rewired to the app's console.
  • runpy_ui.py (app/src/main/python/runpy_ui.py) — the public API scripts can import:
    • runpy_ui.menu(options, prompt="") shows a native, fuzzy-searchable picker dialog. Tapping a row is a shortcut for typing that row's text and submitting it — rofi-style, the user can also just type or paste anything and press Enter/OK to submit that literal text instead, whether or not it matches a listed option. Returns the submitted string, or None if cancelled. Useful for "pick one of these" cases where a plain input() prompt would be tedious — e.g. choosing a URL from a list.
    • runpy_ui.prompt(message="", default="") shows a plain native text-input dialog (no list) and returns the typed text, or None if cancelled.
  • Protocol — the Messenger-based message contract between the activity and the worker service (stdin/stdout/interrupt/done/menu request-reply).
  • IoBridge — a small Kotlin object passed into Python as sys.stdout/stderr; Chaquopy lets Python call its write() directly.
  • UiBridge / FuzzyMatchUiBridge is passed into Python alongside IoBridge so a script's runpy_ui.menu()/runpy_ui.prompt() calls can ask MainActivity to show a dialog (an empty options list renders as a plain input box); FuzzyMatch is a small, dependency-free reimplementation of the subsequence-matching idea behind fzf (there's no real terminal or fzf binary available inside the app's sandboxed Python process, so the live-filtering has to be done natively).
  • RecentScripts — copies each picked file into app-private storage under a random UUID directory (so a hostile filename can't path-traverse) and keeps the 15 most recently used. Entries can be pinned (long-press one in the "Recent scripts" dialog); pinned scripts show up as one-tap buttons at the top of the main screen — tapping one runs it immediately, no separate select-then-Run step — and are protected from the 15-entry eviction that unpinned recents are subject to.

Stop works by killing the whole :pyworker process, which is the only reliable way to interrupt a script blocked in a C-level call (socket accept(), time.sleep(), etc.). Ctrl+C is best-effort: it raises KeyboardInterrupt asynchronously in the script's thread, which only takes effect on the next executed Python bytecode.

By default Path.home() / ~ resolve to the app's private storage. Granting "All files access" (Android 11+) points it at the device's real shared storage instead.

Requirements

  • Android Studio (or the Gradle wrapper + an Android SDK) with SDK 35 installed
  • A device or emulator running Android 7.0+ (API 24), 64-bit (arm64-v8a or x86_64)

Build & run

./gradlew assembleDebug
./gradlew installDebug   # with a device/emulator connected

Or open the project in Android Studio and run the app configuration.

Usage

  1. Launch the app and tap Choose Python File… to pick a .py file (or Recent to reopen one you've run before — long-press an entry there to pin/unpin it).
  2. Optionally grant full storage access so Path.home() points at real device storage.
  3. Tap Run (or tap a pinned script's button at the top to run it immediately). Output streams into the console below; use the stdin box for input(), and Ctrl+C/Ctrl+D for interrupt/EOF.
  4. Tap Stop to hard-kill a running script.

Project layout

app/src/main/java/com/example/runpy/   Kotlin sources (Activity, service, protocol)
app/src/main/python/runner.py          Python execution harness
app/src/main/res/                      Layout, strings, theme
app/src/main/AndroidManifest.xml

TODO

  • support share python script to it to run

About

A vibecoded Android app for running Python scripts without Termux

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors