Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ The scope of what is covered by the version number excludes:

### Version X.Y.Z, unreleased

- a fix
- a change
- feat(progress): strip ANSI sequences for sprite width calculations.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no changelog needed for now, since there is no release yet

Spinner frames and done sprites can now include color/style sequences
without breaking cursor rewind behavior.

### Version 0.1.0, released 01-Jan-2022

Expand Down
14 changes: 13 additions & 1 deletion spec/12-draw_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,18 @@ describe("terminal.draw", function()
assert.are.equal("…llo 测试!", result)
end)


it("preserves ANSI codes in title when no truncation is needed", function()
local result = line.title_seq(10, "\27[31mTest\27[0m")
assert.are.equal("───\27[31mTest\27[0m───", result)
end)


it("strips ANSI codes when title truncation is needed", function()
local result = line.title_seq(8, "\27[31mVeryLongTitle\27[0m")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a nasty edgecase, hadn't thought about it. This needs a solution I think.

assert.are.equal("VeryLon…", result)
end)

end)


Expand Down Expand Up @@ -191,4 +203,4 @@ describe("terminal.draw", function()

end)

end)
end)
87 changes: 87 additions & 0 deletions spec/13-progress_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
local helpers = require "spec.helpers"


describe("terminal.progress", function()

local terminal
local progress

setup(function()
terminal = helpers.load()
progress = require("terminal.progress")
end)


teardown(function()
progress = nil
terminal = nil
helpers.unload()
end)



describe("spinner()", function()

before_each(function()
helpers.clear_output()
end)



it("uses visible width for ANSI-styled single-width sprites", function()
local spinner = progress.spinner({
sprites = {
[0] = "",
"\27[31mX\27[0m",
},
stepsize = 10,
})

spinner(false)

assert.are.equal(
"\27[31mX\27[0m" .. terminal.cursor.position.left_seq(1),
helpers.get_output()
)
end)


it("uses visible width for ANSI-styled double-width sprites", function()
local spinner = progress.spinner({
sprites = {
[0] = "",
"\27[31m界\27[0m",
},
stepsize = 10,
})

spinner(false)

assert.are.equal(
"\27[31m界\27[0m" .. terminal.cursor.position.left_seq(2),
helpers.get_output()
)
end)


it("uses visible width for ANSI-styled done_sprite", function()
local spinner = progress.spinner({
sprites = {
[0] = "x",
"x",
},
done_sprite = "\27[32mOK\27[0m",
stepsize = 10,
})

spinner(true)

assert.are.equal(
"\27[32mOK\27[0m" .. terminal.cursor.position.left_seq(2),
helpers.get_output()
)
end)

end)

end)
19 changes: 15 additions & 4 deletions src/terminal/draw/line.lua
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ end
--- Creates a sequence to draw a horizontal line with a title centered in it without writing it to the terminal.
-- Line is drawn left to right. If the width is too small for the title, the title is truncated.
-- If less than 4 characters are available for the title, the title is omitted altogether.
-- ANSI escape sequences in title/prefix/postfix are ignored for width calculations.
-- If truncation is needed, the rendered title uses plain text.
-- @tparam number width the total width of the line in columns
-- @tparam[opt=""] string title the title to draw (if empty or nil, only the line is drawn)
-- @tparam[opt="─"] string char the line-character to use
Expand All @@ -92,11 +94,20 @@ function M.title_seq(width, title, char, pre, post, type, title_attr)

pre = pre or ""
post = post or ""
local pre_w = text.width.utf8swidth(pre)
local post_w = text.width.utf8swidth(post)
local pre_w = text.width.utf8swidth(utils.strip_ansi(pre))
local post_w = text.width.utf8swidth(utils.strip_ansi(post))
local w_for_title = width - pre_w - post_w

local title, title_w = utils.truncate_ellipsis(w_for_title, title, type)
local stripped_title = utils.strip_ansi(title)
local stripped_title_w = text.width.utf8swidth(stripped_title)
local title_w
if stripped_title_w <= w_for_title then
title_w = stripped_title_w
else
stripped_title, title_w = utils.truncate_ellipsis(w_for_title, stripped_title, type)
title = stripped_title
end

if title_w == 0 then
return M.horizontal_seq(width, char)
end
Expand Down Expand Up @@ -139,4 +150,4 @@ end



return M
return M
15 changes: 12 additions & 3 deletions src/terminal/progress.lua
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ local gettime = require("system").gettime



local function visible_width(str)
return tw.utf8swidth(utils.strip_ansi(str))
end



--- table with predefined sprites for progress spinners.
-- The sprites are tables of strings, where each string is a frame in the spinner animation.
-- The frame at index 0 is optional and is the "done" message, the rest are the animation frames.
Expand Down Expand Up @@ -71,6 +77,8 @@ end
-- If `row` and `col` are given then terminal memory is used to (re)store the cursor position. If they are not given
-- then the spinner will be printed at the current cursor position, and the cursor will return to the same position
-- after each update.
-- ANSI escape sequences in sprites are ignored for width calculations, allowing
-- styled/colorized sprite frames.
-- @tparam table opts a table of options;
-- @tparam table opts.sprites a table of strings to display, one at a time, overwriting the previous one. Index 0 is the "done" message.
-- See `sprites` for a table of predefined sprites.
Expand Down Expand Up @@ -118,10 +126,11 @@ function M.spinner(opts)
if i == 0 then
s = opts.done_sprite or s
end
local w = visible_width(s)
local sequence = Sequence()
sequence[#sequence+1] = pos_set
sequence[#sequence+1] = (i == 0 and attr_push_done) or attr_push or nil
sequence[#sequence+1] = s .. t.cursor.position.left_seq(t.text.width.utf8swidth(s))
sequence[#sequence+1] = s .. t.cursor.position.left_seq(w)
sequence[#sequence+1] = attr_pop
sequence[#sequence+1] = pos_restore
steps[i] = sequence
Expand Down Expand Up @@ -168,7 +177,7 @@ function M.ticker(text, width, text_done)
local max_len = 0
for i = 1, lengths[0] do
result[i] = utils.utf8sub(base, i, i + width - 1)
lengths[i] = tw.utf8swidth(result[i])
lengths[i] = visible_width(result[i])
max_len = math.max(max_len, lengths[i])
end
result[0] = utils.utf8sub(result[0], 1, max_len)
Expand All @@ -185,4 +194,4 @@ end



return M
return M