use NativeCall;
use Notcurses::Native::Types;
use Notcurses::Native::Str;
unit module Notcurses::Native;
# === Library paths ===
# We load three notcurses libs: core (init, context, plane, channels),
# full (adds multimedia/image via ffmpeg), and ffi (C wrappers for
# static-inline functions). NativeCall picks the right one per binding.
#
# We deliberately do NOT use %?RESOURCES for the libs themselves. zef
# stages every resource under a SHA-keyed filename, which breaks the
# inter-dylib references baked into notcurses (@loader_path/libnotcurses
# -core.3.0.17.dylib etc) — the loader can't find sibling libs by their
# real names because they've all been renamed to opaque hashes.
#
# Instead, Build.rakumod stages the libs to a stable XDG-style data
# dir at install time, under their real filenames. The dir is versioned
# by BINARY_TAG (which is small enough to survive %?RESOURCES intact)
# so a downgrade lines up with the right libs.
#
# Lookup precedence (per library):
# 1. $NOTCURSES_NATIVE_LIB_DIR env var — explicit override. Full
# path to a directory containing all the libs. Escape hatch for
# developers compiling notcurses themselves.
#
# CRITICAL: the libnotcurses we ship is patched (see the fork
# pinned by NOTCURSES_FORK for the ncvisual_blit_internal
# begy/begx fix at 0.3.0, in src/lib/visual.c). The patch is
# ABI-compatible at the C-symbol level — every export has the
# same signature — but BEHAVIOURALLY divergent: a clipped
# sprixel blit on vanilla 3.0.17 renders the top of the source
# regardless of begy. Pointing this override at a stock
# system-installed libnotcurses silently misrenders any chat
# avatar / image that's clipped at a viewport edge.
#
# If you're using the override, point it at a notcurses build
# that includes the same patch (the fork at the URL/SHA in
# NOTCURSES_FORK is the reference). The shim ($shim-lib) must
# also be present in the same directory for Selkie's batched
# copy path to engage — see src/notcurses_native_shim.c.
# 2. $NOTCURSES_NATIVE_DATA_DIR — base dir for the staged install
# (defaults to $XDG_DATA_HOME, falling back to platform-typical).
# Combined with BINARY_TAG to pick the version-matched libs.
constant $os is export = $*KERNEL.name.lc;
constant $ext is export = $os ~~ /darwin/ ?? 'dylib'
!! $*DISTRO.is-win ?? 'dll'
!! 'so';
# Where Build.rakumod staged the libs at install time. Must mirror
# Build.rakumod's !staged-lib-dir exactly — both compute the same path
# from the same env vars + the same BINARY_TAG (read from %?RESOURCES,
# the one resource that survives zef's hashing intact since it's a
# plain text file with no inter-file refs).
sub _staged-lib-dir(--> IO::Path) {
my $res = %?RESOURCES;
my Str $tag = ($res.defined && $res.IO.e) ?? $res.IO.slurp.trim !! '';
my Str $base = %*ENV
// %*ENV
// ($*DISTRO.is-win
?? (%*ENV
// "{%*ENV // '.'}\\AppData\\Local")
!! "{%*ENV // '.'}/.local/share");
"$base/Notcurses-Native/$tag/lib".IO;
}
# Resolve a single lib by basename (without extension) within a given
# directory. Tries exact `lib.$ext` first, then versioned variants
# (libfoo.3.dylib, libfoo.so.3, libfoo-3.dll, etc) so we accept whatever
# the prebuilt archive shipped — symlinks, versioned files, both.
sub _find-in(IO::Path $dir, Str $name --> Str) {
return Str unless $dir.d;
my $exact = $dir.add("$name.$ext");
return $exact.Str if $exact.e;
for $dir.dir -> $entry {
next unless $entry.e; # accept symlinks + regular files
my $bn = $entry.basename;
return $entry.Str if $bn.starts-with("$name.") && $bn.contains(".$ext");
return $entry.Str if $bn.starts-with("$name-") && $bn.ends-with(".$ext");
}
Str;
}
sub _resolve-lib(Str $name --> Str) {
# 1. Env-override wins outright.
if (my $override = %*ENV) && $override.IO.d {
with _find-in($override.IO, $name) { return $_ }
}
# 2. Staged install dir.
with _find-in(_staged-lib-dir(), $name) { return $_ }
# Nothing worked. Return the staged path as a hint for the
# NativeCall error message — the user will see it in the
# "Cannot locate native library 'X'" failure.
"{ _staged-lib-dir() }/$name.$ext";
}
# --- Runtime env setup ---
#
# Our bundled libncursesw was compiled against Homebrew's ncurses,
# which bakes the terminfo search path to the Homebrew cellar
# (e.g. /opt/homebrew/opt/ncurses/share/terminfo/). On a system
# without Homebrew ncurses installed, that path doesn't exist and
# ncurses can't find terminal definitions — notcurses_core_init
# fails with "No terminal available" even though the libraries
# loaded fine.
#
# macOS ships a system terminfo at /usr/share/terminfo/ with
# standard entries (xterm, screen, etc.). Set TERMINFO_DIRS so
# ncurses searches the system dir regardless of what's baked in.
# Linux has /usr/share/terminfo/ (or /lib/terminfo/ on some
# distros) and typically doesn't need the override (ncurses's
# compiled-in default already points there), but including it
# doesn't hurt. Respects a user-set TERMINFO_DIRS.
#
# Same Raku-%*ENV-doesn't-reach-C-getenv issue as Vips-Native:
# ncurses reads TERMINFO_DIRS via getenv(3), so we call setenv(3)
# directly via NativeCall. Uses the unified libc resolver in
# Notcurses::Native::Str so musl Alpine + glibc Linux + macOS all
# pick the right library without per-call redeclaration.
sub _setenv_c(Str, Str, int32 --> int32)
is native(&libc-name) is symbol('setenv') { * }
sub _setenv-c(Str $name, Str $value) {
%*ENV{$name} = $value;
return if $*DISTRO.is-win;
my $rv = _setenv_c($name, $value, 1);
if $rv != 0 {
# Stays in stderr — fires before notcurses_init takes the
# terminal, so it's visible. Don't throw: missing TERMINFO_DIRS
# is recoverable in some configurations (compiled-in default
# path may work), and a hard die here masks the surrounding
# ncurses error which is more useful.
note "Notcurses::Native: setenv($name) returned $rv; "
~ "ncurses may not see the value via getenv(3).";
}
}
sub _configure-runtime-env() {
return if %*ENV; # user override
unless %*ENV {
# Colon-separated list. Include both common system paths so
# ncurses finds entries regardless of distro layout. Empty
# trailing component means "the compiled-in default" — if
# Homebrew IS installed, ncurses still searches its own
# cellar path too.
my $system-dirs = '/usr/share/terminfo:/usr/lib/terminfo:/lib/terminfo:';
_setenv-c('TERMINFO_DIRS', $system-dirs);
}
}
_configure-runtime-env();
# Library-path resolvers. State-cached subs rather than `constant`
# bindings because `constant X = _resolve-lib(...)` evaluates at
# compile time and bakes the resolved path into the precompiled
# bytecode — and Rakudo doesn't track `resources/BINARY_TAG` as a
# precomp dependency. Bumping BINARY_TAG (which moves the staged
# libs to a new versioned directory and may garbage-collect the
# previous one) doesn't invalidate the precomp, so a freshly
# installed package can still try to load libs from the *old* path.
# Doing the lookup inside a `state $r = _resolve-lib(...)` sub
# defers it to first call in each process — fresh every time, but
# still O(1) after the first invocation. Pair with
# `is native(&nc-lib)` on each binding (not `is native(&nc-lib)`)
# so NativeCall invokes the resolver lazily.
sub nc-lib is export { state $r = _resolve-lib('libnotcurses'); $r }
sub ffi-lib is export { state $r = _resolve-lib('libnotcurses-ffi'); $r }
sub core-lib is export { state $r = _resolve-lib('libnotcurses-core'); $r }
#|( Resolved path to the perf shim that lives alongside the staged
libnotcurses libs (see src/notcurses_native_shim.c +
Build.rakumod's !try-compile-shim). Contains hot loops that
are unaffordable to express call-per-cell over Raku's NativeCall
boundary — currently just C, used
by Selkie::Widget::ViewportedCardList.
May resolve to a non-existent path if the shim wasn't compiled
(no toolchain at install time AND prebuilt didn't include it);
NativeCall will surface the missing-library error at first
invocation. Selkie's binding tolerates this and falls back to
the per-cell Raku loop.
State-cached sub (not a `constant`) for the same precomp-staleness
reason as nc-lib / ffi-lib / core-lib — see those for the full
rationale. )
sub shim-lib is export { state $r = _resolve-lib('libnotcurses_native_shim'); $r }
# === Version ===
#| OWNED-BY-LIBRARY: static version string baked into libnotcurses;
#| caller MUST NOT free.
sub notcurses_version(--> Str)
is native(&core-lib) is export { * }
sub notcurses_version_components(int32 $major is rw, int32 $minor is rw, int32 $patch is rw, int32 $tweak is rw)
is native(&core-lib) is export { * }
# === Context init/stop (from libnotcurses-core, re-exported by libnotcurses) ===
sub notcurses_core_init(NotcursesOptions $opts, Pointer $fp --> NotcursesHandle)
is native(&core-lib) is export { * }
sub notcurses_init(NotcursesOptions $opts, Pointer $fp --> NotcursesHandle)
is native(&nc-lib) is export { * }
sub notcurses_stop(NotcursesHandle $nc --> int32)
is native(&core-lib) is export { * }
# === Standard plane ===
sub notcurses_stdplane(NotcursesHandle $nc --> NcplaneHandle)
is native(&core-lib) is export { * }
sub notcurses_stdplane_const(NotcursesHandle $nc --> NcplaneHandle)
is native(&core-lib) is export { * }
sub notcurses_stddim_yx(NotcursesHandle $nc, uint32 $rows is rw, uint32 $cols is rw --> NcplaneHandle)
is native(&ffi-lib) is export { * }
# === Rendering ===
sub notcurses_render(NotcursesHandle $nc --> int32)
is native(&ffi-lib) is export { * }
sub notcurses_refresh(NotcursesHandle $nc, uint32 $rows is rw, uint32 $cols is rw --> int32)
is native(&core-lib) is export { * }
# === Terminal dimensions ===
sub notcurses_term_dim_yx(NotcursesHandle $nc, uint32 $rows is rw, uint32 $cols is rw)
is native(&ffi-lib) is export { * }
# === Capabilities ===
sub notcurses_cantruecolor(NotcursesHandle $nc --> bool)
is native(&ffi-lib) is export { * }
sub notcurses_canfade(NotcursesHandle $nc --> bool)
is native(&ffi-lib) is export { * }
sub notcurses_canchangecolor(NotcursesHandle $nc --> bool)
is native(&ffi-lib) is export { * }
sub notcurses_canopen_images(NotcursesHandle $nc --> bool)
is native(&core-lib) is export { * }
sub notcurses_canopen_videos(NotcursesHandle $nc --> bool)
is native(&core-lib) is export { * }
sub notcurses_canbraille(NotcursesHandle $nc --> bool)
is native(&ffi-lib) is export { * }
sub notcurses_cansextant(NotcursesHandle $nc --> bool)
is native(&ffi-lib) is export { * }
sub notcurses_canpixel(NotcursesHandle $nc --> bool)
is native(&ffi-lib) is export { * }
sub notcurses_canutf8(NotcursesHandle $nc --> bool)
is native(&ffi-lib) is export { * }
sub notcurses_canhalfblock(NotcursesHandle $nc --> bool)
is native(&ffi-lib) is export { * }
sub notcurses_canquadrant(NotcursesHandle $nc --> bool)
is native(&ffi-lib) is export { * }
# === Alternate screen ===
sub notcurses_enter_alternate_screen(NotcursesHandle $nc --> int32)
is native(&core-lib) is export { * }
sub notcurses_leave_alternate_screen(NotcursesHandle $nc --> int32)
is native(&core-lib) is export { * }
# === Cursor ===
sub notcurses_cursor_enable(NotcursesHandle $nc, int32 $y, int32 $x --> int32)
is native(&core-lib) is export { * }
sub notcurses_cursor_disable(NotcursesHandle $nc --> int32)
is native(&core-lib) is export { * }
sub notcurses_cursor_yx(NotcursesHandle $nc, int32 $y is rw, int32 $x is rw --> int32)
is native(&core-lib) is export { * }
# === Input ===
sub notcurses_get(NotcursesHandle $nc, Timespec $ts, Ncinput $ni --> uint32)
is native(&core-lib) is export { * }
sub notcurses_get_nblock(NotcursesHandle $nc, Ncinput $ni --> uint32)
is native(&ffi-lib) is export { * }
sub notcurses_get_blocking(NotcursesHandle $nc, Ncinput $ni --> uint32)
is native(&ffi-lib) is export { * }
# === Mouse ===
sub notcurses_mice_enable(NotcursesHandle $nc, uint32 $eventmask --> int32)
is native(&core-lib) is export { * }
sub notcurses_mice_disable(NotcursesHandle $nc --> int32)
is native(&ffi-lib) is export { * }
# === Plane creation/destruction ===
sub ncplane_create(NcplaneHandle $parent, NcplaneOptions $opts --> NcplaneHandle)
is native(&core-lib) is export { * }
sub ncplane_destroy(NcplaneHandle $n --> int32)
is native(&core-lib) is export { * }
# === Plane dimensions ===
sub ncplane_dim_yx(NcplaneHandle $n, uint32 $rows is rw, uint32 $cols is rw)
is native(&core-lib) is export { * }
# FFI functions for inline plane helpers
sub ncplane_dim_y(NcplaneHandle $n --> uint32)
is native(&ffi-lib) is export { * }
sub ncplane_dim_x(NcplaneHandle $n --> uint32)
is native(&ffi-lib) is export { * }
# === Plane output ===
sub ncplane_putchar_yx(NcplaneHandle $n, int32 $y, int32 $x, uint8 $c --> int32)
is native(&ffi-lib) is export { * }
sub ncplane_putstr_yx(NcplaneHandle $n, int32 $y, int32 $x, Str $str --> int32)
is native(&ffi-lib) is export { * }
sub ncplane_putstr_aligned(NcplaneHandle $n, int32 $y, int32 $align, Str $str --> int32)
is native(&ffi-lib) is export { * }
sub ncplane_putnstr_yx(NcplaneHandle $n, int32 $y, int32 $x, size_t $len, Str $str --> int32)
is native(&ffi-lib) is export { * }
# === Plane cursor ===
sub ncplane_cursor_move_yx(NcplaneHandle $n, int32 $y, int32 $x --> int32)
is native(&core-lib) is export { * }
sub ncplane_cursor_move_rel(NcplaneHandle $n, int32 $y, int32 $x --> int32)
is native(&core-lib) is export { * }
sub ncplane_cursor_yx(NcplaneHandle $n, uint32 $y is rw, uint32 $x is rw)
is native(&core-lib) is export { * }
sub ncplane_home(NcplaneHandle $n)
is native(&core-lib) is export { * }
# === Plane styling ===
sub ncplane_set_styles(NcplaneHandle $n, uint32 $styles)
is native(&core-lib) is export { * }
sub ncplane_on_styles(NcplaneHandle $n, uint32 $styles)
is native(&core-lib) is export { * }
sub ncplane_off_styles(NcplaneHandle $n, uint32 $styles)
is native(&core-lib) is export { * }
# === Plane colors (via FFI for inline functions) ===
sub ncplane_set_fg_rgb(NcplaneHandle $n, uint32 $channel --> int32)
is native(&core-lib) is export { * }
sub ncplane_set_bg_rgb(NcplaneHandle $n, uint32 $channel --> int32)
is native(&core-lib) is export { * }
sub ncplane_set_fg_rgb8(NcplaneHandle $n, uint32 $r, uint32 $g, uint32 $b --> int32)
is native(&core-lib) is export { * }
sub ncplane_set_bg_rgb8(NcplaneHandle $n, uint32 $r, uint32 $g, uint32 $b --> int32)
is native(&core-lib) is export { * }
sub ncplane_set_fg_default(NcplaneHandle $n)
is native(&core-lib) is export { * }
sub ncplane_set_bg_default(NcplaneHandle $n)
is native(&core-lib) is export { * }
sub ncplane_set_fg_palindex(NcplaneHandle $n, uint32 $idx --> int32)
is native(&core-lib) is export { * }
sub ncplane_set_bg_palindex(NcplaneHandle $n, uint32 $idx --> int32)
is native(&core-lib) is export { * }
# === Plane erase ===
sub ncplane_erase(NcplaneHandle $n)
is native(&core-lib) is export { * }
sub ncplane_erase_region(NcplaneHandle $n, int32 $ystart, int32 $xstart, int32 $ylen, int32 $xlen --> int32)
is native(&core-lib) is export { * }
# === Plane movement/resize ===
sub ncplane_move_yx(NcplaneHandle $n, int32 $y, int32 $x --> int32)
is native(&core-lib) is export { * }
sub ncplane_resize(NcplaneHandle $n, int32 $keepy, int32 $keepx,
uint32 $keepleny, uint32 $keeplenx,
int32 $yoff, int32 $xoff,
uint32 $ylen, uint32 $xlen --> int32)
is native(&core-lib) is export { * }
# === Plane z-order ===
sub ncplane_move_top(NcplaneHandle $n)
is native(&ffi-lib) is export { * }
sub ncplane_move_bottom(NcplaneHandle $n)
is native(&ffi-lib) is export { * }
sub ncplane_move_above(NcplaneHandle $n, NcplaneHandle $above --> int32)
is native(&core-lib) is export { * }
sub ncplane_move_below(NcplaneHandle $n, NcplaneHandle $below --> int32)
is native(&core-lib) is export { * }
# === Cell functions (FFI) ===
# === Visual (must use &nc-lib to get FFmpeg multimedia backend) ===
sub ncvisual_from_file(Str $file --> NcvisualHandle)
is native(&nc-lib) is export { * }
sub ncvisual_from_rgba(Pointer $rgba, int32 $rows, int32 $rowstride, int32 $cols --> NcvisualHandle)
is native(&nc-lib) is export { * }
sub ncvisual_destroy(NcvisualHandle $v)
is native(&nc-lib) is export { * }
sub ncvisual_decode(NcvisualHandle $v --> int32)
is native(&nc-lib) is export { * }
sub ncvisual_resize(NcvisualHandle $v, int32 $rows, int32 $cols --> int32)
is native(&nc-lib) is export { * }