From b80c354e8b9532d7deec463625f129d106f8f51c Mon Sep 17 00:00:00 2001 From: Takafumi ONAKA Date: Sun, 9 Nov 2025 14:06:21 +0900 Subject: [PATCH 1/5] Fix ANSI carry-over indexes when wrapping Fixes an IndexError raised when an ANSI code starts at the end of a wrapped line and carries over to a shorter next line. The carry-over kept the original column index, so insert_ansi tried to insert beyond the new string length. Normalize carried-over ANSI entries to [code, 0] so they always reapply at the beginning of the next line. --- lib/strings/wrap.rb | 2 +- spec/unit/wrap/insert_ansi_spec.rb | 2 +- spec/unit/wrap/wrap_spec.rb | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/strings/wrap.rb b/lib/strings/wrap.rb index 7dbda07..0e204fe 100644 --- a/lib/strings/wrap.rb +++ b/lib/strings/wrap.rb @@ -146,7 +146,7 @@ def insert_ansi(string, ansi_stack = []) next elsif !matched_reset # ansi without reset matched_reset = false - new_stack << ansi # keep the ansi + new_stack.unshift([ansi[0], 0]) # carry over ANSI to the start of next line preserving order next if ansi[1] == length if output.end_with?(NEWLINE) output.insert(-2, ansi_reset) diff --git a/spec/unit/wrap/insert_ansi_spec.rb b/spec/unit/wrap/insert_ansi_spec.rb index 4497da3..15b84d2 100644 --- a/spec/unit/wrap/insert_ansi_spec.rb +++ b/spec/unit/wrap/insert_ansi_spec.rb @@ -60,6 +60,6 @@ val = Strings::Wrap.insert_ansi(text, stack) expect(val).to eq("\e[32mone\e[0m") - expect(stack).to eq([["\e[33m", 3]]) + expect(stack).to eq([["\e[33m", 0]]) end end diff --git a/spec/unit/wrap/wrap_spec.rb b/spec/unit/wrap/wrap_spec.rb index 1fdcc96..bd11584 100644 --- a/spec/unit/wrap/wrap_spec.rb +++ b/spec/unit/wrap/wrap_spec.rb @@ -178,6 +178,14 @@ ].join("\n")) end + it "wraps when ANSI start carries over to the next line" do + text = "aaaaaaa \e[31mbb" + expect(Strings::Wrap.wrap(text, 8)).to eq([ + "aaaaaaa ", + "\e[31mbb\e[0m" + ].join("\n")) + end + it "applies ANSI codes when below wrap width" do str = "\e[32mone\e[0m\e[33mtwo\e[0m" From 6db3f382a3fb593437199d0805cb5cf3f7061e4a Mon Sep 17 00:00:00 2001 From: Takafumi ONAKA Date: Sun, 9 Nov 2025 15:59:48 +0900 Subject: [PATCH 2/5] Add stacked ANSI wrap reapply order test --- spec/unit/wrap/wrap_spec.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spec/unit/wrap/wrap_spec.rb b/spec/unit/wrap/wrap_spec.rb index bd11584..fcf89e4 100644 --- a/spec/unit/wrap/wrap_spec.rb +++ b/spec/unit/wrap/wrap_spec.rb @@ -186,6 +186,15 @@ ].join("\n")) end + it "applies stacked ANSI colors after wrapping" do + text = "aaaa \e[31mbbbb \e[32mcc" + expect(Strings::Wrap.wrap(text, 5)).to eq([ + "aaaa ", + "\e[31mbbbb \e[0m", + "\e[31m\e[32mcc\e[0m\e[0m" + ].join("\n")) + end + it "applies ANSI codes when below wrap width" do str = "\e[32mone\e[0m\e[33mtwo\e[0m" From 803d27d9345df27f77a4c2dabb74f21875fd3e9c Mon Sep 17 00:00:00 2001 From: Takafumi ONAKA Date: Sun, 9 Nov 2025 17:01:28 +0900 Subject: [PATCH 3/5] Add CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d48391..ff817f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change log +## Unreleased + +### Fixed + +* Fix IndexError in `Strings::Wrap.wrap` and correct ANSI color insertion on wrap (@onk) + ## [v0.2.1] - 2021-03-09 ### Changed From 17ad3906fb58f68fd2fd677b1d967fc95c857add Mon Sep 17 00:00:00 2001 From: Takafumi ONAKA Date: Sun, 9 Nov 2025 17:15:53 +0900 Subject: [PATCH 4/5] Track pending ANSI reset codes to preserve correct carry-over behavior Previously a boolean flag (`matched_reset`) was used, which failed when multiple reset codes appeared. Replacing it with a counter (`pending_resets`) allows nested ANSI states to unwind correctly during line wrapping. --- CHANGELOG.md | 1 + lib/strings/wrap.rb | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff817f7..ecabdef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed * Fix IndexError in `Strings::Wrap.wrap` and correct ANSI color insertion on wrap (@onk) +* Track unclosed ANSI sequences to preserve correct color state across wraps (@onk) ## [v0.2.1] - 2021-03-09 diff --git a/lib/strings/wrap.rb b/lib/strings/wrap.rb index 0e204fe..c4003e4 100644 --- a/lib/strings/wrap.rb +++ b/lib/strings/wrap.rb @@ -135,17 +135,16 @@ def insert_ansi(string, ansi_stack = []) new_stack = [] output = string.dup length = string.size - matched_reset = false + pending_resets = 0 ansi_reset = Strings::ANSI::RESET # Reversed so that string index don't count ansi ansi_stack.reverse_each do |ansi| if ansi[0] =~ /#{Regexp.quote(ansi_reset)}/ - matched_reset = true + pending_resets += 1 output.insert(ansi[1], ansi_reset) next - elsif !matched_reset # ansi without reset - matched_reset = false + elsif pending_resets.zero? # ansi without reset new_stack.unshift([ansi[0], 0]) # carry over ANSI to the start of next line preserving order next if ansi[1] == length if output.end_with?(NEWLINE) @@ -153,6 +152,8 @@ def insert_ansi(string, ansi_stack = []) else output.insert(-1, ansi_reset) # add reset at the end end + else + pending_resets -= 1 end output.insert(ansi[1], ansi[0]) From 29c2d4320d86fbc565bd60c7dd81a8d0f82ced5c Mon Sep 17 00:00:00 2001 From: Takafumi ONAKA Date: Sun, 9 Nov 2025 17:18:12 +0900 Subject: [PATCH 5/5] Enable tests for ANSI wrap behavior that now pass with the corrected carry-over logic --- spec/unit/wrap/wrap_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/unit/wrap/wrap_spec.rb b/spec/unit/wrap/wrap_spec.rb index fcf89e4..8b9e4dd 100644 --- a/spec/unit/wrap/wrap_spec.rb +++ b/spec/unit/wrap/wrap_spec.rb @@ -211,7 +211,7 @@ expect(val).to eq("\e[32mone\e[0m\n\e[33mtwo\e[0m") end - xit "splits ANSI codes matching wrap width" do + it "splits ANSI codes matching wrap width" do str = "\e[32mone\e[0m\e[33mtwo\e[0m" val = Strings::Wrap.wrap(str, 3) @@ -219,14 +219,14 @@ expect(val).to eq("\e[32mone\e[0m\n\e[33mtwo\e[0m") end - xit "wraps deeply nested ANSI codes correctly" do + it "wraps deeply nested ANSI codes correctly" do str = "\e[32mone\e[33mtwo\e[0m\e[0m" val = Strings::Wrap.wrap(str, 3) expect(val).to eq([ "\e[32mone\e[0m", - "\e[33mtwo\e[0m", + "\e[32m\e[33mtwo\e[0m\e[0m", ].join("\n")) end end