# YTile Language Decision > Produced 2026-08-10: five candidate languages evaluated against the architecture digest, judged with a restructured-Rust baseline. Decision: C# / .NET 10, NativeAOT. # YTile Language Decision — Final Judgment ## 1. Ranked Comparison | Rank | Option | Score | One-line judgment | |------|--------|-------|-------------------| | 1 | **C# / .NET 10 (NativeAOT ship, JIT dev loop)** | **8.0** | Every hard requirement verified workable; best-on-Windows iteration speed on a project whose real cost is quirk archaeology; its two weaknesses (GC in a render path, sum types) are neutralized by YTile's own scope cut. | | 2 | Rust, restructured (baseline) | 7.5 | Highest technical ceiling and zero translation risk — but it fails the reason this question exists. | | 3 | Modern C++ (C++20 + WIL) | 7.0 | Interop gold standard, fastest MVP — but a memory-unsafe language, with no TSan on Windows, for a solo long-running daemon whose failure mode is corrupting the user's session. Adjusted down from 7.5: the assessor called the safety regression "the deciding argument against" and then didn't let it decide. | | 4 | Go | 6.5 | Architecturally the cleanest translation (channels, goroutines, recover-loops) and the 2026 GC retires the latency objection — but you permanently own an artisanal uintptr bindings layer and lose the typed protocol. | | 5 | Odin | 6.0 | Alive first-party bindings and ideal runtime shape, but manual memory management in a months-uptime stateful daemon is a permanent risk, not a bounded cost. Adjusted down from 6.5. | | 6 | Zig | 5.5 | Ties Rust on the mechanical hard parts, but two guaranteed-breaking toolchain migrations per year plus co-maintaining your bindings is an unacceptable standing tax on a multi-year project. Adjusted down from 6. | **On the Rust baseline, judged honestly.** A restructured Rust rewrite is technically the strongest option: windows-rs is the maintained, generated, typed binding every other candidate approximates; serde+clap solve the 187-variant protocol/CLI/schema problem in one type; the compiler statically verifies exactly the cross-thread ownership lattice (GWLP_USERDATA pointers, `Box::into_raw` PostMessage handoffs, lossy channels over shared state) that every other candidate polices by discipline; and the community ASC database, the quirk lore, and komorebi's implementation approaches translate at near-zero cost. On pure fit it is a 9. I score it 7.5 because the premise of this exercise is that Rust is unsatisfying enough to shop alternatives — and for a solo, multi-year, motivation-funded project, resenting your language is a real engineering risk, not a soft preference. One important caveat: much of what makes komorebi's Rust unpleasant (the `Arc>` shared by nine threads, `unsafe` callback plumbing, `Box::into_raw` ceremony) is komorebi's architecture, not Rust. The single-actor restructure proposed in §4 would remove most of it. **If, on reflection, the dissatisfaction is with komorebi's shape rather than Rust the language, promote this to #1 and stop reading.** If it is Rust itself — the compile times, the ceremony density, the FFI friction — then: ## 2. Recommendation: C# / .NET 10, NativeAOT Build YTile in C# on .NET 10 (current LTS), developed on CoreCLR JIT, shipped as NativeAOT for both daemon and CLI. Reasoning tied to each workload pillar: - **WinEvent hook + message pump.** `[UnmanagedCallersOnly]` static methods passed as `delegate* unmanaged[Stdcall]` are allocation-free, lifetime-safe (the classic "GC collected my hook delegate" failure is structurally eliminated), and identical under JIT and AOT. The hook thread is a plain managed `Thread` running a `GetMessageW` P/Invoke loop. This matches komorebi's design exactly — static callback, global state, coarse filter, bounded channel. - **COM, including the undocumented ImmersiveShell path.** Documented COM (IDesktopWallpaper) via `[GeneratedComInterface]` — Microsoft's NativeAOT-first story. The undocumented `IApplicationViewCollection`/`IApplicationView::SetCloak` vtables (12 padding slots) are hand-declared exactly as komorebi hand-transcribes them in Rust, with raw `calli` through the vtable as a zero-dependency escape hatch. Effort is a wash versus Rust; the build-fragile IIDs are equally fragile in every language. - **DWM and the full flat-Win32 surface.** CsWin32 (Microsoft-owned, actively released through mid-2026) generates everything in the digest on demand: SetWinEventHook, SetWindowPos, DWMWA_CLOAKED/EXTENDED_FRAME_BOUNDS, QueryDisplayConfig, WTS/power/device notifications, SendInput. No binding layer to own. - **Long-running daemon and the GC.** This is C#'s honest weakness, and YTile's scope decision defuses it: tiling only, no bar, and (per §3) DWMWA_BORDER_COLOR borders instead of D2D overlays means **there is no latency-critical render path in the process**. The event hot path is allocation-free by construction (struct events, pre-filtered callback, bounded channel), so GC pressure comes only from JSON at command/config frequency, not event frequency. The GlazeWM-rewrote-in-Rust precedent is the strongest counter-signal, and I discount it deliberately: GlazeWM carried a WPF-era architecture and a bar; YTile starts NativeAOT-first with none of that. - **IPC.** BCL covers AF_UNIX on Windows, named pipes, and FileSystemWatcher natively — three of komorebi's dependency crates disappear. The sum-type gap (no discriminated unions until C# 15 GAs ~Nov 2026) is the biggest ergonomic loss versus Rust, but v0.1's protocol is ~25 verbs, not 187: a `[JsonDerivedType]` record hierarchy plus one small custom converter is a contained, boring cost — and language-level unions arrive before the protocol grows. - **JSON config.** System.Text.Json with source-generated contexts is AOT-safe and handles the Option-heavy config; .NET's JsonSchemaExporter covers the schemars role. - **The decisive factor: iteration speed.** Every assessor and the digest agree the dominant cost of this project is not code — it is empirically rediscovering Windows behavior (Firefox's NameChange launch, WINWORD's zombie layered window, spurious monitor removals, stale-foreground races). That work rewards the fastest edit-break-inspect loop on the platform, which is Visual Studio/Rider on C#, by a wide margin over every alternative including Rust. **Non-negotiable guardrails, day one:** (1) the CLI binary ships NativeAOT — a JIT CLI at 100ms+ per hotkey breaks the whole whkd-driven design; (2) CI runs the test suite against the AOT-published binary, not just JIT; (3) `DisableRuntimeMarshalling` + CsWin32 `allowMarshaling:false` from the first commit, not retrofitted; (4) every `[UnmanagedCallersOnly]` entry point (hook callback, WndProcs) wraps its body in try/catch — one escaping exception aborts an AOT process; (5) AOT analyzers (`IsAotCompatible`) on from the start. ## 3. YTile v0.1 — Minimal Viable Scope **Build, in this order:** 1. **Event spine.** Dedicated hook thread + `GetMessageW` pump; register *narrow* WinEvent ranges (SHOW/HIDE/DESTROY/CLOAKED/UNCLOAKED/FOCUS, SYSTEM_FOREGROUND, MINIMIZESTART/END, MOVESIZESTART/END, NAMECHANGE) — not EVENT_MIN..MAX; OBJID_WINDOW filter in the callback; struct events into a bounded channel; single WM actor thread. Ship a `--debug-events` dump mode first — it pays for itself immediately. 2. **Eligibility + tracking.** The full should_manage stack (IsWindow, min size, title, DWMWA_CLOAKED, WS_CAPTION+WS_EX_WINDOWEDGE heuristic, reject DLGMODALFRAME/LAYERED-unless-whitelisted), `known_hwnds` map, EnumWindows adoption pass. 3. **Layout.** Monitor > Workspace > Window (no Container — see §4). Two layouts: BSP and Columns, as pure functions `(work_area, count, padding) -> rect[]`. One retile entry point. SetWindowPos with shadow-rect compensation (GetWindowRect vs DWMWA_EXTENDED_FRAME_BOUNDS) and SWP_NOACTIVATE|ASYNCWINDOWPOS|FRAMECHANGED. 4. **Focus.** FocusChange with the stale-foreground check; directional focus/move by index arithmetic; SetForegroundWindow with the SendInput foreground-lock bypass; AllowSetForegroundWindow + SPI foreground-lock-timeout at startup. 5. **Workspaces.** Per-monitor workspaces; hiding via `IApplicationView::SetCloak` (the one COM component in v0.1, ~200 lines) with plain SW_HIDE as a config fallback; HIDDEN_HWNDS-style self-inflicted-event suppression. 6. **Liveness + quirks.** Reaper as a 100–250ms timer tick inside the actor; quirk data consumed from the community `applications.json` — minimum: ignore rules, NameChange-as-Show list, tray/multi-window list, layered whitelist. Do not rediscover this lore. 7. **IPC + CLI + config.** One transport (UDS or named pipe — pick one), newline-delimited JSON both directions, ~25 verbs (focus/move ×4, workspace focus/send, layout set, retile, toggle float/monocle/pause, state, query, stop). NativeAOT CLI. JSON config: monitors → workspaces → layout/padding, exe/class/title rules with Equals + Regex strategies. 8. **Focus indicator.** DWMWA_BORDER_COLOR on the focused window (~30 lines, Win11). 9. **Monitor reconciliation, minimal.** Hidden message window for WM_DISPLAYCHANGE / WM_DEVICECHANGE / WM_SETTINGCHANGE(SPI_SETWORKAREA) / WM_POWERBROADCAST resume; the 3-second removal debounce and the 10-second minimize-suppression grace period (both load-bearing against real OS behavior); on change: re-enumerate by device id, refresh work areas, retile. No workspace cache. 10. **Drag handling.** MoveResizeStart/End with the rect-delta move-vs-resize test; drag-to-swap only. **Defer to v0.2+:** container stacking, delta-based resize + per-layout constraint tables, monocle/maximize toggles (cheap once core works — early v0.2), remaining layouts (Grid, Scrolling, Custom), monitor workspace cache / hot-plug restore, pub/sub subscriptions, state persistence/restore, config hot-reload, multi-monitor DPI double-retile polish, floating-layer rules. **Never (or not until the project demands it):** D2D border overlay windows and their destruction protocol, animations/ghost thumbnails and all the Chromium-cloak lore, stackbars, transparency manager, themes, wallpapers, virtual-desktop gating, TCP remote control, legacy YAML ASC and AHK/PS1 generation, focus-follows-mouse/mouse-follows-focus. Estimated v0.1: **6–9 weeks full-time**, ~6–9k lines. ## 4. Architectural Simplifications vs komorebi 1. **Single-actor state, not a shared mutex.** Replace `Arc>` locked by nine threads with one state-owning thread consuming one queue. OS events, IPC commands, reaper ticks, and monitor notifications all become messages in a single unified enum. This deletes the lock-ordering surface, the `try_lock_for(1s)` silently-dropped-command failure mode, and the entire class of cross-thread state bugs — and it is the change that makes C# (or any language) safe where komorebi needed Rust. 2. **Narrow hook registration.** Komorebi hooks EVENT_MIN..EVENT_MAX and discards ~90% in the callback. Register only the events consumed; the firehose gets cut at the OS. 3. **Never block the hook callback.** Komorebi's callback blocks on a full bounded(20) channel and panics if it closes. Use lossy try-write with drop-oldest, plus a "resync" full-rescan message on overflow — safe because consumers re-derive truth from OS state anyway. 4. **No blocking sleeps in the actor.** Komorebi's slow-application compensation is a literal `thread::sleep` in the event path. Schedule a delayed re-evaluation message instead. Same for the reaper: a timer tick in the actor, owning `known_hwnds` directly — no mirrored HWNDS_CACHE, no separate poller+consumer thread pair. 5. **Config as an immutable snapshot.** Komorebi fans config into ~15 global mutable statics, and its append-only rule vectors mean deleting a rule doesn't take effect until restart. A fresh design builds one immutable Config object per load and swaps it atomically — hot-reload becomes correct for free. 6. **DWM-native chrome only.** DWMWA_BORDER_COLOR replaces the entire D2D border subsystem: per-window message-pump threads, the `Box::into_raw` ownership protocol, the blur-region hack, and the SendNotifyMessageW fast path all cease to exist. This is the single largest complexity deletion available, and it is what makes the GC acceptable. 7. **Flatten the hierarchy.** Drop the Container ring in v0.1 (window = tile); reintroduce it only if stacking ships. Keep `Ring` as the one collection abstraction and `Workspace.Update()` as the one retile entry point — those parts of komorebi are right. 8. **One protocol, framed.** One IPC transport, and give replies actual framing (length-prefix or NDJSON both ways) instead of komorebi's acknowledged-TODO "shutdown signals EOF". Drop TCP, drop legacy named-pipe subscriptions. 9. **Quirks as data, not scattered code.** The Firefox/WINWORD/OleMainThreadWndClass/Steam lore lives in one declarative quirk table (largely the community ASC file) consulted at defined pipeline points — not as special cases threaded through process_event. 10. **Ship no deprecated surface.** No serde aliases for old field names, no Hide/Minimize legacy hiding behaviours as defaults (Cloak primary, SW_HIDE as the documented fallback), no border_overflow ghosts, no AHK/PS1 generators. Komorebi carries five years of compatibility; YTile carries none. **Bottom line:** C#/.NET 10 with NativeAOT, single-actor architecture, DWM-native visuals, v0.1 scope as above. If the Rust dissatisfaction turns out to be komorebi-shaped rather than language-shaped, the same architecture in restructured Rust is the superior technical choice — but pick one and commit; both paths are fully viable, and the quirk lore in this digest is the asset that matters more than either language.