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 · 123 lines · 4.5 KBmarkdown
Blame HistoryOpen raw

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.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# cosmicnim

[libcosmic](https://github.com/pop-os/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.

```c
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:

```nim
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:

```bash
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

```bash
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:

```bash
just build
just run
```

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