wip
This commit is contained in:
+1
-1
@@ -94,7 +94,7 @@ async fn main(spawner: Spawner) {
|
|||||||
let h = EXAMPLE[HEART];
|
let h = EXAMPLE[HEART];
|
||||||
let e = EXAMPLE[EMPTY];
|
let e = EXAMPLE[EMPTY];
|
||||||
let b = EXAMPLE[BOX];
|
let b = EXAMPLE[BOX];
|
||||||
if let Err(e) = lcd.write(&alloc::format!("Custom symbols: {h}{e}{b}")) {
|
if let Err(e) = lcd.write(&alloc::format!("Custom symbols: {h}{e}{b} And more stuff to wrap and cut off please.")) {
|
||||||
log::error!(
|
log::error!(
|
||||||
"Tertiary LCD startup text write failed over shared I2C bus; check the shared bus, wiring, and device responses: {e}"
|
"Tertiary LCD startup text write failed over shared I2C bus; check the shared bus, wiring, and device responses: {e}"
|
||||||
);
|
);
|
||||||
|
|||||||
+347
-192
@@ -36,16 +36,7 @@ impl<'a> TertiaryDisplayPinConfiguration<'a> {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum TertiaryDisplayWriteError<'a> {
|
pub enum TertiaryDisplayWriteError<'a> {
|
||||||
Display(TertiaryDisplayError<'a>),
|
Display(TertiaryDisplayError<'a>),
|
||||||
ColumnOverflow {
|
Format(LinesError),
|
||||||
line: u8,
|
|
||||||
columns: usize,
|
|
||||||
},
|
|
||||||
LineOverflow,
|
|
||||||
InvalidChar {
|
|
||||||
/// Offset in bytes
|
|
||||||
position: usize,
|
|
||||||
symbol: char,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<TertiaryDisplayError<'a>> for TertiaryDisplayWriteError<'a> {
|
impl<'a> From<TertiaryDisplayError<'a>> for TertiaryDisplayWriteError<'a> {
|
||||||
@@ -54,39 +45,35 @@ impl<'a> From<TertiaryDisplayError<'a>> for TertiaryDisplayWriteError<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<LinesError> for TertiaryDisplayWriteError<'_> {
|
||||||
|
fn from(value: LinesError) -> Self {
|
||||||
|
Self::Format(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> core::fmt::Display for TertiaryDisplayWriteError<'a> {
|
impl<'a> core::fmt::Display for TertiaryDisplayWriteError<'a> {
|
||||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
TertiaryDisplayWriteError::Display(e) => write!(f, "display error: {e}"),
|
Self::Display(e) => write!(f, "display error: {e}"),
|
||||||
TertiaryDisplayWriteError::ColumnOverflow { line, columns } => {
|
Self::Format(e) => write!(f, "format error: {e}"),
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"text too long for line {line}: {columns} > {}",
|
|
||||||
TertiaryDisplay::DISPLAY_WIDTH
|
|
||||||
)
|
|
||||||
}
|
|
||||||
TertiaryDisplayWriteError::LineOverflow => write!(f, "text has too many lines"),
|
|
||||||
TertiaryDisplayWriteError::InvalidChar { position, symbol } => {
|
|
||||||
write!(f, "Invalid character {symbol} at offset {position}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TertiaryDisplay<'a> {
|
pub struct TertiaryDisplay<'a> {
|
||||||
display: TertiaryDisplayRaw<'a>,
|
display: TertiaryDisplayRaw<'a>,
|
||||||
charset: CustomCharSet,
|
charset: CustomCharset,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TertiaryDisplay<'a> {
|
impl<'a> TertiaryDisplay<'a> {
|
||||||
pub const DISPLAY_WIDTH: usize = 16;
|
pub const WIDTH: usize = 16;
|
||||||
pub const DISPLAY_ROWS: usize = 2;
|
pub const ROWS: usize = 2;
|
||||||
|
|
||||||
pub fn inner_mut(&mut self) -> &mut TertiaryDisplayRaw<'a> {
|
pub fn inner_mut(&mut self) -> &mut TertiaryDisplayRaw<'a> {
|
||||||
&mut self.display
|
&mut self.display
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_charset(&mut self, charset: CustomCharSet) -> Result<(), TertiaryDisplayError<'a>> {
|
pub fn load_charset(&mut self, charset: CustomCharset) -> Result<(), TertiaryDisplayError<'a>> {
|
||||||
self.charset = charset;
|
self.charset = charset;
|
||||||
for (index, charmap) in self
|
for (index, charmap) in self
|
||||||
.charset
|
.charset
|
||||||
@@ -110,167 +97,13 @@ impl<'a> TertiaryDisplay<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clamp the text's length so that it fits within the character display without overflow.
|
/// Writes text to the LCD. Normal strings wrap by default but this can be controlled
|
||||||
/// Note: clamp does not work properly yet if line 1 is too long with multiple newlines in string.
|
/// via explicit [`Lines::checked`].
|
||||||
pub fn clamp(text: &str) -> &str {
|
pub fn write<'b, L: Into<Lines<'b>>>(
|
||||||
enum NewLines {
|
&mut self,
|
||||||
None,
|
text: L,
|
||||||
One {
|
) -> Result<(), TertiaryDisplayWriteError<'a>> {
|
||||||
offset: usize,
|
let lines = text.into().into_result()?;
|
||||||
},
|
|
||||||
More {
|
|
||||||
/// Offset for the first 2 newlines. We discard everything from 2nd newline onwards anyway.
|
|
||||||
offsets: [usize; 2],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
let pos_of_newlines = |(pos, c)| (c == '\n').then_some(pos);
|
|
||||||
let mut newlines = NewLines::None;
|
|
||||||
for pos in text.char_indices().filter_map(pos_of_newlines) {
|
|
||||||
match newlines {
|
|
||||||
NewLines::None => {
|
|
||||||
newlines = NewLines::One { offset: pos };
|
|
||||||
}
|
|
||||||
NewLines::One { offset } => {
|
|
||||||
newlines = NewLines::More {
|
|
||||||
offsets: [offset, pos],
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
NewLines::More { offsets } => {
|
|
||||||
// unreachable because of break in previous arm
|
|
||||||
newlines = NewLines::More { offsets };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let len_no_newline = text.len()
|
|
||||||
- match newlines {
|
|
||||||
NewLines::None => 0,
|
|
||||||
NewLines::One { .. } => 1,
|
|
||||||
NewLines::More { .. } => 2,
|
|
||||||
};
|
|
||||||
let max = len_no_newline.min(Self::DISPLAY_ROWS * Self::DISPLAY_WIDTH);
|
|
||||||
|
|
||||||
match newlines {
|
|
||||||
NewLines::None => &text[..max],
|
|
||||||
NewLines::More {
|
|
||||||
offsets: [offset, ..],
|
|
||||||
}
|
|
||||||
| NewLines::One { offset }
|
|
||||||
if offset > Self::DISPLAY_ROWS * Self::DISPLAY_WIDTH =>
|
|
||||||
{
|
|
||||||
// first line already fully fills the screen,
|
|
||||||
// so new line does not fit on screen anyway
|
|
||||||
&text[..max]
|
|
||||||
}
|
|
||||||
NewLines::One { offset } => {
|
|
||||||
let line2 = text.split_at(offset).1;
|
|
||||||
let line2_len = line2.len().min(Self::DISPLAY_WIDTH);
|
|
||||||
// new line isn't rendered so string may be one longer
|
|
||||||
&text[..max.min(offset + line2_len) + 1]
|
|
||||||
}
|
|
||||||
NewLines::More {
|
|
||||||
offsets: [eol1, eol2],
|
|
||||||
} => {
|
|
||||||
let line2 = text[..eol2].split_at(eol1).1;
|
|
||||||
let line2_len = line2.len().min(Self::DISPLAY_WIDTH);
|
|
||||||
// new line isn't rendered so string may be one longer
|
|
||||||
&text[..max.min(eol1 + line2_len) + 1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clamp the text's length so that it fits within a single line of the character display without overflow.
|
|
||||||
pub fn clamp_line(text: &str) -> &str {
|
|
||||||
let newline = text.chars().position(|c| c == '\n');
|
|
||||||
let end_of_line = newline.unwrap_or(text.len());
|
|
||||||
let stop_at = end_of_line.min(Self::DISPLAY_WIDTH);
|
|
||||||
&text[..stop_at]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write_line(&mut self, row: u8, text: &str) -> Result<(), TertiaryDisplayWriteError<'a>> {
|
|
||||||
self.invalid_chars(text)?;
|
|
||||||
let line = Self::limit_line(text)?;
|
|
||||||
|
|
||||||
self.display
|
|
||||||
.set_cursor(0, row)
|
|
||||||
.inspect_err(|e| error!("I2C LCD cursor move failed on row {row}: {e}"))?;
|
|
||||||
|
|
||||||
self.display
|
|
||||||
.print(line)
|
|
||||||
.inspect_err(|e| error!("I2C LCD print failed on row {row}: {e}"))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn invalid_chars(&self, text: &str) -> Result<(), TertiaryDisplayWriteError<'a>> {
|
|
||||||
let is_custom = |c: char| u32::from(c) < self.charset.len as u32;
|
|
||||||
let is_newline = |c: char| c == '\n';
|
|
||||||
let invalid = text
|
|
||||||
.char_indices()
|
|
||||||
.find(|&(_, c)| !c.is_ascii() || (c.is_control() && !(is_newline(c) || is_custom(c))));
|
|
||||||
match invalid {
|
|
||||||
Some((position, symbol)) => {
|
|
||||||
Err(TertiaryDisplayWriteError::InvalidChar { position, symbol })
|
|
||||||
}
|
|
||||||
None => Ok(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn limit_line<'b>(text: &'b str) -> Result<&'b str, TertiaryDisplayWriteError<'a>> {
|
|
||||||
if text.contains('\n') {
|
|
||||||
return Err(TertiaryDisplayWriteError::LineOverflow);
|
|
||||||
}
|
|
||||||
|
|
||||||
if text.len() > Self::DISPLAY_WIDTH {
|
|
||||||
Err(TertiaryDisplayWriteError::ColumnOverflow {
|
|
||||||
line: 1,
|
|
||||||
columns: text.len(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Ok(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn split_lines<'b>(text: &'b str) -> Result<[&'b str; 2], TertiaryDisplayWriteError<'a>> {
|
|
||||||
let newlines = text.chars().filter(|&c| c == '\n').count();
|
|
||||||
match newlines {
|
|
||||||
0 => {
|
|
||||||
if text.len() > Self::DISPLAY_ROWS * Self::DISPLAY_WIDTH {
|
|
||||||
Err(TertiaryDisplayWriteError::ColumnOverflow {
|
|
||||||
line: 2,
|
|
||||||
columns: text.len() - Self::DISPLAY_WIDTH,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Ok(text
|
|
||||||
.split_at_checked(Self::DISPLAY_WIDTH)
|
|
||||||
.unwrap_or((text, ""))
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1 => {
|
|
||||||
let lines: [_; 2] = text.split_once('\n').unwrap_or((text, "")).into();
|
|
||||||
|
|
||||||
for (row, line) in lines.into_iter().enumerate() {
|
|
||||||
if line.len() > Self::DISPLAY_WIDTH {
|
|
||||||
return Err(TertiaryDisplayWriteError::ColumnOverflow {
|
|
||||||
line: row as u8 + 1,
|
|
||||||
columns: line.len(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(lines)
|
|
||||||
}
|
|
||||||
_ => Err(TertiaryDisplayWriteError::LineOverflow),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Writes text to the LCD, wrapping to the next line if it exceeds the display width.
|
|
||||||
/// Returns an error if the text would overflow.
|
|
||||||
pub fn write(&mut self, text: &str) -> Result<(), TertiaryDisplayWriteError<'a>> {
|
|
||||||
self.invalid_chars(text)?;
|
|
||||||
let lines = Self::split_lines(text)?;
|
|
||||||
|
|
||||||
self.display
|
self.display
|
||||||
.clear()
|
.clear()
|
||||||
@@ -290,14 +123,180 @@ impl<'a> TertiaryDisplay<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
|
pub enum LinesError {
|
||||||
|
ColumnOverflow { line: u8, columns: usize },
|
||||||
|
LineOverflow,
|
||||||
|
InvalidChar { position: usize, symbol: char },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Display for LinesError {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::ColumnOverflow { line, columns } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"text too long for line {line}: {columns} > {}",
|
||||||
|
TertiaryDisplay::WIDTH
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Self::LineOverflow => write!(f, "text has too many lines"),
|
||||||
|
Self::InvalidChar { position, symbol } => {
|
||||||
|
write!(f, "Invalid character {symbol} at offset {position}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
|
pub struct Lines<'a> {
|
||||||
|
pub lines: Result<[&'a str; TertiaryDisplay::ROWS], LinesError>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Lines<'a> {
|
||||||
|
pub fn into_result(self) -> Result<[&'a str; TertiaryDisplay::ROWS], LinesError> {
|
||||||
|
self.lines
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates that the text contains only non-control ASCII characters,
|
||||||
|
/// newlines, and contiguous custom characters up to `charset.len`.
|
||||||
|
pub fn invalid_chars(text: &str, charset: &CustomCharset) -> Result<(), LinesError> {
|
||||||
|
for (pos, c) in text.char_indices() {
|
||||||
|
let is_standard_valid = c == '\n' || (' '..='~').contains(&c);
|
||||||
|
|
||||||
|
if !is_standard_valid {
|
||||||
|
// If it's not standard ASCII, check if it falls within the allowed custom range.
|
||||||
|
// We cast `c` to usize to compare against length.
|
||||||
|
if (c as usize) >= charset.len {
|
||||||
|
return Err(LinesError::InvalidChar {
|
||||||
|
position: pos,
|
||||||
|
symbol: c,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks pre-split lines strictly for length and valid characters.
|
||||||
|
pub fn by_line(lines: [&'a str; TertiaryDisplay::ROWS]) -> Self {
|
||||||
|
for line in lines.iter() {
|
||||||
|
if let Err(e) = Self::invalid_chars(line, &create_custom_char_set!()) {
|
||||||
|
return Self { lines: Err(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, line) in lines.iter().enumerate() {
|
||||||
|
if line.len() > TertiaryDisplay::WIDTH {
|
||||||
|
return Self {
|
||||||
|
lines: Err(LinesError::ColumnOverflow {
|
||||||
|
line: i as u8,
|
||||||
|
columns: line.len(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self { lines: Ok(lines) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Strictly checks an input string.
|
||||||
|
/// Fails if there are invalid chars, too many newlines, or if any segment exceeds WIDTH.
|
||||||
|
pub fn checked(input: &'a str) -> Self {
|
||||||
|
if let Err(e) = Self::invalid_chars(input, &create_custom_char_set!()) {
|
||||||
|
return Self { lines: Err(e) };
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out = [""; TertiaryDisplay::ROWS];
|
||||||
|
let mut parts = input.split('\n');
|
||||||
|
|
||||||
|
for (i, out) in out.iter_mut().enumerate() {
|
||||||
|
if let Some(part) = parts.next() {
|
||||||
|
if part.len() > TertiaryDisplay::WIDTH {
|
||||||
|
return Self {
|
||||||
|
lines: Err(LinesError::ColumnOverflow {
|
||||||
|
line: i as u8,
|
||||||
|
columns: part.len(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
*out = part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if parts.next().is_some() {
|
||||||
|
return Self {
|
||||||
|
lines: Err(LinesError::LineOverflow),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Self { lines: Ok(out) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wraps text automatically at `WIDTH` or at newlines.
|
||||||
|
/// Discards (clamps) any leftover text that exceeds `ROWS`.
|
||||||
|
/// Fails immediately if invalid characters are present.
|
||||||
|
pub fn clamped(input: &'a str) -> Self {
|
||||||
|
if let Err(e) = Self::invalid_chars(input, &create_custom_char_set!()) {
|
||||||
|
return Self { lines: Err(e) };
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out = [""; TertiaryDisplay::ROWS];
|
||||||
|
let mut remaining = input;
|
||||||
|
|
||||||
|
for out in out.iter_mut() {
|
||||||
|
if remaining.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (segment, rest) = Self::next_segment(remaining, TertiaryDisplay::WIDTH);
|
||||||
|
*out = segment;
|
||||||
|
remaining = rest;
|
||||||
|
}
|
||||||
|
|
||||||
|
Self { lines: Ok(out) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function to slice an ASCII string up to `max_width`
|
||||||
|
/// or until the next newline, whichever comes first.
|
||||||
|
fn next_segment(s: &str, max_width: usize) -> (&str, &str) {
|
||||||
|
let nl_pos = s.find('\n');
|
||||||
|
|
||||||
|
let split_pos = match nl_pos {
|
||||||
|
Some(pos) if pos <= max_width => pos,
|
||||||
|
_ => core::cmp::min(s.len(), max_width),
|
||||||
|
};
|
||||||
|
|
||||||
|
let segment = &s[..split_pos];
|
||||||
|
let mut rest = &s[split_pos..];
|
||||||
|
|
||||||
|
if let Some(stripped) = rest.strip_prefix('\n') {
|
||||||
|
rest = stripped;
|
||||||
|
}
|
||||||
|
|
||||||
|
(segment, rest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a str> for Lines<'a> {
|
||||||
|
fn from(value: &'a str) -> Self {
|
||||||
|
Self::clamped(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a alloc::string::String> for Lines<'a> {
|
||||||
|
fn from(value: &'a alloc::string::String) -> Self {
|
||||||
|
Self::clamped(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 5x8 pixel charmap. Each set bit is a black pixel.
|
/// 5x8 pixel charmap. Each set bit is a black pixel.
|
||||||
pub type CharMap = [u8; 8];
|
pub type CharMap = [u8; 8];
|
||||||
pub struct CustomCharSet {
|
pub struct CustomCharset {
|
||||||
set: [CharMap; CUSTOM_CHAR_COUNT as usize],
|
set: [CharMap; CUSTOM_CHAR_COUNT as usize],
|
||||||
len: usize,
|
len: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl core::ops::Index<CharMap> for CustomCharSet {
|
impl core::ops::Index<CharMap> for CustomCharset {
|
||||||
type Output = &'static str;
|
type Output = &'static str;
|
||||||
|
|
||||||
fn index(&self, map: CharMap) -> &Self::Output {
|
fn index(&self, map: CharMap) -> &Self::Output {
|
||||||
@@ -333,7 +332,7 @@ impl core::ops::Index<CharMap> for CustomCharSet {
|
|||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! create_custom_char_set {
|
macro_rules! create_custom_char_set {
|
||||||
() => {{
|
() => {{
|
||||||
CustomCharSet { set: [EMPTY; CUSTOM_CHAR_COUNT as usize], len: 0 }
|
CustomCharset { set: [EMPTY; CUSTOM_CHAR_COUNT as usize], len: 0 }
|
||||||
}};
|
}};
|
||||||
($($char:expr),+ $(,)?) => {{
|
($($char:expr),+ $(,)?) => {{
|
||||||
// Count how many characters were passed using macro repetition length
|
// Count how many characters were passed using macro repetition length
|
||||||
@@ -358,7 +357,7 @@ macro_rules! create_custom_char_set {
|
|||||||
buffer
|
buffer
|
||||||
};
|
};
|
||||||
|
|
||||||
CustomCharSet {
|
CustomCharset {
|
||||||
set: SET,
|
set: SET,
|
||||||
len: LEN,
|
len: LEN,
|
||||||
}
|
}
|
||||||
@@ -384,4 +383,160 @@ pub const BOX: CharMap = [
|
|||||||
0b00000, // Cursor line (usually left blank)
|
0b00000, // Cursor line (usually left blank)
|
||||||
];
|
];
|
||||||
|
|
||||||
pub const EXAMPLE: CustomCharSet = create_custom_char_set!(HEART, BOX, EMPTY);
|
pub const EXAMPLE: CustomCharset = create_custom_char_set!(HEART, BOX, EMPTY);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const EMPTY_CHAR: CustomCharset = CustomCharset::EMPTY;
|
||||||
|
// Allows custom chars \x00, \x01, \x02
|
||||||
|
const CUSTOM_CHAR: CustomCharset = EXAMPLE;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_chars() {
|
||||||
|
// Valid standard ASCII
|
||||||
|
assert_eq!(
|
||||||
|
Lines::invalid_chars("Hello World! 123", &EMPTY_CHAR),
|
||||||
|
Ok(())
|
||||||
|
);
|
||||||
|
assert_eq!(Lines::invalid_chars("Line1\nLine2", &EMPTY_CHAR), Ok(()));
|
||||||
|
|
||||||
|
// Invalid control characters (e.g., \t, \r)
|
||||||
|
assert_eq!(
|
||||||
|
Lines::invalid_chars("Hello\tWorld", &EMPTY_CHAR),
|
||||||
|
Err(LinesError::InvalidChar {
|
||||||
|
position: 5,
|
||||||
|
symbol: '\t'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Lines::invalid_chars("Hello\r\nWorld", &EMPTY_CHAR),
|
||||||
|
Err(LinesError::InvalidChar {
|
||||||
|
position: 5,
|
||||||
|
symbol: '\r'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Valid custom characters
|
||||||
|
assert_eq!(Lines::invalid_chars("Temp: 20\x00C", &CUSTOM_CHAR), Ok(()));
|
||||||
|
assert_eq!(Lines::invalid_chars("\x00\x01\x02", &CUSTOM_CHAR), Ok(()));
|
||||||
|
|
||||||
|
// Out of bounds custom character (\x03 is >= len 3)
|
||||||
|
assert_eq!(
|
||||||
|
Lines::invalid_chars("Temp: 20\x03C", &CUSTOM_CHAR),
|
||||||
|
Err(LinesError::InvalidChar {
|
||||||
|
position: 8,
|
||||||
|
symbol: '\x03'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Non-ASCII characters
|
||||||
|
assert_eq!(
|
||||||
|
Lines::invalid_chars("Hellö", &EMPTY_CHAR),
|
||||||
|
Err(LinesError::InvalidChar {
|
||||||
|
position: 4,
|
||||||
|
symbol: 'ö'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_by_line() {
|
||||||
|
// Valid lines
|
||||||
|
let valid = Lines::by_line(["Short", "Text"], &EMPTY_CHAR).into_result();
|
||||||
|
assert_eq!(valid, Ok(["Short", "Text"]));
|
||||||
|
|
||||||
|
// Column overflow
|
||||||
|
let overflow = Lines::by_line(["12345678901234567", "Ok"], &EMPTY_CHAR).into_result();
|
||||||
|
assert_eq!(
|
||||||
|
overflow,
|
||||||
|
Err(LinesError::ColumnOverflow {
|
||||||
|
line: 0,
|
||||||
|
columns: 17
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Invalid character in pre-split line
|
||||||
|
let invalid = Lines::by_line(["Good", "B\x05ad"], &EMPTY_CHAR).into_result();
|
||||||
|
assert_eq!(
|
||||||
|
invalid,
|
||||||
|
Err(LinesError::InvalidChar {
|
||||||
|
position: 1,
|
||||||
|
symbol: '\x05'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_checked() {
|
||||||
|
// Valid single line
|
||||||
|
let single = Lines::checked("Single", &EMPTY_CHAR).into_result();
|
||||||
|
assert_eq!(single, Ok(["Single", ""]));
|
||||||
|
|
||||||
|
// Valid exactly two lines
|
||||||
|
let two_lines = Lines::checked("Line1\nLine2", &EMPTY_CHAR).into_result();
|
||||||
|
assert_eq!(two_lines, Ok(["Line1", "Line2"]));
|
||||||
|
|
||||||
|
// Column overflow
|
||||||
|
let col_overflow = Lines::checked("Line1\n12345678901234567", &EMPTY_CHAR).into_result();
|
||||||
|
assert_eq!(
|
||||||
|
col_overflow,
|
||||||
|
Err(LinesError::ColumnOverflow {
|
||||||
|
line: 1,
|
||||||
|
columns: 17
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Lines overflow
|
||||||
|
let lines_overflow = Lines::checked("One\nTwo\nThree", &EMPTY_CHAR).into_result();
|
||||||
|
assert_eq!(lines_overflow, Err(LinesError::LineOverflow));
|
||||||
|
|
||||||
|
// Invalid character detection
|
||||||
|
let bad_char = Lines::checked("One\nTwö", &EMPTY_CHAR).into_result();
|
||||||
|
assert_eq!(
|
||||||
|
bad_char,
|
||||||
|
Err(LinesError::InvalidChar {
|
||||||
|
position: 6,
|
||||||
|
symbol: 'ö'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_clamped() {
|
||||||
|
// Simple string fits on one line
|
||||||
|
let short = Lines::clamped("Hello", &EMPTY_CHAR).into_result();
|
||||||
|
assert_eq!(short, Ok(["Hello", ""]));
|
||||||
|
|
||||||
|
// Exactly width (16)
|
||||||
|
let exact = Lines::clamped("1234567890123456", &EMPTY_CHAR).into_result();
|
||||||
|
assert_eq!(exact, Ok(["1234567890123456", ""]));
|
||||||
|
|
||||||
|
// Wraps perfectly on 16 characters
|
||||||
|
let wrapped = Lines::clamped("1234567890123456789", &EMPTY_CHAR).into_result();
|
||||||
|
assert_eq!(wrapped, Ok(["1234567890123456", "789"]));
|
||||||
|
|
||||||
|
// Manual newlines
|
||||||
|
let newlines = Lines::clamped("A\nB", &EMPTY_CHAR).into_result();
|
||||||
|
assert_eq!(newlines, Ok(["A", "B"]));
|
||||||
|
|
||||||
|
// Exact match with a manual newline
|
||||||
|
let wrap_with_newline = Lines::clamped("1234567890123456\nNext", &EMPTY_CHAR).into_result();
|
||||||
|
assert_eq!(wrap_with_newline, Ok(["1234567890123456", "Next"]));
|
||||||
|
|
||||||
|
// Clamps excess lines silently (truncates after row 2)
|
||||||
|
let clamped = Lines::clamped("A\nB\nC", &EMPTY_CHAR).into_result();
|
||||||
|
assert_eq!(clamped, Ok(["A", "B"])); // "C" is discarded
|
||||||
|
|
||||||
|
// Invalid char returns error immediately
|
||||||
|
let invalid = Lines::clamped("1234\t5", &EMPTY_CHAR).into_result();
|
||||||
|
assert_eq!(
|
||||||
|
invalid,
|
||||||
|
Err(LinesError::InvalidChar {
|
||||||
|
position: 4,
|
||||||
|
symbol: '\t'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user