diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 8c56c4bc..776ca380 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -103,6 +103,19 @@ for user input events and a TUI handle to update the TUI. The types are: | term_input | Input handling (reading events from `stdin`) | | termbox_simple | Terminal manipulation (drawing) | | libtiny_common | The "channel name" type | +| libtiny_wire | Parsing IRC message formatting characters (colors etc.) | + +### libtiny_logger + +Implements logging IRC events (incoming messages, user left/joined etc.) to +user-specified log directory. + +#### Dependencies of `libtiny_logger`: + +| Dependency | Used for | +| -------------- | ------------- | +| libtiny_common | The "channel name" type | +| libtiny_wire | Filtering out IRC message formatting characters (colors etc.) | ### libtiny_wire diff --git a/CHANGELOG.md b/CHANGELOG.md index 5930c73d..7de69b89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ - `/join` (without arguments) now rejoins the current channel. (#334) - Key bindings can be configured in the config file. See the [wiki page][key-bindings-wiki] for details. (#328, #336) +- Handling of IRC formatting characters (colors etc.) in TUI and logger + improved: + - TUI now handles "reset" control character, to reset the text style to the + default. + - Logger now filters out all control characters before writing to the file. + (#360) [key-bindings-wiki]: https://github.com/osa1/tiny/wiki/Configuring-key-bindings diff --git a/Cargo.lock b/Cargo.lock index 273428d5..de39aa3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -359,6 +359,7 @@ name = "libtiny_logger" version = "0.1.0" dependencies = [ "libtiny_common", + "libtiny_wire", "log", "time", ] @@ -370,6 +371,7 @@ dependencies = [ "bencher", "libc", "libtiny_common", + "libtiny_wire", "log", "mio", "notify-rust", diff --git a/assets/README.md b/assets/README.md new file mode 100644 index 00000000..6b6643f3 --- /dev/null +++ b/assets/README.md @@ -0,0 +1,6 @@ +Update `crate_deps.png` with: + +``` +dot -Tpng crate_deps.dot > crate_deps.png +optipng crate_deps.png +``` diff --git a/assets/crate_deps.dot b/assets/crate_deps.dot index 5c161bb2..70b88224 100644 --- a/assets/crate_deps.dot +++ b/assets/crate_deps.dot @@ -24,8 +24,10 @@ digraph mygraph { "libtiny_client" -> "libtiny_wire" "libtiny_logger" -> "libtiny_common" + "libtiny_logger" -> "libtiny_wire" "libtiny_tui" -> "libtiny_common" + "libtiny_tui" -> "libtiny_wire" "libtiny_tui" -> "term_input" "libtiny_tui" -> "termbox_simple" diff --git a/assets/crate_deps.png b/assets/crate_deps.png index 0022296e..e953bcf2 100644 Binary files a/assets/crate_deps.png and b/assets/crate_deps.png differ diff --git a/crates/libtiny_logger/Cargo.toml b/crates/libtiny_logger/Cargo.toml index 73fd20e6..de0942f0 100644 --- a/crates/libtiny_logger/Cargo.toml +++ b/crates/libtiny_logger/Cargo.toml @@ -7,5 +7,6 @@ edition = "2018" [dependencies] libtiny_common = { path = "../libtiny_common" } +libtiny_wire = { path = "../libtiny_wire" } log = "0.4" time = "0.1" diff --git a/crates/libtiny_logger/src/lib.rs b/crates/libtiny_logger/src/lib.rs index 3ce2f710..71025402 100644 --- a/crates/libtiny_logger/src/lib.rs +++ b/crates/libtiny_logger/src/lib.rs @@ -9,6 +9,7 @@ use std::rc::Rc; use time::Tm; use libtiny_common::{ChanName, ChanNameRef, MsgTarget}; +use libtiny_wire::formatting::remove_irc_control_chars; #[macro_use] extern crate log; @@ -306,6 +307,7 @@ impl LoggerInner { _highlight: bool, is_action: bool, ) { + let msg = remove_irc_control_chars(msg); self.apply_to_target(target, |fd: &mut File, report_err: &dyn Fn(String)| { let io_ret = if is_action { writeln!(fd, "[{}] {} {}", strf(&ts), sender, msg) diff --git a/crates/libtiny_tui/Cargo.toml b/crates/libtiny_tui/Cargo.toml index 7e778daf..0b09e0f5 100644 --- a/crates/libtiny_tui/Cargo.toml +++ b/crates/libtiny_tui/Cargo.toml @@ -14,6 +14,7 @@ desktop-notifications = ["notify-rust"] [dependencies] libtiny_common = { path = "../libtiny_common" } +libtiny_wire = { path = "../libtiny_wire" } log = "0.4" notify-rust = { version = "3", optional = true } serde = { version = "1.0", features = ["derive"] } diff --git a/crates/libtiny_tui/src/config.rs b/crates/libtiny_tui/src/config.rs index ed523b5e..730a707c 100644 --- a/crates/libtiny_tui/src/config.rs +++ b/crates/libtiny_tui/src/config.rs @@ -34,10 +34,10 @@ fn default_max_nick_length() -> usize { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Style { - /// Termbox fg. + /// Termbox fg pub fg: u16, - /// Termbox bg. + /// Termbox bg pub bg: u16, } diff --git a/crates/libtiny_tui/src/msg_area/line.rs b/crates/libtiny_tui/src/msg_area/line.rs index 9568eea5..af5a9a8c 100644 --- a/crates/libtiny_tui/src/msg_area/line.rs +++ b/crates/libtiny_tui/src/msg_area/line.rs @@ -1,10 +1,7 @@ -use crate::line_split::LineType; -use crate::{ - config::{Colors, Style}, - line_split::LineDataCache, - utils::translate_irc_control_chars, -}; -use std::mem; +use crate::config::{Colors, Style}; +use crate::line_split::{LineDataCache, LineType}; + +use libtiny_wire::formatting::{parse_irc_formatting, Color, IrcFormatEvent}; use termbox_simple::{self, Termbox}; /// A single line added to the widget. May be rendered as multiple lines on the @@ -13,6 +10,7 @@ use termbox_simple::{self, Termbox}; pub(crate) struct Line { /// Line segments. segments: Vec, + /// The segment we're currently extending. current_seg: StyledString, @@ -35,7 +33,7 @@ pub(crate) enum SegStyle { /// of the color list, so make sure to use mod. NickColor(usize), - /// A style from the current color scheme. + // Rest of the styles are from the color scheme UserMsg, ErrMsg, Topic, @@ -78,9 +76,6 @@ impl Default for StyledString { } } -// TODO get rid of this -const TERMBOX_COLOR_PREFIX: char = '\x00'; - impl Line { pub(crate) fn new() -> Line { Line { @@ -103,7 +98,7 @@ impl Line { if self.current_seg.string.is_empty() { self.current_seg.style = style; } else if self.current_seg.style != style { - let seg = mem::replace( + let seg = std::mem::replace( &mut self.current_seg, StyledString { string: String::new(), @@ -115,31 +110,36 @@ impl Line { } fn add_text_inner(&mut self, str: &str) { - fn push_color(ret: &mut String, irc_fg: u8, irc_bg: Option) { - ret.push(TERMBOX_COLOR_PREFIX); - ret.push(0 as char); // style - ret.push(irc_color_to_termbox(irc_fg) as char); - ret.push( - irc_bg - .map(irc_color_to_termbox) - .unwrap_or(termbox_simple::TB_DEFAULT as u8) as char, - ); - } - let str = translate_irc_control_chars(str, push_color); - self.current_seg.string.reserve(str.len()); - - let mut iter = str.chars(); - while let Some(char) = iter.next() { - if char == TERMBOX_COLOR_PREFIX { - let st = iter.next().unwrap() as u8; - let fg = iter.next().unwrap() as u8; - let bg = iter.next().unwrap() as u8; - let fg = (u16::from(st) << 8) | u16::from(fg); - let bg = u16::from(bg); - let style = Style { fg, bg }; - self.set_message_style(SegStyle::Fixed(style)); - } else if char > '\x08' { - self.current_seg.string.push(char); + for format_event in parse_irc_formatting(str) { + match format_event { + IrcFormatEvent::Bold + | IrcFormatEvent::Italic + | IrcFormatEvent::Underline + | IrcFormatEvent::Strikethrough + | IrcFormatEvent::Monospace => { + // TODO + } + IrcFormatEvent::Text(text) => { + self.current_seg.string.push_str(text); + } + IrcFormatEvent::Color { fg, bg } => { + let style = SegStyle::Fixed(Style { + fg: u16::from(irc_color_to_termbox(fg)), + bg: bg + .map(|bg| u16::from(irc_color_to_termbox(bg))) + .unwrap_or(termbox_simple::TB_DEFAULT), + }); + + self.set_message_style(style); + } + IrcFormatEvent::ReverseColor => { + if let SegStyle::Fixed(Style { fg, bg }) = self.current_seg.style { + self.set_message_style(SegStyle::Fixed(Style { fg: bg, bg: fg })); + } + } + IrcFormatEvent::Reset => { + self.set_message_style(SegStyle::UserMsg); + } } } } @@ -150,7 +150,6 @@ impl Line { } pub(crate) fn add_char(&mut self, char: char, style: SegStyle) { - assert_ne!(char, TERMBOX_COLOR_PREFIX); self.set_message_style(style); self.current_seg.string.push(char); } @@ -230,28 +229,28 @@ impl Line { //////////////////////////////////////////////////////////////////////////////// -// IRC colors: http://en.wikichip.org/wiki/irc/colors // Termbox colors: http://www.calmar.ws/vim/256-xterm-24bit-rgb-color-chart.html // (alternatively just run `cargo run --example colors`) -fn irc_color_to_termbox(irc_color: u8) -> u8 { +fn irc_color_to_termbox(irc_color: Color) -> u8 { match irc_color { - 0 => 15, // white - 1 => 0, // black - 2 => 17, // navy - 3 => 2, // green - 4 => 9, // red - 5 => 88, // maroon - 6 => 5, // purple - 7 => 130, // olive - 8 => 11, // yellow - 9 => 10, // light green - 10 => 6, // teal - 11 => 14, // cyan - 12 => 12, // awful blue - 13 => 13, // magenta - 14 => 8, // gray - 15 => 7, // light gray - _ => termbox_simple::TB_DEFAULT as u8, + Color::White => 255, + Color::Black => 16, + Color::Blue => 21, + Color::Green => 46, + Color::Red => 196, + Color::Brown => 88, + Color::Magenta => 93, + Color::Orange => 210, + Color::Yellow => 228, + Color::LightGreen => 154, + Color::Cyan => 75, + Color::LightCyan => 39, + Color::LightBlue => 38, + Color::Pink => 129, + Color::Grey => 243, + Color::LightGrey => 249, + Color::Default => termbox_simple::TB_DEFAULT as u8, + Color::Ansi(ansi_color) => ansi_color, } } diff --git a/crates/libtiny_tui/src/notifier.rs b/crates/libtiny_tui/src/notifier.rs index 0f2d49c1..f507186f 100644 --- a/crates/libtiny_tui/src/notifier.rs +++ b/crates/libtiny_tui/src/notifier.rs @@ -1,4 +1,6 @@ -use crate::{utils::remove_irc_control_chars, MsgTarget}; +use crate::MsgTarget; + +use libtiny_wire::formatting::remove_irc_control_chars; #[cfg(feature = "desktop-notifications")] use notify_rust::Notification; diff --git a/crates/libtiny_tui/src/utils.rs b/crates/libtiny_tui/src/utils.rs index ceed3f8c..0b81a5eb 100644 --- a/crates/libtiny_tui/src/utils.rs +++ b/crates/libtiny_tui/src/utils.rs @@ -52,106 +52,3 @@ pub(crate) fn is_nick_char(c: char) -> bool { || c == '-' // not valid according to RFC 2812 but servers accept it and I've seen nicks with // this char in the wild } - -//////////////////////////////////////////////////////////////////////////////// - -use std::{iter::Peekable, str::Chars}; - -/// Parse at least one, at most two digits. Does not consume the iterator when -/// result is `None`. -fn parse_color_code(chars: &mut Peekable) -> Option { - fn to_dec(ch: char) -> Option { - ch.to_digit(10).map(|c| c as u8) - } - - let c1_char = *chars.peek()?; - let c1_digit = match to_dec(c1_char) { - None => { - return None; - } - Some(c1_digit) => { - chars.next(); - c1_digit - } - }; - - match chars.peek().cloned() { - None => Some(c1_digit), - Some(c2) => match to_dec(c2) { - None => Some(c1_digit), - Some(c2_digit) => { - chars.next(); - Some(c1_digit * 10 + c2_digit) - } - }, - } -} - -//////////////////////////////////////////////////////////////////////////////// - -/// Translate IRC color codes using the callback, replace tabs with 8 spaces, and remove other -/// ASCII control characters from the input. -pub(crate) fn translate_irc_control_chars( - str: &str, - push_color: fn(ret: &mut String, fg: u8, bg: Option), -) -> String { - let mut ret = String::with_capacity(str.len()); - let mut iter = str.chars().peekable(); - - while let Some(char) = iter.next() { - if char == '\x03' { - match parse_color_code(&mut iter) { - None => { - // just skip the control char - } - Some(fg) => { - if let Some(char) = iter.peek().cloned() { - if char == ',' { - iter.next(); // consume ',' - match parse_color_code(&mut iter) { - None => { - // comma was not part of the color code, - // add it to the new string - push_color(&mut ret, fg, None); - ret.push(char); - } - Some(bg) => { - push_color(&mut ret, fg, Some(bg)); - } - } - } else { - push_color(&mut ret, fg, None); - } - } else { - push_color(&mut ret, fg, None); - } - } - } - } else if char == '\t' { - ret.push_str(" "); - } else if !char.is_ascii_control() { - ret.push(char); - } - } - - ret -} - -/// Like `translate_irc_control_chars`, but skips color codes. -pub(crate) fn remove_irc_control_chars(str: &str) -> String { - fn push_color(_ret: &mut String, _fg: u8, _bg: Option) {} - translate_irc_control_chars(str, push_color) -} - -#[test] -fn test_translate_irc_control_chars() { - assert_eq!( - remove_irc_control_chars(" Le Voyageur imprudent "), - " Le Voyageur imprudent " - ); - assert_eq!(remove_irc_control_chars("\x0301,02foo"), "foo"); - assert_eq!(remove_irc_control_chars("\x0301,2foo"), "foo"); - assert_eq!(remove_irc_control_chars("\x031,2foo"), "foo"); - assert_eq!(remove_irc_control_chars("\x031,foo"), ",foo"); - assert_eq!(remove_irc_control_chars("\x03,foo"), ",foo"); -} diff --git a/crates/libtiny_wire/src/formatting.rs b/crates/libtiny_wire/src/formatting.rs new file mode 100644 index 00000000..74111cf9 --- /dev/null +++ b/crates/libtiny_wire/src/formatting.rs @@ -0,0 +1,535 @@ +//! Implements parsing IRC formatting characters. Reference: +//! https://modern.ircdocs.horse/formatting.html + +const CHAR_BOLD: char = '\x02'; +const CHAR_ITALIC: char = '\x1D'; +const CHAR_UNDERLINE: char = '\x1F'; +const CHAR_STRIKETHROUGH: char = '\x1E'; +const CHAR_MONOSPACE: char = '\x11'; +const CHAR_COLOR: char = '\x03'; +const CHAR_HEX_COLOR: char = '\x04'; +const CHAR_REVERSE_COLOR: char = '\x16'; +const CHAR_RESET: char = '\x0F'; + +#[derive(Debug, PartialEq, Eq)] +pub enum IrcFormatEvent<'a> { + Text(&'a str), + + Bold, + Italic, + Underline, + Strikethrough, + Monospace, + + Color { + fg: Color, + bg: Option, + }, + + /// Reverse current background and foreground + ReverseColor, + + /// Reset formatting to the default + Reset, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Color { + White, + Black, + Blue, + Green, + Red, + Brown, + Magenta, + Orange, + Yellow, + LightGreen, + Cyan, + LightCyan, + LightBlue, + Pink, + Grey, + LightGrey, + Default, + Ansi(u8), +} + +impl Color { + fn from_code(code: u8) -> Self { + match code { + 0 => Color::White, + 1 => Color::Black, + 2 => Color::Blue, + 3 => Color::Green, + 4 => Color::Red, + 5 => Color::Brown, + 6 => Color::Magenta, + 7 => Color::Orange, + 8 => Color::Yellow, + 9 => Color::LightGreen, + 10 => Color::Cyan, + 11 => Color::LightCyan, + 12 => Color::LightBlue, + 13 => Color::Pink, + 14 => Color::Grey, + 15 => Color::LightGrey, + 16 => Color::Ansi(52), + 17 => Color::Ansi(94), + 18 => Color::Ansi(100), + 19 => Color::Ansi(58), + 20 => Color::Ansi(22), + 21 => Color::Ansi(29), + 22 => Color::Ansi(23), + 23 => Color::Ansi(24), + 24 => Color::Ansi(17), + 25 => Color::Ansi(54), + 26 => Color::Ansi(53), + 27 => Color::Ansi(89), + 28 => Color::Ansi(88), + 29 => Color::Ansi(130), + 30 => Color::Ansi(142), + 31 => Color::Ansi(64), + 32 => Color::Ansi(28), + 33 => Color::Ansi(35), + 34 => Color::Ansi(30), + 35 => Color::Ansi(25), + 36 => Color::Ansi(18), + 37 => Color::Ansi(91), + 38 => Color::Ansi(90), + 39 => Color::Ansi(125), + 40 => Color::Ansi(124), + 41 => Color::Ansi(166), + 42 => Color::Ansi(184), + 43 => Color::Ansi(106), + 44 => Color::Ansi(34), + 45 => Color::Ansi(49), + 46 => Color::Ansi(37), + 47 => Color::Ansi(33), + 48 => Color::Ansi(19), + 49 => Color::Ansi(129), + 50 => Color::Ansi(127), + 51 => Color::Ansi(161), + 52 => Color::Ansi(196), + 53 => Color::Ansi(208), + 54 => Color::Ansi(226), + 55 => Color::Ansi(154), + 56 => Color::Ansi(46), + 57 => Color::Ansi(86), + 58 => Color::Ansi(51), + 59 => Color::Ansi(75), + 60 => Color::Ansi(21), + 61 => Color::Ansi(171), + 62 => Color::Ansi(201), + 63 => Color::Ansi(198), + 64 => Color::Ansi(203), + 65 => Color::Ansi(215), + 66 => Color::Ansi(227), + 67 => Color::Ansi(191), + 68 => Color::Ansi(83), + 69 => Color::Ansi(122), + 70 => Color::Ansi(87), + 71 => Color::Ansi(111), + 72 => Color::Ansi(63), + 73 => Color::Ansi(177), + 74 => Color::Ansi(207), + 75 => Color::Ansi(205), + 76 => Color::Ansi(217), + 77 => Color::Ansi(223), + 78 => Color::Ansi(229), + 79 => Color::Ansi(193), + 80 => Color::Ansi(157), + 81 => Color::Ansi(158), + 82 => Color::Ansi(159), + 83 => Color::Ansi(153), + 84 => Color::Ansi(147), + 85 => Color::Ansi(183), + 86 => Color::Ansi(219), + 87 => Color::Ansi(212), + 88 => Color::Ansi(16), + 89 => Color::Ansi(233), + 90 => Color::Ansi(235), + 91 => Color::Ansi(237), + 92 => Color::Ansi(239), + 93 => Color::Ansi(241), + 94 => Color::Ansi(244), + 95 => Color::Ansi(247), + 96 => Color::Ansi(250), + 97 => Color::Ansi(254), + 98 => Color::Ansi(231), + _ => Color::Default, + } + } +} + +struct FormatEventParser<'a> { + str: &'a str, + + /// Current index in `str`. We maintain indices to be able to extract slices from `str`. + cursor: usize, +} + +impl<'a> FormatEventParser<'a> { + fn new(str: &'a str) -> Self { + Self { str, cursor: 0 } + } + + fn peek(&self) -> Option { + self.str[self.cursor..].chars().next() + } + + fn next(&mut self) -> Option { + let next = self.str[self.cursor..].chars().next(); + if let Some(char) = next { + self.cursor += char.len_utf8(); + } + next + } + + fn bump(&mut self, amt: usize) { + self.cursor += amt; + } + + fn parse_text(&mut self) -> &'a str { + let cursor = self.cursor; + while let Some(next) = self.next() { + if is_irc_format_char(next) { + self.cursor -= 1; + return &self.str[cursor..self.cursor]; + } + } + &self.str[cursor..] + } + + /// Parse a color code. Expects the color code prefix ('\x03') to be consumed. Does not + /// increment the cursor when result is `None`. + fn parse_color(&mut self) -> Option<(Color, Option)> { + match self.parse_color_code() { + None => None, + Some(fg) => { + if let Some(char) = self.peek() { + if char == ',' { + let cursor = self.cursor; + self.bump(1); // consume ',' + match self.parse_color_code() { + None => { + // comma was not part of the color code, revert the cursor + self.cursor = cursor; + Some((fg, None)) + } + Some(bg) => Some((fg, Some(bg))), + } + } else { + Some((fg, None)) + } + } else { + Some((fg, None)) + } + } + } + } + + /// Parses at least one, at most two digits. Does not increment the cursor when result is `None`. + fn parse_color_code(&mut self) -> Option { + fn to_dec(ch: char) -> Option { + ch.to_digit(10).map(|c| c as u8) + } + + let c1_char = self.peek()?; + let c1_digit = match to_dec(c1_char) { + None => { + return None; + } + Some(c1_digit) => { + self.bump(1); // consume digit + c1_digit + } + }; + + match self.peek() { + None => Some(Color::from_code(c1_digit)), + Some(c2) => match to_dec(c2) { + None => Some(Color::from_code(c1_digit)), + Some(c2_digit) => { + self.bump(1); // consume digit + Some(Color::from_code(c1_digit * 10 + c2_digit)) + } + }, + } + } + + fn skip_hex_code(&mut self) { + // rrggbb + for _ in 0..6 { + // Use `next` here to avoid incrementing cursor too much + let _ = self.next(); + } + } +} + +/// Is the character start of an IRC formatting char? +fn is_irc_format_char(c: char) -> bool { + matches!( + c, + CHAR_BOLD + | CHAR_ITALIC + | CHAR_UNDERLINE + | CHAR_STRIKETHROUGH + | CHAR_MONOSPACE + | CHAR_COLOR + | CHAR_HEX_COLOR + | CHAR_REVERSE_COLOR + | CHAR_RESET + ) +} + +impl<'a> Iterator for FormatEventParser<'a> { + type Item = IrcFormatEvent<'a>; + + fn next(&mut self) -> Option { + loop { + let next = match self.peek() { + None => return None, + Some(next) => next, + }; + + match next { + CHAR_BOLD => { + self.bump(1); + return Some(IrcFormatEvent::Bold); + } + + CHAR_ITALIC => { + self.bump(1); + return Some(IrcFormatEvent::Italic); + } + + CHAR_UNDERLINE => { + self.bump(1); + return Some(IrcFormatEvent::Underline); + } + + CHAR_STRIKETHROUGH => { + self.bump(1); + return Some(IrcFormatEvent::Strikethrough); + } + + CHAR_MONOSPACE => { + self.bump(1); + return Some(IrcFormatEvent::Monospace); + } + + CHAR_COLOR => { + self.bump(1); + match self.parse_color() { + Some((fg, bg)) => return Some(IrcFormatEvent::Color { fg, bg }), + None => { + // Just skip the control char + } + } + } + + CHAR_HEX_COLOR => { + self.bump(1); + self.skip_hex_code(); + } + + CHAR_REVERSE_COLOR => { + self.bump(1); + return Some(IrcFormatEvent::ReverseColor); + } + + CHAR_RESET => { + self.bump(1); + return Some(IrcFormatEvent::Reset); + } + + other if other.is_ascii_control() => { + self.bump(1); + continue; + } + + _other => return Some(IrcFormatEvent::Text(self.parse_text())), + } + } + } +} + +pub fn parse_irc_formatting<'a>(s: &'a str) -> impl Iterator + 'a { + FormatEventParser::new(s) +} + +/// Removes all IRC formatting characters and ASCII control characters. +pub fn remove_irc_control_chars(str: &str) -> String { + let mut s = String::with_capacity(str.len()); + + for event in parse_irc_formatting(str) { + match event { + IrcFormatEvent::Bold + | IrcFormatEvent::Italic + | IrcFormatEvent::Underline + | IrcFormatEvent::Strikethrough + | IrcFormatEvent::Monospace + | IrcFormatEvent::Color { .. } + | IrcFormatEvent::ReverseColor + | IrcFormatEvent::Reset => {} + IrcFormatEvent::Text(text) => s.push_str(text), + } + } + + s +} + +#[test] +fn test_translate_irc_control_chars() { + assert_eq!( + remove_irc_control_chars(" Le Voyageur imprudent "), + " Le Voyageur imprudent " + ); + assert_eq!(remove_irc_control_chars("\x0301,02foo"), "foo"); + assert_eq!(remove_irc_control_chars("\x0301,2foo"), "foo"); + assert_eq!(remove_irc_control_chars("\x031,2foo"), "foo"); + assert_eq!(remove_irc_control_chars("\x031,foo"), ",foo"); + assert_eq!(remove_irc_control_chars("\x03,foo"), ",foo"); +} + +#[test] +fn test_parse_text_1() { + let s = "just \x02\x1d\x1f\x1e\x11\x04rrggbb\x16\x0f testing"; + let mut parser = parse_irc_formatting(s); + assert_eq!(parser.next(), Some(IrcFormatEvent::Text("just "))); + assert_eq!(parser.next(), Some(IrcFormatEvent::Bold)); + assert_eq!(parser.next(), Some(IrcFormatEvent::Italic)); + assert_eq!(parser.next(), Some(IrcFormatEvent::Underline)); + assert_eq!(parser.next(), Some(IrcFormatEvent::Strikethrough)); + assert_eq!(parser.next(), Some(IrcFormatEvent::Monospace)); + assert_eq!(parser.next(), Some(IrcFormatEvent::ReverseColor)); + assert_eq!(parser.next(), Some(IrcFormatEvent::Reset)); + assert_eq!(parser.next(), Some(IrcFormatEvent::Text(" testing"))); + assert_eq!(parser.next(), None); +} + +#[test] +fn test_parse_text_2() { + let s = "a\x03"; + let mut parser = parse_irc_formatting(s); + assert_eq!(parser.next(), Some(IrcFormatEvent::Text("a"))); + assert_eq!(parser.next(), None); +} + +#[test] +fn test_parse_text_3() { + let s = "a\x03b"; + let mut parser = parse_irc_formatting(s); + assert_eq!(parser.next(), Some(IrcFormatEvent::Text("a"))); + assert_eq!(parser.next(), Some(IrcFormatEvent::Text("b"))); + assert_eq!(parser.next(), None); +} + +#[test] +fn test_parse_text_4() { + let s = "a\x031,2b"; + let mut parser = parse_irc_formatting(s); + assert_eq!(parser.next(), Some(IrcFormatEvent::Text("a"))); + assert_eq!( + parser.next(), + Some(IrcFormatEvent::Color { + fg: Color::Black, + bg: Some(Color::Blue) + }) + ); + assert_eq!(parser.next(), Some(IrcFormatEvent::Text("b"))); + assert_eq!(parser.next(), None); +} + +#[test] +fn test_parse_text_5() { + let s = "\x0301,02a"; + let mut parser = parse_irc_formatting(s); + assert_eq!( + parser.next(), + Some(IrcFormatEvent::Color { + fg: Color::Black, + bg: Some(Color::Blue), + }) + ); + assert_eq!(parser.next(), Some(IrcFormatEvent::Text("a"))); + assert_eq!(parser.next(), None); + + let s = "\x0301,2a"; + let mut parser = parse_irc_formatting(s); + assert_eq!( + parser.next(), + Some(IrcFormatEvent::Color { + fg: Color::Black, + bg: Some(Color::Blue), + }) + ); + assert_eq!(parser.next(), Some(IrcFormatEvent::Text("a"))); + assert_eq!(parser.next(), None); + + let s = "\x031,2a"; + let mut parser = parse_irc_formatting(s); + assert_eq!( + parser.next(), + Some(IrcFormatEvent::Color { + fg: Color::Black, + bg: Some(Color::Blue), + }) + ); + assert_eq!(parser.next(), Some(IrcFormatEvent::Text("a"))); + assert_eq!(parser.next(), None); + + let s = "\x031,a"; + let mut parser = parse_irc_formatting(s); + assert_eq!( + parser.next(), + Some(IrcFormatEvent::Color { + fg: Color::Black, + bg: None, + }) + ); + assert_eq!(parser.next(), Some(IrcFormatEvent::Text(",a"))); + assert_eq!(parser.next(), None); + + let s = "\x03,a"; + let mut parser = parse_irc_formatting(s); + assert_eq!(parser.next(), Some(IrcFormatEvent::Text(",a"))); + assert_eq!(parser.next(), None); +} + +#[test] +fn test_parse_color() { + let s = ""; + let mut parser = FormatEventParser::new(s); + assert_eq!(parser.parse_color(), None); + + let s = "a"; + let mut parser = FormatEventParser::new(s); + assert_eq!(parser.parse_color(), None); + + let s = "1a"; + let mut parser = FormatEventParser::new(s); + assert_eq!(parser.parse_color(), Some((Color::Black, None))); + + let s = "1,2a"; + let mut parser = FormatEventParser::new(s); + assert_eq!( + parser.parse_color(), + Some((Color::Black, Some(Color::Blue))) + ); + + let s = "01,2a"; + let mut parser = FormatEventParser::new(s); + assert_eq!( + parser.parse_color(), + Some((Color::Black, Some(Color::Blue))) + ); + + let s = "01,02a"; + let mut parser = FormatEventParser::new(s); + assert_eq!( + parser.parse_color(), + Some((Color::Black, Some(Color::Blue))) + ); +} diff --git a/crates/libtiny_wire/src/lib.rs b/crates/libtiny_wire/src/lib.rs index 07a23162..a5d900d5 100644 --- a/crates/libtiny_wire/src/lib.rs +++ b/crates/libtiny_wire/src/lib.rs @@ -5,6 +5,8 @@ //! This library is for implementing clients rather than servers or services, and does not support //! the IRC message format in full generality. +pub mod formatting; + use std::str; use libtiny_common::{ChanName, ChanNameRef};