Skip to content

Commit 880d77e

Browse files
committed
Massively improved ANSI removal on terminals
1 parent b21a7b0 commit 880d77e

File tree

2 files changed

+40
-38
lines changed

2 files changed

+40
-38
lines changed

src/editor/interface.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ use crate::error::{OxError, Result};
55
use crate::events::wait_for_event_hog;
66
use crate::ui::{key_event, size, Feedback};
77
#[cfg(not(target_os = "windows"))]
8-
use crate::ui::{
9-
remove_ansi_codes, replace_reset_background, replace_reset_foreground, strip_escape_codes,
10-
};
8+
use crate::ui::{remove_ansi_codes, replace_reset, strip_escape_codes};
119
use crate::{config, display, handle_lua_error};
1210
use crossterm::{
1311
event::{KeyCode as KCode, KeyModifiers as KMod},
@@ -656,8 +654,7 @@ impl Editor {
656654
let line = line.replace(['\n', '\r'], "");
657655
let mut visible_line = strip_escape_codes(&line);
658656
// Replace resets with editor style
659-
visible_line = replace_reset_background(&visible_line, &editor_bg);
660-
visible_line = replace_reset_foreground(&visible_line, &editor_fg);
657+
visible_line = replace_reset(&visible_line, &editor_bg, &editor_fg);
661658
let mut w = width(&remove_ansi_codes(&line), 4);
662659
// Work out if this is where the cursor should be
663660
if n_lines.saturating_sub(shift_down) == y && self.ptr == *fc {

src/ui.rs

Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -342,18 +342,33 @@ pub fn get_xterm_lookup() -> HashMap<u8, (u8, u8, u8)> {
342342
result
343343
}
344344

345+
/// Define various ANSI code regex patterns for matching
346+
#[cfg(not(target_os = "windows"))]
347+
pub const CURSORS: &str = r"\x1b(?:\[(?:\d+(?:;\d+)?(?:A|B|C|D|F|f|G|H)|(?:#[A-G]|6n|s|u|H)|[0-6] q|(?:[0-9]{1,3};)*[0-9]{1,3}t)|[7-8])";
348+
#[cfg(not(target_os = "windows"))]
349+
pub const ERASING: &str = r"\x1b\[[0-3]?(?:J|K)";
350+
#[cfg(not(target_os = "windows"))]
351+
pub const DISPLAY: &str =
352+
r"\x1b\[(?:(?:[0-9]|22|23|24|25|27|28|29)(?:t|m)|(?:[0-9]{1,3};)*[0-9]{1,3}m)";
353+
#[cfg(not(target_os = "windows"))]
354+
pub const SC_MODE: &str = r"\x1b\[(?:=|\?)[0-9]{1,4}(?:h|l)";
355+
#[cfg(not(target_os = "windows"))]
356+
pub const AC_JUNK: &str = r"(\a|\b|\n|\v|\f|\r)";
357+
358+
/// Global ANSI removal
359+
fn global() -> String {
360+
format!("({CURSORS}|{ERASING}|{DISPLAY}|{SC_MODE}|{AC_JUNK})")
361+
}
362+
345363
#[cfg(not(target_os = "windows"))]
346364
/// Remove ANSI codes from a string
347365
pub fn remove_ansi_codes(input: &str) -> String {
348366
// Define a regular expression to match ANSI escape codes and other control sequences
349-
let invisible_regex =
350-
// Regex::new(r"[\x00-\x1F\x7F-\x9F]|\x1b(?:[@-_]|\[[0-9;?]*[a-zA-Z])|\[[0-9;?]*[a-zA-Z]")
351-
Regex::new(r"\x1b\[[0-9;?]*[a-zA-Z]|\x1b\([a-zA-Z]|\x1b\].*?(\x07|\x1b\\)|\x1b(:?>|=)")
352-
.unwrap();
367+
let re = Regex::new(&global()).unwrap();
353368
let weird_newline = Regex::new(r"⏎\s*⏎\s?").unwrap();
354369
let long_spaces = Regex::new(r"%(?:\x1b\[1m)?\s{5,}").unwrap();
355370
// Replace all matches with an empty string
356-
let result = invisible_regex.replace_all(input, "").to_string();
371+
let result = re.replace_all(input, "").to_string();
357372
// Replace weird new line stuff
358373
let result = weird_newline.replace_all(&result, "").to_string();
359374
// Replace long spaces
@@ -365,27 +380,15 @@ pub fn remove_ansi_codes(input: &str) -> String {
365380
#[cfg(not(target_os = "windows"))]
366381
/// Remove all ANSI codes outside of color and attribute codes
367382
pub fn strip_escape_codes(input: &str) -> String {
368-
// Define a regular expression to match all escape sequences
369-
// let re = Regex::new(r"\x1b\[[0-9;?]*[a-zA-Z]").unwrap();
370-
let re =
371-
Regex::new(r"\x1b\[[0-9;?]*[a-zA-Z]|\x1b\([a-zA-Z]|\x1b\].*?(\x07|\x1b\\)|\x1b(:?>|=)")
372-
.unwrap();
383+
let re = Regex::new(&global()).unwrap();
384+
let display = Regex::new(DISPLAY).unwrap();
373385
let weird_newline = Regex::new(r"⏎\s*⏎\s?").unwrap();
374386
let long_spaces = Regex::new(r"%(?:\x1b\[1m)?\s{5,}").unwrap();
375387
// Replace escape sequences, keeping those for attributes and colors
376388
let result = re
377389
.replace_all(input, |caps: &regex::Captures| {
378390
let code = caps.get(0).unwrap().as_str();
379-
380-
// Check if the escape code is for an allowed attribute or color
381-
if code.contains("1m") // Bold
382-
|| code.contains("4m") // Underline
383-
|| code.contains("38;5") // Foreground color (256-color mode)
384-
|| code.contains("48;5") // Background color (256-color mode)
385-
|| code.contains("38;2") // Foreground color (true color)
386-
|| code.contains("48;2")
387-
// Background color (true color)
388-
{
391+
if display.is_match(code) {
389392
// Return the escape code unchanged
390393
code.to_string()
391394
} else {
@@ -403,19 +406,21 @@ pub fn strip_escape_codes(input: &str) -> String {
403406
}
404407

405408
#[cfg(not(target_os = "windows"))]
406-
/// Replace reset background ANSI codes with a custom background color.
407-
pub fn replace_reset_background(input: &str, custom_bg: &str) -> String {
409+
#[allow(clippy::similar_names)]
410+
/// Replace reset colour ANSI codes with a custom background color.
411+
pub fn replace_reset(input: &str, custom_bg: &str, custom_fg: &str) -> String {
412+
// Replace total reset with appropriate codes
413+
let total_reset_regex = Regex::new(r"\x1b\[0m").unwrap();
408414
// Define the regex to match reset background ANSI codes
409-
let reset_bg_regex = Regex::new(r"\x1b\[49m|\x1b\[0m").unwrap();
410-
// Replace reset background with the custom background color
411-
reset_bg_regex.replace_all(input, custom_bg).to_string()
412-
}
413-
414-
#[cfg(not(target_os = "windows"))]
415-
/// Replace reset foreground ANSI codes with a custom foreground color.
416-
pub fn replace_reset_foreground(input: &str, custom_fg: &str) -> String {
415+
let reset_bg_regex = Regex::new(r"\x1b\[49m").unwrap();
417416
// Define the regex to match reset foreground ANSI codes
418-
let reset_fg_regex = Regex::new(r"\x1b\[39m|\x1b\[0m").unwrap();
419-
// Replace reset foreground with the custom foreground color
420-
reset_fg_regex.replace_all(input, custom_fg).to_string()
417+
let reset_fg_regex = Regex::new(r"\x1b\[39m").unwrap();
418+
// Replace reset background with the custom background color
419+
let attr_full_reset =
420+
"\u{1b}[22m\u{1b}[23m\u{1b}[24m\u{1b}[25m\u{1b}[27m\u{1b}[28m\u{1b}[29m".to_string();
421+
let full_reset = format!("{custom_bg}{custom_fg}{attr_full_reset}");
422+
let input = total_reset_regex.replace_all(input, full_reset).to_string();
423+
let input = reset_bg_regex.replace_all(&input, custom_bg).to_string();
424+
let input = reset_fg_regex.replace_all(&input, custom_fg).to_string();
425+
input
421426
}

0 commit comments

Comments
 (0)