Skip to main content

shuyi_playdate/
text.rs

1//! Text handed to the SDK, which takes C strings (`NUL`-terminated).
2
3use alloc::vec::Vec;
4use core::fmt;
5
6/// `text` as a C string: its bytes without any `NUL`, then one `NUL`.
7pub fn c_string(text: &str) -> Vec<u8> {
8    let mut bytes: Vec<u8> = text.bytes().filter(|&byte| byte != 0).collect();
9    bytes.push(0);
10    bytes
11}
12
13/// A C string of at most `N - 1` bytes written with [`fmt::Write`], without
14/// allocating: the panic handler's message, which must not fail when the
15/// heap is what ran out. Text that doesn't fit is cut at a character
16/// boundary; `NUL`s are left out. `N` is at least 1.
17#[derive(Debug)]
18pub struct FixedText<const N: usize> {
19    bytes: [u8; N],
20    len: usize,
21    /// Something didn't fit: nothing more is written, so the text is always
22    /// a beginning of what was written.
23    full: bool,
24}
25
26impl<const N: usize> FixedText<N> {
27    /// Empty text.
28    pub const fn new() -> Self {
29        Self {
30            bytes: [0; N],
31            len: 0,
32            full: false,
33        }
34    }
35
36    /// The text so far with its `NUL`, for the SDK.
37    pub fn as_c_bytes(&self) -> &[u8] {
38        &self.bytes[..=self.len]
39    }
40}
41
42impl<const N: usize> Default for FixedText<N> {
43    fn default() -> Self {
44        Self::new()
45    }
46}
47
48impl<const N: usize> fmt::Write for FixedText<N> {
49    fn write_str(&mut self, text: &str) -> fmt::Result {
50        if self.full {
51            return Ok(());
52        }
53        for ch in text.chars().filter(|&ch| ch != '\0') {
54            let mut utf8 = [0; 4];
55            let encoded = ch.encode_utf8(&mut utf8).as_bytes();
56            // One byte stays free for the `NUL`.
57            let end = self.len + encoded.len();
58            if end + 1 > N {
59                self.full = true;
60                return Ok(());
61            }
62            self.bytes[self.len..end].copy_from_slice(encoded);
63            self.len = end;
64        }
65        Ok(())
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use core::fmt::Write as _;
72
73    use super::*;
74
75    #[test]
76    fn a_c_string_has_one_nul_at_its_end() {
77        assert_eq!(c_string("a\0b"), b"ab\0");
78        assert_eq!(c_string(""), b"\0");
79    }
80
81    #[test]
82    fn fixed_text_keeps_a_byte_for_its_nul() {
83        let mut text = FixedText::<4>::new();
84        let _ = write!(text, "abcd");
85        assert_eq!(text.as_c_bytes(), b"abc\0");
86    }
87
88    #[test]
89    fn fixed_text_is_cut_at_a_character_and_stays_cut() {
90        let mut text = FixedText::<5>::new();
91        let _ = write!(text, "ab\0é");
92        assert_eq!(text.as_c_bytes(), "abé\0".as_bytes());
93        let _ = write!(text, "é");
94        let _ = write!(text, "c");
95        assert_eq!(text.as_c_bytes(), "abé\0".as_bytes());
96    }
97}