diff --git a/src/renderer.rs b/src/renderer.rs index 3f274cd..a74e21d 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -420,7 +420,11 @@ fn draw_content(f: &mut Frame, b: &Browser, area: Rect) { // Live form input let w = (content_width as usize).saturating_sub(4).min(60); spans.push(Span::styled( - format!(" {:width$}", format!("{}▎", b.form_text), width = w), + format!( + " {:width$}", + format!("{}▎", sanitize_terminal_text(&b.form_text)), + width = w + ), Style::default().fg(FG).bg(FORM_ACT_BG).add_modifier(Modifier::BOLD), )); } else { @@ -444,7 +448,7 @@ fn draw_content(f: &mut Frame, b: &Browser, area: Rect) { } else { base_style(&seg.style) }; - spans.push(Span::styled(seg.text.clone(), style)); + spans.push(Span::styled(sanitize_terminal_text(&seg.text), style)); } } @@ -516,21 +520,21 @@ fn draw_bottombar(f: &mut Frame, b: &Browser, area: Rect) { Line::from(vec![ Span::styled(" GO ", Style::default().fg(BG).bg(ACCENT).add_modifier(Modifier::BOLD)), Span::styled(" ", Style::default().bg(BG_SURFACE)), - Span::styled(format!("{}▎", b.url_input), Style::default().fg(FG).bg(BG_SURFACE)), + Span::styled(format!("{}▎", sanitize_terminal_text(&b.url_input)), Style::default().fg(FG).bg(BG_SURFACE)), ]) } InputMode::Search => { Line::from(vec![ Span::styled(" / ", Style::default().fg(BG).bg(GOLD).add_modifier(Modifier::BOLD)), Span::styled(" ", Style::default().bg(BG_SURFACE)), - Span::styled(format!("{}▎", b.search_input), Style::default().fg(FG).bg(BG_SURFACE)), + Span::styled(format!("{}▎", sanitize_terminal_text(&b.search_input)), Style::default().fg(FG).bg(BG_SURFACE)), ]) } InputMode::GotoNum => { Line::from(vec![ Span::styled(" # ", Style::default().fg(BG).bg(MAGENTA).add_modifier(Modifier::BOLD)), Span::styled(" Link: ", Style::default().fg(FG_DIM).bg(BG_SURFACE)), - Span::styled(format!("{}▎", b.goto_input), Style::default().fg(FG).bg(BG_SURFACE)), + Span::styled(format!("{}▎", sanitize_terminal_text(&b.goto_input)), Style::default().fg(FG).bg(BG_SURFACE)), ]) } InputMode::FormField => { @@ -571,29 +575,29 @@ fn draw_bottombar(f: &mut Frame, b: &Browser, area: Rect) { ( " ✓ ", Style::default().fg(BG).bg(GREEN).add_modifier(Modifier::BOLD), - b.status_msg.clone(), + sanitize_terminal_text(&b.status_msg), ) } } } else { - (" ✓ ", Style::default().fg(BG).bg(GREEN).add_modifier(Modifier::BOLD), b.status_msg.clone()) + (" ✓ ", Style::default().fg(BG).bg(GREEN).add_modifier(Modifier::BOLD), sanitize_terminal_text(&b.status_msg)) } } else { match &b.state { LoadState::Loading => ( " ⟳ ", Style::default().fg(BG).bg(GOLD).add_modifier(Modifier::BOLD), - b.status_msg.clone(), + sanitize_terminal_text(&b.status_msg), ), LoadState::Error(_) => ( " ✗ ", Style::default().fg(FG).bg(ERR_FG).add_modifier(Modifier::BOLD), - b.status_msg.clone(), + sanitize_terminal_text(&b.status_msg), ), LoadState::Idle => ( " ✓ ", Style::default().fg(BG).bg(GREEN).add_modifier(Modifier::BOLD), - b.status_msg.clone(), + sanitize_terminal_text(&b.status_msg), ), } }; @@ -655,14 +659,19 @@ fn draw_bottombar(f: &mut Frame, b: &Browser, area: Rect) { fn draw_overlay_list(f: &mut Frame, b: &Browser, area: Rect, is_bookmarks: bool) { let (title, items, cursor) = if is_bookmarks { let items: Vec = b.bookmarks.items.iter().enumerate() - .map(|(i, bm)| format!(" {}. {} {}", i + 1, bm.title, bm.url)) + .map(|(i, bm)| format!( + " {}. {} {}", + i + 1, + sanitize_terminal_text(&bm.title), + sanitize_terminal_text(&bm.url) + )) .collect(); (" ★ Bookmarks ", items, b.bookmark_cursor) } else { let items: Vec = b.history.iter().enumerate() .map(|(i, url)| { let marker = if i + 1 == b.history_pos { "▸" } else { " " }; - format!(" {} {}", marker, url) + format!(" {} {}", marker, sanitize_terminal_text(url)) }) .collect(); (" ⟳ History ", items, b.history_cursor) @@ -722,11 +731,11 @@ fn draw_page_info(f: &mut Frame, b: &Browser, area: Rect) { let info = vec![ Line::from(vec![ Span::styled(" Title ", label_style), - Span::styled(&b.page_title, value_style.add_modifier(Modifier::BOLD)), + Span::styled(sanitize_terminal_text(&b.page_title), value_style.add_modifier(Modifier::BOLD)), ]), Line::from(vec![ Span::styled(" URL ", label_style), - Span::styled(&b.current_url, Style::default().fg(ACCENT)), + Span::styled(sanitize_terminal_text(&b.current_url), Style::default().fg(ACCENT)), ]), Line::from(vec![ Span::styled(" Lines ", label_style), @@ -843,18 +852,34 @@ fn centered_rect(w: u16, h: u16, area: Rect) -> Rect { } fn truncate_str(s: &str, max: usize) -> String { - if s.len() <= max { return s.to_string(); } - if max < 4 { return s.chars().take(max).collect(); } + let sanitized = sanitize_terminal_text(s); + if sanitized.len() <= max { return sanitized; } + if max < 4 { return sanitized.chars().take(max).collect(); } let take = max.saturating_sub(1); - format!("{}…", &s.chars().take(take).collect::()) + format!("{}…", &sanitized.chars().take(take).collect::()) } fn split_url_parts(url: &str) -> (String, String) { if let Ok(parsed) = url::Url::parse(url) { - let domain = parsed.host_str().unwrap_or("").to_string(); - let path = format!("{}{}", parsed.path(), parsed.query().map(|q| format!("?{}", q)).unwrap_or_default()); + let domain = sanitize_terminal_text(parsed.host_str().unwrap_or("")); + let path = sanitize_terminal_text(&format!( + "{}{}", + parsed.path(), + parsed.query().map(|q| format!("?{}", q)).unwrap_or_default() + )); (domain, path) } else { - (url.to_string(), String::new()) + (sanitize_terminal_text(url), String::new()) } } + +fn sanitize_terminal_text(s: &str) -> String { + s.chars() + .filter_map(|c| match c { + '\n' | '\r' | '\t' => Some(' '), + '\u{7F}'..='\u{9F}' => None, + c if c <= '\u{1F}' => None, + _ => Some(c), + }) + .collect() +}