Revision history for Notcurses::Native

0.4.0  2026-05-20T09:19:16+01:00
    - CI fix: codec-probe PNG fixture moved from chunli44.png →
      chunli01.png. The notcurses repo has ~11 chunli* PNGs that are
      symlinks to earlier frames (chunli32-37, 39, 41-44 → others).
      On Unix git checks them out as real symlinks; on Windows git's
      default core.symlinks=false materializes each as a 12-byte
      text file containing the target's name, so the codec probe
      opened "chunli40.png" as content, libavcodec read 8 bytes,
      saw "chunli40" instead of the PNG magic 89 50 4E 47 0D 0A 1A
      0A, and rejected the file with "Invalid PNG signature
      0x6368756E6C693430". chunli01.png is a regular file and works
      identically on every lane. Header comment in codec-probe.c
      now warns future authors to pick regular files only.
    - CI fix: fetch-notcurses-source.sh now strips trailing \r from
      each parsed line of NOTCURSES_FORK before applying the 40-char
      lowercase-hex SHA check. Windows checkouts with
      core.autocrlf=true (default for Git for Windows) were leaving
      a CR on the SHA, pushing it to 41 chars and failing the regex
      with "sha=… must be a 40-char lowercase hex string." `read -r`
      strips \n but not \r, so the fix is one `${line%$'\r'}` trim
      per iteration. Linux glibc/musl workflows' `awk` SHA extraction
      gains a `tr -d '\r'` for the same belt-and-braces reason.
      Primary fix: new top-level .gitattributes pins NOTCURSES_FORK
      / BINARY_TAG / *.sh / *.yml / *.yaml to `text eol=lf` so fresh
      checkouts no longer get CRLF in the first place. Existing
      clones renormalize via `git add --renormalize .` (or the
      bash/awk hardening covers them transparently).
    - vendor/notcurses/ deleted from the repo. Build.rakumod's shim-
      compile path now resolves include headers (and the Windows
      import lib) by calling !ensure-notcurses-source — the same SHA-
      keyed git fetch the source-build fallback uses, wrapped in a
      try{} so prebuilt-only installs that hit the shim short-circuit
      don't trigger an unnecessary fetch. Selkie's
      `examples/viewported-card-list.raku` previously reached into
      the sibling Notcurses-Native vendor/notcurses/data/ dir for
      sample images; Selkie 0.7.5+ vendors the 10 referenced images
      under its own examples/data/ tree. .gitignore drops the now-
      meaningless `vendor/notcurses/build/` entry.
    - CI/CD: all four prebuilt-binary lanes (macOS arm64/x86_64,
      Linux glibc x86_64/aarch64, Linux musl x86_64/aarch64, Windows
      x86_64/arm64) and the bundle / codec-probe helpers now fetch
      notcurses from the SHA pinned in NOTCURSES_FORK instead of
      reading from vendor/notcurses. Single source of truth — both
      install-time (Build.rakumod) and release-time (CI) build from
      the exact same commit, so no more "I bumped NOTCURSES_FORK
      and forgot to sync vendor/notcurses" drift. New shared script
      scripts/ci/fetch-notcurses-source.sh mirrors Build.rakumod's
      !ensure-notcurses-source: git init + fetch-by-SHA into
      $NOTCURSES_SRC_CACHE/<sha>/, idempotent reuse on cache-hit,
      writes NOTCURSES_SRC_DIR=<path> to $GITHUB_ENV when running
      under GHA. Workflows cache the per-SHA checkout via
      actions/cache keyed on the pin so bumping NOTCURSES_FORK auto-
      invalidates. Migrations:
        * scripts/ci/build-linux-glibc.sh + build-linux-musl.sh +
          bundle-elf.sh now consume $NOTCURSES_SRC_DIR. musl lane
          adds `git` to its `apk add` list.
        * .github/workflows/_build-{macos,windows}.yml gain a
          "Fetch notcurses source" step; subsequent steps use
          $NOTCURSES_SRC_DIR. Windows lane adds `git` to its msys2
          install set.
        * .github/actions/bundle-{dll,macos}/action.yml read
          $NOTCURSES_SRC_DIR/build instead of vendor/notcurses/build.
          bundle-dll converts the Windows-style env value through
          cygpath for MSYS2 bash; bundle-elf captures $workspace
          before cd'ing into the build dir so absolute-path copies
          don't depend on `cd ../../..` relative math.
        * scripts/ci/codec-probe.c switches from $WORKSPACE_DIR + a
          hardcoded vendor/notcurses/data relpath to a single
          $NOTCURSES_DATA_DIR env var (set by run-codec-probe.sh to
          $NOTCURSES_SRC_DIR/data). FIXTURES paths now bare basenames.
        * NOTCURSES_FORK header updated — drops the now-obsolete
          "CI prebuilt lanes still read from vendor; collapse onto
          the fetch once CI is migrated" caveat.
        * .gitignore: ignore _ci-cache/ and bundle/.
      Escape hatch unchanged: NOTCURSES_NATIVE_VENDOR_DIR=<path>
      short-circuits the fetch for fork iteration / airgapped builds.
    - new module lib/Notcurses/Native/Str.rakumod with three helpers:
      libc-name() (musl/glibc/macOS/Windows libc resolver shared with
      Native.rakumod's setenv path), strdup-copy-and-free() (decodes
      a malloc'd C char* and frees via libc free), and
      borrowed-str-from-pointer() (decodes a pointer into caller-
      provided storage without freeing). Used to fix the memory-leak
      class below.
    - memory leak fix: every binding that returned a heap-allocated
      char* and was declared `--> Str` silently leaked the original
      malloc'd pointer at every call. MoarVM's NativeCall copies the
      bytes into a Raku Str and does NOT free the source. Affected
      functions in Context.rakumod (ncwcsrtombs, notcurses_at_yx,
      notcurses_detected_terminal, notcurses_accountname,
      notcurses_hostname, notcurses_osversion), Cell.rakumod
      (nccell_extract, nccell_strdup), Plane.rakumod
      (ncplane_at_cursor, ncplane_at_yx, ncplane_contents),
      Direct.rakumod (ncdirect_readline, ncdirect_detected_terminal),
      and Widgets.rakumod (ncreader_contents). Each now rebinds to
      `--> Pointer` under a `_<name>_raw` symbol and exposes the
      public name as a Raku wrapper that calls strdup-copy-and-free.
    - pointer-into-buf returns (Context.rakumod's ncnmetric, ncqprefix,
      nciprefix, ncbprefix family) rebind similarly but route through
      borrowed-str-from-pointer so the caller's CArray[uint8] $buf
      isn't double-freed.
    - library-owned static pointer returns (notcurses_str_blitter,
      notcurses_str_scalemode, notcurses_version, ncplane_name,
      ncselector_selected/previtem/nextitem, ncmenu_selected/
      mouse_selected, nctab_name, nctabbed_separator,
      nccell_extended_gcluster) keep their `--> Str` binding and
      gain an `OWNED-BY-LIBRARY` Pod comment so a future audit
      doesn't flag them as leaks.
    - new API: ncpile-render-to-string(NcplaneHandle --> Str) in
      Context.rakumod. Owns the malloc'd buffer lifecycle for
      ncpile_render_to_buffer — no more "caller forgets to free
      the output pointer" leaks for snapshot/rendering consumers.
      The raw ncpile_render_to_buffer binding stays exported for
      callers who want to manage the lifecycle themselves.
    - behavior fix: setenv(3) failures in lib/Notcurses/Native.rakumod
      now `note` to stderr instead of being silently ignored. Pre-fix,
      a libc setenv failure (ENOMEM/EINVAL) silently desynced %*ENV
      and the C environment — ncurses then couldn't find terminfo and
      notcurses_core_init failed with a vague error. The libc resolver
      replaces the previous hardcoded 'libc.so.6' so musl Alpine /
      distroless containers no longer dlopen-fail at module load.
      The per-call redeclaration of mac_setenv / linux_setenv is
      gone too — one module-level binding now serves every call.

0.3.4  2026-05-16T04:01:59+01:00
    - Build.rakumod: drop $MIN-GLIBC from v2.35 to v2.28. The Linux
      glibc lanes are rebased onto manylinux_2_28 containers
      (quay.io/pypa/manylinux_2_28_{x86_64,aarch64}, RHEL 8 baseline)
      so the prebuilts now load on every glibc Linux distro under
      active maintenance in 2026 — RHEL 8+, Ubuntu 18.10+, Debian
      10+. Previously the 2.35 floor meant Ubuntu 20.04 / Debian 11
      users fell back to a 5-15 min CMake source build. (manylinux2014
      / RHEL 7 / glibc 2.17 was the first target but pypa retired it
      in March 2025 and its CentOS 7 yum mirrors are decaying after
      the June 2024 EOL — manylinux_2_28 is the maintained successor.)
    - Build.rakumod: add libc axis to detect-platform. Linux keys
      now carry a libc suffix (`linux-x86_64-glibc`,
      `linux-x86_64-musl`, etc.); non-Linux keys unchanged. New
      `detect-libc` method probes `/lib/ld-musl-*.so.1` then falls
      back to `!detect-glibc-version`. Implicitly fixes a silent bug
      where musl users (Alpine / Postmarket OS / Void) were
      downloading the glibc artefact and segfaulting at first dlopen
      because `ldd --version` returns non-zero on musl, which
      short-circuited the glibc-too-old guard.
    - %PLATFORM-SLUGS: add linux-x86_64-musl, linux-aarch64-musl,
      linux-x86_64-glibc, linux-aarch64-glibc keys (the glibc entries
      are renamed from the previous `linux-<arch>` keys). The musl
      lanes are built in alpine:3.20 containers (musl 1.2.5 headers,
      1.20+ runtime floor per notcurses' declared support level).
    - Unknown-platform diagnostic now reports the detected libc on
      Linux, so a user on an unsupported libc-arch combination can
      see exactly which axis didn't match.
    - %PLATFORM-SLUGS: add 'darwin-x86_64' => 'macos-x86_64' so Intel
      Macs (Mac Pro 2013, iMac Pro, the 2016-2020 Intel MacBook Pro
      line, Mac Pro 2019) and Hackintoshes get a prebuilt download
      instead of a 5-15 min CMake source build. The CI repo
      (m-doughty/Notcurses-Native) produces the artefact on an arm64
      GHA runner under Rosetta 2 — clang under Rosetta emits ordinary
      x86_64 Mach-O that native Intel Macs run identically. Pinned at
      MACOSX_DEPLOYMENT_TARGET=10.15 (Catalina) so the artefact loads
      on every Intel Mac Apple supports back to ~2012 hardware. The
      build path was chosen because GitHub's macos-13 native-x86_64
      runner is on its way out; arm64-with-Rosetta is the long-lived
      option. Side effect: x86_64 Rakudo running under Rosetta on
      Apple Silicon now gets a working prebuilt too instead of
      falling through to source build — not the audience this lane
      targets, but a free win.
    - NOTE: this version (0.3.4) still ships BINARY_TAG=r5. The
      darwin-x86_64 slug-map entry only becomes useful once the
      m-doughty/Notcurses-Native CI repo publishes a binaries-
      notcurses-3.0.17-r6 release that includes the new
      notcurses-macos-x86_64.tar.gz artefact; until then, Intel Mac
      installs will still take the source-build fallback (just like
      pre-0.3.4). When r6 ships, BINARY_TAG → r6 + resources/
      checksums.txt update happen as a single follow-up commit
      (resources/checksums.txt has the procedure documented at the
      top of the file). That commit cuts 0.3.5.
    - Build.rakumod unknown-platform diagnostic: dropped the
      Apple-Silicon-Rosetta hint introduced in 0.3.3. With
      darwin-x86_64 now mapped, that case never reaches the
      `without $plat` branch — the hint was unreachable and would
      have misled anyone hitting the diagnostic for an actually-novel
      platform.
    - t/16-build-detect-platform.rakutest: updated darwin/x86_64
      assertion (now expects 'macos-x86_64') and added darwin-x86_64
      to known-platform-keys expected set. Unknown-platform Str:U
      coverage stays via the freebsd/riscv64 case.
    - Build.rakumod: fix `zef install` aborting with "Type check failed
      for return value; expected Str but got Any" on platforms not
      mapped in %PLATFORM-SLUGS. The headline case is x86_64 Rakudo
      running under Rosetta on Apple Silicon, which reports
      $*KERNEL.hardware = 'x86_64' → key 'darwin-x86_64' → unmapped.
      Root cause: the slug-map hash was untyped, so missing-key lookups
      returned `Any` (not `Str`) and tripped detect-platform's `--> Str`
      constraint before the intended source-build fallback at line 102
      could fire. Typed the hash `my Str %PLATFORM-SLUGS`. While here,
      promoted `!detect-platform` to a public `detect-platform(:$os,
      :$hardware --> Str)` so tests can inject kernel pairs without
      having to override `$*KERNEL`, and added `detect-platform-key` /
      `known-platform-keys` helpers used by the unknown-platform
      diagnostic.
    - Build.rakumod: the unknown-platform warning now lists the exact
      key that was looked up (e.g. 'darwin-x86_64'), the values of
      $*KERNEL.name / $*KERNEL.hardware, the full set of platforms
      that DO have prebuilts, and an Apple-Silicon-under-Rosetta hint.
      Saves the next person an hour of staring at the previous
      one-line message.
    - t/16-build-detect-platform.rakutest: new regression test
      covering every entry in %PLATFORM-SLUGS plus the
      darwin-x86_64 (Rosetta) and freebsd-riscv64 (fully unknown)
      fall-throughs. Would have failed on 0.3.2.

0.3.2  2026-05-12T16:55:21+01:00
    - lib/Notcurses/Native.rakumod: convert the `$nc-lib`,
      `$ffi-lib`, `$core-lib`, `$shim-lib` library-path bindings
      from `constant` to state-cached subs (`sub nc-lib { state $r =
      _resolve-lib(...); $r }`). `constant X = _resolve-lib(...)`
      ran at compile time and baked the resolved path into the
      precompiled bytecode — and Rakudo doesn't track
      `resources/BINARY_TAG` as a precomp dependency. A BINARY_TAG
      bump (which moves staged libs to a new versioned directory
      and may GC the previous one) would leave the precomp pointing
      at the old path, producing "Cannot locate native library"
      errors on freshly installed packages until the user manually
      ran `rm -rf ~/.raku/precomp/`. Deferring resolution to first
      sub-call means each process picks up the current tag,
      regardless of when the precomp was built. NativeCall accepts
      a Callable for `is native()` and invokes it lazily on first
      use of each bound sub. All 600+ bindings in `lib/Notcurses/
      Native/*.rakumod` updated from `is native($X-lib)` to
      `is native(&X-lib)` to match.
    - t/15-shim-presence.rakutest: updated to call `shim-lib()`
      instead of treating `$shim-lib` as a Str — the constant is
      now a state-cached sub.
    - BINARY_TAG bumped to binaries-notcurses-3.0.17-r5. The r4 tag
      was never published — the workflow that produces its archives
      failed before the release step on Linux (the
      --unresolved-symbols=ignore-in-shared-libs flag was misnamed
      and left object-file unresolved refs intact) and on Windows
      (the objdump-based export check matched the wrong format).
      The shim binary's runtime contract changed as part of fixing
      those builds — Linux now links directly against
      libnotcurses-core with DT_NEEDED + DT_RUNPATH=$ORIGIN
      instead of deferring resolution to the host process's flat
      symbol namespace; macOS now reserves -Wl,-headerpad_max_-
      install_names so any future install_name_tool relocation
      doesn't overflow the Mach-O header — so the new artifact
      isn't bit-identical to the r4 spec even though the API is
      the same. Bumping the tag avoids any ambiguity about which
      shim build downstream consumers are running against.
    - .github/workflows/build-binaries.yml: Linux shim step now
      links the shim explicitly against bundle's libnotcurses-core
      (-Lbundle -lnotcurses-core) and sets DT_RUNPATH=$ORIGIN via
      patchelf. Mirrors Vips-Native's working pattern. Drops the
      strip --strip-unneeded step that was clearing the regular
      symtab and breaking the post-strip nm -g check.
    - .github/workflows/build-binaries.yml: Windows shim step
      replaces the objdump -p export-table regex with
      nm -g --defined-only (matches Vips-Native; stable across
      MinGW/UCRT/CLANGARM64 binutils versions where the objdump
      output format differs). Drops the strip --strip-unneeded
      step too — same regular-symtab issue manifests on PE.
    - .github/workflows/build-binaries.yml: macOS shim step adds
      -Wl,-headerpad_max_install_names so any future
      install_name_tool relocation has the load-command padding
      it needs (defensive — Build.rakumod's
      !rewrite-macos-install-names explicitly skips the shim
      today, but this is belt-and-braces against future relocators).
    - Build.rakumod: !try-compile-shim mirrors the workflow
      changes — Linux links explicitly, macOS adds the
      headerpad option. Source-build path now matches the
      prebuilt path in every meaningful way.
    - Build.rakumod: !rewrite-macos-install-names glob now
      excludes `libnotcurses_native_shim.dylib` via
      `!~~ /'_shim'/`. The shim has no @rpath/libnotcurses*.dylib
      dependencies to rewrite (it's compiled -undefined dynamic_-
      lookup) and its short @loader_path install-name can't be
      replaced with the absolute staged path without headerpad
      space — on a force-install the previous run's shim was
      already on disk and the rewrite pass loudly refused it.

0.3.1  2026-05-12T16:01:23+01:00
    - CI/CD republish binaries
    - src/notcurses_native_shim.c: new C-side perf-shim module with
      batched primitives that are unaffordable to express call-per-
      cell over Raku's NativeCall boundary. Initial export
      `notcurses_native_copy_cells`, a direct port of
      Selkie::Widget::ViewportedCardList's per-cell read+write loop
      (copies a rows × cols slice from one ncplane to another with
      base-cell substitution for empty source cells, matching
      ncplane_at_yx semantics). Selkie's VCL!copy-cells used to spend
      ~75,000 NativeCall trips per render on a chat with five visible
      cards × five widget planes × ~3000 cells; the shim collapses
      that to one. Linked with -undefined dynamic_lookup (macOS) or
      -Wl,--unresolved-symbols=ignore-in-shared-libs (Linux) so it
      has no link-time dependency on libnotcurses — symbols resolve
      at runtime against the host process's already-loaded
      libnotcurses.
    - Build.rakumod: !try-compile-shim stages a compiled
      libnotcurses_native_shim alongside the existing notcurses libs
      on every install path (prebuilt, source build, fallback).
      Non-fatal when no C toolchain is available — Selkie's binding
      flips an internal latch and falls back to the per-cell Raku
      merge with a one-shot user-visible warning so the perf cost is
      attributable.
    - lib/Notcurses/Native.rakumod: $shim-lib constant exported,
      resolved by the same _resolve-lib lookup as the core libs.
    - lib/Notcurses/Native/Plane.rakumod: notcurses_native_copy_cells
      Raku binding bound to $shim-lib.
    - .github/workflows/build-binaries.yml: CI builds + ships the
      shim binary in the prebuilt archives so users on supported
      platforms don't need a C toolchain at install time.
    - BINARY_TAG bumped to binaries-notcurses-3.0.17-r4 so prebuilt
      caches invalidate and consumers pick up archives that include
      the shim.
    - t/15-shim-presence.rakutest: assert libnotcurses_native_shim
      is staged. Gated on NOTCURSES_NATIVE_REQUIRE_SHIM=1 — set in
      both test.yml and glibc-fallback.yml workflows so CI fails
      loudly if either the prebuilt archive drops the shim or
      Build.rakumod's !try-compile-shim silently failed on the
      source-build path. End-user installs without the env var skip
      the test cleanly so a missing toolchain doesn't break the
      install.
    - lib/Notcurses/Native.rakumod: $NOTCURSES_NATIVE_LIB_DIR
      override doc tightened to spell out that the patched
      libnotcurses we ship (0.3.0's ncvisual_blit_internal begy/begx
      fix) is ABI-compatible at the C symbol level but BEHAVIOURALLY
      incompatible — pointing the override at vanilla system
      notcurses 3.0.17 silently misrenders any clipped sprixel (chat
      avatars at the top of the scroll, for example).

0.3.0  2026-05-12T00:11:57+01:00
    - vendor/notcurses (src/lib/visual.c): patch ncvisual_blit_internal
      to honor ncvisual_options.begy/begx/leny/lenx for all blit paths
      (generic resize, FFmpeg, OIIO, all sprixel and cell blitters).
      Upstream notcurses 3.0.17 silently drops these fields whenever
      the source needs resizing — sprixel blitters take only
      (data, leny, lenx) with no begy/begx parameter and consume from
      data[0], so a cropped blit shows the top of the source instead
      of the requested sub-region. Cell blitters reference begy/begx
      but index the resized buffer with the input-space offset, which
      is a latent out-of-bounds read for begy > 0.
      The fix introduces a static ncvisual_subregion_internal helper
      that materializes the requested source region once at the entry
      of ncvisual_blit_internal (single alloc + single row-loop
      memcpy, honoring source rowstride padding and re-padding the
      destination via pad_for_image). Downstream backends then see a
      "full source" and consume normally, with begy/begx/leny/lenx
      zeroed in a local blitterargs copy.
      Fixes Selkie's ViewportedCardList rendering the wrong rows when
      an image is partially clipped at the top of the viewport, and
      incidentally addresses the FIXMEs at upstream
      src/lib/internal.h (blitterargs comment) and src/media/oiio.cpp.
    - Build.rakumod: rewrite macOS install-names to absolute staged
      paths via install_name_tool after staging the dylibs. Without
      this, dyld resolves `@rpath/libnotcurses-core.3.dylib` (and
      siblings) through the LOADER CHAIN'S rpaths, which on a typical
      Homebrew-Raku setup means `raku`'s `@executable_path/../lib`
      (= `/opt/homebrew/lib`) is searched first — and if Homebrew's
      notcurses is also installed there, dyld silently loads
      Homebrew's unpatched library instead of ours. Tests and the
      module load succeed (path resolution at the Raku level still
      reports our staged path) but the actual symbol resolution
      runs the wrong code. Baking the absolute path into the
      install-name eliminates dyld's discretion.
      Also expanded find-lib to stage every version variant
      (`libfoo.dylib`, `libfoo.3.dylib`, `libfoo.3.0.17.dylib`)
      because the rewritten install-names point to the
      `.3.dylib` symlinks, which must exist at the staged path.
    - BINARY_TAG bumped to binaries-notcurses-3.0.17-r3 to invalidate
      prebuilt binary caches and force consumers to pick up the
      patched library.

0.2.6  2026-04-29T23:53:41+01:00
    - Bump Github actions to use node 24+
    - `Build.rakumod` now garbage-collects sibling staged dirs for
      older BINARY_TAGs after each successful install. Without this,
      every release accumulated another `binaries-notcurses-*` dir
      under `~/.local/share/Notcurses-Native/`, where stale Raku
      precomp could load the older libs alongside the new ones —
      cf. the Vips::Native r7→r8 incident that revealed this class
      of bug. Set `NOTCURSES_NATIVE_KEEP_OLD_STAGES=1` to opt out
      (e.g. when intentionally pinning multiple versions for testing).

0.2.5  2026-04-16T03:16:51+01:00
    - Native.rakumod: set TERMINFO_DIRS at module load via libc
      setenv(3) so ncurses finds terminal definitions on systems
      without Homebrew ncurses installed. Our bundled libncursesw
      was compiled against Homebrew's ncurses, which bakes the
      terminfo search path to the Homebrew cellar — on a fresh
      Mac without `brew install ncurses`, that path doesn't exist
      and notcurses_core_init fails with "No terminal available"
      even though the libraries loaded fine. The fix points
      TERMINFO_DIRS at macOS's system /usr/share/terminfo/ (always
      present) plus common Linux paths. Uses the same _setenv-c
      pattern as Vips-Native (Raku's %*ENV doesn't propagate to C
      getenv on macOS). Respects user-set TERMINFO_DIRS.
    - test.yml: run prebuilt-path t/ tests before installing
      system deps (brew/apt) so the self-contained bundle is
      exercised with no system notcurses present. xt/ tests
      (terminal-dependent) run after system deps install since
      they need terminfo data on macOS. Saves several minutes per
      failed run.

0.2.4  2026-04-16T02:58:01+01:00
    - Build.rakumod: detect system glibc via `ldd --version` and
      fall back to CMake source compile when it's older than the
      prebuilt target (currently v2.35, matching the ubuntu-22.04
      CI runner). Previously, users on Ubuntu 20.04 / Debian 11 /
      RHEL 8 downloaded prebuilt libnotcurses + ffmpeg libs that
      loaded but failed at first symbol use with "GLIBC_2.xx not
      found". The guard fires before the download so affected users
      just see a one-line note and a ~3–5 min source compile
      (core-only if their ffmpeg dev packages are missing) instead
      of a broken install. NOTCURSES_NATIVE_BINARY_ONLY=1 now
      hard-fails with a clear message on old-glibc systems rather
      than producing a broken install.
    - New CI workflow .github/workflows/glibc-fallback.yml: runs
      `zef install .` inside an ubuntu:20.04 container (glibc 2.31)
      with apt-installed cmake + ncurses/unistring/deflate dev
      packages and asserts both that the fallback message appears
      in the build log and that the source-compiled libs load.

0.2.3  2026-04-15T02:52:48+01:00
    - CI: rework test.yml to install via zef (which runs Build.rakumod
      → downloads prebuilt → SHA-verifies → stages libs to the XDG
      data dir) instead of building notcurses by hand and dropping
      libs into resources/lib/. The hand-build step was a vestige of
      the pre-XDG-staging layout and stopped working when META6.json
      dropped the lib resource entries in 0.2.2 — tests started
      failing with "cannot open shared object file" because nothing
      was actually staging libs to where Native.rakumod now looks.
      Workflow now also re-installs with NOTCURSES_NATIVE_BUILD_FROM_
      SOURCE=1 as a second pass to keep the CMake fallback path
      covered on every CI run.

0.2.2  2026-04-15T02:46:26+01:00
    - Stage native libs to an XDG-style data dir
      ($XDG_DATA_HOME/Notcurses-Native/<binary-tag>/lib/, or
      $LOCALAPPDATA on Windows, ~/.local/share fallback) instead of
      the dist's resources/. Reason: zef hashes every staged resource
      filename to a SHA-keyed name, which silently breaks the inter-
      dylib references baked into notcurses (libnotcurses.dylib loads
      libnotcurses-core.3.0.17.dylib via @loader_path; same on Linux
      with $ORIGIN, same on Windows with sibling-DLL search). Hashed
      filenames meant the loader couldn't find any sibling lib by its
      real name, and `Notcurses::Native` died at first dlopen with a
      cryptic "Library not loaded" error post-install. Tests passed at
      install time (pre-staging) so the bug only surfaced when
      consumers like Selkie tried to actually use the module.
    - META6.json no longer lists the 9 dylib/.so/.dll entries; only
      checksums.txt and a new BINARY_TAG resource (a tiny text file
      immune to the hash-renaming problem, used by Native.rakumod to
      locate the staged-libs directory at runtime).
    - Native.rakumod resolver: env override
      (NOTCURSES_NATIVE_LIB_DIR) → XDG-staged dir → fail with the
      staged path in the error message for actionable debug.
    - New env knob: NOTCURSES_NATIVE_DATA_DIR to override the XDG base
      directory (e.g. for system-wide installs or sandboxed envs).

0.2.1  2026-04-15T02:23:12+01:00
    - Build: extract Windows .zip prebuilts via PowerShell's
      Expand-Archive instead of `tar`. GNU tar (which is first on
      PATH inside MSYS2 install environments) parses `D:\...` as a
      remote `host:path` and bombs with "Cannot connect to D:
      resolve failed". Expand-Archive ships on every supported
      Windows and has no such quirk. macOS / Linux still use `tar`.

0.2.0  2026-04-15T02:17:17+01:00
    - Prebuilt-binary-first install path. Build.rakumod now attempts
      to download a per-platform archive from the repo's GitHub
      Releases containing all three notcurses libs (libnotcurses,
      libnotcurses-core, libnotcurses-ffi) plus the ffmpeg sibling
      dylibs notcurses dyn-links, before falling back to the existing
      CMake source compilation. Saves the 5–15 minute CMake build +
      sidesteps the "install these 10 -dev packages first" pain
      that the HN thread surfaced.
    - ffmpeg sibling bundling with rpath relocation: @loader_path/
      on macOS via dylibbundler, $ORIGIN on Linux via patchelf,
      sibling-DLL layout on Windows. Archive self-contained — no
      system ffmpeg/ncurses/libunistring/libdeflate needed at
      runtime.
    - SHA256 verification against bundled resources/checksums.txt;
      refuses any prebuilt whose hash isn't recorded (hard security
      boundary).
    - Cache downloaded archives in $XDG_CACHE_HOME/Notcurses-Native-
      binaries/ (or $HOME/.cache/ fallback).
    - New env knobs: NOTCURSES_NATIVE_BUILD_FROM_SOURCE=1 to skip
      prebuilts; NOTCURSES_NATIVE_BINARY_ONLY=1 to refuse fallback;
      NOTCURSES_NATIVE_BINARY_URL to override release base URL;
      NOTCURSES_NATIVE_CACHE_DIR to override cache dir;
      NOTCURSES_NATIVE_LIB_DIR for runtime lib-dir override.
    - New BINARY_TAG file at repo root as single source of truth for
      the pinned binary release tag (binaries-notcurses-<upstream>-
      r<recipe-rev>), read by both Build.rakumod and the CI
      workflow.
    - New .github/workflows/build-binaries.yml: builds + publishes
      prebuilt archives for five platforms (macOS arm64, Linux
      x86_64/aarch64 glibc, Windows x86_64/arm64) on manual dispatch
      or binaries-* tag push. macOS uses dylibbundler, Linux uses
      recursive ldd walk + patchelf, Windows uses recursive ldd on
      MSYS2 + sibling DLL layout.
    - macOS is arm64-only for v1. Intel Macs fall through to the
      compile fallback; universal builds need cross-arch brew
      setup that isn't worth the CI complexity for initial ship.
      Can revisit if Intel Mac users complain.
    - FFI lookup in Notcurses::Native now respects the
      NOTCURSES_NATIVE_LIB_DIR env override before falling back to
      %?RESOURCES. Escape hatch for custom notcurses builds.
    - Fixed $os.contains('win') bug in FFI lookup: "darwin" matches
      "/win/", causing file-extension detection to pick 'dll' on
      macOS. Now uses $*DISTRO.is-win.

0.1.5  2026-04-12T21:17:32+01:00
    - Build: tighten library-matching regex in Build.rakumod so that
      staging `libnotcurses` doesn't accidentally pick up
      `libnotcurses-ffi` (only `.` is a valid separator after the
      library name, never `-`). The 3.0.16 → 3.0.17 bump surfaced this:
      filesystem ordering began handing us the FFI shim first, which
      lacks `notcurses_init` and quietly broke every terminal-dependent
      test.
    - Bump vendored notcurses 3.0.16 → 3.0.17, which upstream describes
      as "Fix build problems on Windows and Mac OSX." The public API is
      unchanged between these releases (single-character attribute-macro
      typo fix in notcurses.h, no ABI impact), so our bindings need no
      changes. Drops the Windows termios workaround that would otherwise
      have been needed for 3.0.16.

0.1.4  2026-04-12T20:19:12+01:00
    - CI: add Windows (MSYS2 UCRT64) to the GitHub Actions test matrix.
      Uses OpenImageIO as the multimedia backend (per upstream notcurses
      Windows recommendation). Build-only since notcurses-tester is
      Unix-only.
    - README: document system dependencies with per-OS install commands
      for Linux (Debian/Fedora), macOS (Homebrew), and Windows (MSYS2
      UCRT64). Added a core-only (no multimedia) note.

0.1.3  2026-04-09T23:56:25+01:00
    - Switch library resolution from $?FILE to %?RESOURCES for portable
      loading when used as a dependency by other modules

0.1.2  2026-04-09T18:06:28+01:00
    - Move terminal-dependent tests to xt/ to avoid prove6 TAP harness
      bug during zef install (t/ has pure-Raku tests only)
    - CI runs prove6 on t/ and Perl 5 prove on xt/
    - Fix NcBlitter enum values (NCBLIT_PIXEL was 6, should be 7)
    - Fix visual functions: use $nc-lib (full) not $core-lib for FFmpeg
    - Add 130 NCKEY_* key code constants, NcPixelImpl enum, NCBOX_*,
      NCMICE_*, NCALPHA_*, NC_BG_* channel bitmasks
    - Add NcvisualOptions.set-plane for correct plane compositing
    - Add 8 example programs (hello, colors, boxes, input, clock,
      image viewer with kitty pixel support, direct mode, progress bars)
    - Build.rakumod: search /opt/homebrew/bin for cmake when PATH
      is stripped by mi6/zef subprocess
    - Windows CI disabled pending upstream notcurses termios fix

0.1.1  2026-04-09T17:23:50+01:00
    - Fix TAP harness corruption: redirect stdout/stderr to /dev/null
      before notcurses init, reroute $*OUT via /dev/fd/N for TAP output
    - Fix Build.rakumod: search /opt/homebrew/bin for cmake when PATH
      is stripped by mi6/zef subprocess
    - Fix NcBlitter enum values (NCBLIT_PIXEL was 6, should be 7)
    - Fix visual functions: use $nc-lib (full) not $core-lib for FFmpeg
    - Skip Unicode cell tests on non-UTF-8 environments
    - Set LANG/LC_ALL=en_US.UTF-8 in CI for proper UTF-8 detection
    - CI uses prove (Perl 5) instead of prove6 to avoid TAP parser bug
    - Windows CI disabled pending upstream notcurses termios fix
    - Add installation troubleshooting to README

0.1.0  2026-04-09T16:41:32+01:00
    - Complete NativeCall wrapper for notcurses 3.0.16 TUI library
    - 606 functions bound across 9 modules (100% of bindable symbols)
    - Vendored notcurses 3.0.16 built with FFmpeg multimedia + FFI lib
    - Build.rakumod: CMake build for macOS, Linux, Windows (MSYS2)
    - Modules: Native (core), Types, Plane, Cell, Channel, Context,
      Direct, Input, Visual, Widgets
    - 25 CStruct types: all notcurses options structs, nccell, ncinput,
      ncstats, nccapabilities, ncvgeom, ncvisual_options, timespec,
      widget options (selector, menu, tree, tabbed, plot, reader, etc.)
    - 19 opaque CPointer handle types for type-safe FFI
    - 226 constants: NCKEY_* (130 key codes), NCSTYLE_*, NCOPTION_*,
      NCALPHA_*, NCVISUAL_OPTION_*, NCMICE_*, NCBOX_*, NC_BG_* bitmasks
    - 7 enums: NcLogLevel, NcAlign, NcBlitter, NcScale, NcInputType,
      NcPixelImpl, NcBlitter (with corrected values matching C header)
    - CStruct Str field workaround: set-cstruct-str helper + multi
      method new constructors for all structs with string fields
    - NcvisualOptions.set-plane method for correct plane compositing
    - Visual functions use libnotcurses (full) for FFmpeg backend
    - Variadic printf bindings (Rakudo 2026.03+)
    - 16 test files, 161 subtests, 748+ assertions
    - Tests cover: channel math, cell operations, plane lifecycle,
      widget lifecycle, input handling, context/capabilities, direct
      mode, visual/image loading, rendered output verification
    - Render verification tests: exact text, color, style, z-order,
      box drawing, erase, merge, and gradient checks via notcurses_at_yx
    - 4x4 PNG test fixture for visual pipeline testing
    - 8 example programs: hello, colors, boxes, input, clock,
      image viewer (with kitty pixel protocol), direct mode, progress bars
    - GitHub Actions CI for Linux, macOS, Windows
    - Only unbound: 4 vprintf variants (va_list is not FFI-bridgeable)
