Skip to content

Releases: cdin-editor/cdin

cdin v0.2.1-beta.9 (pre-release)

Pre-release

Choose a tag to compare

@github-actions github-actions released this 14 Oct 14:11

⚠️ Pre-release — this is not a stable cdin

این یک نسخه پیش‌انتشار است، نه نسخه اصلی و نه نسخه پایدار.
A rolling pre-release built from main. It is published as a GitHub
prerelease, it can break at any moment, and none of it is covered by
the stable-release policy. Use it to see what is landing on main.

Version 0.2.1-beta.9
Previous pre-release v0.2.1-beta.8
Commit 8994103
Built 2026-10-14 14:11 UTC
Triggered by m-mdy-m
Workflow run https://github.com/cdin-editor/cdin/actions/runs/38577636504

python3 cdin.py update installs the newest stable release and never
this one: GitHub keeps prereleases out of /releases/latest, and this
workflow never publishes one as stable.

📋 Unreleased, from the changelog

⚠️ BREAKING CHANGES

cdin is now GPL-3.0-or-later

  • The LICENSE file is the GNU General Public License v3. README.md's badge and licence line say so, and NOTICE now lists the third-party components that keep their own licences. Code copied out of cdin, and a fork of it, are now under GPL-3.0-or-later terms rather than MIT.

Added

  • Automated pre-release versioning and publishing. Every qualifying push to main or master now computes the next pre-release version from Git tag history, builds Linux and Windows packages, and publishes a GitHub pre-release for the exact commit. Versions progress through alpha → beta → rc, increment numbered stages numerically, and start a new patch cycle after the release-candidate stage. Documentation-only changes are ignored. Pre-releases never publish macOS packages or Docker images; stable releases remain restricted to version tags without a hyphen (vX.Y.Z).

  • An accounting allocator every subsystem is tagged into. cdin_alloc / cdin_calloc / cdin_realloc carry a CDIN_MEM_* tag and keep live, peak and block counts per tag; cdin_free reads the tag out of the header the allocator wrote, so one untagged free subtracts from the right subsystem, refuses a pointer that never came from here, and catches a double free in a debug build. A tag on free would mean the caller has to remember it, and a caller that remembers one wrong corrupts the one thing this exists to get right.

    It also carries a fault injector, cdin_alloc_fail_after(n), which refuses every allocation after the nth. That is how the out-of-memory paths get exercised on purpose instead of by really running out.

    arena_alloc is the other half: one allocation handed out by bumping a pointer and thrown away in one step, for the paths that must not allocate at all. It is not a general allocator — nothing can free an individual allocation — so it carries a hard cap rather than growing until the process dies. Tag values are append-only and stable, because reports name them.

  • An error convention, CdinErr, for every fallible C function. A small classification enum plus one out-parameter for the part that is data rather than class (a path, a line number, a token). CDIN_OK is zero so that err != CDIN_OK and if (err) cannot be confused by muscle memory. It carries no allocation, which is why there is no cdin_err_free.

  • A handle convention for every piece of C-owned state Lua can reach. A userdata with a __gc and a close(), its type name in the metatable, and a destructor that runs exactly once. The type is void * at the Lua boundary on purpose: a caller cannot reach a field, cannot compute an offset into a neighbouring allocation, and cannot hand the pointer to free(). A closed handle is detectably closed — cdin_handle_check refuses it, so a use-after-close is a Lua error naming the type rather than a pointer into freed memory.

  • A uniform error table for the boundary. cdin_error_push returns { code, name, detail, line, os } rather than raising, because raising discards everything except the message and "close the document, fall back to the next font" needs something to recover with. detail is never nil, because a missing detail and an empty one are different facts.

  • Asynchronous file I/O off the frame thread (libuv). src/loop/ reads a whole file on a libuv worker and hands the bytes back to the frame thread, which splits them into lines when it is ready. This is the piece that satisfies two rules which pull against each other: the Lua state is single-threaded, and the frame must not block. So the split is by data, not by phase — a worker cannot build the line table, because that is Lua data, but it can do the part that is bytes and nothing else.

    Buffers are capped at 1 GiB and a larger file is refused, not truncated: a silent short read would be a corrupted document.

  • core:memory() — one call that says where the bytes are. It reads the C counters the host publishes and adds what only the Lua side can know, the Lua heap and the documents. The C half is one table (system.memory_stats) rather than several functions, so one call is one consistent snapshot. Unavailable is -1, not nil, so a caller can subtract without a guard — and "not measurable" is never printed as "0 bytes of glyphs", which is how a regression hides.

  • The renderer's allocations are tagged CDIN_MEM_FONT, and its glyph cache reports glyph_bytes and glyph_pages. Before this the renderer used raw malloc and was invisible to the per-subsystem report, which is why "2.1 MiB of glyphs" was a number core:memory knew but could not attribute.

  • Lua-visible additions. system.memory_stats, system.jobs_run, system.jobs_pending, system.alloc_fail_after, system.alloc_failures, system.alloc_fail_disable.

  • Environment variables for diagnosis. CDIN_ALLOC_FAIL_AFTER=<n> arms the fault injector at startup; CDIN_FULL_REDRAW=1 turns damage tracking off so every frame is presented whole, which answers "is the glitch in what the cache believes is on screen?"; CDIN_LOG_FILE and CDIN_LOG_LEVEL were already there.

  • A C unit test runner. tests/c covers the logger, the damage cache, the allocator, the arena and CdinErr, the job loop, glyph paging and the fallback chain, and the handle convention — linked against stubs so the damage cache can be driven without SDL, in its own output directory so a test run never touches the shipping binary, and built -O0 -g3 -DDEBUG because the allocator compiles its leak registry and free quarantine under CDIN_ALLOC_DEBUG, which is tied to NDEBUG.

  • Three fuzz harnesses and their seed corpora. fuzz_eol, fuzz_path and fuzz_utf8 each assert the properties that make a parser trustworthy: no read outside the input, no write outside it, no nontermination, and bytes that come back out equal to bytes that went in. That last one is what AGENTS.md §2.6 depends on. Four seeds are named regress-* and are kept because each found something.

  • Measurement harnesses that print numbers. measure_runtime.py (startup and idle CPU), measure_floor.py, measure_fonts.py, measure_redraw.py, and bench.py — synthetic files of 4/16/64/256 MiB opened through the real Doc path, with the Lua heap and RSS read before load, after load, after highlighting and after close. size.py is now what make size runs; it was bench.py's job, which meant asking for binary sizes started a memory benchmark.

  • make bare — the runtime with an empty site directory: core, fonts and themes, no plugins, no extension manager, no cdin-x at run time. It is the only tree whose behaviour depends on nothing outside this repository, which makes it the right tree for measuring startup and deciding whether a failure is cdin's or the bundle's. core is copied rather than linked, because a stale core is the worst possible outcome.

  • .luacheckrc, declaring the host globals. That is a contract (docs/architecture/extension-contract.md, "Globals and the host tables"), and a contract only written in prose is not checked.

  • pre-release.yml — every push gets its own version. A push to main that is not documentation-only now builds Windows and Linux, reads the next version off the tag history and publishes a GitHub prerelease for that exact commit: v0.2.1-alpha → v0.2.1-beta.1 → v0.2.1-beta.2, with the stage promoted (alpha → beta → rc) when it runs out and a new patch cycle opened after that. It reuses release-linux.yml and release-windows.yml rather than duplicating a build, and it publishes neither macOS nor Docker.

    The release body says, in Persian and English, that this is not the official build, carries the [Unreleased] changelog and every commit since the previous pre-release, and lists each asset with its size. release.yml (and the three per-platform workflows) now ignore tags with a hyphen ('!v*-*'), so a pre-release tag cannot trigger a stable release or a Docker Hub push.

  • release-linux.yml / release-windows.yml take a version input. Left empty, both still derive the version from the tag being built, which is what a tag push gets. pre-release.yml passes the version it computed, which is what makes the two build paths the same build.

  • docker.yml runs on pull requests only. It never pushed, but it built an image on every push to main — including every push that produces a pre-release. A published image still comes from release.yml on a stable vX.Y.Z tag.

  • libuv is installed by CI. Both release workflows gained the install step, because src/loop/ needs it and a missing dependency would otherwise surface as a wall of undefined symbols rather than as a missing dependency.

Changed

  • src/core/utils.c and src/core/boot.c are replaced by src/platform/. The platform layer is the single place in the runtime that branches on the operating system for a reason SDL cannot paper over, and it is t...
Read more

cdin v0.2.1-beta.10 (pre-release)

Pre-release

Choose a tag to compare

@github-actions github-actions released this 14 Oct 16:01
8ad865b

⚠️ Pre-release — this is not a stable cdin

این یک نسخه پیش‌انتشار است، نه نسخه اصلی و نه نسخه پایدار.
A rolling pre-release built from main. It is published as a GitHub
prerelease, it can break at any moment, and none of it is covered by
the stable-release policy. Use it to see what is landing on main.

Version 0.2.1-beta.10
Previous pre-release v0.2.1-beta.9
Commit 8ad865b
Built 2026-10-14 16:01 UTC
Triggered by m-mdy-m
Workflow run https://github.com/cdin-editor/cdin/actions/runs/38593813872

python3 cdin.py update installs the newest stable release and never
this one: GitHub keeps prereleases out of /releases/latest, and this
workflow never publishes one as stable.

📋 Unreleased, from the changelog

⚠️ BREAKING CHANGES

cdin is now GPL-3.0-or-later

  • The LICENSE file is the GNU General Public License v3. README.md's badge and licence line say so, and NOTICE now lists the third-party components that keep their own licences. Code copied out of cdin, and a fork of it, are now under GPL-3.0-or-later terms rather than MIT.

Added

  • Automated pre-release versioning and publishing. Every qualifying push to main or master now computes the next pre-release version from Git tag history, builds Linux and Windows packages, and publishes a GitHub pre-release for the exact commit. Versions progress through alpha → beta → rc, increment numbered stages numerically, and start a new patch cycle after the release-candidate stage. Documentation-only changes are ignored. Pre-releases never publish macOS packages or Docker images; stable releases remain restricted to version tags without a hyphen (vX.Y.Z).

  • An accounting allocator every subsystem is tagged into. cdin_alloc / cdin_calloc / cdin_realloc carry a CDIN_MEM_* tag and keep live, peak and block counts per tag; cdin_free reads the tag out of the header the allocator wrote, so one untagged free subtracts from the right subsystem, refuses a pointer that never came from here, and catches a double free in a debug build. A tag on free would mean the caller has to remember it, and a caller that remembers one wrong corrupts the one thing this exists to get right.

    It also carries a fault injector, cdin_alloc_fail_after(n), which refuses every allocation after the nth. That is how the out-of-memory paths get exercised on purpose instead of by really running out.

    arena_alloc is the other half: one allocation handed out by bumping a pointer and thrown away in one step, for the paths that must not allocate at all. It is not a general allocator — nothing can free an individual allocation — so it carries a hard cap rather than growing until the process dies. Tag values are append-only and stable, because reports name them.

  • An error convention, CdinErr, for every fallible C function. A small classification enum plus one out-parameter for the part that is data rather than class (a path, a line number, a token). CDIN_OK is zero so that err != CDIN_OK and if (err) cannot be confused by muscle memory. It carries no allocation, which is why there is no cdin_err_free.

  • A handle convention for every piece of C-owned state Lua can reach. A userdata with a __gc and a close(), its type name in the metatable, and a destructor that runs exactly once. The type is void * at the Lua boundary on purpose: a caller cannot reach a field, cannot compute an offset into a neighbouring allocation, and cannot hand the pointer to free(). A closed handle is detectably closed — cdin_handle_check refuses it, so a use-after-close is a Lua error naming the type rather than a pointer into freed memory.

  • A uniform error table for the boundary. cdin_error_push returns { code, name, detail, line, os } rather than raising, because raising discards everything except the message and "close the document, fall back to the next font" needs something to recover with. detail is never nil, because a missing detail and an empty one are different facts.

  • Asynchronous file I/O off the frame thread (libuv). src/loop/ reads a whole file on a libuv worker and hands the bytes back to the frame thread, which splits them into lines when it is ready. This is the piece that satisfies two rules which pull against each other: the Lua state is single-threaded, and the frame must not block. So the split is by data, not by phase — a worker cannot build the line table, because that is Lua data, but it can do the part that is bytes and nothing else.

    Buffers are capped at 1 GiB and a larger file is refused, not truncated: a silent short read would be a corrupted document.

  • core:memory() — one call that says where the bytes are. It reads the C counters the host publishes and adds what only the Lua side can know, the Lua heap and the documents. The C half is one table (system.memory_stats) rather than several functions, so one call is one consistent snapshot. Unavailable is -1, not nil, so a caller can subtract without a guard — and "not measurable" is never printed as "0 bytes of glyphs", which is how a regression hides.

  • The renderer's allocations are tagged CDIN_MEM_FONT, and its glyph cache reports glyph_bytes and glyph_pages. Before this the renderer used raw malloc and was invisible to the per-subsystem report, which is why "2.1 MiB of glyphs" was a number core:memory knew but could not attribute.

  • Lua-visible additions. system.memory_stats, system.jobs_run, system.jobs_pending, system.alloc_fail_after, system.alloc_failures, system.alloc_fail_disable.

  • Environment variables for diagnosis. CDIN_ALLOC_FAIL_AFTER=<n> arms the fault injector at startup; CDIN_FULL_REDRAW=1 turns damage tracking off so every frame is presented whole, which answers "is the glitch in what the cache believes is on screen?"; CDIN_LOG_FILE and CDIN_LOG_LEVEL were already there.

  • A C unit test runner. tests/c covers the logger, the damage cache, the allocator, the arena and CdinErr, the job loop, glyph paging and the fallback chain, and the handle convention — linked against stubs so the damage cache can be driven without SDL, in its own output directory so a test run never touches the shipping binary, and built -O0 -g3 -DDEBUG because the allocator compiles its leak registry and free quarantine under CDIN_ALLOC_DEBUG, which is tied to NDEBUG.

  • Three fuzz harnesses and their seed corpora. fuzz_eol, fuzz_path and fuzz_utf8 each assert the properties that make a parser trustworthy: no read outside the input, no write outside it, no nontermination, and bytes that come back out equal to bytes that went in. That last one is what AGENTS.md §2.6 depends on. Four seeds are named regress-* and are kept because each found something.

  • Measurement harnesses that print numbers. measure_runtime.py (startup and idle CPU), measure_floor.py, measure_fonts.py, measure_redraw.py, and bench.py — synthetic files of 4/16/64/256 MiB opened through the real Doc path, with the Lua heap and RSS read before load, after load, after highlighting and after close. size.py is now what make size runs; it was bench.py's job, which meant asking for binary sizes started a memory benchmark.

  • make bare — the runtime with an empty site directory: core, fonts and themes, no plugins, no extension manager, no cdin-x at run time. It is the only tree whose behaviour depends on nothing outside this repository, which makes it the right tree for measuring startup and deciding whether a failure is cdin's or the bundle's. core is copied rather than linked, because a stale core is the worst possible outcome.

  • .luacheckrc, declaring the host globals. That is a contract (docs/architecture/extension-contract.md, "Globals and the host tables"), and a contract only written in prose is not checked.

  • pre-release.yml — every push gets its own version. A push to main that is not documentation-only now builds Windows and Linux, reads the next version off the tag history and publishes a GitHub prerelease for that exact commit: v0.2.1-alpha → v0.2.1-beta.1 → v0.2.1-beta.2, with the stage promoted (alpha → beta → rc) when it runs out and a new patch cycle opened after that. It reuses release-linux.yml and release-windows.yml rather than duplicating a build, and it publishes neither macOS nor Docker.

    The release body says, in Persian and English, that this is not the official build, carries the [Unreleased] changelog and every commit since the previous pre-release, and lists each asset with its size. release.yml (and the three per-platform workflows) now ignore tags with a hyphen ('!v*-*'), so a pre-release tag cannot trigger a stable release or a Docker Hub push.

  • release-linux.yml / release-windows.yml take a version input. Left empty, both still derive the version from the tag being built, which is what a tag push gets. pre-release.yml passes the version it computed, which is what makes the two build paths the same build.

  • docker.yml runs on pull requests only. It never pushed, but it built an image on every push to main — including every push that produces a pre-release. A published image still comes from release.yml on a stable vX.Y.Z tag.

  • libuv is installed by CI. Both release workflows gained the install step, because src/loop/ needs it and a missing dependency would otherwise surface as a wall of undefined symbols rather than as a missing dependency.

Changed

  • src/core/utils.c and src/core/boot.c are replaced by src/platform/. The platform layer is the single place in the runtime that branches on the operating system for a reason SDL cannot paper over, and it is ...
Read more

cdin v0.2.1-beta.8 (pre-release)

Pre-release

Choose a tag to compare

@github-actions github-actions released this 13 Oct 19:55
a452bf3

⚠️ Pre-release — this is not a stable cdin

این یک نسخه پیش‌انتشار است، نه نسخه اصلی و نه نسخه پایدار.
A rolling pre-release built from main. It is published as a GitHub
prerelease, it can break at any moment, and none of it is covered by
the stable-release policy. Use it to see what is landing on main.

Version 0.2.1-beta.8
Previous pre-release v0.2.1-beta.7
Commit a452bf3
Built 2026-10-13 19:55 UTC
Triggered by m-mdy-m
Workflow run https://github.com/cdin-editor/cdin/actions/runs/38457711944

python3 cdin.py update installs the newest stable release and never
this one: GitHub keeps prereleases out of /releases/latest, and this
workflow never publishes one as stable.

📋 Unreleased, from the changelog

⚠️ BREAKING CHANGES

cdin is now GPL-3.0-or-later

  • The LICENSE file is the GNU General Public License v3. README.md's badge and licence line say so, and NOTICE now lists the third-party components that keep their own licences. Code copied out of cdin, and a fork of it, are now under GPL-3.0-or-later terms rather than MIT.

Added

  • An accounting allocator every subsystem is tagged into. cdin_alloc / cdin_calloc / cdin_realloc carry a CDIN_MEM_* tag and keep live, peak and block counts per tag; cdin_free reads the tag out of the header the allocator wrote, so one untagged free subtracts from the right subsystem, refuses a pointer that never came from here, and catches a double free in a debug build. A tag on free would mean the caller has to remember it, and a caller that remembers one wrong corrupts the one thing this exists to get right.

    It also carries a fault injector, cdin_alloc_fail_after(n), which refuses every allocation after the nth. That is how the out-of-memory paths get exercised on purpose instead of by really running out.

    arena_alloc is the other half: one allocation handed out by bumping a pointer and thrown away in one step, for the paths that must not allocate at all. It is not a general allocator — nothing can free an individual allocation — so it carries a hard cap rather than growing until the process dies. Tag values are append-only and stable, because reports name them.

  • An error convention, CdinErr, for every fallible C function. A small classification enum plus one out-parameter for the part that is data rather than class (a path, a line number, a token). CDIN_OK is zero so that err != CDIN_OK and if (err) cannot be confused by muscle memory. It carries no allocation, which is why there is no cdin_err_free.

  • A handle convention for every piece of C-owned state Lua can reach. A userdata with a __gc and a close(), its type name in the metatable, and a destructor that runs exactly once. The type is void * at the Lua boundary on purpose: a caller cannot reach a field, cannot compute an offset into a neighbouring allocation, and cannot hand the pointer to free(). A closed handle is detectably closed — cdin_handle_check refuses it, so a use-after-close is a Lua error naming the type rather than a pointer into freed memory.

  • A uniform error table for the boundary. cdin_error_push returns { code, name, detail, line, os } rather than raising, because raising discards everything except the message and "close the document, fall back to the next font" needs something to recover with. detail is never nil, because a missing detail and an empty one are different facts.

  • Asynchronous file I/O off the frame thread (libuv). src/loop/ reads a whole file on a libuv worker and hands the bytes back to the frame thread, which splits them into lines when it is ready. This is the piece that satisfies two rules which pull against each other: the Lua state is single-threaded, and the frame must not block. So the split is by data, not by phase — a worker cannot build the line table, because that is Lua data, but it can do the part that is bytes and nothing else.

    Buffers are capped at 1 GiB and a larger file is refused, not truncated: a silent short read would be a corrupted document.

  • core:memory() — one call that says where the bytes are. It reads the C counters the host publishes and adds what only the Lua side can know, the Lua heap and the documents. The C half is one table (system.memory_stats) rather than several functions, so one call is one consistent snapshot. Unavailable is -1, not nil, so a caller can subtract without a guard — and "not measurable" is never printed as "0 bytes of glyphs", which is how a regression hides.

  • The renderer's allocations are tagged CDIN_MEM_FONT, and its glyph cache reports glyph_bytes and glyph_pages. Before this the renderer used raw malloc and was invisible to the per-subsystem report, which is why "2.1 MiB of glyphs" was a number core:memory knew but could not attribute.

  • Lua-visible additions. system.memory_stats, system.jobs_run, system.jobs_pending, system.alloc_fail_after, system.alloc_failures, system.alloc_fail_disable.

  • Environment variables for diagnosis. CDIN_ALLOC_FAIL_AFTER=<n> arms the fault injector at startup; CDIN_FULL_REDRAW=1 turns damage tracking off so every frame is presented whole, which answers "is the glitch in what the cache believes is on screen?"; CDIN_LOG_FILE and CDIN_LOG_LEVEL were already there.

  • A C unit test runner. tests/c covers the logger, the damage cache, the allocator, the arena and CdinErr, the job loop, glyph paging and the fallback chain, and the handle convention — linked against stubs so the damage cache can be driven without SDL, in its own output directory so a test run never touches the shipping binary, and built -O0 -g3 -DDEBUG because the allocator compiles its leak registry and free quarantine under CDIN_ALLOC_DEBUG, which is tied to NDEBUG.

  • Three fuzz harnesses and their seed corpora. fuzz_eol, fuzz_path and fuzz_utf8 each assert the properties that make a parser trustworthy: no read outside the input, no write outside it, no nontermination, and bytes that come back out equal to bytes that went in. That last one is what AGENTS.md §2.6 depends on. Four seeds are named regress-* and are kept because each found something.

  • Measurement harnesses that print numbers. measure_runtime.py (startup and idle CPU), measure_floor.py, measure_fonts.py, measure_redraw.py, and bench.py — synthetic files of 4/16/64/256 MiB opened through the real Doc path, with the Lua heap and RSS read before load, after load, after highlighting and after close. size.py is now what make size runs; it was bench.py's job, which meant asking for binary sizes started a memory benchmark.

  • make bare — the runtime with an empty site directory: core, fonts and themes, no plugins, no extension manager, no cdin-x at run time. It is the only tree whose behaviour depends on nothing outside this repository, which makes it the right tree for measuring startup and deciding whether a failure is cdin's or the bundle's. core is copied rather than linked, because a stale core is the worst possible outcome.

  • .luacheckrc, declaring the host globals. That is a contract (docs/architecture/extension-contract.md, "Globals and the host tables"), and a contract only written in prose is not checked.

  • pre-release.yml — every push gets its own version. A push to main that is not documentation-only now builds Windows and Linux, reads the next version off the tag history and publishes a GitHub prerelease for that exact commit: v0.2.1-alpha → v0.2.1-beta.1 → v0.2.1-beta.2, with the stage promoted (alpha → beta → rc) when it runs out and a new patch cycle opened after that. It reuses release-linux.yml and release-windows.yml rather than duplicating a build, and it publishes neither macOS nor Docker.

    The release body says, in Persian and English, that this is not the official build, carries the [Unreleased] changelog and every commit since the previous pre-release, and lists each asset with its size. release.yml (and the three per-platform workflows) now ignore tags with a hyphen ('!v*-*'), so a pre-release tag cannot trigger a stable release or a Docker Hub push.

  • release-linux.yml / release-windows.yml take a version input. Left empty, both still derive the version from the tag being built, which is what a tag push gets. pre-release.yml passes the version it computed, which is what makes the two build paths the same build.

  • docker.yml runs on pull requests only. It never pushed, but it built an image on every push to main — including every push that produces a pre-release. A published image still comes from release.yml on a stable vX.Y.Z tag.

  • libuv is installed by CI. Both release workflows gained the install step, because src/loop/ needs it and a missing dependency would otherwise surface as a wall of undefined symbols rather than as a missing dependency.

Changed

  • src/core/utils.c and src/core/boot.c are replaced by src/platform/. The platform layer is the single place in the runtime that branches on the operating system for a reason SDL cannot paper over, and it is three questions: where this binary is, what has to be set before SDL starts, and what the display's content scale is. utils.c and boot.c each carried their own #ifdef _WIN32, and keeping OS guards contained in one file is what stops the next platform-specific bug from arriving.

  • The file log rotates mid-session, and the logger owns the stream. log_add_fp used to borrow a FILE * that main.c closed. It had to: a rotated stream has to be closed, renamed and reopened by whoever holds it, and main.c held it. The cap is 4 MiB, one previous run is kept as `cdin-log....

Read more

cdin v0.2.1-beta.7 (pre-release)

Pre-release

Choose a tag to compare

@github-actions github-actions released this 13 Oct 19:34
0443580

⚠️ Pre-release — this is not a stable cdin

این یک نسخه پیش‌انتشار است، نه نسخه اصلی و نه نسخه پایدار.
A rolling pre-release built from main. It is published as a GitHub
prerelease, it can break at any moment, and none of it is covered by
the stable-release policy. Use it to see what is landing on main.

Version 0.2.1-beta.7
Previous pre-release v0.2.1-beta.6
Commit 0443580
Built 2026-10-13 19:34 UTC
Triggered by m-mdy-m
Workflow run https://github.com/cdin-editor/cdin/actions/runs/38455423755

python3 cdin.py update installs the newest stable release and never
this one: GitHub keeps prereleases out of /releases/latest, and this
workflow never publishes one as stable.

📋 Unreleased, from the changelog

⚠️ BREAKING CHANGES

cdin is now GPL-3.0-or-later

  • The LICENSE file is the GNU General Public License v3. README.md's badge and licence line say so, and NOTICE now lists the third-party components that keep their own licences. Code copied out of cdin, and a fork of it, are now under GPL-3.0-or-later terms rather than MIT.

Added

  • An accounting allocator every subsystem is tagged into. cdin_alloc / cdin_calloc / cdin_realloc carry a CDIN_MEM_* tag and keep live, peak and block counts per tag; cdin_free reads the tag out of the header the allocator wrote, so one untagged free subtracts from the right subsystem, refuses a pointer that never came from here, and catches a double free in a debug build. A tag on free would mean the caller has to remember it, and a caller that remembers one wrong corrupts the one thing this exists to get right.

    It also carries a fault injector, cdin_alloc_fail_after(n), which refuses every allocation after the nth. That is how the out-of-memory paths get exercised on purpose instead of by really running out.

    arena_alloc is the other half: one allocation handed out by bumping a pointer and thrown away in one step, for the paths that must not allocate at all. It is not a general allocator — nothing can free an individual allocation — so it carries a hard cap rather than growing until the process dies. Tag values are append-only and stable, because reports name them.

  • An error convention, CdinErr, for every fallible C function. A small classification enum plus one out-parameter for the part that is data rather than class (a path, a line number, a token). CDIN_OK is zero so that err != CDIN_OK and if (err) cannot be confused by muscle memory. It carries no allocation, which is why there is no cdin_err_free.

  • A handle convention for every piece of C-owned state Lua can reach. A userdata with a __gc and a close(), its type name in the metatable, and a destructor that runs exactly once. The type is void * at the Lua boundary on purpose: a caller cannot reach a field, cannot compute an offset into a neighbouring allocation, and cannot hand the pointer to free(). A closed handle is detectably closed — cdin_handle_check refuses it, so a use-after-close is a Lua error naming the type rather than a pointer into freed memory.

  • A uniform error table for the boundary. cdin_error_push returns { code, name, detail, line, os } rather than raising, because raising discards everything except the message and "close the document, fall back to the next font" needs something to recover with. detail is never nil, because a missing detail and an empty one are different facts.

  • Asynchronous file I/O off the frame thread (libuv). src/loop/ reads a whole file on a libuv worker and hands the bytes back to the frame thread, which splits them into lines when it is ready. This is the piece that satisfies two rules which pull against each other: the Lua state is single-threaded, and the frame must not block. So the split is by data, not by phase — a worker cannot build the line table, because that is Lua data, but it can do the part that is bytes and nothing else.

    Buffers are capped at 1 GiB and a larger file is refused, not truncated: a silent short read would be a corrupted document.

  • core:memory() — one call that says where the bytes are. It reads the C counters the host publishes and adds what only the Lua side can know, the Lua heap and the documents. The C half is one table (system.memory_stats) rather than several functions, so one call is one consistent snapshot. Unavailable is -1, not nil, so a caller can subtract without a guard — and "not measurable" is never printed as "0 bytes of glyphs", which is how a regression hides.

  • The renderer's allocations are tagged CDIN_MEM_FONT, and its glyph cache reports glyph_bytes and glyph_pages. Before this the renderer used raw malloc and was invisible to the per-subsystem report, which is why "2.1 MiB of glyphs" was a number core:memory knew but could not attribute.

  • Lua-visible additions. system.memory_stats, system.jobs_run, system.jobs_pending, system.alloc_fail_after, system.alloc_failures, system.alloc_fail_disable.

  • Environment variables for diagnosis. CDIN_ALLOC_FAIL_AFTER=<n> arms the fault injector at startup; CDIN_FULL_REDRAW=1 turns damage tracking off so every frame is presented whole, which answers "is the glitch in what the cache believes is on screen?"; CDIN_LOG_FILE and CDIN_LOG_LEVEL were already there.

  • A C unit test runner. tests/c covers the logger, the damage cache, the allocator, the arena and CdinErr, the job loop, glyph paging and the fallback chain, and the handle convention — linked against stubs so the damage cache can be driven without SDL, in its own output directory so a test run never touches the shipping binary, and built -O0 -g3 -DDEBUG because the allocator compiles its leak registry and free quarantine under CDIN_ALLOC_DEBUG, which is tied to NDEBUG.

  • Three fuzz harnesses and their seed corpora. fuzz_eol, fuzz_path and fuzz_utf8 each assert the properties that make a parser trustworthy: no read outside the input, no write outside it, no nontermination, and bytes that come back out equal to bytes that went in. That last one is what AGENTS.md §2.6 depends on. Four seeds are named regress-* and are kept because each found something.

  • Measurement harnesses that print numbers. measure_runtime.py (startup and idle CPU), measure_floor.py, measure_fonts.py, measure_redraw.py, and bench.py — synthetic files of 4/16/64/256 MiB opened through the real Doc path, with the Lua heap and RSS read before load, after load, after highlighting and after close. size.py is now what make size runs; it was bench.py's job, which meant asking for binary sizes started a memory benchmark.

  • make bare — the runtime with an empty site directory: core, fonts and themes, no plugins, no extension manager, no cdin-x at run time. It is the only tree whose behaviour depends on nothing outside this repository, which makes it the right tree for measuring startup and deciding whether a failure is cdin's or the bundle's. core is copied rather than linked, because a stale core is the worst possible outcome.

  • .luacheckrc, declaring the host globals. That is a contract (docs/architecture/extension-contract.md, "Globals and the host tables"), and a contract only written in prose is not checked.

  • pre-release.yml — every push gets its own version. A push to main that is not documentation-only now builds Windows and Linux, reads the next version off the tag history and publishes a GitHub prerelease for that exact commit: v0.2.1-alpha → v0.2.1-beta.1 → v0.2.1-beta.2, with the stage promoted (alpha → beta → rc) when it runs out and a new patch cycle opened after that. It reuses release-linux.yml and release-windows.yml rather than duplicating a build, and it publishes neither macOS nor Docker.

    The release body says, in Persian and English, that this is not the official build, carries the [Unreleased] changelog and every commit since the previous pre-release, and lists each asset with its size. release.yml (and the three per-platform workflows) now ignore tags with a hyphen ('!v*-*'), so a pre-release tag cannot trigger a stable release or a Docker Hub push.

  • release-linux.yml / release-windows.yml take a version input. Left empty, both still derive the version from the tag being built, which is what a tag push gets. pre-release.yml passes the version it computed, which is what makes the two build paths the same build.

  • docker.yml runs on pull requests only. It never pushed, but it built an image on every push to main — including every push that produces a pre-release. A published image still comes from release.yml on a stable vX.Y.Z tag.

  • libuv is installed by CI. Both release workflows gained the install step, because src/loop/ needs it and a missing dependency would otherwise surface as a wall of undefined symbols rather than as a missing dependency.

Changed

  • src/core/utils.c and src/core/boot.c are replaced by src/platform/. The platform layer is the single place in the runtime that branches on the operating system for a reason SDL cannot paper over, and it is three questions: where this binary is, what has to be set before SDL starts, and what the display's content scale is. utils.c and boot.c each carried their own #ifdef _WIN32, and keeping OS guards contained in one file is what stops the next platform-specific bug from arriving.

  • The file log rotates mid-session, and the logger owns the stream. log_add_fp used to borrow a FILE * that main.c closed. It had to: a rotated stream has to be closed, renamed and reopened by whoever holds it, and main.c held it. The cap is 4 MiB, one previous run is kept as `cdin-log....

Read more

cdin v0.2.1-beta.6 (pre-release)

Pre-release

Choose a tag to compare

@github-actions github-actions released this 13 Oct 13:12
d65e534

⚠️ Pre-release — this is not a stable cdin

این یک نسخه پیش‌انتشار است، نه نسخه اصلی و نه نسخه پایدار.
A rolling pre-release built from main. It is published as a GitHub
prerelease, it can break at any moment, and none of it is covered by
the stable-release policy. Use it to see what is landing on main.

Version 0.2.1-beta.6
Previous pre-release v0.2.1-beta.5
Commit d65e534
Built 2026-10-13 13:12 UTC
Triggered by m-mdy-m
Workflow run https://github.com/cdin-editor/cdin/actions/runs/38402558870

python3 cdin.py update installs the newest stable release and never
this one: GitHub keeps prereleases out of /releases/latest, and this
workflow never publishes one as stable.

📋 Unreleased, from the changelog

⚠️ BREAKING CHANGES

cdin is now GPL-3.0-or-later

  • The LICENSE file is the GNU General Public License v3. README.md's badge and licence line say so, and NOTICE now lists the third-party components that keep their own licences. Code copied out of cdin, and a fork of it, are now under GPL-3.0-or-later terms rather than MIT.

Added

  • An accounting allocator every subsystem is tagged into. cdin_alloc / cdin_calloc / cdin_realloc carry a CDIN_MEM_* tag and keep live, peak and block counts per tag; cdin_free reads the tag out of the header the allocator wrote, so one untagged free subtracts from the right subsystem, refuses a pointer that never came from here, and catches a double free in a debug build. A tag on free would mean the caller has to remember it, and a caller that remembers one wrong corrupts the one thing this exists to get right.

    It also carries a fault injector, cdin_alloc_fail_after(n), which refuses every allocation after the nth. That is how the out-of-memory paths get exercised on purpose instead of by really running out.

    arena_alloc is the other half: one allocation handed out by bumping a pointer and thrown away in one step, for the paths that must not allocate at all. It is not a general allocator — nothing can free an individual allocation — so it carries a hard cap rather than growing until the process dies. Tag values are append-only and stable, because reports name them.

  • An error convention, CdinErr, for every fallible C function. A small classification enum plus one out-parameter for the part that is data rather than class (a path, a line number, a token). CDIN_OK is zero so that err != CDIN_OK and if (err) cannot be confused by muscle memory. It carries no allocation, which is why there is no cdin_err_free.

  • A handle convention for every piece of C-owned state Lua can reach. A userdata with a __gc and a close(), its type name in the metatable, and a destructor that runs exactly once. The type is void * at the Lua boundary on purpose: a caller cannot reach a field, cannot compute an offset into a neighbouring allocation, and cannot hand the pointer to free(). A closed handle is detectably closed — cdin_handle_check refuses it, so a use-after-close is a Lua error naming the type rather than a pointer into freed memory.

  • A uniform error table for the boundary. cdin_error_push returns { code, name, detail, line, os } rather than raising, because raising discards everything except the message and "close the document, fall back to the next font" needs something to recover with. detail is never nil, because a missing detail and an empty one are different facts.

  • Asynchronous file I/O off the frame thread (libuv). src/loop/ reads a whole file on a libuv worker and hands the bytes back to the frame thread, which splits them into lines when it is ready. This is the piece that satisfies two rules which pull against each other: the Lua state is single-threaded, and the frame must not block. So the split is by data, not by phase — a worker cannot build the line table, because that is Lua data, but it can do the part that is bytes and nothing else.

    Buffers are capped at 1 GiB and a larger file is refused, not truncated: a silent short read would be a corrupted document.

  • core:memory() — one call that says where the bytes are. It reads the C counters the host publishes and adds what only the Lua side can know, the Lua heap and the documents. The C half is one table (system.memory_stats) rather than several functions, so one call is one consistent snapshot. Unavailable is -1, not nil, so a caller can subtract without a guard — and "not measurable" is never printed as "0 bytes of glyphs", which is how a regression hides.

  • The renderer's allocations are tagged CDIN_MEM_FONT, and its glyph cache reports glyph_bytes and glyph_pages. Before this the renderer used raw malloc and was invisible to the per-subsystem report, which is why "2.1 MiB of glyphs" was a number core:memory knew but could not attribute.

  • Lua-visible additions. system.memory_stats, system.jobs_run, system.jobs_pending, system.alloc_fail_after, system.alloc_failures, system.alloc_fail_disable.

  • Environment variables for diagnosis. CDIN_ALLOC_FAIL_AFTER=<n> arms the fault injector at startup; CDIN_FULL_REDRAW=1 turns damage tracking off so every frame is presented whole, which answers "is the glitch in what the cache believes is on screen?"; CDIN_LOG_FILE and CDIN_LOG_LEVEL were already there.

  • A C unit test runner. tests/c covers the logger, the damage cache, the allocator, the arena and CdinErr, the job loop, glyph paging and the fallback chain, and the handle convention — linked against stubs so the damage cache can be driven without SDL, in its own output directory so a test run never touches the shipping binary, and built -O0 -g3 -DDEBUG because the allocator compiles its leak registry and free quarantine under CDIN_ALLOC_DEBUG, which is tied to NDEBUG.

  • Three fuzz harnesses and their seed corpora. fuzz_eol, fuzz_path and fuzz_utf8 each assert the properties that make a parser trustworthy: no read outside the input, no write outside it, no nontermination, and bytes that come back out equal to bytes that went in. That last one is what AGENTS.md §2.6 depends on. Four seeds are named regress-* and are kept because each found something.

  • Measurement harnesses that print numbers. measure_runtime.py (startup and idle CPU), measure_floor.py, measure_fonts.py, measure_redraw.py, and bench.py — synthetic files of 4/16/64/256 MiB opened through the real Doc path, with the Lua heap and RSS read before load, after load, after highlighting and after close. size.py is now what make size runs; it was bench.py's job, which meant asking for binary sizes started a memory benchmark.

  • make bare — the runtime with an empty site directory: core, fonts and themes, no plugins, no extension manager, no cdin-x at run time. It is the only tree whose behaviour depends on nothing outside this repository, which makes it the right tree for measuring startup and deciding whether a failure is cdin's or the bundle's. core is copied rather than linked, because a stale core is the worst possible outcome.

  • .luacheckrc, declaring the host globals. That is a contract (docs/architecture/extension-contract.md, "Globals and the host tables"), and a contract only written in prose is not checked.

  • pre-release.yml — every push gets its own version. A push to main that is not documentation-only now builds Windows and Linux, reads the next version off the tag history and publishes a GitHub prerelease for that exact commit: v0.2.1-alpha → v0.2.1-beta.1 → v0.2.1-beta.2, with the stage promoted (alpha → beta → rc) when it runs out and a new patch cycle opened after that. It reuses release-linux.yml and release-windows.yml rather than duplicating a build, and it publishes neither macOS nor Docker.

    The release body says, in Persian and English, that this is not the official build, carries the [Unreleased] changelog and every commit since the previous pre-release, and lists each asset with its size. release.yml (and the three per-platform workflows) now ignore tags with a hyphen ('!v*-*'), so a pre-release tag cannot trigger a stable release or a Docker Hub push.

  • release-linux.yml / release-windows.yml take a version input. Left empty, both still derive the version from the tag being built, which is what a tag push gets. pre-release.yml passes the version it computed, which is what makes the two build paths the same build.

  • docker.yml runs on pull requests only. It never pushed, but it built an image on every push to main — including every push that produces a pre-release. A published image still comes from release.yml on a stable vX.Y.Z tag.

  • libuv is installed by CI. Both release workflows gained the install step, because src/loop/ needs it and a missing dependency would otherwise surface as a wall of undefined symbols rather than as a missing dependency.

Changed

  • src/core/utils.c and src/core/boot.c are replaced by src/platform/. The platform layer is the single place in the runtime that branches on the operating system for a reason SDL cannot paper over, and it is three questions: where this binary is, what has to be set before SDL starts, and what the display's content scale is. utils.c and boot.c each carried their own #ifdef _WIN32, and keeping OS guards contained in one file is what stops the next platform-specific bug from arriving.

  • The file log rotates mid-session, and the logger owns the stream. log_add_fp used to borrow a FILE * that main.c closed. It had to: a rotated stream has to be closed, renamed and reopened by whoever holds it, and main.c held it. The cap is 4 MiB, one previous run is kept as `cdin-log....

Read more

cdin v0.2.1-beta.5 (pre-release)

Pre-release

Choose a tag to compare

@github-actions github-actions released this 13 Oct 13:01
3751e05

⚠️ Pre-release — this is not a stable cdin

این یک نسخه پیش‌انتشار است، نه نسخه اصلی و نه نسخه پایدار.
A rolling pre-release built from main. It is published as a GitHub
prerelease, it can break at any moment, and none of it is covered by
the stable-release policy. Use it to see what is landing on main.

Version 0.2.1-beta.5
Previous pre-release v0.2.1-beta.4
Commit 3751e05
Built 2026-10-13 13:01 UTC
Triggered by m-mdy-m
Workflow run https://github.com/cdin-editor/cdin/actions/runs/38400672354

python3 cdin.py update installs the newest stable release and never
this one: GitHub keeps prereleases out of /releases/latest, and this
workflow never publishes one as stable.

📋 Unreleased, from the changelog

Added

  • pre-release.yml — every push gets its own version. A push to main that is not documentation-only now builds Windows and Linux, reads the next version off the tag history and publishes a GitHub prerelease for that exact commit: v0.2.1-alpha → v0.2.1-beta.1 → v0.2.1-beta.2, with the stage promoted (alpha → beta → rc) when it runs out and a new patch cycle opened after that. It reuses release-linux.yml and release-windows.yml rather than duplicating a build, and it publishes neither macOS nor Docker.

    The release body says, in Persian and English, that this is not the official build, carries the [Unreleased] changelog and every commit since the previous pre-release, and lists each asset with its size. release.yml (and the three per-platform workflows) now ignore tags with a hyphen ('!v*-*'), so a pre-release tag cannot trigger a stable release or a Docker Hub push.

  • release-linux.yml / release-windows.yml take a version input. Left empty, both still derive the version from the tag being built, which is what a tag push gets. pre-release.yml passes the version it computed, which is what makes the two build paths the same build.

  • docker.yml runs on pull requests only. It never pushed, but it built an image on every push to main — including every push that produces a pre-release. A published image still comes from release.yml on a stable vX.Y.Z tag.

Fixed

  • A .deb built from a pre-release version was rejected by dpkg-deb. Debian reads the last hyphen as the boundary between the upstream version and the Debian revision, so 0.2.1-beta.1 is 0.2.1 + revision beta.1. The version is now rewritten to 0.2.1~beta.1, which is valid and — because ~ sorts before an absent segment — orders before 0.2.1. Stable versions carry no hyphen and are unchanged.

🔀 Commits since v0.2.1-beta.4

  • 3751e05 ci: add install libuv - i hope fix (Mahdi)
  • 84c4da0 ci: add install libuv (Mahdi)
  • 1cdece1 ci: add install libuv (Mahdi)

🐧 Linux (x86-64)

File Size Download
cdin-0.2.1-beta.5-linux-x86_64.tar.gz 2.1M ⬇️ download
cdin_0.2.1~beta.5_amd64.deb 3.3M ⬇️ download

🪟 Windows (x86-64)

File Size Download
cdin-0.2.1-beta.5-windows-setup.exe 5.1M ⬇️ download
cdin-0.2.1-beta.5-windows-x86_64.zip 4.2M ⬇️ download

Installing this pre-release

Requires Python 3.8+ — pre-installed on every supported platform.

🐧 Linux (x86-64)

# .deb (Debian / Ubuntu). SDL3 ships inside the package.
sudo apt install ./cdin_*_amd64.deb

# tarball, any distro
tar xzf cdin-0.2.1-beta.5-linux-x86_64.tar.gz
cd cdin-0.2.1-beta.5-linux-x86_64

python3 cdin.py                             # run it
python3 cdin.py install --prefix ~/.local    # or install it

🪟 Windows (x86-64)

# installer
.\cdin-0.2.1-beta.5-windows-setup.exe

# or portable, no install
Expand-Archive .\cdin-0.2.1-beta.5-windows-x86_64.zip
cd cdin-0.2.1-beta.5-windows-x86_64
python cdin.py

No macOS build here: a pre-release publishes Windows and Linux only, and
never touches Docker.


برای نسخه پایدار و اصلی منتظر تگ vX.Y.Z باشید.
For the stable, official build, wait for a vX.Y.Z tag release.

cdin v0.2.1-beta.4 (pre-release)

Pre-release

Choose a tag to compare

@github-actions github-actions released this 13 Oct 12:57
0ff5024

⚠️ Pre-release — this is not a stable cdin

این یک نسخه پیش‌انتشار است، نه نسخه اصلی و نه نسخه پایدار.
A rolling pre-release built from main. It is published as a GitHub
prerelease, it can break at any moment, and none of it is covered by
the stable-release policy. Use it to see what is landing on main.

Version 0.2.1-beta.4
Previous pre-release v0.2.1-beta.3
Commit 0ff5024
Built 2026-10-13 12:57 UTC
Triggered by m-mdy-m
Workflow run https://github.com/cdin-editor/cdin/actions/runs/38400246964

python3 cdin.py update installs the newest stable release and never
this one: GitHub keeps prereleases out of /releases/latest, and this
workflow never publishes one as stable.

📋 Unreleased, from the changelog

Added

  • pre-release.yml — every push gets its own version. A push to main that is not documentation-only now builds Windows and Linux, reads the next version off the tag history and publishes a GitHub prerelease for that exact commit: v0.2.1-alpha → v0.2.1-beta.1 → v0.2.1-beta.2, with the stage promoted (alpha → beta → rc) when it runs out and a new patch cycle opened after that. It reuses release-linux.yml and release-windows.yml rather than duplicating a build, and it publishes neither macOS nor Docker.

    The release body says, in Persian and English, that this is not the official build, carries the [Unreleased] changelog and every commit since the previous pre-release, and lists each asset with its size. release.yml (and the three per-platform workflows) now ignore tags with a hyphen ('!v*-*'), so a pre-release tag cannot trigger a stable release or a Docker Hub push.

  • release-linux.yml / release-windows.yml take a version input. Left empty, both still derive the version from the tag being built, which is what a tag push gets. pre-release.yml passes the version it computed, which is what makes the two build paths the same build.

  • docker.yml runs on pull requests only. It never pushed, but it built an image on every push to main — including every push that produces a pre-release. A published image still comes from release.yml on a stable vX.Y.Z tag.

Fixed

  • A .deb built from a pre-release version was rejected by dpkg-deb. Debian reads the last hyphen as the boundary between the upstream version and the Debian revision, so 0.2.1-beta.1 is 0.2.1 + revision beta.1. The version is now rewritten to 0.2.1~beta.1, which is valid and — because ~ sorts before an absent segment — orders before 0.2.1. Stable versions carry no hyphen and are unchanged.

🔀 Commits since v0.2.1-beta.3

  • 0ff5024 ci: add install libuv (Mahdi)
  • 7779ea0 ci: add docker ci and update other workflows (Mahdi)
  • a95b020 chore: update gitignore (Mahdi)
  • 8f1b1e6 test(fuzz): add the seed corpus the three harnesses run against (Mahdi)
  • 9696161 docs(agents): rewrite the working rules as an owner contract (Mahdi)
  • 135ff1b build: split the verification targets out of mk/build.mk (Mahdi)
  • aa2e8c6 chore(measure): startup, frame and memory harnesses that print numbers (Mahdi)
  • 20fdc94 chore(check): static analysis, luacheck and the recorded warning profile (Mahdi)
  • 7272cab bench: record the font-face sharing baseline and the number to beat (Mahdi)
  • 8af05de test(fuzz): harnesses for the three parsers that read external bytes (Mahdi)
  • 3a56ee8 test(c): unit suites for logger, damage cache, allocator and job loop (Mahdi)
  • 7679287 fix(test): assert the reverse binding, not which stroke survived (Mahdi)
  • 6d2dc44 fix(rootview): find the content leaf, not the title bar (Mahdi)
  • 783e8ff fix(rootview): a locked pane split out of another locked one doubled its size (Mahdi)
  • 38854e9 fix(font): check the font handle instead of trusting the metatable (Mahdi)
  • dc58c16 feat(memory): tag the renderer's bytes and report where they are (Mahdi)
  • 0d5a66e feat(loop): read files off the frame thread with libuv (Mahdi)
  • ca88597 refactor(platform): replace utils.c and boot.c with one platform layer (Mahdi)
  • 0e9bea3 feat(lua): add the handle convention and the uniform error table (Mahdi)
  • 9bd3937 feat(base): add the tagged accounting allocator and the arena (Mahdi)
  • d647c8b feat(base): add CdinErr, one error convention for src/ (Mahdi)

🐧 Linux (x86-64)

File Size Download
cdin-0.2.1-beta.4-linux-x86_64.tar.gz 2.1M ⬇️ download
cdin_0.2.1~beta.4_amd64.deb 3.3M ⬇️ download

🪟 Windows (x86-64)

File Size Download
cdin-0.2.1-beta.4-windows-setup.exe 5.1M ⬇️ download
cdin-0.2.1-beta.4-windows-x86_64.zip 4.2M ⬇️ download

Installing this pre-release

Requires Python 3.8+ — pre-installed on every supported platform.

🐧 Linux (x86-64)

# .deb (Debian / Ubuntu). SDL3 ships inside the package.
sudo apt install ./cdin_*_amd64.deb

# tarball, any distro
tar xzf cdin-0.2.1-beta.4-linux-x86_64.tar.gz
cd cdin-0.2.1-beta.4-linux-x86_64

python3 cdin.py                             # run it
python3 cdin.py install --prefix ~/.local    # or install it

🪟 Windows (x86-64)

# installer
.\cdin-0.2.1-beta.4-windows-setup.exe

# or portable, no install
Expand-Archive .\cdin-0.2.1-beta.4-windows-x86_64.zip
cd cdin-0.2.1-beta.4-windows-x86_64
python cdin.py

No macOS build here: a pre-release publishes Windows and Linux only, and
never touches Docker.


برای نسخه پایدار و اصلی منتظر تگ vX.Y.Z باشید.
For the stable, official build, wait for a vX.Y.Z tag release.

cdin v0.2.1-beta.3 (pre-release)

Pre-release

Choose a tag to compare

@github-actions github-actions released this 10 Oct 22:38

⚠️ Pre-release — this is not a stable cdin

این یک نسخه پیش‌انتشار است، نه نسخه اصلی و نه نسخه پایدار.
A rolling pre-release built from main. It is published as a GitHub
prerelease, it can break at any moment, and none of it is covered by
the stable-release policy. Use it to see what is landing on main.

Version 0.2.1-beta.3
Previous pre-release v0.2.1-beta.2
Commit dd4efed
Built 2026-10-10 22:38 UTC
Triggered by m-mdy-m
Workflow run https://github.com/m-mdy-m/cdin/actions/runs/38091698540

python3 cdin.py update installs the newest stable release and never
this one: GitHub keeps prereleases out of /releases/latest, and this
workflow never publishes one as stable.

📋 Unreleased, from the changelog

Added

  • pre-release.yml — every push gets its own version. A push to main that is not documentation-only now builds Windows and Linux, reads the next version off the tag history and publishes a GitHub prerelease for that exact commit: v0.2.1-alpha → v0.2.1-beta.1 → v0.2.1-beta.2, with the stage promoted (alpha → beta → rc) when it runs out and a new patch cycle opened after that. It reuses release-linux.yml and release-windows.yml rather than duplicating a build, and it publishes neither macOS nor Docker.

    The release body says, in Persian and English, that this is not the official build, carries the [Unreleased] changelog and every commit since the previous pre-release, and lists each asset with its size. release.yml (and the three per-platform workflows) now ignore tags with a hyphen ('!v*-*'), so a pre-release tag cannot trigger a stable release or a Docker Hub push.

  • release-linux.yml / release-windows.yml take a version input. Left empty, both still derive the version from the tag being built, which is what a tag push gets. pre-release.yml passes the version it computed, which is what makes the two build paths the same build.

  • docker.yml runs on pull requests only. It never pushed, but it built an image on every push to main — including every push that produces a pre-release. A published image still comes from release.yml on a stable vX.Y.Z tag.

Fixed

  • A .deb built from a pre-release version was rejected by dpkg-deb. Debian reads the last hyphen as the boundary between the upstream version and the Debian revision, so 0.2.1-beta.1 is 0.2.1 + revision beta.1. The version is now rewritten to 0.2.1~beta.1, which is valid and — because ~ sorts before an absent segment — orders before 0.2.1. Stable versions carry no hyphen and are unchanged.

🔀 Commits since v0.2.1-beta.2

  • dd4efed docs: clarify cdin licensing and third-party notices (Genix)
  • 3997151 docs: update cdin license references (Genix)

🐧 Linux (x86-64)

File Size Download
cdin-0.2.1-beta.3-linux-x86_64.tar.gz 2.0M ⬇️ download
cdin_0.2.1~beta.3_amd64.deb 3.2M ⬇️ download

🪟 Windows (x86-64)

File Size Download
cdin-0.2.1-beta.3-windows-setup.exe 5.0M ⬇️ download
cdin-0.2.1-beta.3-windows-x86_64.zip 3.9M ⬇️ download

Installing this pre-release

Requires Python 3.8+ — pre-installed on every supported platform.

🐧 Linux (x86-64)

# .deb (Debian / Ubuntu). SDL3 ships inside the package.
sudo apt install ./cdin_*_amd64.deb

# tarball, any distro
tar xzf cdin-0.2.1-beta.3-linux-x86_64.tar.gz
cd cdin-0.2.1-beta.3-linux-x86_64

python3 cdin.py                             # run it
python3 cdin.py install --prefix ~/.local    # or install it

🪟 Windows (x86-64)

# installer
.\cdin-0.2.1-beta.3-windows-setup.exe

# or portable, no install
Expand-Archive .\cdin-0.2.1-beta.3-windows-x86_64.zip
cd cdin-0.2.1-beta.3-windows-x86_64
python cdin.py

No macOS build here: a pre-release publishes Windows and Linux only, and
never touches Docker.


برای نسخه پایدار و اصلی منتظر تگ vX.Y.Z باشید.
For the stable, official build, wait for a vX.Y.Z tag release.

cdin v0.2.1-beta.2 (pre-release)

Pre-release

Choose a tag to compare

@github-actions github-actions released this 10 Oct 22:34

⚠️ Pre-release — this is not a stable cdin

این یک نسخه پیش‌انتشار است، نه نسخه اصلی و نه نسخه پایدار.
A rolling pre-release built from main. It is published as a GitHub
prerelease, it can break at any moment, and none of it is covered by
the stable-release policy. Use it to see what is landing on main.

Version 0.2.1-beta.2
Previous pre-release v0.2.1-beta.1
Commit f623036
Built 2026-10-10 22:34 UTC
Triggered by m-mdy-m
Workflow run https://github.com/m-mdy-m/cdin/actions/runs/38091694178

python3 cdin.py update installs the newest stable release and never
this one: GitHub keeps prereleases out of /releases/latest, and this
workflow never publishes one as stable.

📋 Unreleased, from the changelog

Added

  • pre-release.yml — every push gets its own version. A push to main that is not documentation-only now builds Windows and Linux, reads the next version off the tag history and publishes a GitHub prerelease for that exact commit: v0.2.1-alpha → v0.2.1-beta.1 → v0.2.1-beta.2, with the stage promoted (alpha → beta → rc) when it runs out and a new patch cycle opened after that. It reuses release-linux.yml and release-windows.yml rather than duplicating a build, and it publishes neither macOS nor Docker.

    The release body says, in Persian and English, that this is not the official build, carries the [Unreleased] changelog and every commit since the previous pre-release, and lists each asset with its size. release.yml (and the three per-platform workflows) now ignore tags with a hyphen ('!v*-*'), so a pre-release tag cannot trigger a stable release or a Docker Hub push.

  • release-linux.yml / release-windows.yml take a version input. Left empty, both still derive the version from the tag being built, which is what a tag push gets. pre-release.yml passes the version it computed, which is what makes the two build paths the same build.

  • docker.yml runs on pull requests only. It never pushed, but it built an image on every push to main — including every push that produces a pre-release. A published image still comes from release.yml on a stable vX.Y.Z tag.

Fixed

  • A .deb built from a pre-release version was rejected by dpkg-deb. Debian reads the last hyphen as the boundary between the upstream version and the Debian revision, so 0.2.1-beta.1 is 0.2.1 + revision beta.1. The version is now rewritten to 0.2.1~beta.1, which is valid and — because ~ sorts before an absent segment — orders before 0.2.1. Stable versions carry no hyphen and are unchanged.

🔀 Commits since v0.2.1-beta.1

  • f623036 licensing: adopt GPL-3.0-or-later for cdin (Genix)

🐧 Linux (x86-64)

File Size Download
cdin-0.2.1-beta.2-linux-x86_64.tar.gz 2.0M ⬇️ download
cdin_0.2.1~beta.2_amd64.deb 3.2M ⬇️ download

🪟 Windows (x86-64)

File Size Download
cdin-0.2.1-beta.2-windows-setup.exe 5.0M ⬇️ download
cdin-0.2.1-beta.2-windows-x86_64.zip 3.9M ⬇️ download

Installing this pre-release

Requires Python 3.8+ — pre-installed on every supported platform.

🐧 Linux (x86-64)

# .deb (Debian / Ubuntu). SDL3 ships inside the package.
sudo apt install ./cdin_*_amd64.deb

# tarball, any distro
tar xzf cdin-0.2.1-beta.2-linux-x86_64.tar.gz
cd cdin-0.2.1-beta.2-linux-x86_64

python3 cdin.py                             # run it
python3 cdin.py install --prefix ~/.local    # or install it

🪟 Windows (x86-64)

# installer
.\cdin-0.2.1-beta.2-windows-setup.exe

# or portable, no install
Expand-Archive .\cdin-0.2.1-beta.2-windows-x86_64.zip
cd cdin-0.2.1-beta.2-windows-x86_64
python cdin.py

No macOS build here: a pre-release publishes Windows and Linux only, and
never touches Docker.


برای نسخه پایدار و اصلی منتظر تگ vX.Y.Z باشید.
For the stable, official build, wait for a vX.Y.Z tag release.

cdin v0.2.1-alpha

Choose a tag to compare

@github-actions github-actions released this 04 Oct 06:54
1cd32ef

[0.2.1-alpha] — 2026-10-04

Fixed

  • A side panel could not be resized by dragging its edge. The drag was never going to work: RootView:on_mouse_moved wrote node.divider, and calc_split_sizes does not read divider when either child has a locked size — it takes the locked child's size as given and hands the sibling the remainder.

    local n
    if     x1 then n = x1 + ds
    elseif x2 then n = self.size[x] - x2
    else         n = math.floor(self.size[x] * self.divider)

    So every frame of every drag moved a number the next layout pass discarded, and the pane kept the width it had. get_locked_size is the read side of a contract whose write side — set_locked_size(axis, value) on the pane's view — nothing in data/core/ had ever called.

    Node:drag_divider now asks the split which side is locked before deciding where the delta goes: no locked side moves divider exactly as before; a locked side gets the delta through the hook, a growing as the pointer moves right and b shrinking; a locked side whose view has no hook falls back to the divider, which is no worse than before and honest about there being nothing to talk to. Both ends clamp — eight cells, because a pane narrower than its own edge cannot be grabbed again, and because the layout would otherwise hand the sibling a negative size.

    cdin-x's treeview has implemented set_locked_size since it was written, so it resizes on the first load of this. cdin-x's manager panel implemented nothing, so it does not; that half is cdin-x's, and until it lands that one panel is draggable only with [ and ].

  • The divider was thinner to grab than anything else on screen. The hit test used a margin of six pixels on every display, while the rule itself is 1 * SCALE wide — three logical pixels either side of it on a 2x screen. The report was "the cursor does not change to a resize icon", which is the same fact as "the drag does not start": the hover and the press ask the same question, and the answer was no.

    The margin is in cells now, six of them — which is what six pixels was at 1x, so the zone widens on a scaled display and is unchanged on an unscaled one. Wider still starts costing the panel its own right-aligned column.

Documentation

  • docs/architecture/extension-contract.md documents set_locked_size beside attach_side_view: it is part of the side-panel interface now that something calls it, and an extension writing a panel from that page needs to know the axis is named rather than assumed.

  • docs/guides/commands.md says where the mouse resizes a split, next to the keyboard bindings that go quiet while a locked pane holds focus.


Installation

Requires Python 3.8+ — comes pre-installed on all supported platforms.

Linux (x86_64)

.deb (Debian/Ubuntu) — native package:

sudo apt install ./cdin_0.2.1-alpha_amd64.deb

SDL3 is bundled inside the package, so no extra runtime install is needed.
Uninstall with sudo apt remove cdin.

Tarball (any distro):

tar xzf cdin-v0.2.1-alpha-linux-x86_64.tar.gz
cd cdin-v0.2.1-alpha-linux-x86_64

# Interactive (recommended for first-time install):
python3 cdin.py

# Or non-interactive:
python3 cdin.py install                        # → ~/.local  (no sudo)
python3 cdin.py install --prefix /usr/local    # system-wide (needs sudo)
python3 cdin.py install --shortcut             # also create .desktop entry

SDL3 runtime — needed at runtime if not already installed:

  • Ubuntu 24.04+: sudo apt install libsdl3-0
  • Other distros: see SDL3 releases

Windows (x86_64)

Installer — native, recommended:

  1. Download cdin-0.2.1-alpha-windows-setup.exe and run it.
  2. Follow the wizard (Next › Next › Install). Optionally adds a desktop
    shortcut and adds cdin to your PATH.
  3. Uninstall anytime from Settings › Apps or the Start Menu shortcut.

Zip (portable, no install):

  1. Download cdin-v0.2.1-alpha-windows-x86_64.zip and extract it.
  2. Open PowerShell or Command Prompt inside the extracted folder and run:
# Interactive (recommended):
python cdin.py

# Or non-interactive:
python cdin.py install
python cdin.py install --shortcut              # also create desktop shortcut
python cdin.py install --register-filetypes    # set as default for text/code files
python cdin.py install --prefix C:\Tools\cdin  # custom install location

cdin.exe, SDL3.dll, and the Lua DLL are added to %LOCALAPPDATA%\cdin\bin
and that folder is added to your user PATH automatically.

macOS (arm64)

  1. Download cdin-v0.2.1-alpha-macos.dmg and double-click it.
  2. Drag cdin into the Applications folder shortcut.
  3. Eject the disk image and open cdin from Launchpad or Spotlight.

SDL3 is bundled inside the .app — no Homebrew or extra installs needed.


Updating & Uninstalling

python3 cdin.py update              # update to latest release
python3 cdin.py update --check      # just check, don't install
python3 cdin.py update --version 0.2.0

python3 cdin.py uninstall           # remove installed files

Contributors (v0.2.0-alpha → v0.2.1-alpha)