Repository navigation
Releases: cdin-editor/cdin
Release list
cdin v0.2.1-beta.9 (pre-release)
⚠️ Pre-release — this is not a stable cdin
این یک نسخه پیشانتشار است، نه نسخه اصلی و نه نسخه پایدار.
A rolling pre-release built frommain. 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 onmain.
| 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 updateinstalls 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
LICENSEfile is the GNU General Public License v3.README.md's badge and licence line say so, andNOTICEnow 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
mainormasternow 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 throughalpha → 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_realloccarry aCDIN_MEM_*tag and keep live, peak and block counts per tag;cdin_freereads 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 onfreewould 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_allocis 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_OKis zero so thaterr != CDIN_OKandif (err)cannot be confused by muscle memory. It carries no allocation, which is why there is nocdin_err_free. -
A handle convention for every piece of C-owned state Lua can reach. A userdata with a
__gcand aclose(), its type name in the metatable, and a destructor that runs exactly once. The type isvoid *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 tofree(). A closed handle is detectably closed —cdin_handle_checkrefuses 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_pushreturns{ 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.detailis 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, notnil, 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 reportsglyph_bytesandglyph_pages. Before this the renderer used rawmallocand was invisible to the per-subsystem report, which is why "2.1 MiB of glyphs" was a numbercore:memoryknew 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=1turns damage tracking off so every frame is presented whole, which answers "is the glitch in what the cache believes is on screen?";CDIN_LOG_FILEandCDIN_LOG_LEVELwere already there. -
A C unit test runner.
tests/ccovers the logger, the damage cache, the allocator, the arena andCdinErr, 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 -DDEBUGbecause the allocator compiles its leak registry and free quarantine underCDIN_ALLOC_DEBUG, which is tied toNDEBUG. -
Three fuzz harnesses and their seed corpora.
fuzz_eol,fuzz_pathandfuzz_utf8each 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 namedregress-*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, andbench.py— synthetic files of 4/16/64/256 MiB opened through the realDocpath, with the Lua heap and RSS read before load, after load, after highlighting and after close.size.pyis now whatmake sizeruns; it wasbench.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.coreis 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 tomainthat 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 reusesrelease-linux.ymlandrelease-windows.ymlrather 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.ymltake aversioninput. Left empty, both still derive the version from the tag being built, which is what a tag push gets.pre-release.ymlpasses the version it computed, which is what makes the two build paths the same build. -
docker.ymlruns on pull requests only. It never pushed, but it built an image on every push tomain— including every push that produces a pre-release. A published image still comes fromrelease.ymlon a stablevX.Y.Ztag. -
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.candsrc/core/boot.care replaced bysrc/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...
cdin v0.2.1-beta.10 (pre-release)
⚠️ Pre-release — this is not a stable cdin
این یک نسخه پیشانتشار است، نه نسخه اصلی و نه نسخه پایدار.
A rolling pre-release built frommain. 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 onmain.
| 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 updateinstalls 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
LICENSEfile is the GNU General Public License v3.README.md's badge and licence line say so, andNOTICEnow 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
mainormasternow 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 throughalpha → 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_realloccarry aCDIN_MEM_*tag and keep live, peak and block counts per tag;cdin_freereads 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 onfreewould 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_allocis 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_OKis zero so thaterr != CDIN_OKandif (err)cannot be confused by muscle memory. It carries no allocation, which is why there is nocdin_err_free. -
A handle convention for every piece of C-owned state Lua can reach. A userdata with a
__gcand aclose(), its type name in the metatable, and a destructor that runs exactly once. The type isvoid *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 tofree(). A closed handle is detectably closed —cdin_handle_checkrefuses 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_pushreturns{ 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.detailis 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, notnil, 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 reportsglyph_bytesandglyph_pages. Before this the renderer used rawmallocand was invisible to the per-subsystem report, which is why "2.1 MiB of glyphs" was a numbercore:memoryknew 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=1turns damage tracking off so every frame is presented whole, which answers "is the glitch in what the cache believes is on screen?";CDIN_LOG_FILEandCDIN_LOG_LEVELwere already there. -
A C unit test runner.
tests/ccovers the logger, the damage cache, the allocator, the arena andCdinErr, 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 -DDEBUGbecause the allocator compiles its leak registry and free quarantine underCDIN_ALLOC_DEBUG, which is tied toNDEBUG. -
Three fuzz harnesses and their seed corpora.
fuzz_eol,fuzz_pathandfuzz_utf8each 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 namedregress-*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, andbench.py— synthetic files of 4/16/64/256 MiB opened through the realDocpath, with the Lua heap and RSS read before load, after load, after highlighting and after close.size.pyis now whatmake sizeruns; it wasbench.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.coreis 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 tomainthat 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 reusesrelease-linux.ymlandrelease-windows.ymlrather 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.ymltake aversioninput. Left empty, both still derive the version from the tag being built, which is what a tag push gets.pre-release.ymlpasses the version it computed, which is what makes the two build paths the same build. -
docker.ymlruns on pull requests only. It never pushed, but it built an image on every push tomain— including every push that produces a pre-release. A published image still comes fromrelease.ymlon a stablevX.Y.Ztag. -
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.candsrc/core/boot.care replaced bysrc/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 ...
cdin v0.2.1-beta.8 (pre-release)
⚠️ Pre-release — this is not a stable cdin
این یک نسخه پیشانتشار است، نه نسخه اصلی و نه نسخه پایدار.
A rolling pre-release built frommain. 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 onmain.
| 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 updateinstalls 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
LICENSEfile is the GNU General Public License v3.README.md's badge and licence line say so, andNOTICEnow 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_realloccarry aCDIN_MEM_*tag and keep live, peak and block counts per tag;cdin_freereads 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 onfreewould 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_allocis 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_OKis zero so thaterr != CDIN_OKandif (err)cannot be confused by muscle memory. It carries no allocation, which is why there is nocdin_err_free. -
A handle convention for every piece of C-owned state Lua can reach. A userdata with a
__gcand aclose(), its type name in the metatable, and a destructor that runs exactly once. The type isvoid *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 tofree(). A closed handle is detectably closed —cdin_handle_checkrefuses 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_pushreturns{ 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.detailis 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, notnil, 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 reportsglyph_bytesandglyph_pages. Before this the renderer used rawmallocand was invisible to the per-subsystem report, which is why "2.1 MiB of glyphs" was a numbercore:memoryknew 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=1turns damage tracking off so every frame is presented whole, which answers "is the glitch in what the cache believes is on screen?";CDIN_LOG_FILEandCDIN_LOG_LEVELwere already there. -
A C unit test runner.
tests/ccovers the logger, the damage cache, the allocator, the arena andCdinErr, 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 -DDEBUGbecause the allocator compiles its leak registry and free quarantine underCDIN_ALLOC_DEBUG, which is tied toNDEBUG. -
Three fuzz harnesses and their seed corpora.
fuzz_eol,fuzz_pathandfuzz_utf8each 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 namedregress-*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, andbench.py— synthetic files of 4/16/64/256 MiB opened through the realDocpath, with the Lua heap and RSS read before load, after load, after highlighting and after close.size.pyis now whatmake sizeruns; it wasbench.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.coreis 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 tomainthat 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 reusesrelease-linux.ymlandrelease-windows.ymlrather 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.ymltake aversioninput. Left empty, both still derive the version from the tag being built, which is what a tag push gets.pre-release.ymlpasses the version it computed, which is what makes the two build paths the same build. -
docker.ymlruns on pull requests only. It never pushed, but it built an image on every push tomain— including every push that produces a pre-release. A published image still comes fromrelease.ymlon a stablevX.Y.Ztag. -
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.candsrc/core/boot.care replaced bysrc/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.candboot.ceach 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_fpused to borrow aFILE *thatmain.cclosed. It had to: a rotated stream has to be closed, renamed and reopened by whoever holds it, andmain.cheld it. The cap is 4 MiB, one previous run is kept as `cdin-log....
cdin v0.2.1-beta.7 (pre-release)
⚠️ Pre-release — this is not a stable cdin
این یک نسخه پیشانتشار است، نه نسخه اصلی و نه نسخه پایدار.
A rolling pre-release built frommain. 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 onmain.
| 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 updateinstalls 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
LICENSEfile is the GNU General Public License v3.README.md's badge and licence line say so, andNOTICEnow 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_realloccarry aCDIN_MEM_*tag and keep live, peak and block counts per tag;cdin_freereads 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 onfreewould 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_allocis 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_OKis zero so thaterr != CDIN_OKandif (err)cannot be confused by muscle memory. It carries no allocation, which is why there is nocdin_err_free. -
A handle convention for every piece of C-owned state Lua can reach. A userdata with a
__gcand aclose(), its type name in the metatable, and a destructor that runs exactly once. The type isvoid *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 tofree(). A closed handle is detectably closed —cdin_handle_checkrefuses 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_pushreturns{ 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.detailis 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, notnil, 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 reportsglyph_bytesandglyph_pages. Before this the renderer used rawmallocand was invisible to the per-subsystem report, which is why "2.1 MiB of glyphs" was a numbercore:memoryknew 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=1turns damage tracking off so every frame is presented whole, which answers "is the glitch in what the cache believes is on screen?";CDIN_LOG_FILEandCDIN_LOG_LEVELwere already there. -
A C unit test runner.
tests/ccovers the logger, the damage cache, the allocator, the arena andCdinErr, 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 -DDEBUGbecause the allocator compiles its leak registry and free quarantine underCDIN_ALLOC_DEBUG, which is tied toNDEBUG. -
Three fuzz harnesses and their seed corpora.
fuzz_eol,fuzz_pathandfuzz_utf8each 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 namedregress-*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, andbench.py— synthetic files of 4/16/64/256 MiB opened through the realDocpath, with the Lua heap and RSS read before load, after load, after highlighting and after close.size.pyis now whatmake sizeruns; it wasbench.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.coreis 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 tomainthat 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 reusesrelease-linux.ymlandrelease-windows.ymlrather 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.ymltake aversioninput. Left empty, both still derive the version from the tag being built, which is what a tag push gets.pre-release.ymlpasses the version it computed, which is what makes the two build paths the same build. -
docker.ymlruns on pull requests only. It never pushed, but it built an image on every push tomain— including every push that produces a pre-release. A published image still comes fromrelease.ymlon a stablevX.Y.Ztag. -
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.candsrc/core/boot.care replaced bysrc/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.candboot.ceach 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_fpused to borrow aFILE *thatmain.cclosed. It had to: a rotated stream has to be closed, renamed and reopened by whoever holds it, andmain.cheld it. The cap is 4 MiB, one previous run is kept as `cdin-log....
cdin v0.2.1-beta.6 (pre-release)
⚠️ Pre-release — this is not a stable cdin
این یک نسخه پیشانتشار است، نه نسخه اصلی و نه نسخه پایدار.
A rolling pre-release built frommain. 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 onmain.
| 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 updateinstalls 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
LICENSEfile is the GNU General Public License v3.README.md's badge and licence line say so, andNOTICEnow 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_realloccarry aCDIN_MEM_*tag and keep live, peak and block counts per tag;cdin_freereads 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 onfreewould 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_allocis 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_OKis zero so thaterr != CDIN_OKandif (err)cannot be confused by muscle memory. It carries no allocation, which is why there is nocdin_err_free. -
A handle convention for every piece of C-owned state Lua can reach. A userdata with a
__gcand aclose(), its type name in the metatable, and a destructor that runs exactly once. The type isvoid *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 tofree(). A closed handle is detectably closed —cdin_handle_checkrefuses 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_pushreturns{ 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.detailis 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, notnil, 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 reportsglyph_bytesandglyph_pages. Before this the renderer used rawmallocand was invisible to the per-subsystem report, which is why "2.1 MiB of glyphs" was a numbercore:memoryknew 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=1turns damage tracking off so every frame is presented whole, which answers "is the glitch in what the cache believes is on screen?";CDIN_LOG_FILEandCDIN_LOG_LEVELwere already there. -
A C unit test runner.
tests/ccovers the logger, the damage cache, the allocator, the arena andCdinErr, 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 -DDEBUGbecause the allocator compiles its leak registry and free quarantine underCDIN_ALLOC_DEBUG, which is tied toNDEBUG. -
Three fuzz harnesses and their seed corpora.
fuzz_eol,fuzz_pathandfuzz_utf8each 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 namedregress-*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, andbench.py— synthetic files of 4/16/64/256 MiB opened through the realDocpath, with the Lua heap and RSS read before load, after load, after highlighting and after close.size.pyis now whatmake sizeruns; it wasbench.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.coreis 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 tomainthat 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 reusesrelease-linux.ymlandrelease-windows.ymlrather 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.ymltake aversioninput. Left empty, both still derive the version from the tag being built, which is what a tag push gets.pre-release.ymlpasses the version it computed, which is what makes the two build paths the same build. -
docker.ymlruns on pull requests only. It never pushed, but it built an image on every push tomain— including every push that produces a pre-release. A published image still comes fromrelease.ymlon a stablevX.Y.Ztag. -
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.candsrc/core/boot.care replaced bysrc/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.candboot.ceach 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_fpused to borrow aFILE *thatmain.cclosed. It had to: a rotated stream has to be closed, renamed and reopened by whoever holds it, andmain.cheld it. The cap is 4 MiB, one previous run is kept as `cdin-log....
cdin v0.2.1-beta.5 (pre-release)
⚠️ Pre-release — this is not a stable cdin
این یک نسخه پیشانتشار است، نه نسخه اصلی و نه نسخه پایدار.
A rolling pre-release built frommain. 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 onmain.
| 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 updateinstalls 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 tomainthat 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 reusesrelease-linux.ymlandrelease-windows.ymlrather 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.ymltake aversioninput. Left empty, both still derive the version from the tag being built, which is what a tag push gets.pre-release.ymlpasses the version it computed, which is what makes the two build paths the same build. -
docker.ymlruns on pull requests only. It never pushed, but it built an image on every push tomain— including every push that produces a pre-release. A published image still comes fromrelease.ymlon a stablevX.Y.Ztag.
Fixed
- A
.debbuilt from a pre-release version was rejected bydpkg-deb. Debian reads the last hyphen as the boundary between the upstream version and the Debian revision, so0.2.1-beta.1is0.2.1+ revisionbeta.1. The version is now rewritten to0.2.1~beta.1, which is valid and — because~sorts before an absent segment — orders before0.2.1. Stable versions carry no hyphen and are unchanged.
🔀 Commits since v0.2.1-beta.4
3751e05ci: add install libuv - i hope fix (Mahdi)84c4da0ci: add install libuv (Mahdi)1cdece1ci: 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.pyNo 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 avX.Y.Ztag release.
cdin v0.2.1-beta.4 (pre-release)
⚠️ Pre-release — this is not a stable cdin
این یک نسخه پیشانتشار است، نه نسخه اصلی و نه نسخه پایدار.
A rolling pre-release built frommain. 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 onmain.
| 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 updateinstalls 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 tomainthat 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 reusesrelease-linux.ymlandrelease-windows.ymlrather 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.ymltake aversioninput. Left empty, both still derive the version from the tag being built, which is what a tag push gets.pre-release.ymlpasses the version it computed, which is what makes the two build paths the same build. -
docker.ymlruns on pull requests only. It never pushed, but it built an image on every push tomain— including every push that produces a pre-release. A published image still comes fromrelease.ymlon a stablevX.Y.Ztag.
Fixed
- A
.debbuilt from a pre-release version was rejected bydpkg-deb. Debian reads the last hyphen as the boundary between the upstream version and the Debian revision, so0.2.1-beta.1is0.2.1+ revisionbeta.1. The version is now rewritten to0.2.1~beta.1, which is valid and — because~sorts before an absent segment — orders before0.2.1. Stable versions carry no hyphen and are unchanged.
🔀 Commits since v0.2.1-beta.3
0ff5024ci: add install libuv (Mahdi)7779ea0ci: add docker ci and update other workflows (Mahdi)a95b020chore: update gitignore (Mahdi)8f1b1e6test(fuzz): add the seed corpus the three harnesses run against (Mahdi)9696161docs(agents): rewrite the working rules as an owner contract (Mahdi)135ff1bbuild: split the verification targets out of mk/build.mk (Mahdi)aa2e8c6chore(measure): startup, frame and memory harnesses that print numbers (Mahdi)20fdc94chore(check): static analysis, luacheck and the recorded warning profile (Mahdi)7272cabbench: record the font-face sharing baseline and the number to beat (Mahdi)8af05detest(fuzz): harnesses for the three parsers that read external bytes (Mahdi)3a56ee8test(c): unit suites for logger, damage cache, allocator and job loop (Mahdi)7679287fix(test): assert the reverse binding, not which stroke survived (Mahdi)6d2dc44fix(rootview): find the content leaf, not the title bar (Mahdi)783e8fffix(rootview): a locked pane split out of another locked one doubled its size (Mahdi)38854e9fix(font): check the font handle instead of trusting the metatable (Mahdi)dc58c16feat(memory): tag the renderer's bytes and report where they are (Mahdi)0d5a66efeat(loop): read files off the frame thread with libuv (Mahdi)ca88597refactor(platform): replace utils.c and boot.c with one platform layer (Mahdi)0e9bea3feat(lua): add the handle convention and the uniform error table (Mahdi)9bd3937feat(base): add the tagged accounting allocator and the arena (Mahdi)d647c8bfeat(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.pyNo 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 avX.Y.Ztag release.
cdin v0.2.1-beta.3 (pre-release)
⚠️ Pre-release — this is not a stable cdin
این یک نسخه پیشانتشار است، نه نسخه اصلی و نه نسخه پایدار.
A rolling pre-release built frommain. 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 onmain.
| 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 updateinstalls 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 tomainthat 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 reusesrelease-linux.ymlandrelease-windows.ymlrather 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.ymltake aversioninput. Left empty, both still derive the version from the tag being built, which is what a tag push gets.pre-release.ymlpasses the version it computed, which is what makes the two build paths the same build. -
docker.ymlruns on pull requests only. It never pushed, but it built an image on every push tomain— including every push that produces a pre-release. A published image still comes fromrelease.ymlon a stablevX.Y.Ztag.
Fixed
- A
.debbuilt from a pre-release version was rejected bydpkg-deb. Debian reads the last hyphen as the boundary between the upstream version and the Debian revision, so0.2.1-beta.1is0.2.1+ revisionbeta.1. The version is now rewritten to0.2.1~beta.1, which is valid and — because~sorts before an absent segment — orders before0.2.1. Stable versions carry no hyphen and are unchanged.
🔀 Commits since v0.2.1-beta.2
dd4efeddocs: clarify cdin licensing and third-party notices (Genix)3997151docs: 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.pyNo 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 avX.Y.Ztag release.
cdin v0.2.1-beta.2 (pre-release)
⚠️ Pre-release — this is not a stable cdin
این یک نسخه پیشانتشار است، نه نسخه اصلی و نه نسخه پایدار.
A rolling pre-release built frommain. 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 onmain.
| 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 updateinstalls 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 tomainthat 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 reusesrelease-linux.ymlandrelease-windows.ymlrather 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.ymltake aversioninput. Left empty, both still derive the version from the tag being built, which is what a tag push gets.pre-release.ymlpasses the version it computed, which is what makes the two build paths the same build. -
docker.ymlruns on pull requests only. It never pushed, but it built an image on every push tomain— including every push that produces a pre-release. A published image still comes fromrelease.ymlon a stablevX.Y.Ztag.
Fixed
- A
.debbuilt from a pre-release version was rejected bydpkg-deb. Debian reads the last hyphen as the boundary between the upstream version and the Debian revision, so0.2.1-beta.1is0.2.1+ revisionbeta.1. The version is now rewritten to0.2.1~beta.1, which is valid and — because~sorts before an absent segment — orders before0.2.1. Stable versions carry no hyphen and are unchanged.
🔀 Commits since v0.2.1-beta.1
f623036licensing: 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.pyNo 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 avX.Y.Ztag release.
cdin v0.2.1-alpha
[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_movedwrotenode.divider, andcalc_split_sizesdoes not readdividerwhen 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_sizeis the read side of a contract whose write side —set_locked_size(axis, value)on the pane's view — nothing indata/core/had ever called.Node:drag_dividernow asks the split which side is locked before deciding where the delta goes: no locked side movesdividerexactly as before; a locked side gets the delta through the hook,agrowing as the pointer moves right andbshrinking; 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_sizesince 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 * SCALEwide — 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.mddocumentsset_locked_sizebesideattach_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.mdsays 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.debSDL3 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 entrySDL3 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:
- Download
cdin-0.2.1-alpha-windows-setup.exeand run it. - Follow the wizard (Next › Next › Install). Optionally adds a desktop
shortcut and addscdinto your PATH. - Uninstall anytime from Settings › Apps or the Start Menu shortcut.
Zip (portable, no install):
- Download
cdin-v0.2.1-alpha-windows-x86_64.zipand extract it. - 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)
- Download
cdin-v0.2.1-alpha-macos.dmgand double-click it. - Drag cdin into the Applications folder shortcut.
- 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