morse_codec/message.rs
1//! Message struct to hold decoded message or message to be encoded.
2//!
3//! Client code can use this to access and manipulate the
4//! internal message of MorseDecoder or MorseEncoder:
5//!
6//! ```ignore
7//! // Get a decoded message
8//! let decoded_message = decoder.message.as_str();
9//! let decoded_message_chars = decoder.message.as_charray();
10//! // ...Do something with the message...
11//!
12//! // Clear the message
13//! decoder.message.clear();
14//!
15//! // Set message to something different
16//! // and continue decoding from the end
17//! decoder.message.set_message("SOME INITIAL MESSAGE", true);
18//!
19//! // We continue sending signals
20//! decoder.signal_event(125, true);
21//! decoder.signal_event(200, false);
22//! ....
23//!
24//! // To show an editing cursor on the screen
25//! let editing_position = decoder.message.get_edit_pos();
26//! ```
27
28use crate::{
29 FILLER,
30 FILLER_CHAR,
31 Character,
32};
33
34#[cfg(feature = "utf8")]
35use core::fmt::Display;
36
37#[cfg(feature = "utf8")]
38#[derive(Debug)]
39/// When "utf8" feature is enabled, instead of &str
40/// we return this new type struct as a placeholder for &str,
41/// because it's still hard to use arithmetic operations in
42/// const expressions. In the future if this PR gets merged:
43/// <https://github.com/rust-lang/rust/issues/76560>
44/// We might update the code to do stuff like:
45/// let chars: [0; MSG_MAX * 4] = ...
46pub struct Utf8Charray<'a>(&'a [char]);
47
48#[cfg(feature = "utf8")]
49impl Display for Utf8Charray<'_> {
50 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
51 for ch in self.0 {
52 write!(f, "{}", ch)?;
53 }
54
55 Ok(())
56 }
57}
58
59#[cfg(feature = "utf8")]
60impl PartialEq<&str> for Utf8Charray<'_> {
61 fn eq(&self, other: &&str) -> bool {
62 let mut other_chars = other.chars();
63 for &ch in self.0.iter() {
64 let other_char = other_chars.next();
65 if other_char.is_none() || ch != other_char.unwrap() {
66 return false;
67 }
68 }
69
70 true
71 }
72}
73
74#[cfg(feature = "utf8")]
75impl Utf8Charray<'_> {
76 pub fn iter(&self) -> impl Iterator<Item = &char> {
77 self.0.iter()
78 }
79}
80
81/// This struct holds the message in human readable format.
82///
83/// It also provides functions to do edit position manipulation,
84/// getting or setting characters at index positions.
85pub struct Message<const MSG_MAX: usize> {
86 chars: [Character; MSG_MAX],
87 edit_pos: usize,
88 last_change_index: usize,
89 clamp_edit_pos: bool,
90}
91
92impl<const MSG_MAX: usize> Default for Message<MSG_MAX> {
93 fn default() -> Self {
94 Self {
95 chars: [FILLER; MSG_MAX],
96 edit_pos: 0,
97 last_change_index: 0,
98 clamp_edit_pos: false,
99 }
100 }
101}
102
103// Constructor with a starter string
104impl<const MSG_MAX: usize> Message<MSG_MAX> {
105 /// Maximum index editing position can be at
106 pub const POS_MAX: usize = MSG_MAX - 1;
107
108 /// Get an instance of Message starting from an &str.
109 ///
110 /// edit_pos_end means client code wants to continue editing this
111 /// text at the end.
112 pub fn new(message_str: &str, edit_pos_end: bool, clamp_edit_pos: bool) -> Self {
113 let mut new_self = Self {
114 chars: Self::str_to_chars(message_str),
115 clamp_edit_pos,
116 ..Self::default()
117 };
118
119 if edit_pos_end {
120 new_self.edit_pos = new_self.len().clamp(0, Self::POS_MAX);
121 }
122
123 new_self
124 }
125
126 #[cfg(not(feature = "utf8"))]
127 // Static member utility function to convert an &str to character array internal format.
128 fn str_to_chars(str: &str) -> [u8; MSG_MAX] {
129 let mut str_iter = str.chars()
130 .take(MSG_MAX)
131 .filter(|ch| ch.is_ascii());
132
133 core::array::from_fn(|_|
134 str_iter.next()
135 .unwrap_or(FILLER_CHAR)
136 .to_ascii_uppercase() as u8
137 )
138 }
139
140 #[cfg(feature = "utf8")]
141 // Static member utility function to convert an &str to charray internal format.
142 fn str_to_chars(str: &str) -> [Character; MSG_MAX] {
143 let mut str_iter = str.chars()
144 .take(MSG_MAX);
145
146 core::array::from_fn(|_|
147 str_iter.next()
148 .unwrap_or(FILLER_CHAR)
149 .to_uppercase()
150 .next()
151 .unwrap()
152 )
153 }
154}
155
156// Private stuff
157impl<const MSG_MAX: usize> Message<MSG_MAX> {
158 // Index of last character before the last FILLERs
159 fn last_char_index(&self) -> Option<usize> {
160 self.chars.iter().rposition(|ch| *ch != FILLER)
161 }
162
163 // Check if any FILLER characters are between normal chars
164 // and convert them to ' ' space characters.
165 fn update_empty_chars(&mut self) {
166 if let Some(last_index) = self.last_char_index() {
167 self.chars.iter_mut().enumerate().for_each(|(index, ch)| {
168 if *ch == FILLER && index < last_index {
169 *ch = ' ' as Character;
170 }
171 });
172 }
173 }
174}
175
176// Public API
177impl<const MSG_MAX: usize> Message<MSG_MAX> {
178 /// Get an iterator to the message chars contained within.
179 pub fn iter(&self) -> MessageIterator<MSG_MAX> {
180 MessageIterator {
181 message: self,
182 index: 0,
183 }
184 }
185
186 /// Sets current editing position to given value.
187 pub fn set_edit_pos(&mut self, pos: usize) {
188 self.edit_pos = pos.clamp(0, Self::POS_MAX);
189 }
190
191 /// Change the clamping behaviour of the edit position to
192 /// wrapping (default) or clamping.
193 ///
194 /// With clamping set, when edit position is shifted to left or right,
195 /// it won't cycle forward to maximum position or revert back to zero position,
196 /// effectively remaining within the limits of the message no matter current position is.
197 pub fn set_edit_position_clamp(&mut self, clamp: bool) {
198 self.clamp_edit_pos = clamp;
199 }
200
201 /// Returns if edit position movement is clamping to the ends of the message
202 pub fn is_edit_clamped(&self) -> bool {
203 self.clamp_edit_pos
204 }
205
206 /// Returns current editing position.
207 pub fn get_edit_pos(&self) -> usize {
208 self.edit_pos
209 }
210
211 /// Returns index of last added character
212 pub fn get_last_changed_index(&self) -> usize {
213 self.last_change_index
214 }
215
216 /// Returns the character at the index of last change
217 pub fn get_last_changed_char(&self) -> Character {
218 self.chars[self.last_change_index]
219 }
220
221 /// Move editing position to the left.
222 /// By default it will wrap to the end if position is 0
223 pub fn shift_edit_left(&mut self) {
224 self.edit_pos = match self.edit_pos {
225 0 => if self.clamp_edit_pos { 0 } else { Self::POS_MAX },
226 p => p - 1,
227 }
228 }
229
230 /// Move editing position to the right.
231 /// By default it will wrap to the beginning if position is POS_MAX
232 pub fn shift_edit_right(&mut self) {
233 self.edit_pos = match self.edit_pos {
234 p if p == Self::POS_MAX => if self.clamp_edit_pos { Self::POS_MAX } else { 0 },
235 p => p + 1,
236 }
237 }
238
239 /// Insert character at the editing position.
240 ///
241 /// If any characters before the character are [FILLER]s
242 /// They'll automatically be converted to empty characters ' '
243 /// which means the user wants some space between words.
244 pub fn add_char(&mut self, ch: Character) {
245 self.chars[self.edit_pos] = ch;
246 // This is only necessary if client code sets edit position
247 // manually and adds a character after it, but hey.
248 self.update_empty_chars();
249 self.last_change_index = self.edit_pos;
250 }
251
252 /// Insert character at index.
253 ///
254 /// If any characters before the character are [FILLER]s
255 /// They'll automatically be converted to empty characters ' '
256 /// which means the user wants some space between words.
257 pub fn put_char_at(&mut self, index: usize, ch: Character) -> Result<(), &str> {
258 if index < MSG_MAX {
259 self.chars[index] = ch;
260 self.update_empty_chars();
261 self.last_change_index = index;
262
263 Ok(())
264 } else {
265 Err("Put char index doesn't fit into message length")
266 }
267 }
268
269 /// Returns character at an index
270 pub fn char_at(&self, index: usize) -> Character {
271 self.chars[index]
272 }
273
274 /// Returns current length of the message discarding empty FILLER characters at the end.
275 ///
276 /// This is useful for creating ranged loops of actual characters decoded or can be encoded.
277 pub fn len(&self) -> usize {
278 let index = self.last_char_index();
279 match index {
280 Some(i) if i < MSG_MAX => i + 1,
281 Some(i) if i == MSG_MAX => MSG_MAX,
282 _ => 0,
283 }
284 }
285
286 /// Returns true if the message is empty, false otherwise.
287 ///
288 /// This method discards FILLER characters and only takes
289 /// normal characters into account.
290 pub fn is_empty(&self) -> bool {
291 self.last_char_index().is_none()
292 }
293
294 /// Manually set the message from an &str.
295 ///
296 /// edit_pos_end flag means we'll continue from the end of this string when
297 /// we continue decoding or encoding.
298 pub fn set_message(&mut self, message_str: &str, edit_pos_end: bool) -> Result<(), &str> {
299 if message_str.len() > MSG_MAX {
300 Err("Message string can't be longer than MSG_MAX.")
301 } else {
302 self.chars = Self::str_to_chars(message_str);
303
304 if edit_pos_end {
305 self.edit_pos = self.len().clamp(0, Self::POS_MAX);
306 } else {
307 self.edit_pos = 0;
308 }
309
310 self.last_change_index = self.edit_pos;
311
312 Ok(())
313 }
314 }
315
316 /// Returns the message as it is now in a character array format.
317 ///
318 /// Note that this also includes 'empty' [FILLER] characters.
319 /// Client code can use return value of len() which is the actual length
320 /// to loop through it or filter the fillers manually in a loop or iterator.
321 pub fn as_charray(&self) -> [Character; MSG_MAX] {
322 self.chars
323 }
324
325 /// Returns the message as it is now as &str slice.
326 /// Or as a [Utf8Charray] if "utf8" feature is enabled.
327 ///
328 /// Note that this *does not* include empty [FILLER] characters.
329 #[cfg(not(feature = "utf8"))]
330 pub fn as_str(&self) -> &str {
331 core::str::from_utf8(self.chars[0..self.len()].as_ref()).unwrap()
332 }
333
334 #[cfg(feature = "utf8")]
335 pub fn as_str(&self) -> Utf8Charray {
336 // Fixme: Update the code to use buffer copy,
337 // after const generic expressions become stable in Rust.
338 // https://github.com/rust-lang/rust/issues/76560
339 //
340 // let mut buffer = [0u8; MSG_MAX * 4];
341 // let mut pos: usize = 0;
342 // for ch in self.chars {
343 // pos += ch.encode_utf8(&mut buffer).len();
344 // }
345 //
346 // core::str::from_utf8(buffer[0..pos].asref()).unwrap()
347
348 Utf8Charray(self.chars[..self.len()].as_ref())
349 }
350
351 /// Pop the last character from the message and return it
352 /// deleting it from the message in the process.
353 pub fn pop(&mut self) -> Option<Character> {
354 if self.len() > 0 {
355 let last = self.chars[self.len() - 1];
356 self.chars[self.len() - 1] = FILLER;
357
358 Some(last)
359 } else {
360 None
361 }
362 }
363
364 /// Clear the message and start over.
365 pub fn clear(&mut self) {
366 self.chars = [FILLER; MSG_MAX];
367 self.edit_pos = 0;
368 }
369}
370
371/// Message iterator provides a convenient way to iterate over
372/// message characters. This doesn't include empty FILLER chars.
373pub struct MessageIterator<'a, const MSG_MAX: usize> {
374 message: &'a Message<MSG_MAX>,
375 index: usize,
376}
377
378impl<'a, const MSG_MAX: usize> Iterator for MessageIterator<'a, MSG_MAX> {
379 type Item = &'a Character;
380
381 fn next(&mut self) -> Option<Self::Item> {
382 if self.index < self.message.len() {
383 let result = Some(&self.message.chars[self.index]);
384 self.index += 1;
385
386 result
387 } else {
388 None
389 }
390 }
391}