Building a TUI Library from Scratch: Part 6 - Stabilization and Release Pain

Things I learned:

Part 5 ended with paint/layout in Rust. Performance looked great.

Then I hit the less fun part: stability and release workflow.

Responsiveness regression

I had a bug where responsiveness degraded after resize/reinit. Root cause: I freed buffers but forgot to reset first-diff state.

pub extern "C" fn free_buffer() -> c_int {
    *CURRENT_BUFFER.lock().unwrap() = None;
    *LAST_BUFFER.lock().unwrap() = None;
    *FIRST_DIFF.lock().unwrap() = true;
    // ...
}

One boolean, noticeable UX impact.

Locking cleanup in paint

I also removed unnecessary buffer locks in Rust paint. Before, helper draw functions locked buffer internally. After, paint() takes one lock and passes &mut [u64] down.

// before: each draw_* locked CURRENT_BUFFER internally
fn draw_text_at(x: f32, y: f32, text: &str, ...)

// after: paint() takes one lock, helpers just write
fn draw_text_at(buf: &mut [u64], x: f32, y: f32, text: &str, ...)

Less lock churn in hot path.

Benchmark measurement fixes

I also fixed benchmark wrappers for OpenTUI and pi-mono. I was measuring at the wrong point for their render scheduling. Switched to measuring after scheduled completion (setTimeout(0) in adapters).

After this pass, sample metrics were around 0.5ms avg and ~1900fps in that scene.

Research before next optimization

Once paint was stable, text path became the obvious target. I wrote TEXT_SERIALIZATION_OPTIMIZATIONS.md to map waste before coding changes.

Main findings:

I also added a small stress script for raw terminal writes as calibration.

Repo refactor + release cycle

I did a big structure cleanup before release:

Then I shipped v0.0.10 and immediately hit publish/package issues. Fixed package files/deps, then hit another issue: logger could crash in some environments.

I changed logger init to safe fallback:

function createLogWriter() {
  try {
    return Bun.file("dump/logs.txt").writer();
  } catch {
    return { write() {}, flush() {} };
  }
}

That became v0.0.11.

End of part 6

Part 5 was mostly raw speed. Part 6 was making that speed usable and shippable.

Part 7 is where text/style sync architecture changed for real.