1use core::fmt;
2use core::ops::Deref;
3
4#[derive(Clone, Copy)]
24pub struct TextBuf<const N: usize> {
25 buf: [u8; N],
26 len: usize,
27 truncated: bool,
28}
29
30impl<const N: usize> TextBuf<N> {
31 pub const fn new() -> Self {
33 Self {
34 buf: [0; N],
35 len: 0,
36 truncated: false,
37 }
38 }
39
40 pub fn format(args: fmt::Arguments<'_>) -> Self {
43 let mut s = Self::new();
44 let _ = fmt::Write::write_fmt(&mut s, args);
45 s
46 }
47
48 pub fn as_str(&self) -> &str {
50 core::str::from_utf8(&self.buf[..self.len]).unwrap_or("")
52 }
53
54 pub fn clear(&mut self) {
56 self.len = 0;
57 self.truncated = false;
58 }
59
60 pub fn len(&self) -> usize {
62 self.len
63 }
64
65 pub fn is_empty(&self) -> bool {
67 self.len == 0
68 }
69
70 pub const fn capacity(&self) -> usize {
72 N
73 }
74
75 pub fn truncated(&self) -> bool {
78 self.truncated
79 }
80
81 pub fn pad(&mut self, width: usize) -> &mut Self {
85 let chars = self.as_str().chars().count();
86 for _ in chars..width {
87 if self.len == N {
88 break;
89 }
90 self.buf[self.len] = b' ';
91 self.len += 1;
92 }
93 self
94 }
95}
96
97impl<const N: usize> Default for TextBuf<N> {
98 fn default() -> Self {
99 Self::new()
100 }
101}
102
103impl<const N: usize> fmt::Write for TextBuf<N> {
104 fn write_str(&mut self, s: &str) -> fmt::Result {
105 let room = N - self.len;
106 let take = if s.len() <= room {
107 s.len()
108 } else {
109 self.truncated = true;
110 let mut n = room;
112 while !s.is_char_boundary(n) {
113 n -= 1;
114 }
115 n
116 };
117 self.buf[self.len..self.len + take].copy_from_slice(&s.as_bytes()[..take]);
118 self.len += take;
119 Ok(())
120 }
121}
122
123impl<const N: usize> Deref for TextBuf<N> {
124 type Target = str;
125 fn deref(&self) -> &str {
126 self.as_str()
127 }
128}
129
130impl<const N: usize> AsRef<str> for TextBuf<N> {
131 fn as_ref(&self) -> &str {
132 self.as_str()
133 }
134}
135
136impl<const N: usize> fmt::Display for TextBuf<N> {
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 f.write_str(self.as_str())
139 }
140}
141
142impl<const N: usize> fmt::Debug for TextBuf<N> {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 fmt::Debug::fmt(self.as_str(), f)
145 }
146}
147
148impl<const N: usize> PartialEq<str> for TextBuf<N> {
149 fn eq(&self, other: &str) -> bool {
150 self.as_str() == other
151 }
152}
153
154impl<const N: usize> PartialEq<&str> for TextBuf<N> {
155 fn eq(&self, other: &&str) -> bool {
156 self.as_str() == *other
157 }
158}
159
160#[cfg(feature = "defmt")]
161impl<const N: usize> defmt::Format for TextBuf<N> {
162 fn format(&self, f: defmt::Formatter<'_>) {
163 defmt::write!(f, "{}", self.as_str())
164 }
165}
166
167#[macro_export]
170macro_rules! text {
171 ($n:literal, $($arg:tt)*) => {
172 $crate::TextBuf::<$n>::format(::core::format_args!($($arg)*))
173 };
174}
175
176#[cfg(test)]
177mod tests {
178 use core::fmt::Write as _;
179
180 use super::*;
181
182 #[test]
183 fn truncates_at_char_boundary() {
184 let mut t = TextBuf::<5>::new();
185 write!(t, "ab€cd").unwrap(); assert_eq!(t, "ab€");
187 assert!(t.truncated());
188 let mut t = TextBuf::<4>::new();
189 write!(t, "ab€cd").unwrap(); assert_eq!(t, "ab");
191 assert!(t.truncated());
192 }
193
194 #[test]
195 fn pad_counts_characters() {
196 let mut t = text!(8, "é");
197 t.pad(3);
198 assert_eq!(t, "é ");
199 assert_eq!(t.len(), 4);
200 t.pad(100);
201 assert_eq!(t.len(), 8);
202 }
203
204 #[test]
205 fn clear_resets_everything() {
206 let mut t = text!(2, "abc");
207 assert!(t.truncated());
208 t.clear();
209 assert!(t.is_empty() && !t.truncated());
210 write!(t, "x").unwrap();
211 assert_eq!(&*t, "x");
212 }
213}