# zcalc

A small, fast expression calculator written in Zig — with a CLI and a GUI.

Supports `+ - * / % ^`, parentheses, unary minus, and decimal numbers with
standard precedence (`^` binds tightest and is right-associative).

## CLI usage

Three modes:

    zcalc "2 + 3 * 4"     # one-shot: evaluates the argument and exits
    echo "2 ^ 10" | zcalc # pipe: one expression per line, no prompts
    zcalc                 # interactive REPL (enter expressions, `quit` to exit)

## GUI (Dear ImGui)

One GUI app per platform, each rendering through **that platform's native
GPU API** — no software fallback in the shipping builds:

| Binary                       | Window + GPU backend                  |
|------------------------------|----------------------------------------|
| `zcalc-gui-linux-x86_64`     | X11 + OpenGL 3.3 core (GLX)            |
| `zcalc-gui-linux-aarch64`    | X11 + OpenGL 3.3 core (GLX)            |
| `zcalc-gui-windows-x86_64.exe` | Win32 + Direct3D 11                  |
| `zcalc-android.apk`          | NativeActivity + EGL / OpenGL ES 3.0   |

The GUI shows the active graphics driver (`glGetString(GL_RENDERER)` /
DXGI adapter name) and the frame rate in its status bar, and prints the
renderer on startup — so "which GPU is this using?" is always visible.

Usage: type an expression or click the keypad, press **Enter** (or `=`) to
evaluate. The history pane keeps the last 48 results. On Android the keypad
is the input method (no soft-keyboard integration).

### Implementation: Zig everywhere except ImGui

The UI, windowing, input translation and both GPU renderers are Zig:

| Part | Language |
|---|---|
| `gui/app_ui.zig` — calculator UI | Zig |
| `gui/gl_renderer.zig` — OpenGL 3.3 / GLES3 renderer | Zig |
| `gui/dx11_renderer.zig` — Direct3D 11 renderer | Zig |
| `gui/platform_{x11_glx,win32_dx11,android}.zig` — window/input/GL(X)|EGL | Zig |
| `gui/imgui_shim.cpp/.h` — thin C ABI bridge (~330 lines) | C++ |
| `vendor/imgui` — Dear ImGui **core only** (no backends) | C++ (external) |
| X11 types/constants | declared in Zig (no X11 headers) |
| Android NDK `native_app_glue.c` | C (kept platform helper) |

The only external source dependency is Dear ImGui. Its platform/renderer
backends are gone: the OpenGL and D3D11 renderers, the ImGui 1.93 dynamic
texture handling, X11/Win32 input translation and Android touch/key input
are all implemented in Zig against the ImGui core through the shim.

### Building the GUI

    zig build gui-linux-x86_64     # Linux x86_64  (OpenGL)
    zig build gui-linux-aarch64    # Linux ARM64   (OpenGL)
    zig build gui-windows-x86_64   # Windows x64    (Direct3D 11)
    zig build gui                 # all three desktop targets
    zig build gui-smoke           # headless render: capture screenshots and
                                  # compare them to tests/golden/*.ppm
    zig build gui-android         # Android APK (NDK clang + aapt2 + Zig tooling)

`zig build` (default) builds the CLI **and** the desktop GUI apps.
`zig build dist` additionally packages tarballs of everything plus the APK
into `zig-out/dist/`.

### Screenshot regression tests

`zig build gui-smoke` is a visual regression check. It runs each desktop GUI
headless (Xvfb; the Windows binary through Wine), renders a deterministic
frame (fixed sample session, no FPS counter, no caret, no hover states),
captures the framebuffer to a PPM image and compares it against
`tests/golden/gui-<target>.ppm` with a small per-channel tolerance
(`tools/imgcmp.zig`).

- **Match** → the captured screenshot is discarded and the build passes.
- **Mismatch** → the build fails, reporting the differing pixel count and
  the first differing coordinate; the capture is kept in
  `.zig-cache/screenshots/` for inspection.
- Delete a golden file to re-bootstrap it on the next run.

Android notes:
- **No Java anywhere.** The app contains zero Java/Kotlin bytecode
  (`hasCode="false"`); `android.app.NativeActivity` is only the framework
  ABI entry point the OS calls into, and everything else runs natively
  through the NDK. The final APK is just: binary manifest, empty resource
  table, and `lib/arm64-v8a/libzcalc.so`.
- **No build scripts either.** The whole APK pipeline is plain Zig build
  logic: NDK clang compiles the library, `aapt2` produces the binary
  manifest, and `tools/android_apk.zig` assembles the ZIP (with zipalign
  semantics) and signs it with APK Signature Scheme v2 — structural
  packaging and RSA PKCS#1 v1.5 signing are implemented from scratch on
  `std.math.big.int`. There is no Gradle, AGP, CMake, Ninja, Java,
  apksigner, Python, shell script or openssl in the build.
- `zig build gui-android` first compiles the Zig engine as an
  `aarch64-freestanding` static library (`zig-out/lib/libzcengine.a`, no libc
  dependencies) and links it into `libzcalc.so`.
- Runtime dependencies are only OS libraries: bionic libc, libm, libandroid,
  libEGL, libGLESv3 and liblog.
- Only `arm64-v8a` is packaged (the engine is aarch64-only).
- The APK is reproducible: rebuilding produces byte-identical output.
- `android/devkey.pem` / `android/devcert.pem` are a development signing
  identity (regenerate once with `openssl req -x509 -newkey rsa:2048 ...`
  for a real release key; openssl is only needed for key generation, never
  by the build).

Linux notes:
- The GUI binaries are *dynamically linked* on purpose: a GUI needs the
  system graphics stack (X11 + the OpenGL driver) at runtime, so a fully
  static binary is not meaningful. They link only libc/libm/libdl; X11 and
  GL are resolved with `dlopen`, which is also what allows cross-compiling
  the aarch64 build on an x86_64 host. X11/GL headers are vendored under
  `vendor/x11`, so builds need no system development packages.
- Any OpenGL 3.3+ driver works (Mesa llvmpipe/radv, NVIDIA, AMD).

## CLI build & test

Requires Zig 0.17.1 (`/opt/zig` on this server, on PATH for user `prog`).

    zig build test                  # unit tests for the expression engine
    zig build smoke                  # end-to-end CLI checks (one-shot, pipe, errors)
    zig build run -Dexpr="1+2*3"     # build and run natively (development)
    zig build                        # cross-compile every shipping target
    zig build ship                   # same as the default install
    zig build windows-x86_64         # cross-compile a single CLI target
    zig build dist                   # every target + .tar.gz packages in zig-out/dist
    zig build publish                # copy artifacts to dev.albertovillar.com.br


Optimization: defaults to **ReleaseFast**. Override with `-Doptimize=Debug`,
`-Doptimize=ReleaseSafe`, or `-Doptimize=ReleaseSmall` (smallest binaries).

## CLI shipping targets

Binaries land in `zig-out/bin/`:

| Binary                     | Target                        | libc                |
|----------------------------|-------------------------------|---------------------|
| `zcalc-linux-x86_64`       | Linux x86_64                  | musl (static)      |
| `zcalc-linux-aarch64`      | Linux ARM64                   | musl (static)      |
| `zcalc-windows-x86_64.exe` | Windows x64                   | mingw CRT (static)  |
| `zcalc-android-aarch64`    | Android ARM64 (Termux, adb)   | musl (static)      |

All CLI binaries are stripped, single-threaded, statically linked to libc, and
compiled for baseline CPU models — they run on any machine of their
architecture with no runtime dependencies.

Note on Android: modern NDKs no longer ship static bionic, so the Android CLI
target is a fully static musl ELF; it runs under Termux or via `adb shell`
on the Android kernel. The GUI ships as a normal APK instead.

## Publishing

`zig build publish` (optional) builds everything (`dist`) and copies the
artifacts to the download site, regenerating a simple HTML index page:

- publishes to `/var/www/dev` (override with `-Dpublish-dir=...`)
- links use `https://dev.albertovillar.com.br` (override with `-Dpublish-url=...`)
- copies the tarballs, APK, raw executables and README
- computes `SHA256SUMS` (`sha256sum -c` compatible) and writes `index.html`
  with per-file sizes and checksums
- removes previously published files first, so removed artifacts disappear

The site is served by nginx (`/etc/nginx/sites-available/dev-dl`,
described below as `dev.albertovillar.com.br`). To regenerate after a new
build, run:

    zig build publish

## Project layout

    src/calc.zig          expression engine (pure Zig, unit-tested)
    src/engine_c.zig      C ABI wrapper (zc_eval) for the GUI front-ends
    src/main.zig          CLI entry point
    gui/zcalc_gui.cpp     shared calculator UI (Dear ImGui)
    gui/platform_*.cpp    per-platform window + GPU backend
    gui/zcalc_gui.h       shared GUI header
    vendor/imgui         Dear ImGui v1.93.0 (vendored)
    vendor/x11           X11/GL headers for the Linux backend (no system deps)
    android/             manifest + dev signing key (no Gradle/CMake/Java)
    tools/android_apk.zig  ZIP assembly + APK v2 signing (build tool)
    tools/imgcmp.zig       screenshot comparison for the smoke tests
    tools/publish.zig      download site publishing + HTML index generation
    tests/golden/          golden screenshots (PPM) for gui-smoke
