Skip to content

feat: strip ANSI escape codes for text element#169

Draft
bytesnake wants to merge 1 commit intoccbrown:mainfrom
bytesnake:strip-ansi
Draft

feat: strip ANSI escape codes for text element#169
bytesnake wants to merge 1 commit intoccbrown:mainfrom
bytesnake:strip-ansi

Conversation

@bytesnake
Copy link

What It Does

Remove ANSI escape codes for Text element. A major drawback is that this pulls in regex dependency

Related Issues

#168

@ccbrown
Copy link
Owner

ccbrown commented Feb 26, 2026

I'm fine with the added dependency.

For inspiration, I took a look at what Ink does.

Similar issue: vadimdemedes/ink#362

They strip all ANSI codes out for measurement purposes, but leave certain ANSI codes in during rendering if they wouldn't break the layout. That might be something to consider later, but I'm okay stripping all of them out for now.

Their sanitization: https://github.com/vadimdemedes/ink/blob/70477953d4c97bb2e1754aef9770317d4d05f043/src/sanitize-ansi.ts#L9

Their string measurement: https://github.com/sindresorhus/string-width/blob/main/index.js

I did some benchmarks to see if this would have a notable impact on performance. For strings that don't contain ANSI sequences, the impact is pretty negligible. However, I think it is worth returning the Cow from the strip_ansi function:

diff --git a/packages/iocraft/src/components/text.rs b/packages/iocraft/src/components/text.rs
index 247c4f8..024168c 100644
--- a/packages/iocraft/src/components/text.rs
+++ b/packages/iocraft/src/components/text.rs
@@ -228,7 +228,7 @@ impl Component for Text {
             underline: props.decoration == TextDecoration::Underline,
             italic: props.italic,
         };
-        self.content = strip_ansi(&props.content);
+        self.content = strip_ansi(&props.content).into_owned();
         self.wrap = props.wrap;
         self.align = props.align;
         updater.set_measure_func(Self::measure_func(self.content.clone(), props.wrap));
diff --git a/packages/iocraft/src/strip_ansi.rs b/packages/iocraft/src/strip_ansi.rs
index 56f711e..2fcbd28 100644
--- a/packages/iocraft/src/strip_ansi.rs
+++ b/packages/iocraft/src/strip_ansi.rs
@@ -1,5 +1,5 @@
-use std::sync::LazyLock;
 use regex::Regex;
+use std::{borrow::Cow, sync::LazyLock};
 
 pub const ANSI_REGEX_PATTERN: &str = concat!(
     // OSC branch
@@ -28,6 +28,6 @@ pub const ANSI_REGEX_PATTERN: &str = concat!(
 static ANSI_REGEX: LazyLock<Regex> =
     LazyLock::new(|| Regex::new(ANSI_REGEX_PATTERN).expect("valid ANSI regex"));
 
-pub(crate) fn strip_ansi(string: &str) -> String {
-    ANSI_REGEX.replace_all(string, "").to_string()
+pub(crate) fn strip_ansi(string: &str) -> Cow<str> {
+    ANSI_REGEX.replace_all(string, "")
 }

That makes strip_ansi ~30% faster in the fast case. I don't think this actually matters, since we immediately call into_owned in the text component, but still, it might be a little cleaner and may matter for future uses of the function.

It seems like it's probably worth stripping ANSI in MixedText too?

@codecov
Copy link

codecov bot commented Feb 27, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.06%. Comparing base (5a3d4c6) to head (621e336).

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main     #169   +/-   ##
=======================================
  Coverage   90.06%   90.06%           
=======================================
  Files          32       33    +1     
  Lines        5333     5337    +4     
  Branches     5333     5337    +4     
=======================================
+ Hits         4803     4807    +4     
  Misses        427      427           
  Partials      103      103           
Files with missing lines Coverage Δ
packages/iocraft/src/components/text.rs 98.95% <100.00%> (ø)
packages/iocraft/src/strip_ansi.rs 100.00% <100.00%> (ø)

Impacted file tree graph

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants