ucharm is a CLI toolkit for building beautiful, fast, tiny command-line applications with Python syntax powered by PocketPy. The goal is a Pythonic DX with instant startup and tiny single-file binaries.
Repository: https://github.com/ucharmdev/ucharm
┌─────────────────────────────────────┐
│ Your Python Code │
│ (standard Python syntax) │
├─────────────────────────────────────┤
│ PocketPy VM │
│ (bytecode interpreter) │
├─────────────────────────────────────┤
│ Runtime Modules (Zig) │
│ term, ansi, charm, input, ... │
├─────────────────────────────────────┤
│ Single Binary │
│ (universal, no dependencies) │
└─────────────────────────────────────┘
The ucharm CLI is a fully self-contained binary that embeds:
- pocketpy-ucharm: PocketPy with runtime modules implemented in Zig
This means ucharm run script.py works with zero external dependencies. All TUI functionality (boxes, colors, prompts) is provided by Zig runtime modules.
ucharm/
├── cli/ # Zig CLI tool (ucharm)
│ ├── src/
│ │ ├── main.zig # Entry point, command routing
│ │ ├── build_cmd.zig # Build command (single/executable/universal)
│ │ ├── init_cmd.zig # Initialize project (stubs, AI instructions)
│ │ ├── new_cmd.zig # Project scaffolding
│ │ ├── run_cmd.zig # Run scripts (embeds pocketpy)
│ │ ├── stubs/ # Embedded binaries and type stubs
│ │ └── templates/ # AI instruction templates (edit these!)
│ └── build.zig # Zig build configuration
├── runtime/ # Zig runtime modules (PocketPy bindings + cores)
│ ├── ucharm/ # ucharm-native UX modules
│ │ ├── ansi/
│ │ ├── charm/
│ │ ├── input/
│ │ ├── term/
│ │ └── args/
│ └── compat/ # CPython-compat modules (argparse, csv, etc.)
├── pocketpy/ # PocketPy runtime build + Zig modules
│ ├── src/modules/ # PocketPy module bindings (Zig)
│ ├── src/runtime.zig # Module registration
│ └── build.zig # pocketpy-ucharm build
├── loader/ # Universal binary loader (Zig)
├── website/ # Documentation site (Fumadocs + Next.js)
│ ├── content/docs/ # MDX documentation files
│ ├── src/components/ # React components (Terminal, etc.)
│ └── src/app/ # Next.js app
├── scripts/
├── tests/
└── README.md
# Build PocketPy runtime
cd pocketpy && zig build -Doptimize=ReleaseSmall
# Build CLI
cd cli && zig build -Doptimize=ReleaseSmall
# Run Vision tests
python3 tests/vision/run_vision.py --timeout 20 --runtime ./pocketpy/zig-out/bin/pocketpy-ucharm
# Run compatibility tests (defaults to pocketpy-ucharm)
python3 tests/compat_runner.py --report --runtime ./pocketpy/zig-out/bin/pocketpy-ucharm
# Run a script
./cli/zig-out/bin/ucharm run examples/demo.pyAll new functionality must be implemented in Zig. Do not add C or Python implementations for runtime modules. Use the PocketPy C API from Zig as needed, but keep the module logic in Zig.
This ensures:
- Consistent architecture
- Small binaries
- High performance
Avoid direct edits to pocketpy/vendor/pocketpy.c / pocketpy.h. If a vendor change is necessary, capture it as a patch in pocketpy/patches/ and update pocketpy/patches/manifest.json so scripts/apply-pocketpy-patches.sh stays idempotent.
PocketPy is vendored from upstream releases. Any patches become a maintenance burden that must be re-applied on every update. Instead:
-
Extend existing modules from Zig - Use
c.py_getmodule("modulename")to get a built-in module and add functions to it. Example:runtime/compat/math.zigextends the built-inmathmodule withsinh,cosh,tanh,frexp,ldexp. -
Create new modules in Zig - For missing stdlib modules, implement them entirely in Zig under
runtime/compat/. -
Report upstream issues - If PocketPy is missing functionality that can't be added via Zig, open an issue or PR upstream.
To update PocketPy:
# Download latest release
curl -sL https://github.com/pocketpy/pocketpy/releases/download/vX.Y.Z/pocketpy.c -o pocketpy/vendor/pocketpy.c
curl -sL https://github.com/pocketpy/pocketpy/releases/download/vX.Y.Z/pocketpy.h -o pocketpy/vendor/pocketpy.hIf we must patch PocketPy, keep it:
- Small and surgical
- Marked with
ucharm patch:anchors inpocketpy/vendor/pocketpy.c - Tracked as a re-applicable patch file under
pocketpy/patches/
After updating PocketPy, re-apply and verify:
./scripts/apply-pocketpy-patches.sh
python3 scripts/verify-pocketpy-patches.py --check-upstreamPocketPy treats match as a hard keyword, but Python 3.10+ treats it as a soft keyword (only a keyword in pattern matching contexts). This breaks re.match() and similar APIs.
After updating PocketPy, apply this patch to pocketpy/vendor/pocketpy.c in the exprAttrib function:
static Error* exprAttrib(Compiler* self) {
// ucharm patch: allow 'match' soft keyword as attribute name (for re.match, etc.)
if(curr()->type == TK_MATCH) {
advance();
} else {
consume(TK_ID);
}
py_Name name = py_namev(Token__sv(prev()));
// ... rest of function
}This patch is tracked in pocketpy/patches/0001-match-soft-keyword.patch (along with a small patchset required for CPython-compat modules).
Prefer upstream fixes where possible; keep the local patchset minimal.
Some Python features are not supported by PocketPy:
-
Implicit string concatenation -
"a" "b"orf"a" f"b"syntax is not supported. Use explicit concatenation:"a" + "b". -
Native function kwargs - Use
funcSigWrappedinstead offuncWrappedfor kwargs support (see Kwargs Support section below). -
Some stdlib modules - Missing modules are implemented in
runtime/compat/. Runpython3 tests/compat_runner.py --reportto see current compatibility status.
Goals: fast startup, small code, predictable behavior.
Guidelines:
- Keep APIs minimal and explicit; mirror CPython only where needed.
- Prefer pure Zig for logic; use OS syscalls via
std.posix. - Validate inputs early and return clear errors via PocketPy exceptions.
- Avoid allocations in hot paths; use stack buffers and small helpers.
- Keep module state explicit and minimal (no hidden globals unless required).
- Add short comments only when the intent is non-obvious.
- Core logic in
runtime/<module>/<module>.zig(if shared elsewhere). - PocketPy bindings in
runtime/**/pocketpy.zig. - Register modules in
pocketpy/src/runtime.zig. - Update type stubs in
stubs/and copy tocli/src/stubs/.
Use pocketpy/src/pk.zig for all new module bindings. It provides safer, ergonomic wrappers:
const pk = @import("../../pocketpy/src/pk.zig");
const c = pk.c;
fn greetFn(ctx: *pk.Context) bool {
const name = ctx.argStr(0) orelse return ctx.typeError("expected string");
// ... do work ...
return ctx.returnStr(result);
}
pub fn register() void {
const builder = pk.ModuleBuilder.new("mymodule");
_ = builder.funcWrapped("greet", 1, 1, greetFn);
}Key types:
pk.Value- Safe wrapper aroundpy_TValuewith type checks and extractionpk.Context- Argument access (argStr,argInt,argFloat,argBool) and returns (returnStr,returnInt, etc.)pk.ModuleBuilder- Fluent API for module creation and function bindingpk.TypeBuilder- Fluent API for custom type creation with methods and properties
Critical pattern - String arguments:
// CORRECT: Use argStr which accesses argv directly
const s = ctx.argStr(0) orelse return ctx.typeError("expected string");
// OK (but prefer argStr for simple args): Value.toStr() returns a slice into the
// Python string data. Keep the Value alive for as long as you need the slice.
var v = ctx.arg(0) orelse return false;
const s = v.toStr() orelse return ctx.typeError("expected string");Register clobbering: The PocketPy C API uses global registers (py_r0(), py_r1(), etc.) that get overwritten by many API calls. The pk.Value type copies values to local storage; prefer ctx.argStr() for direct argv access when extracting string arguments.
Native functions (nativefunc) in PocketPy do NOT support keyword arguments. To get kwargs support, use signature-based binding which creates a function object instead:
// NO kwargs support - uses py_bindfunc internally
builder.funcWrapped("style", 1, 8, styleFn);
// WITH kwargs support - uses py_bind with signature
builder.funcSigWrapped("style(text, fg=None, bg=None, bold=False)", 1, 8, styleFn);The signature tells PocketPy the parameter names and defaults, enabling style("hello", bold=True) syntax.
You can add methods to built-in types like str from Zig without modifying pocketpy.c:
pub fn register() void {
const str_type = c.py_tpobject(c.tp_str);
c.py_bind(str_type, "isdigit(self)", pk.wrapFn(1, 1, isdigitFn));
}Example: runtime/compat/str_ext.zig adds isdigit(), isalpha(), isalnum(), etc. to the str type.
For edge cases where pk.zig doesn't fit:
- Use
py_bind(module, "fn(sig)", fn)for functions. - Use
py_check*andpy_to*to validate/convert values. - Return strings with
py_newstr/py_newstrn/py_newstrv. - Use
py_exception(tp_TypeError, "...")for invalid arguments. - Use stack-allocated
py_TValuefor dict items -py_dict_setitemtakespy_Refpointers that must remain valid.
When adding or modifying runtime modules, update:
stubs/*.pyiand copy tocli/src/stubs/.cli/src/templates/AGENTS.md,cli/src/templates/CLAUDE.md, andcli/src/templates/copilot-instructions.md.README.mdif public APIs or workflows changed.website/content/docs/if module APIs or features changed.
The documentation website is in website/ and deployed via Vercel.
# Run dev server
cd website && bun run dev
# Build for production
cd website && bun run buildUpdate docs when:
- Adding new modules →
website/content/docs/modules/ - Changing APIs → Update relevant MDX files
- Adding features → Update getting started guides
Use the /commit command before committing. It runs the repo checklist and keeps docs/templates/stubs in sync.