Description
The CLI::UI::Truncater correctly skips CSI sequences (\x1b[...) when calculating printable width, but it doesn't handle OSC sequences (\x1b]...). This causes strings containing hyperlinks to be truncated much more aggressively than expected, since the URL inside the hyperlink escape codes is counted as printable characters.
Reproduction
require 'cli/ui'
# Create a hyperlink
url = "https://github.com/Shopify/shopify/pull/12345"
link = CLI::UI.link(url, "#12345") # displays as "#12345" but links to full URL
puts "Link string: #{link.inspect}"
puts "Printing width (ANSI.printing_width): #{CLI::UI::ANSI.printing_width(link)}"
# Try to truncate to 20 chars - should be plenty for "#12345"
truncated = CLI::UI::Truncater.call(link, 20)
puts "Truncated to 20: #{truncated.inspect}"
Expected behavior
ANSI.printing_width correctly returns 6 (the length of #12345)
Truncater.call(link, 20) should return the original string unchanged since #12345 is only 6 characters
Actual behavior
ANSI.printing_width correctly returns 6 ✓
Truncater.call(link, 20) truncates the string because it counts 8;;https://github.com/Shopify/shopify/pull/12345 (the URL inside the OSC sequence) as printable characters
Root cause
In lib/cli/ui/truncater.rb, the escape sequence parser only handles CSI sequences:
when PARSE_ESC
mode = case cp
when LEFT_SQUARE_BRACKET # 0x5b = '['
PARSE_ANSI
else
PARSE_ROOT # <-- OSC sequences start with ']' (0x5d), so they fall through here
end
OSC sequences have the format \x1b]...ST where ST is either \x07 (BEL) or \x1b\ (ESC + backslash). Since the Truncater doesn't recognize ] after ESC, it returns to PARSE_ROOT and counts the OSC content as visible characters.
Impact
This affects any use of CLI::UI::SpinGroup (spinners) with hyperlinks in titles. The spinner uses Truncater to fit titles to terminal width, causing hyperlink titles to be truncated far more than necessary.
Example output with a hyperlink in a spinner title:
┃ ⚠ https://github.com/Shop…
Even though there's plenty of terminal width remaining, the title is truncated because the hidden URL is counted.
Suggested fix
Add OSC sequence handling to the Truncater, similar to how ANSI.strip_codes already handles it:
RIGHT_SQUARE_BRACKET = 0x5d # ]
BACKSLASH = 0x5c # \
BEL = 0x07
# In the parser:
when PARSE_ESC
mode = case cp
when LEFT_SQUARE_BRACKET
PARSE_ANSI
when RIGHT_SQUARE_BRACKET
PARSE_OSC # New state for OSC sequences
else
PARSE_ROOT
end
when PARSE_OSC
# OSC sequences end with BEL or ESC+backslash
case cp
when BEL
mode = PARSE_ROOT
when ESC
mode = PARSE_OSC_END
end
when PARSE_OSC_END
# Expecting backslash to complete ST
mode = PARSE_ROOT
Description
The
CLI::UI::Truncatercorrectly skips CSI sequences (\x1b[...) when calculating printable width, but it doesn't handle OSC sequences (\x1b]...). This causes strings containing hyperlinks to be truncated much more aggressively than expected, since the URL inside the hyperlink escape codes is counted as printable characters.Reproduction
Expected behavior
ANSI.printing_widthcorrectly returns6(the length of#12345)Truncater.call(link, 20)should return the original string unchanged since#12345is only 6 charactersActual behavior
ANSI.printing_widthcorrectly returns6✓Truncater.call(link, 20)truncates the string because it counts8;;https://github.com/Shopify/shopify/pull/12345(the URL inside the OSC sequence) as printable charactersRoot cause
In
lib/cli/ui/truncater.rb, the escape sequence parser only handles CSI sequences:OSC sequences have the format
\x1b]...STwhere ST is either\x07(BEL) or\x1b\(ESC + backslash). Since the Truncater doesn't recognize]after ESC, it returns toPARSE_ROOTand counts the OSC content as visible characters.Impact
This affects any use of
CLI::UI::SpinGroup(spinners) with hyperlinks in titles. The spinner usesTruncaterto fit titles to terminal width, causing hyperlink titles to be truncated far more than necessary.Example output with a hyperlink in a spinner title:
Even though there's plenty of terminal width remaining, the title is truncated because the hidden URL is counted.
Suggested fix
Add OSC sequence handling to the Truncater, similar to how
ANSI.strip_codesalready handles it: