use std::{fmt::Debug, sync::LazyLock};
use gtk::{pango, prelude::TextBufferExt};
use regex::bytes::Regex;
use tracing::{debug, error, info, warn};
use crate::utils::{
more_colors::{ColorCodeError, Intensity, TermColor, get_256color},
writer::UnitInfoWriter,
};
static RE: LazyLock<Regex> = LazyLock::new(|| {
match Regex::new(
r"(?x)
\u{1b} # ESC
(?: # 7-bit C1 Fe (except CSI)
[@-Z\^-_]
| # or [ for CSI, followed by a control sequence
\[
([0-?]*) # Parameter bytes
[ -/]* # Intermediate bytes
([@-~]) # Final byte
|
# or ] for OSC hyperlink
\]8;;
([\s!-~]*) #link
[\u{7}\\]
(.*) #link text
\u{1b}\]8;;[\u{7}\\]
)",
) {
Ok(ok) => ok,
Err(e) => {
let error_msg = format!("Regex compile error : {:?}", e);
error!("{error_msg}");
panic!("{error_msg}")
}
}
});
pub fn write(
writer: &mut UnitInfoWriter,
text: &str,
token_list: &mut Vec<Token>,
added_tokens: &[Token],
) {
token_list.clear();
token_list.extend_from_slice(added_tokens);
get_tokens(token_list, text);
write_text(token_list, writer, text);
}
pub fn get_tokens(token_list: &mut Vec<Token>, text: &str) {
let mut last_end: usize = 0;
for captures in RE.captures_iter(text.as_bytes()) {
let main_match = captures.get(0).expect("not supose to happen");
let end = main_match.end();
let start = main_match.start();
if start != last_end {
token_list.push(Token::Text(last_end, start));
}
last_end = end;
if let Some(osc_contol) = captures.get(2) {
let control = osc_contol.as_bytes();
if control == b"m"
&& let Some(select_graphic_rendition_match) = captures.get(1)
{
let select_graphic_rendition = select_graphic_rendition_match.as_bytes();
match capture_code(select_graphic_rendition, token_list) {
Ok(_) => {
continue;
}
Err(e) => {
warn!(
"while parsing {} got error {:?}",
String::from_utf8_lossy(select_graphic_rendition),
e
)
}
};
}
} else if let Some(link_match) = captures.get(3)
&& let Some(link_text_match) = captures.get(4)
{
token_list.push(Token::Hyperlink(
link_match.start(),
link_match.end(),
link_text_match.start(),
link_text_match.end(),
));
continue;
}
let s = bytes_to_string(main_match.as_bytes());
token_list.push(Token::UnHandled(s));
}
if text.len() != last_end {
token_list.push(Token::Text(last_end, text.len()));
}
}
fn bytes_to_string(main_match: &[u8]) -> String {
match String::from_utf8(main_match.to_vec()) {
Ok(s) => s,
Err(e) => {
warn!("while parsing {:?} got error {:?}", main_match, e);
String::from_utf8_lossy(main_match).to_string()
}
}
}
pub(super) fn write_text(tokens: &Vec<Token>, writer: &mut UnitInfoWriter, text: &str) {
let tag_table = writer.buffer.tag_table();
let mut select_graphic_rendition = SelectGraphicRendition::default();
for token in tokens {
match token {
Token::Text(start, end) => {
let start_offset = writer.text_iterator.offset();
let sub_text = &text[*start..*end];
writer.buffer.insert(&mut writer.text_iterator, sub_text);
let start_iter = writer.buffer.iter_at_offset(start_offset);
select_graphic_rendition.apply_tags(
&tag_table,
&writer.buffer,
&start_iter,
&writer.text_iterator,
);
}
Token::Intensity(intensity) => select_graphic_rendition.set_intensity(Some(*intensity)),
Token::FgColor(term_color) => {
select_graphic_rendition.set_foreground_color(Some(*term_color))
}
Token::BgColor(term_color) => {
select_graphic_rendition.set_background_color(Some(*term_color))
}
Token::Italic => select_graphic_rendition.set_italic(true),
Token::Underline(underline) => select_graphic_rendition.set_underline(*underline),
Token::Blink => select_graphic_rendition.set_blink(true),
Token::Reversed => select_graphic_rendition.set_reversed(true),
Token::Hidden => select_graphic_rendition.set_hidden(true),
Token::Strikeout => select_graphic_rendition.set_strikeout(true),
Token::Hyperlink(link_start, link_end, link_text_stert, link_text_end) => {
let link = &text[*link_start..*link_end];
let link_text = &text[*link_text_stert..*link_text_end];
debug!("Do hyperlink {link} {link_text}");
}
Token::UnHandledCode(code) => info!("UnHandledCode {code}"),
Token::UnHandled(a) => debug!("UnHandled {a}"),
Token::Reset(reset_type) => match reset_type {
ResetType::All => select_graphic_rendition.reset(),
ResetType::FgColor => select_graphic_rendition.set_foreground_color(None),
ResetType::BgColor => select_graphic_rendition.set_background_color(None),
ResetType::Intensity => select_graphic_rendition.set_intensity(None),
ResetType::Hidden => select_graphic_rendition.set_hidden(false),
},
}
}
}
fn bsplit(b: &u8) -> bool {
matches!(b, b';' | b':')
}
fn capture_code(code_line: &[u8], vec: &mut Vec<Token>) -> Result<(), ColorCodeError> {
let mut it: std::slice::Split<'_, u8, fn(&u8) -> bool> = code_line.split(bsplit);
while let Some(code) = it.next() {
let token = match code {
b"0" => Token::Reset(ResetType::All),
b"1" => Token::Intensity(Intensity::Bold),
b"2" => Token::Intensity(Intensity::Faint),
b"3" => Token::Italic,
b"4" => Token::Underline(Underline::Single),
b"5" => Token::Blink,
b"6" => Token::Blink,
b"7" => Token::Reversed,
b"8" => Token::Hidden,
b"9" => Token::Strikeout,
b"22" => Token::Reset(ResetType::Intensity),
b"28" => Token::Reset(ResetType::Hidden),
b"21" => Token::Underline(Underline::Double),
b"30" => Token::FgColor(TermColor::Black),
b"31" => Token::FgColor(TermColor::Red),
b"32" => Token::FgColor(TermColor::Green),
b"33" => Token::FgColor(TermColor::Yellow),
b"34" => Token::FgColor(TermColor::Blue),
b"35" => Token::FgColor(TermColor::Magenta),
b"36" => Token::FgColor(TermColor::Cyan),
b"37" => Token::FgColor(TermColor::White),
b"38" => {
let color = find_color(&mut it)?;
Token::FgColor(color)
}
b"39" => Token::Reset(ResetType::FgColor),
b"40" => Token::BgColor(TermColor::Black),
b"41" => Token::BgColor(TermColor::Red),
b"42" => Token::BgColor(TermColor::Green),
b"43" => Token::BgColor(TermColor::Yellow),
b"44" => Token::BgColor(TermColor::Blue),
b"45" => Token::BgColor(TermColor::Magenta),
b"46" => Token::BgColor(TermColor::Cyan),
b"47" => Token::BgColor(TermColor::White),
b"48" => {
let color = find_color(&mut it)?;
Token::BgColor(color)
}
b"49" => Token::Reset(ResetType::BgColor),
b"90" => Token::FgColor(TermColor::BrightBlack),
b"91" => Token::FgColor(TermColor::BrightRed),
b"92" => Token::FgColor(TermColor::BrightGreen),
b"93" => Token::FgColor(TermColor::BrightYellow),
b"94" => Token::FgColor(TermColor::BrightBlue),
b"95" => Token::FgColor(TermColor::BrightMagenta),
b"96" => Token::FgColor(TermColor::BrightCyan),
b"97" => Token::FgColor(TermColor::BrightWhite),
b"100" => Token::BgColor(TermColor::BrightBlack),
b"101" => Token::BgColor(TermColor::BrightRed),
b"102" => Token::BgColor(TermColor::BrightGreen),
b"103" => Token::BgColor(TermColor::BrightYellow),
b"104" => Token::BgColor(TermColor::BrightBlue),
b"105" => Token::BgColor(TermColor::BrightMagenta),
b"106" => Token::BgColor(TermColor::BrightCyan),
b"107" => Token::BgColor(TermColor::BrightWhite),
unknown_code => Token::UnHandledCode(bytes_to_string(unknown_code)),
};
vec.push(token)
}
Ok(())
}
fn find_color(
it: &mut std::slice::Split<'_, u8, fn(&u8) -> bool>,
) -> Result<TermColor, ColorCodeError> {
let Some(sub_code) = it.next() else {
return Err(ColorCodeError::Malformed);
};
let color = match sub_code {
b"5" => {
if let Some(color_code) = it.next() {
get_256color(color_code[0])
} else {
return Err(ColorCodeError::Malformed);
}
}
b"2" => {
let Some(r) = it.next() else {
return Err(ColorCodeError::Malformed);
};
let Some(g) = it.next() else {
return Err(ColorCodeError::Malformed);
};
let Some(b) = it.next() else {
return Err(ColorCodeError::Malformed);
};
let r = str::from_utf8(r)?;
let g = str::from_utf8(g)?;
let b = str::from_utf8(b)?;
TermColor::new_vga(r, g, b)?
}
unexpected_code => {
return Err(ColorCodeError::UnexpectedCode(bytes_to_string(
unexpected_code,
)));
}
};
Ok(color)
}
#[derive(Debug, Clone)]
pub(super) enum Token {
FgColor(TermColor),
BgColor(TermColor),
Intensity(Intensity),
Italic,
Underline(Underline),
Blink,
Reversed,
Hidden,
Strikeout,
Text(usize, usize),
Reset(ResetType),
Hyperlink(usize, usize, usize, usize),
UnHandledCode(String),
UnHandled(String),
}
#[derive(Debug, Clone)]
pub(super) enum ResetType {
All,
FgColor,
BgColor,
Intensity,
Hidden,
}
#[derive(Default, PartialEq, Eq)]
pub struct SelectGraphicRendition {
foreground_color: Option<TermColor>,
background_color: Option<TermColor>,
intensity: Option<Intensity>,
italic: Option<bool>,
underline: Option<Underline>,
blink: Option<bool>,
reversed: Option<bool>,
hidden: Option<bool>,
strikeout: Option<bool>,
}
impl SelectGraphicRendition {
fn reset(&mut self) {
self.foreground_color = None;
self.background_color = None;
self.intensity = None;
self.italic = None;
self.underline = None;
self.reversed = None;
self.hidden = None;
self.strikeout = None;
self.blink = None;
}
fn set_intensity(&mut self, intensity: Option<Intensity>) {
self.intensity = intensity;
}
fn set_italic(&mut self, italic: bool) {
self.italic = Some(italic);
}
fn set_underline(&mut self, underline: Underline) {
self.underline = Some(underline);
}
fn set_reversed(&mut self, reversed: bool) {
self.reversed = Some(reversed);
}
fn set_strikeout(&mut self, strikeout: bool) {
self.strikeout = Some(strikeout);
}
fn set_hidden(&mut self, hidden: bool) {
self.hidden = Some(hidden);
}
fn set_foreground_color(&mut self, color: Option<TermColor>) {
self.foreground_color = color;
}
fn set_background_color(&mut self, color: Option<TermColor>) {
self.background_color = color;
}
fn set_blink(&mut self, blink: bool) {
self.blink = Some(blink);
}
fn apply_tags(
&mut self,
tag_table: >k::TextTagTable,
buf: >k::TextBuffer,
start_iter: >k::TextIter,
end_iter: >k::TextIter,
) {
if let Some(underline) = self.underline {
let tt = gtk::TextTag::builder().underline(underline.pango()).build();
tag_table.add(&tt);
buf.apply_tag(&tt, start_iter, end_iter);
}
if let Some(strikeout) = self.strikeout {
let tt = gtk::TextTag::builder().strikethrough(strikeout).build();
tag_table.add(&tt);
buf.apply_tag(&tt, start_iter, end_iter);
}
if let Some(_italic) = self.italic {
const ITALIC: &str = "italic";
let tag = if let Some(tag) = tag_table.lookup(ITALIC) {
tag
} else {
let tag = gtk::TextTag::builder()
.style(pango::Style::Italic)
.name(ITALIC)
.build();
tag_table.add(&tag);
tag
};
buf.apply_tag(&tag, start_iter, end_iter);
}
if let Some(intensity) = self.intensity {
let name = intensity.pango_str();
let tt: gtk::TextTag = if let Some(tag) = tag_table.lookup(name) {
tag
} else {
let mut tag_builder = gtk::TextTag::builder()
.weight(intensity.pango_i32())
.name(name);
if intensity == Intensity::Faint {
tag_builder =
tag_builder.foreground_rgba(&TermColor::Vga(94, 94, 94).get_rgba());
}
let tag = tag_builder.build();
tag_table.add(&tag);
tag
};
buf.apply_tag(&tt, start_iter, end_iter);
}
if let Some(color) = self.foreground_color {
let tt = gtk::TextTag::builder()
.foreground_rgba(&color.get_rgba())
.build();
tag_table.add(&tt);
buf.apply_tag(&tt, start_iter, end_iter);
}
if let Some(color) = self.background_color {
let tt = gtk::TextTag::builder()
.background_rgba(&color.get_rgba())
.build();
tag_table.add(&tt);
buf.apply_tag(&tt, start_iter, end_iter);
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Underline {
Single,
Double,
}
impl Underline {
fn pango(&self) -> pango::Underline {
match self {
Underline::Single => pango::Underline::Single,
Underline::Double => pango::Underline::Double,
}
}
}
#[cfg(test)]
mod tests {
use gtk::gdk;
use test_base::init_logs;
use super::*;
const TEST_STRS: [&str; 4] = [
"This is \u{1b}[4mvery\u{1b}[0m\u{1b}[1m\u{1b}[96m Important\u{1b}[0m",
"asdf \u{1b}[38;2;255;140;0;48;2;255;228;225mExample 24 bit color escape sequence\u{1b}[0m",
"0:13:37 fedora abrt-server[90694]: \u{1b}[0;1;38;5;185m\u{1b}[0;1;39m\u{1b}[0;1;38;5;185m'post-create' on '/var/spool/abrt/ccpp-2024-10-08-10:13:37.85581-16875' exited with 1\u{1b}[0m",
"nothing \u{1b}[91mframed\u{1b}[7m test ok\u{1b}[0m",
];
#[test]
fn test_display() {
for (line, s) in TEST_STRS.into_iter().enumerate() {
println!("line {} {}", line, s);
}
}
#[test]
fn test_tokens() {
for (line, s) in TEST_STRS.into_iter().enumerate() {
println!("\nLine {line}");
println!("{}", s);
let result = get_tokens_test(s);
println!("{:?}", result);
}
}
#[test]
fn test_color_regex() {
let mut results = vec![];
for (line, haystack) in TEST_STRS.into_iter().enumerate() {
for capt in RE.captures_iter(haystack.as_bytes()) {
results.push((line, capt));
}
}
for capt in results {
println!("line {} capture: {:#?}", capt.0, capt.1)
}
}
#[test]
fn test_capture_code() {
let mut vec = Vec::<Token>::new();
let _res = capture_code(b"0;1;38;5;185", &mut vec);
println!("{:?}", vec)
}
#[test]
fn test_convert_color() {
for (a, b) in [
(gdk::RGBA::WHITE, TermColor::BrightWhite),
(gdk::RGBA::RED, TermColor::BrightRed),
(gdk::RGBA::GREEN, TermColor::BrightGreen),
(gdk::RGBA::BLUE, TermColor::BrightBlue),
(gdk::RGBA::BLACK, TermColor::Black),
] {
assert_convert_color(a, b);
}
}
fn assert_convert_color(color_gtk: gdk::RGBA, color_term: TermColor) {
let color: TermColor = color_gtk.into();
println!("{:?} {:?}", color, color_gtk);
assert_eq!(color, color_term.get_vga(),);
}
#[test]
fn test_link_regex() {
let test_str = "\x1b[35;47mANSI? \x1b[0m\x1b[1;32mSI\x1b[0m \x1b]8;;man:abrt(1)\x1b\u{07}[🡕]\x1b]8;;\x1b\u{7} test \x1b[0m".as_bytes();
for capt in RE.captures_iter(test_str) {
println!("capture: {:#?}", capt)
}
}
#[test]
fn test_link_regex2() {
let test_text = "Oct 15 08:07:19 fedora abrt-notification[160431]: \u{1b}]8;;man:abrt(1)\u{7}[🡕]\u{1b}]8;;\u{7} end of line".as_bytes();
for capt in RE.captures_iter(test_text) {
println!("capture: {:#?}", capt);
assert_eq!(b"man:abrt(1)", capt.get(3).unwrap().as_bytes());
assert_eq!(
"[🡕]",
str::from_utf8(capt.get(4).unwrap().as_bytes()).unwrap()
);
}
}
pub fn get_tokens_test(text: &str) -> Vec<Token> {
let mut token_list = Vec::<Token>::new();
super::get_tokens(&mut token_list, text);
token_list
}
#[test]
fn test_tok_amp() {
let test_text = "Gnome & Co";
let token_list = get_tokens_test(test_text);
println!("out {:?}", token_list);
}
#[test]
fn test_rust_log() {
let logs = r#"
Oct 10 02:02:44 systemd[1]: tiny_daemon.service: Deactivated successfully.
Oct 10 02:02:44 systemd[1]: Stopped It is tiny, but is not the tiniest.
Oct 10 02:02:44 systemd[1]: Started It is tiny, but is not the tiniest.
Oct 10 02:02:44 tiny_daemon[338370]: [2m2025-10-10T06:02:44.657790Z[0m [32m INFO[0m [2mtiny_daemon[0m[2m:[0m Starting tiny_daemon...
Oct 10 02:02:44 tiny_daemon[338370]: [2m2025-10-10T06:02:44.657890Z[0m [32m INFO[0m [2mtiny_daemon[0m[2m:[0m SIGRTMIN() + 1 = 35!!!
Oct 10 02:02:44 tiny_daemon[338370]: [2m2025-10-10T06:02:44.657898Z[0m [33m WARN[0m [2mtiny_daemon[0m[2m:[0m test warning message
Oct 10 02:02:44 tiny_daemon[338370]: [2m2025-10-10T06:02:44.657903Z[0m [31mERROR[0m [2mtiny_daemon[0m[2m:[0m test error message
Oct 10 02:02:44 tiny_daemon[338370]: [2m2025-10-10T06:02:44.657956Z[0m [32m INFO[0m [2mtiny_daemon[0m[2m:[0m Tiny Daemon listening on 127.0.0.1:33001
"#;
for (line, s) in logs.lines().enumerate() {
println!("\nLine {line}");
println!("{}", s);
let result = get_tokens_test(s);
println!("{:?}", result);
}
}
#[test]
fn test_color_out() {
let escape = '\u{001b}';
for x in 0..5 {
println!("---");
for z in [0, 10, 60, 70] {
for y in 30..37 {
let y = y + z;
let label = format!("\\e[{x};{y}m");
print!("{escape}[{x};{y}m {label: ^10} {escape}[0m");
}
println!()
}
}
}
#[test]
fn test_color_out2() {
let escape = '\u{001b}';
for code in 0..255 {
println!("{escape}[38;5;{code}m[38;5;'{code}m{escape}[0m");
}
}
#[test]
fn test_char_boundary() {
let s = "ab早cd";
for (index, character) in s.char_indices() {
println!("Character '{}' starts at byte index {}", character, index);
}
}
#[test]
fn test_char_boundary2() {
let s = "Löwe 老虎 Léopard";
for index in 0..s.len() {
println!(
"Character at {} is char boundary {}",
index,
s.is_char_boundary(index)
);
}
}
#[test]
fn test_multiple_escape_sequences() {
let test_str =
"\u{1b}[1mBold\u{1b}[0m and \u{1b}[3mItalic\u{1b}[0m and \u{1b}[4mUnderline\u{1b}[0m";
let tokens = get_tokens_test(test_str);
let mut found_bold = false;
let mut found_italic = false;
let mut found_underline = false;
for token in &tokens {
match token {
Token::Intensity(Intensity::Bold) => found_bold = true,
Token::Italic => found_italic = true,
Token::Underline(Underline::Single) => found_underline = true,
_ => {}
}
}
assert!(found_bold, "Bold token not found");
assert!(found_italic, "Italic token not found");
assert!(found_underline, "Underline token not found");
}
#[test]
fn test_unhandled_escape_sequence() {
let test_str = "\u{1b}[999mUnknown\u{1b}[0m";
let tokens = get_tokens_test(test_str);
let mut found_unhandled = false;
for token in &tokens {
if let Token::UnHandledCode(code) = token {
assert_eq!(code, "999");
found_unhandled = true;
}
}
assert!(
found_unhandled,
"UnHandledCode token not found for unknown code"
);
}
#[test]
fn test_reset_types() {
let test_str = "\u{1b}[0mResetAll\u{1b}[39mResetFg\u{1b}[49mResetBg\u{1b}[22mResetIntensity\u{1b}[28mResetHidden";
let tokens = get_tokens_test(test_str);
let mut found_all = false;
let mut found_fg = false;
let mut found_bg = false;
let mut found_intensity = false;
let mut found_hidden = false;
for token in &tokens {
match token {
Token::Reset(ResetType::All) => found_all = true,
Token::Reset(ResetType::FgColor) => found_fg = true,
Token::Reset(ResetType::BgColor) => found_bg = true,
Token::Reset(ResetType::Intensity) => found_intensity = true,
Token::Reset(ResetType::Hidden) => found_hidden = true,
_ => {}
}
}
assert!(found_all, "ResetType::All not found");
assert!(found_fg, "ResetType::FgColor not found");
assert!(found_bg, "ResetType::BgColor not found");
assert!(found_intensity, "ResetType::Intensity not found");
assert!(found_hidden, "ResetType::Hidden not found");
}
#[test]
fn test_24bit_color_parsing() {
let test_str = "\u{1b}[38;2;12;34;56m24bitFG\u{1b}[48;2;78;90;123m24bitBG\u{1b}[0m";
let tokens = get_tokens_test(test_str);
let mut found_fg = false;
let mut found_bg = false;
for token in &tokens {
match token {
Token::FgColor(TermColor::Vga(r, g, b)) if *r == 12 && *g == 34 && *b == 56 => {
found_fg = true
}
Token::BgColor(TermColor::Vga(r, g, b)) if *r == 78 && *g == 90 && *b == 123 => {
found_bg = true
}
_ => {}
}
}
assert!(found_fg, "24-bit foreground color not parsed");
assert!(found_bg, "24-bit background color not parsed");
}
#[test]
fn test_hyperlink_token() {
let test_str = "before \u{1b}]8;;https://example.com\u{7}link\u{1b}]8;;\u{7} after";
let tokens = get_tokens_test(test_str);
let mut found_hyperlink = false;
for token in &tokens {
if let Token::Hyperlink(link_start, link_end, text_start, text_end) = token {
let link = &test_str[*link_start..*link_end];
let link_text = &test_str[*text_start..*text_end];
assert_eq!(link, "https://example.com");
assert_eq!(link_text, "link");
found_hyperlink = true;
}
}
assert!(found_hyperlink, "Hyperlink token not found");
}
#[test]
fn test_multi_lines() {
init_logs();
let s = r#"JS ERROR: Error: Impossible to remove untracked message
_removeMessage@resource:///org/gnome/shell/ui/messageList.js:1599:19
_removePlayer@resource:///org/gnome/shell/ui/messageList.js:1773:14
_setupMpris/<@resource:///org/gnome/shell/ui/messageList.js:1757:51
_onNameOwnerChanged@resource:///org/gnome/shell/ui/mpris.js:247:22
_callHandlers@resource:///org/gnome/gjs/modules/core/_signals.js:130:42
_emit@resource:///org/gnome/gjs/modules/core/_signals.js:119:10
_convertToNativeSignal@resource:///org/gnome/gjs/modules/core/overrides/Gio.js:153:19
@resource:///org/gnome/shell/ui/init.js:21:20"#;
let lines = s.lines();
let mut token_list = Vec::<Token>::new();
for line in lines {
info!("{line}");
get_tokens(&mut token_list, line);
info!("{:?} line len {}", token_list, line.len());
}
}
}