From 2c52a3d378f87ed98eeed70ed2c19d744dc18224 Mon Sep 17 00:00:00 2001 From: Thijs Schreijer Date: Tue, 10 Feb 2026 21:51:37 +0100 Subject: [PATCH] feat(utils): add function to strip ansi sequences --- spec/00-utils_spec.lua | 57 ++++++++++++++++++++++++++++++++++++++++++ src/terminal/utils.lua | 19 ++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/spec/00-utils_spec.lua b/spec/00-utils_spec.lua index 5ecc8786..be62d21c 100644 --- a/spec/00-utils_spec.lua +++ b/spec/00-utils_spec.lua @@ -206,4 +206,61 @@ describe("Utils", function() end) + + + describe("strip_ansi()", function() + + it("returns empty string unchanged", function() + assert.are.equal("", utils.strip_ansi("")) + end) + + + it("errors on nil", function() + assert.has_error(function() + utils.strip_ansi(nil) + end) + end) + + + it("returns plain text unchanged", function() + assert.are.equal("plain", utils.strip_ansi("plain")) + assert.are.equal("hello world", utils.strip_ansi("hello world")) + end) + + + it("strips SGR (color/attribute) sequences", function() + local red = "\27[31m" + local reset = "\27[0m" + assert.are.equal("red", utils.strip_ansi(red .. "red" .. reset)) + assert.are.equal("bold", utils.strip_ansi("\27[1mbold\27[0m")) + end) + + + it("strips CSI cursor/control sequences", function() + assert.are.equal("", utils.strip_ansi("\27[2J\27[H")) + assert.are.equal("x", utils.strip_ansi("\27[10;20Hx\27[6n")) + assert.are.equal("ab", utils.strip_ansi("a\27[1Kb")) + end) + + + it("strips OSC sequences (until BEL or ST)", function() + assert.are.equal("", utils.strip_ansi("\27]0;title\7")) + assert.are.equal("xy", utils.strip_ansi("x\27]1;http://example.com\27\\y")) + assert.are.equal("y", utils.strip_ansi("\27]1;url\27\\y")) + end) + + + it("strips C1 CSI (0x9b)", function() + -- 0x9b is C1 CSI (same as ESC [); following bytes are params + final, e.g. "31m" + assert.are.equal("x", utils.strip_ansi("\15531mx")) + end) + + + it("strips mixed content", function() + local s = "\27[1m\27[31mbold red\27[0m and \27[32mgreen\27[0m" + assert.are.equal("bold red and green", utils.strip_ansi(s)) + end) + + end) + end) diff --git a/src/terminal/utils.lua b/src/terminal/utils.lua index 3a58996e..b9bcd196 100644 --- a/src/terminal/utils.lua +++ b/src/terminal/utils.lua @@ -357,6 +357,25 @@ end +--- Strips all ANSI escape sequences from a string. +-- Removes CSI (e.g. colors, cursor movement, SGR), OSC, DCS, SOS, PM, APC, +-- two-byte sequences, and C1 control codes. Use when you need plain text +-- without terminal control sequences. +-- @tparam string str The string that may contain ANSI sequences. +-- @treturn string The string with all ANSI sequences removed. +-- @usage +-- strip_ansi("\27[31mred\27[0m") -- "red" +-- @within Text +function M.strip_ansi(str) + return str -- Note: order is important here + :gsub("\27%].-\7", "") -- OSC (terminated by BEL) + :gsub("\27%].-\27%\\", "") -- OSC (terminated by ST) + :gsub("\27[P^X_].-\27%\\", "") -- DCS, SOS, PM, APC + :gsub("\27%[[0-?]*[ -/]*[@-~]", "") -- CSI + :gsub("\155[0-?]*[ -/]*[@-~]", "") -- C1 CSI + :gsub("\27[@-_]", "") -- two-byte sequences +end + --- Truncates text to fit within a given width, adding ellipsis as needed.