From 50baaade5d16d29a3f0d7271bd2aba3b80faaeb4 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Thu, 8 Feb 2024 23:00:47 -0300 Subject: [PATCH 1/3] chore: change message enums from tuples to structs --- src/command_bar.rs | 20 ++-- src/messages.rs | 218 ++++++++++++++++++++++++++++-------------- src/plugin_manager.rs | 25 ++++- src/serial.rs | 127 ++++++++++++++---------- 4 files changed, 256 insertions(+), 134 deletions(-) diff --git a/src/command_bar.rs b/src/command_bar.rs index ded8a81..78bd3dc 100644 --- a/src/command_bar.rs +++ b/src/command_bar.rs @@ -363,7 +363,10 @@ impl CommandBar { let data_to_send = yaml_content.get(key).unwrap(); let data_to_send = data_to_send.replace("\\r", "\r").replace("\\n", "\n"); let interface = self.interface.lock().unwrap(); - interface.send(UserTxData::Command(key.to_string(), data_to_send)); + interface.send(UserTxData::Command { + command_name: key.to_string(), + content: data_to_send, + }); } '!' => { let command_line_split = command_line @@ -386,11 +389,12 @@ impl CommandBar { msg_lut.insert("reload".to_string(), "Plugin reloaded!"); let mut text_view = self.text_view.lock().unwrap(); - text_view.add_data_out(SerialRxData::Plugin( - Local::now(), + text_view.add_data_out(SerialRxData::Plugin { + timestamp: Local::now(), plugin_name, - msg_lut[&cmd].to_string(), - )) + content: msg_lut[&cmd].to_string(), + is_successful: true, + }) } Err(err_msg) => { self.set_error_pop_up(err_msg); @@ -422,11 +426,13 @@ impl CommandBar { }; let interface = self.interface.lock().unwrap(); - interface.send(UserTxData::HexString(bytes)); + interface.send(UserTxData::HexString { content: bytes }); } _ => { let interface = self.interface.lock().unwrap(); - interface.send(UserTxData::Data(command_line)); + interface.send(UserTxData::Data { + content: command_line, + }); } } diff --git a/src/messages.rs b/src/messages.rs index 608ba22..81eb183 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -4,33 +4,61 @@ use tui::style::Color; pub enum UserTxData { Exit, - Data(String), - Command(String, String), - HexString(Vec), - PluginSerialTx(String, Vec), + Data { + content: String, + }, + Command { + command_name: String, + content: String, + }, + HexString { + content: Vec, + }, + PluginSerialTx { + plugin_name: String, + content: Vec, + }, } #[derive(Clone)] pub enum SerialRxData { - Data(DateTime, String), - ConfirmData(DateTime, String), - ConfirmCommand(DateTime, String, String), - ConfirmHexString(DateTime, Vec), - Plugin(DateTime, String, String), - ConfirmPluginSerialTx(DateTime, String, Vec), - FailPlugin(DateTime, String, String), - FailData(DateTime, String), - FailCommand(DateTime, String, String), - FailHexString(DateTime, Vec), - FailPluginSerialTx(DateTime, String, Vec), + RxData { + timestamp: DateTime, + content: String, + }, + TxData { + timestamp: DateTime, + content: String, + is_successful: bool, + }, + Command { + timestamp: DateTime, + command_name: String, + content: String, + is_successful: bool, + }, + HexString { + timestamp: DateTime, + content: Vec, + is_successful: bool, + }, + Plugin { + timestamp: DateTime, + plugin_name: String, + content: String, + is_successful: bool, + }, + PluginSerialTx { + timestamp: DateTime, + plugin_name: String, + content: Vec, + is_successful: bool, + }, } impl SerialRxData { pub fn is_plugin_serial_tx(&self) -> bool { - matches!( - self, - SerialRxData::ConfirmPluginSerialTx(..) | SerialRxData::FailPluginSerialTx(..) - ) + matches!(self, SerialRxData::PluginSerialTx { .. }) } } @@ -38,66 +66,112 @@ impl SerialRxData { impl Into for SerialRxData { fn into(self) -> ViewData { match self { - SerialRxData::Data(timestamp, content) => { + SerialRxData::RxData { timestamp, content } => { ViewData::new(timestamp, content, Color::Reset, Color::Reset) } - SerialRxData::ConfirmData(timestamp, content) => { - ViewData::new(timestamp, content, Color::Black, Color::LightCyan) - } - SerialRxData::ConfirmCommand(timestamp, cmd_name, content) => ViewData::new( - timestamp, - format!(" {}", cmd_name, content), - Color::Black, - Color::LightGreen, - ), - SerialRxData::ConfirmHexString(timestamp, bytes) => ViewData::new( - timestamp, - format!("{:02x?}", &bytes), - Color::Black, - Color::Yellow, - ), - SerialRxData::Plugin(timestamp, plugin_name, message) => ViewData::new( - timestamp, - format!(" [{plugin_name}] {message} "), - Color::Black, - Color::White, - ), - SerialRxData::ConfirmPluginSerialTx(timestamp, plugin_name, message) => ViewData::new( - timestamp, - format!(" [{plugin_name}] => {:02x?} ", message), - Color::Black, - Color::White, - ), - SerialRxData::FailData(timestamp, content) => ViewData::new( + SerialRxData::TxData { timestamp, - format!("Cannot send \"{}\"", content), - Color::White, - Color::LightRed, - ), - SerialRxData::FailCommand(timestamp, cmd_name, _content) => ViewData::new( + content, + is_successful, + } => { + if is_successful { + ViewData::new(timestamp, content, Color::Black, Color::LightCyan) + } else { + ViewData::new( + timestamp, + format!("Cannot send \"{}\"", content), + Color::White, + Color::LightRed, + ) + } + } + SerialRxData::Command { timestamp, - format!("Cannot send ", cmd_name), - Color::White, - Color::LightRed, - ), - SerialRxData::FailHexString(timestamp, bytes) => ViewData::new( + command_name, + content, + is_successful, + } => { + if is_successful { + ViewData::new( + timestamp, + format!(" {}", command_name, content), + Color::Black, + Color::LightGreen, + ) + } else { + ViewData::new( + timestamp, + format!("Cannot send ", command_name), + Color::White, + Color::LightRed, + ) + } + } + SerialRxData::HexString { timestamp, - format!("Cannot send {:02x?}", &bytes), - Color::White, - Color::LightRed, - ), - SerialRxData::FailPlugin(timestamp, plugin_name, message) => ViewData::new( + content, + is_successful, + } => { + if is_successful { + ViewData::new( + timestamp, + format!("{:02x?}", &content), + Color::Black, + Color::Yellow, + ) + } else { + ViewData::new( + timestamp, + format!("Cannot send {:02x?}", &content), + Color::White, + Color::LightRed, + ) + } + } + SerialRxData::Plugin { timestamp, - format!(" [{plugin_name}] {message} "), - Color::White, - Color::Red, - ), - SerialRxData::FailPluginSerialTx(timestamp, pluging_name, message) => ViewData::new( + plugin_name, + content, + is_successful, + } => { + if is_successful { + ViewData::new( + timestamp, + format!(" [{plugin_name}] {content} "), + Color::Black, + Color::White, + ) + } else { + ViewData::new( + timestamp, + format!(" [{plugin_name}] {content} "), + Color::White, + Color::Red, + ) + } + } + SerialRxData::PluginSerialTx { timestamp, - format!(" [{pluging_name}] => Fail to send {:02x?} ", message), - Color::White, - Color::Red, - ), + plugin_name, + content, + is_successful, + } => { + if is_successful { + ViewData::new( + timestamp, + format!(" [{plugin_name}] => {:02x?} ", content), + Color::Black, + Color::White, + ) + } else { + ViewData::new( + timestamp, + format!(" [{plugin_name}] => Fail to send {:02x?} ", content), + Color::White, + Color::Red, + ) + } + } } } } diff --git a/src/plugin_manager.rs b/src/plugin_manager.rs index dfd8347..2d1993c 100644 --- a/src/plugin_manager.rs +++ b/src/plugin_manager.rs @@ -156,7 +156,11 @@ impl PluginManager { pub fn call_plugins_serial_rx(&self, data_out: SerialRxData) { for plugin in self.plugins.values().cloned().collect::>() { - let SerialRxData::Data(_timestamp, line) = &data_out else { + let SerialRxData::RxData { + timestamp: _timestamp, + content: line, + } = &data_out + else { continue; }; @@ -178,11 +182,21 @@ impl PluginManager { match req { PluginRequest::Println { msg } => { let mut text_view = text_view.lock().unwrap(); - text_view.add_data_out(SerialRxData::Plugin(Local::now(), plugin_name, msg)) + text_view.add_data_out(SerialRxData::Plugin { + timestamp: Local::now(), + plugin_name, + content: msg, + is_successful: true, + }) } PluginRequest::Eprintln { msg } => { let mut text_view = text_view.lock().unwrap(); - text_view.add_data_out(SerialRxData::FailPlugin(Local::now(), plugin_name, msg)) + text_view.add_data_out(SerialRxData::Plugin { + timestamp: Local::now(), + plugin_name, + content: msg, + is_successful: false, + }) } PluginRequest::Connect { .. } => {} PluginRequest::Disconnect => {} @@ -190,7 +204,10 @@ impl PluginManager { PluginRequest::SerialTx { msg } => { let mut text_view = text_view.lock().unwrap(); let interface = interface.lock().unwrap(); - interface.send(UserTxData::PluginSerialTx(plugin_name, msg)); + interface.send(UserTxData::PluginSerialTx { + plugin_name, + content: msg, + }); 'plugin_serial_tx: loop { match interface.recv() { diff --git a/src/serial.rs b/src/serial.rs index 33e7724..09b04a6 100644 --- a/src/serial.rs +++ b/src/serial.rs @@ -117,71 +117,90 @@ impl SerialIF { if let Ok(data_to_send) = serial_rx.try_recv() { match data_to_send { UserTxData::Exit => break 'task, - UserTxData::Data(data_to_send) => { - match serial.write(format!("{data_to_send}\r\n").as_bytes()) { + UserTxData::Data { content } => { + match serial.write(format!("{content}\r\n").as_bytes()) { Ok(_) => { data_tx - .send(SerialRxData::ConfirmData(Local::now(), data_to_send)) + .send(SerialRxData::TxData { + timestamp: Local::now(), + content, + is_successful: true, + }) .expect("Cannot send data confirm"); } Err(err) => { data_tx - .send(SerialRxData::FailData( - Local::now(), - data_to_send + &err.to_string(), - )) + .send(SerialRxData::TxData { + timestamp: Local::now(), + content: content + &err.to_string(), + is_successful: false, + }) .expect("Cannot send data fail"); } } } - UserTxData::Command(command_name, data_to_send) => { - match serial.write(format!("{data_to_send}\r\n").as_bytes()) { - Ok(_) => { - data_tx - .send(SerialRxData::ConfirmCommand( - Local::now(), - command_name, - data_to_send, - )) - .expect("Cannot send command confirm"); - } - Err(_) => { - data_tx - .send(SerialRxData::FailCommand( - Local::now(), - command_name, - data_to_send, - )) - .expect("Cannot send command fail"); - } + UserTxData::Command { + command_name, + content, + } => match serial.write(format!("{content}\r\n").as_bytes()) { + Ok(_) => { + data_tx + .send(SerialRxData::Command { + timestamp: Local::now(), + command_name, + content, + is_successful: true, + }) + .expect("Cannot send command confirm"); } - } - UserTxData::HexString(bytes) => match serial.write(&bytes) { + Err(_) => { + data_tx + .send(SerialRxData::Command { + timestamp: Local::now(), + command_name, + content, + is_successful: false, + }) + .expect("Cannot send command fail"); + } + }, + UserTxData::HexString { content } => match serial.write(&content) { Ok(_) => data_tx - .send(SerialRxData::ConfirmHexString(Local::now(), bytes)) + .send(SerialRxData::HexString { + timestamp: Local::now(), + content, + is_successful: true, + }) .expect("Cannot send hex string comfirm"), Err(_) => data_tx - .send(SerialRxData::FailHexString(Local::now(), bytes)) + .send(SerialRxData::HexString { + timestamp: Local::now(), + content, + is_successful: false, + }) + .expect("Cannot send hex string fail"), + }, + UserTxData::PluginSerialTx { + plugin_name, + content, + } => match serial.write(&content) { + Ok(_) => data_tx + .send(SerialRxData::PluginSerialTx { + timestamp: Local::now(), + plugin_name, + content, + is_successful: true, + }) + .expect("Cannot send hex string comfirm"), + Err(_) => data_tx + .send(SerialRxData::PluginSerialTx { + timestamp: Local::now(), + plugin_name, + content, + is_successful: false, + }) .expect("Cannot send hex string fail"), }, - UserTxData::PluginSerialTx(plugin_name, content) => { - match serial.write(&content) { - Ok(_) => data_tx - .send(SerialRxData::ConfirmPluginSerialTx( - Local::now(), - plugin_name, - content, - )) - .expect("Cannot send hex string comfirm"), - Err(_) => data_tx - .send(SerialRxData::FailPluginSerialTx( - Local::now(), - plugin_name, - content, - )) - .expect("Cannot send hex string fail"), - } - } } } @@ -190,7 +209,10 @@ impl SerialIF { line.push(buffer[0] as char); if buffer[0] == b'\n' { data_tx - .send(SerialRxData::Data(Local::now(), line.clone())) + .send(SerialRxData::RxData { + timestamp: Local::now(), + content: line.clone(), + }) .expect("Cannot forward message read from serial"); line.clear(); now = Instant::now(); @@ -214,7 +236,10 @@ impl SerialIF { if !line.is_empty() { data_tx - .send(SerialRxData::Data(Local::now(), line.clone())) + .send(SerialRxData::RxData { + timestamp: Local::now(), + content: line.clone(), + }) .expect("Cannot forward message read from serial"); line.clear(); } From 65cea975e36094400f2512977966792ff4ed322a Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Fri, 16 Feb 2024 13:04:04 -0300 Subject: [PATCH 2/3] feat: add rich text to handle invisible character and ANSI color codes --- src/main.rs | 1 + src/messages.rs | 61 +++++--- src/plugin_manager.rs | 2 +- src/rich_string.rs | 343 ++++++++++++++++++++++++++++++++++++++++++ src/serial.rs | 4 +- src/text.rs | 193 +++--------------------- 6 files changed, 408 insertions(+), 196 deletions(-) create mode 100644 src/rich_string.rs diff --git a/src/main.rs b/src/main.rs index ebed7b3..45e5f5a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,6 +23,7 @@ mod messages; mod plugin; mod plugin_installer; mod plugin_manager; +mod rich_string; mod serial; mod text; diff --git a/src/messages.rs b/src/messages.rs index 81eb183..7d6be1f 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -1,3 +1,4 @@ +use crate::rich_string::RichText; use crate::text::ViewData; use chrono::{DateTime, Local}; use tui::style::Color; @@ -24,7 +25,7 @@ pub enum UserTxData { pub enum SerialRxData { RxData { timestamp: DateTime, - content: String, + content: Vec, }, TxData { timestamp: DateTime, @@ -67,7 +68,10 @@ impl Into for SerialRxData { fn into(self) -> ViewData { match self { SerialRxData::RxData { timestamp, content } => { - ViewData::new(timestamp, content, Color::Reset, Color::Reset) + RichText::new(content, Color::Reset, Color::Reset) + .decode_ansi_color() + .highlight_invisible() + .into_view_data(timestamp) } SerialRxData::TxData { timestamp, @@ -75,14 +79,17 @@ impl Into for SerialRxData { is_successful, } => { if is_successful { - ViewData::new(timestamp, content, Color::Black, Color::LightCyan) + RichText::from_string(content, Color::Black, Color::LightCyan) + .highlight_invisible() + .into_view_data(timestamp) } else { - ViewData::new( - timestamp, + RichText::from_string( format!("Cannot send \"{}\"", content), Color::White, Color::LightRed, ) + .highlight_invisible() + .into_view_data(timestamp) } } SerialRxData::Command { @@ -92,19 +99,21 @@ impl Into for SerialRxData { is_successful, } => { if is_successful { - ViewData::new( - timestamp, + RichText::from_string( format!(" {}", command_name, content), Color::Black, Color::LightGreen, ) + .highlight_invisible() + .into_view_data(timestamp) } else { - ViewData::new( - timestamp, + RichText::from_string( format!("Cannot send ", command_name), Color::White, Color::LightRed, ) + .highlight_invisible() + .into_view_data(timestamp) } } SerialRxData::HexString { @@ -113,19 +122,17 @@ impl Into for SerialRxData { is_successful, } => { if is_successful { - ViewData::new( - timestamp, - format!("{:02x?}", &content), - Color::Black, - Color::Yellow, - ) + RichText::from_string(format!("{:02x?}", &content), Color::Black, Color::Yellow) + .highlight_invisible() + .into_view_data(timestamp) } else { - ViewData::new( - timestamp, + RichText::from_string( format!("Cannot send {:02x?}", &content), Color::White, Color::LightRed, ) + .highlight_invisible() + .into_view_data(timestamp) } } SerialRxData::Plugin { @@ -135,19 +142,21 @@ impl Into for SerialRxData { is_successful, } => { if is_successful { - ViewData::new( - timestamp, + RichText::from_string( format!(" [{plugin_name}] {content} "), Color::Black, Color::White, ) + .highlight_invisible() + .into_view_data(timestamp) } else { - ViewData::new( - timestamp, + RichText::from_string( format!(" [{plugin_name}] {content} "), Color::White, Color::Red, ) + .highlight_invisible() + .into_view_data(timestamp) } } SerialRxData::PluginSerialTx { @@ -157,19 +166,21 @@ impl Into for SerialRxData { is_successful, } => { if is_successful { - ViewData::new( - timestamp, + RichText::from_string( format!(" [{plugin_name}] => {:02x?} ", content), Color::Black, Color::White, ) + .highlight_invisible() + .into_view_data(timestamp) } else { - ViewData::new( - timestamp, + RichText::from_string( format!(" [{plugin_name}] => Fail to send {:02x?} ", content), Color::White, Color::Red, ) + .highlight_invisible() + .into_view_data(timestamp) } } } diff --git a/src/plugin_manager.rs b/src/plugin_manager.rs index 2d1993c..3fce1c6 100644 --- a/src/plugin_manager.rs +++ b/src/plugin_manager.rs @@ -164,7 +164,7 @@ impl PluginManager { continue; }; - let serial_rx_call = plugin.serial_rx_call(line.as_bytes().to_vec()); + let serial_rx_call = plugin.serial_rx_call(line.clone()); let plugin_name = plugin.name().to_string(); self.serial_rx_tx .send((plugin_name, serial_rx_call)) diff --git a/src/rich_string.rs b/src/rich_string.rs new file mode 100644 index 0000000..3f3ad46 --- /dev/null +++ b/src/rich_string.rs @@ -0,0 +1,343 @@ +use crate::text::ViewData; +use chrono::{DateTime, Local}; +use std::collections::HashMap; +use tui::style::{Color, Style}; +use tui::text::Span; + +pub struct RichText { + content: Vec, + fg: Color, + bg: Color, +} + +impl RichText { + pub fn new(content: Vec, fg: Color, bg: Color) -> Self { + Self { content, fg, bg } + } + + pub fn from_string(content: String, fg: Color, bg: Color) -> Self { + Self { + content: content.as_bytes().to_vec(), + fg, + bg, + } + } + + pub fn decode_ansi_color(self) -> RichTextAnsi { + RichTextAnsi::new(self) + } + + pub fn highlight_invisible(self) -> RichTextWithInvisible { + RichTextWithInvisible::new(self) + } + + pub fn to_span<'a>(&self) -> Span<'a> { + Span::styled( + String::from_utf8_lossy(&self.content).to_string(), + Style::default().bg(self.bg).fg(self.fg), + ) + } + + pub fn crop_prefix_len(&self, len: usize) -> Self { + Self { + content: if len >= self.content.len() { + vec![] + } else { + self.content[len..].to_vec() + }, + fg: self.fg, + bg: self.bg, + } + } +} + +pub struct RichTextWithInvisible { + rich_texts: Vec, +} + +impl RichTextWithInvisible { + pub fn into_view_data(self, timestamp: DateTime) -> ViewData { + ViewData::new(timestamp, self.rich_texts) + } + + fn new(rich_text: RichText) -> Self { + if rich_text.content.is_empty() { + return Self { rich_texts: vec![] }; + } + + enum State { + None, + Visible, + Invisible, + } + + let (fg, bg) = (rich_text.fg, rich_text.bg); + let (hl_fg, hl_bg) = Self::get_colors(rich_text.fg, rich_text.fg, true); + + let (buffer, state, acc) = rich_text.content.into_iter().fold( + (vec![], State::None, vec![]), + |(buffer, state, acc), byte| match state { + State::None => ( + vec![byte], + Self::is_visible(byte) + .then_some(State::Visible) + .unwrap_or(State::Invisible), + acc, + ), + State::Visible => { + if Self::is_visible(byte) { + ( + buffer.into_iter().chain([byte]).collect(), + State::Visible, + acc, + ) + } else { + ( + vec![], + State::Invisible, + acc.into_iter() + .chain([RichText::new(buffer, rich_text.fg, rich_text.bg)]) + .collect(), + ) + } + } + State::Invisible => { + if Self::is_visible(byte) { + ( + vec![], + State::Visible, + acc.into_iter() + .chain([RichText::new(Self::bytes_to_rich(buffer), hl_fg, hl_bg)]) + .collect(), + ) + } else { + ( + buffer.into_iter().chain([byte]).collect(), + State::Invisible, + acc, + ) + } + } + }, + ); + + Self { + rich_texts: if buffer.is_empty() { + acc + } else { + let (fg, bg) = Self::get_colors(fg, bg, matches!(state, State::Invisible)); + let buffer = if matches!(state, State::Invisible) { + Self::bytes_to_rich(buffer) + } else { + buffer + }; + + acc.into_iter() + .chain([RichText::new(buffer, fg, bg)]) + .collect() + }, + } + } + + fn is_visible(byte: u8) -> bool { + 0x20 <= byte && byte <= 0x7E + } + + fn bytes_to_rich(bytes: Vec) -> Vec { + bytes + .into_iter() + .flat_map(|b| match b { + b'\n' => b"\\n".to_vec(), + b'\r' => b"\\r".to_vec(), + b'\0' => b"\\0".to_vec(), + x => format!("\\x{:02x}", x).as_bytes().to_vec(), + }) + .collect() + } + + fn get_colors(fg: Color, bg: Color, is_highlight: bool) -> (Color, Color) { + if !is_highlight { + return (fg, bg); + } + + if bg == Color::Magenta + || bg == Color::LightMagenta + || fg == Color::Magenta + || fg == Color::LightMagenta + { + (Color::LightYellow, bg) + } else { + (Color::LightMagenta, bg) + } + } +} + +pub struct RichTextAnsi { + rich_texts: Vec, +} + +impl RichTextAnsi { + pub fn highlight_invisible(self) -> RichTextWithInvisible { + let rich_texts = self + .rich_texts + .into_iter() + .flat_map(|rich_text| RichTextWithInvisible::new(rich_text).rich_texts) + .collect::>(); + + RichTextWithInvisible { rich_texts } + } + + fn new(rich_text: RichText) -> Self { + if rich_text.content.is_empty() { + return RichTextAnsi { rich_texts: vec![] }; + } + + enum State { + None, + Escape, + Normal, + } + + let (fg, bg) = (rich_text.fg, rich_text.bg); + let (text_buffer, _color_pattern, state, acc, current_fg, current_bg) = + rich_text.content.into_iter().fold( + (vec![], vec![], State::None, Vec::::new(), fg, bg), + |(text_buffer, color_pattern, state, acc, current_fg, current_bg), byte| match state + { + State::None => { + if byte == 0x1B { + ( + vec![], + vec![byte], + State::Escape, + acc, + current_fg, + current_bg, + ) + } else { + ( + vec![byte], + vec![], + State::Normal, + acc, + current_fg, + current_bg, + ) + } + } + State::Escape => { + let color_pattern_ext = + color_pattern.into_iter().chain([byte]).collect::>(); + match RichTextAnsi::match_color_pattern(&color_pattern_ext) { + Ok(Some((new_fg, new_bg))) => ( + vec![], + vec![], + State::Normal, + acc.into_iter() + .chain([RichText { + content: text_buffer, + fg: current_fg, + bg: current_bg, + }]) + .collect::>(), + new_fg, + new_bg, + ), + Ok(None) => ( + text_buffer, + color_pattern_ext, + State::Escape, + acc, + current_fg, + current_bg, + ), + Err(_) => ( + text_buffer + .into_iter() + .chain(color_pattern_ext) + .collect::>(), + vec![], + State::Normal, + acc, + current_fg, + current_bg, + ), + } + } + State::Normal => { + if byte == 0x1B { + ( + text_buffer, + vec![byte], + State::Escape, + acc, + current_fg, + current_bg, + ) + } else { + ( + text_buffer.into_iter().chain([byte]).collect(), + vec![], + State::Normal, + acc, + current_fg, + current_bg, + ) + } + } + }, + ); + + Self { + rich_texts: if text_buffer.is_empty() { + acc + } else { + acc.into_iter() + .chain([match state { + State::None => unreachable!(), + State::Normal | State::Escape => RichText { + content: text_buffer, + fg: current_fg, + bg: current_bg, + }, + }]) + .collect() + }, + } + } + + fn match_color_pattern(pattern: &[u8]) -> Result, ()> { + let pattern_lut = HashMap::new() + .into_iter() + .chain([ + (b"\x1B[0m".to_vec(), (Color::Reset, Color::Reset)), + (b"\x1B[30m".to_vec(), (Color::Black, Color::Reset)), + (b"\x1B[0;30m".to_vec(), (Color::Black, Color::Reset)), + (b"\x1B[31m".to_vec(), (Color::Red, Color::Reset)), + (b"\x1B[0;31m".to_vec(), (Color::Red, Color::Reset)), + (b"\x1B[32m".to_vec(), (Color::Green, Color::Reset)), + (b"\x1B[0;32m".to_vec(), (Color::Green, Color::Reset)), + (b"\x1B[33m".to_vec(), (Color::Yellow, Color::Reset)), + (b"\x1B[0;33m".to_vec(), (Color::Yellow, Color::Reset)), + (b"\x1B[34m".to_vec(), (Color::Blue, Color::Reset)), + (b"\x1B[0;34m".to_vec(), (Color::Blue, Color::Reset)), + (b"\x1B[35m".to_vec(), (Color::Magenta, Color::Reset)), + (b"\x1B[0;35m".to_vec(), (Color::Magenta, Color::Reset)), + (b"\x1B[36m".to_vec(), (Color::Cyan, Color::Reset)), + (b"\x1B[0;36m".to_vec(), (Color::Cyan, Color::Reset)), + (b"\x1B[37m".to_vec(), (Color::Gray, Color::Reset)), + (b"\x1B[0;37m".to_vec(), (Color::Gray, Color::Reset)), + ]) + .collect::, (Color, Color)>>(); + + if let Some((fg, bg)) = pattern_lut.get(pattern) { + return Ok(Some((*fg, *bg))); + } + + if pattern_lut.keys().any(|x| x.starts_with(pattern)) { + return Ok(None); + } + + Err(()) + } +} diff --git a/src/serial.rs b/src/serial.rs index 09b04a6..626713a 100644 --- a/src/serial.rs +++ b/src/serial.rs @@ -109,7 +109,7 @@ impl SerialIF { is_connected.clone(), ); - let mut line = String::new(); + let mut line = vec![]; let mut buffer = [0u8]; let mut now = Instant::now(); @@ -206,7 +206,7 @@ impl SerialIF { match serial.read(&mut buffer) { Ok(_) => { - line.push(buffer[0] as char); + line.push(buffer[0]); if buffer[0] == b'\n' { data_tx .send(SerialRxData::RxData { diff --git a/src/text.rs b/src/text.rs index 9105607..6243db6 100644 --- a/src/text.rs +++ b/src/text.rs @@ -1,4 +1,5 @@ use crate::messages::SerialRxData; +use crate::rich_string::RichText; use chrono::{DateTime, Local}; use std::marker::PhantomData; use tui::backend::Backend; @@ -40,135 +41,6 @@ impl TextView { } } - fn is_visible(x: char) -> bool { - 0x20 <= x as u8 && x as u8 <= 0x7E - } - - fn print_invisible(data: String) -> String { - data.chars() - .map(|x| match x { - x if TextView::::is_visible(x) => x.to_string(), - '\0' => "\\0".to_string(), - '\n' => "\\n".to_string(), - '\r' => "\\r".to_string(), - _ => format!("\\x{:02x}", x as u8), - }) - .collect::>() - .join("") - } - - fn highlight_invisible(&self, in_text: &str, color: Color) -> Vec<(String, Color)> { - #[derive(PartialEq)] - enum Mode { - Visible, - Invisible, - } - - let highlight_color = if color == Color::LightMagenta { - Color::LightCyan - } else { - Color::LightMagenta - }; - let mut output = vec![]; - let mut text = "".to_string(); - let mut highlight_text = "".to_string(); - let mut mode = Mode::Visible; - - for ch in in_text.chars() { - if TextView::::is_visible(ch) { - text.push(ch); - if mode == Mode::Invisible { - output.push(( - TextView::::print_invisible(highlight_text.clone()), - highlight_color, - )); - highlight_text.clear(); - } - mode = Mode::Visible; - } else { - highlight_text.push(ch); - if mode == Mode::Visible { - output.push((text.clone(), color)); - text.clear(); - } - mode = Mode::Invisible; - } - } - - if !text.is_empty() { - output.push((text.clone(), color)); - } else if !highlight_text.is_empty() { - output.push(( - TextView::::print_invisible(highlight_text.clone()), - highlight_color, - )); - } - - output - } - - fn decode_ansi_color(&self, text: &str, color: Color) -> Vec<(String, Color)> { - if text.is_empty() { - return vec![]; - } - - let splitted = text.split("\x1B[").collect::>(); - let mut res = vec![]; - - let pattern_n_color = [ - ("0m", Color::Reset), - ("30m", Color::Black), - ("0;30m", Color::Black), - ("31m", Color::Red), - ("0;31m", Color::Red), - ("32m", Color::Green), - ("0;32m", Color::Green), - ("33m", Color::Yellow), - ("0;33m", Color::Yellow), - ("34m", Color::Blue), - ("0;34m", Color::Blue), - ("35m", Color::Magenta), - ("0;35m", Color::Magenta), - ("36m", Color::Cyan), - ("0;36m", Color::Cyan), - ("37m", Color::Gray), - ("0;37m", Color::Gray), - ]; - - for splitted_str in splitted.iter() { - if splitted_str.is_empty() { - continue; - } - - if pattern_n_color.iter().all(|(pattern, color)| { - if splitted_str.starts_with(pattern) { - let final_str = splitted_str - .to_string() - .replace(pattern, "") - .trim() - .to_string(); - if final_str.is_empty() { - return true; - } - - res.push((final_str, *color)); - return false; - } - - true - }) { - let fmt_splitted_str = if splitted_str.starts_with("0m") { - splitted_str.replace("0m", "") - } else { - splitted_str.to_string() - }; - res.push((fmt_splitted_str, color)); - } - } - - res - } - pub fn draw(&self, f: &mut Frame, rect: Rect) { let scroll = if self.auto_scroll { (self.max_main_axis(), self.scroll.1) @@ -200,34 +72,27 @@ impl TextView { ) }; - let text = - coll.iter() - .map(|x| { - let scroll = scroll.1 as usize; - let content = if scroll >= x.data.len() { - "" - } else { - &x.data[scroll..] - }; - - let texts_colors = self.decode_ansi_color(content, x.fg); - let texts_colors = texts_colors - .into_iter() - .flat_map(|(text, color)| self.highlight_invisible(&text, color)) - .collect::>(); - let timestamp_span = Span::styled( - format!("{} ", x.timestamp.format("%H:%M:%S.%3f")), - Style::default().fg(Color::DarkGray), - ); - let mut content = vec![timestamp_span]; - - content.extend(texts_colors.into_iter().map(|(text, color)| { - Span::styled(text, Style::default().bg(x.bg).fg(color)) - })); - - Spans::from(content) - }) - .collect::>(); + let text = coll + .iter() + .map(|ViewData { data, timestamp }| { + let timestamp_span = Span::styled( + format!("{} ", timestamp.format("%H:%M:%S.%3f")), + Style::default().fg(Color::DarkGray), + ); + let content = vec![timestamp_span] + .into_iter() + .chain(data.iter().enumerate().map(|(i, rich_text)| { + if i == 0 { + rich_text.crop_prefix_len(scroll.1 as usize).to_span() + } else { + rich_text.to_span() + } + })) + .collect::>(); + + Spans::from(content) + }) + .collect::>(); let paragraph = Paragraph::new(text).block(block); f.render_widget(paragraph, rect); } @@ -294,21 +159,13 @@ impl TextView { } } -#[derive(Clone)] pub struct ViewData { timestamp: DateTime, - data: String, - fg: Color, - bg: Color, + data: Vec, } impl ViewData { - pub fn new(timestamp: DateTime, data: String, fg: Color, bg: Color) -> Self { - Self { - timestamp, - data, - fg, - bg, - } + pub fn new(timestamp: DateTime, data: Vec) -> Self { + Self { timestamp, data } } } From d4add344056c1b3fffdd279b01c8071bffd77dc6 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Wed, 21 Feb 2024 08:42:41 -0300 Subject: [PATCH 3/3] fix: fixes plugin serial tx presentation, richtext highlight background and the presentation of user data and command --- src/messages.rs | 7 +++++-- src/rich_string.rs | 6 +++--- src/serial.rs | 50 ++++++++++++++++++++++++++-------------------- 3 files changed, 36 insertions(+), 27 deletions(-) diff --git a/src/messages.rs b/src/messages.rs index 7d6be1f..e0143e2 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -167,7 +167,7 @@ impl Into for SerialRxData { } => { if is_successful { RichText::from_string( - format!(" [{plugin_name}] => {:02x?} ", content), + format!(" [{plugin_name}] => {} ", String::from_utf8_lossy(&content)), Color::Black, Color::White, ) @@ -175,7 +175,10 @@ impl Into for SerialRxData { .into_view_data(timestamp) } else { RichText::from_string( - format!(" [{plugin_name}] => Fail to send {:02x?} ", content), + format!( + " [{plugin_name}] => Fail to send {} ", + String::from_utf8_lossy(&content) + ), Color::White, Color::Red, ) diff --git a/src/rich_string.rs b/src/rich_string.rs index 3f3ad46..fcdb094 100644 --- a/src/rich_string.rs +++ b/src/rich_string.rs @@ -72,7 +72,7 @@ impl RichTextWithInvisible { } let (fg, bg) = (rich_text.fg, rich_text.bg); - let (hl_fg, hl_bg) = Self::get_colors(rich_text.fg, rich_text.fg, true); + let (hl_fg, hl_bg) = Self::get_colors(rich_text.fg, rich_text.bg, true); let (buffer, state, acc) = rich_text.content.into_iter().fold( (vec![], State::None, vec![]), @@ -93,7 +93,7 @@ impl RichTextWithInvisible { ) } else { ( - vec![], + vec![byte], State::Invisible, acc.into_iter() .chain([RichText::new(buffer, rich_text.fg, rich_text.bg)]) @@ -104,7 +104,7 @@ impl RichTextWithInvisible { State::Invisible => { if Self::is_visible(byte) { ( - vec![], + vec![byte], State::Visible, acc.into_iter() .chain([RichText::new(Self::bytes_to_rich(buffer), hl_fg, hl_bg)]) diff --git a/src/serial.rs b/src/serial.rs index 626713a..0c7be97 100644 --- a/src/serial.rs +++ b/src/serial.rs @@ -118,7 +118,9 @@ impl SerialIF { match data_to_send { UserTxData::Exit => break 'task, UserTxData::Data { content } => { - match serial.write(format!("{content}\r\n").as_bytes()) { + let content = format!("{content}\r\n"); + + match serial.write(content.as_bytes()) { Ok(_) => { data_tx .send(SerialRxData::TxData { @@ -142,28 +144,32 @@ impl SerialIF { UserTxData::Command { command_name, content, - } => match serial.write(format!("{content}\r\n").as_bytes()) { - Ok(_) => { - data_tx - .send(SerialRxData::Command { - timestamp: Local::now(), - command_name, - content, - is_successful: true, - }) - .expect("Cannot send command confirm"); - } - Err(_) => { - data_tx - .send(SerialRxData::Command { - timestamp: Local::now(), - command_name, - content, - is_successful: false, - }) - .expect("Cannot send command fail"); + } => { + let content = format!("{content}\r\n"); + + match serial.write(content.as_bytes()) { + Ok(_) => { + data_tx + .send(SerialRxData::Command { + timestamp: Local::now(), + command_name, + content, + is_successful: true, + }) + .expect("Cannot send command confirm"); + } + Err(_) => { + data_tx + .send(SerialRxData::Command { + timestamp: Local::now(), + command_name, + content, + is_successful: false, + }) + .expect("Cannot send command fail"); + } } - }, + } UserTxData::HexString { content } => match serial.write(&content) { Ok(_) => data_tx .send(SerialRxData::HexString {