nandi/cosmicnimpublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/nandi/cosmicnim.git
git clone ssh://git@rickub.com/nandi/cosmicnim.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

README.md

cosmicnim

libcosmic behind a small C ABI, driven from Nim.

libcosmic's API is generics, traits and closures, so it has no C ABI of its own —
building libcosmic itself as a cdylib exports nothing. The cosmic_ffi crate
therefore defines the C surface: it depends on libcosmic as a normal Rust
crate and exports a handful of functions.

cosmic_ffi/       Rust cdylib -> libcosmic_ffi.so + cosmic_ffi.h
nim/              the Nim binding (cosmicnim.nim), and where the .so lands
examples/         calculator.nim, a worked example
tests/            headless tests of the example's logic
cosmicnim.nimble  package metadata and the fetchLib task
justfile          build / run / fetch tasks

The contract

The host owns the application state and the shape of the window. Once per
frame libcosmic calls back through on_view with an opaque builder, and the
host describes its widget tree by calling the builder functions. Widget memory
never crosses the boundary — the host only ever issues calls, and the builder
handle is dead the moment on_view returns.

Interaction runs the other way. A button carries an id the host chose; pressing
it calls on_press with that id, the host mutates its own state, and the next
on_view reflects it.

typedef void (*cosmic_on_view)(void *ctx, CosmicBuilder *b, float w, float h);
typedef void (*cosmic_on_press)(void *ctx, int32_t id);
typedef void (*cosmic_on_key)(void *ctx, const char *key);
uint32_t cosmic_abi_version(void);
int32_t cosmic_run(const CosmicConfig *config);   /* blocks until closed */

on_view is handed the space actually available, which is not the size the
config asked for once a tiling compositor has had its say. on_key reports
the typed character for ordinary keys and the key's name otherwise.

ABI version

Check cosmic_abi_version before cosmic_run; the Nim binding does it for
you and refuses to run on a mismatch. A changed callback signature leaves
every symbol resolvable, so without the check a stale library reads its
arguments from whatever is in the registers — which looks like a layout bug,
not a version problem.

Because the tree is rebuilt every frame, it can depend on state: a button
disappears, or goes inert, simply by not being described that way this time
round. See cosmic_ffi.h for the full list of containers, leaves and
attributes.

cosmic_run must be called from the main thread, and ctx is only ever
touched from that thread.

From Nim

Containers are block templates, so the source has the same shape as the window:

proc onView(ctx: pointer; b: Builder) {.cdecl.} =
  let c = cast[ptr Counter](ctx)
  b.container:
    b.fill(); b.alignCenter(); b.spacing(space(SpaceM))
    b.text($c.value, TextTitle1)
    b.row:
      b.spacing(space(SpaceS))
      b.button("−", IdDec)
      b.button("Reset", IdReset, ButtonDestructive, enabled = c.value != 0)
      b.button("+", IdInc, ButtonSuggested)

Running it

The demo needs nim/libcosmic_ffi.so. Take it from a release:

just fetch          # or: nimble fetchLib
just run            # the calculator
just test           # its arithmetic, headless — no window, no cdylib call

The package version is the ABI version: cosmicnim 0.2.0 fetches the
v0.2.0 release and no other, so a stale library cannot load against newer
bindings. just fetch v0.1.2 (or COSMICNIM_TAG=v0.1.2 nimble fetchLib)
overrides that when you want it.

Signal handling

Compile anything that loads this library with -d:noSignalHandler
(examples/nim.cfg does it for the demo, and the binding warns if you
forget).

libcosmic creates threads Nim knows nothing about — tokio workers and a
notify-rs inotify watcher. Nim installs its signal handlers process-wide,
and the handler allocates a string to build a traceback, so a signal
delivered to one of those threads faults inside the handler. The crash it
produces points at rawAlloc, which tells you nothing about the real
cause; the handler has already destroyed that evidence.

As a dependency

nimble install      # fetches the cdylib too, and ships it with the package

The binding resolves the library by absolute path at compile time, so an
installed package finds its .so in the nimble store without any rpath or
LD_LIBRARY_PATH fiddling. Override with -d:libCosmicFfi=/some/path.so.

or build it yourself — the cold build is long, which is what the release CI is
for:

just build
just run

just check type-checks the Nim without needing the library at all, since the
binding loads it lazily.