nmbrs_runtime/readouts/buf.rs
1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! [`ReadoutBuf`] — the surface-supplied output buffer.
5//!
6//! Push 1 only needs the [`String`]-backed buffer that the
7//! terminal-mode log path uses (ANSI escape sequences land
8//! inline as text). Later pushes will add a `Vec<Span>`-backed
9//! buffer for the TUI surface; the [`ReadoutBuf`] trait is
10//! the single write surface a [`Readout`](crate::Readout)
11//! impl sees regardless of which backing store is in use.
12
13use std::fmt::{self, Write as _};
14
15/// Append-only write surface a readout writes its rendered
16/// piece into. Implementations decide how to handle styling
17/// (ANSI inline vs. typed style runs) and ANSI stripping
18/// (TTY vs. non-TTY sinks).
19///
20/// The trait is intentionally minimal — `write_str` is
21/// enough for the Push 1 built-in. Later pushes will extend
22/// it with style-run pushes (`push_styled`) once the colour
23/// / style sub-language lands; until then, readouts emit
24/// ANSI escapes as part of `write_str` payloads when they
25/// want styling, and the buffer's own ANSI policy decides
26/// whether to keep or strip.
27pub trait ReadoutBuf {
28 /// Append the given UTF-8 text. Must not allocate
29 /// independently of the underlying storage's growth.
30 fn write_str(&mut self, s: &str) -> fmt::Result;
31
32 /// Hint about how many more bytes will be appended.
33 /// Implementations may use this to reserve capacity.
34 /// Optional — default no-op.
35 fn reserve(&mut self, _additional: usize) {}
36}
37
38/// `String`-backed buffer. Used by the terminal-mode log
39/// surface, which routes through `nmbrs-runtime::observer::log`
40/// and ultimately `eprint!`. ANSI escape sequences pass
41/// through unchanged; the surface above strips them when the
42/// destination is a non-TTY pipe.
43pub struct StringBuf<'a> {
44 inner: &'a mut String,
45}
46
47impl<'a> StringBuf<'a> {
48 pub fn new(inner: &'a mut String) -> Self {
49 Self { inner }
50 }
51}
52
53impl ReadoutBuf for StringBuf<'_> {
54 fn write_str(&mut self, s: &str) -> fmt::Result {
55 self.inner.write_str(s)
56 }
57
58 fn reserve(&mut self, additional: usize) {
59 self.inner.reserve(additional);
60 }
61}
62
63/// Convenience: render a readout into a freshly allocated
64/// `String`. Used by the in-process tests and the
65/// `crate::diag!` bridge in Push 1; later pushes will route
66/// directly through a borrowed buffer instead.
67pub fn render_to_string<F>(estimated_size: usize, render: F) -> String
68where
69 F: FnOnce(&mut StringBuf<'_>) -> usize,
70{
71 let mut s = String::with_capacity(estimated_size);
72 {
73 let mut buf = StringBuf::new(&mut s);
74 let _ = render(&mut buf);
75 }
76 s
77}