summaryrefslogtreecommitdiff
path: root/xmake/modules/utils/progress.lua
diff options
context:
space:
mode:
authorruki <[email protected]>2025-10-30 17:48:35 +0800
committerGitHub <[email protected]>2025-10-30 17:48:35 +0800
commitfe201a2122485acc09e5c5bda75cdb14a7d4d50e (patch)
treefe8a6eabbec8412f135b00fe3b7c3e1ec12d21ad /xmake/modules/utils/progress.lua
parentca24c32ea55d64c99d8cb911e03a02272b9350c4 (diff)
parent1a6b82357bb1ba437984fc46c77aa812873a169c (diff)
Merge pull request #6974 from xmake-io/progress
Support for multi-row refresh progress output
Diffstat (limited to 'xmake/modules/utils/progress.lua')
-rw-r--r--xmake/modules/utils/progress.lua332
1 files changed, 297 insertions, 35 deletions
diff --git a/xmake/modules/utils/progress.lua b/xmake/modules/utils/progress.lua
index 264ab3bfd..a29d7cd2b 100644
--- a/xmake/modules/utils/progress.lua
+++ b/xmake/modules/utils/progress.lua
@@ -23,11 +23,17 @@ import("core.base.option")
import("core.base.object")
import("core.base.colors")
import("core.base.tty")
+import("core.base.scheduler")
import("core.theme.theme")
-- define module
local progress = progress or object { _init = { "_RUNNING", "_INDEX", "_STREAM", "_OPT" } }
+-- cache color strings
+local COLOR_SUPERSLOW = "${color.build.progress_superslow}"
+local COLOR_VERYSLOW = "${color.build.progress_veryslow}"
+local COLOR_SLOW = "${color.build.progress_slow}"
+
-- stop the progress indicator, clear written frames
function progress:stop()
if self._RUNNING ~= 0 then
@@ -68,54 +74,312 @@ function progress:running()
return self._RUNNING and true or false
end
--- showing progress line without scroll?
-function showing_without_scroll()
- return _g.showing_without_scroll
+-- is scroll output?
+function _is_scroll()
+ local is_scroll = _g.is_scroll
+ if is_scroll == nil then
+ local style = theme.get("text.build.progress_style") or "scroll"
+ if style == "scroll" then
+ is_scroll = true
+ end
+ _g.is_scroll = is_scroll
+ end
+ return is_scroll
+end
+
+-- is multi-row refresh output?
+function _is_multirow_refresh()
+ local is_multirow_refresh = _g.is_multirow_refresh
+ if is_multirow_refresh == nil then
+ local style = theme.get("text.build.progress_style")
+ if style == "multirow_refresh" and tty.has_vtansi() and io.isatty() then
+ is_multirow_refresh = true
+ end
+ _g.is_multirow_refresh = is_multirow_refresh
+ end
+ return is_multirow_refresh
+end
+
+-- is single-row refresh output?
+function _is_singlerow_refresh()
+ local is_singlerow_refresh = _g.is_singlerow_refresh
+ if is_singlerow_refresh == nil then
+ local style = theme.get("text.build.progress_style")
+ if style == "singlerow_refresh" and tty.has_vtansi() and io.isatty() then
+ is_singlerow_refresh = true
+ end
+ _g.is_singlerow_refresh = is_singlerow_refresh
+ end
+ return is_singlerow_refresh
+end
+
+-- get progress prefix
+function _get_progress_prefix()
+ if not _g.progress_prefix then
+ _g.progress_prefix = "${color.build.progress}" .. theme.get("text.build.progress_format") .. ":${clear} "
+ end
+ return _g.progress_prefix
+end
+
+-- strip progress line
+function _strip_progress_line(msg, maxwidth)
+ local msg_plain = colors.translate(msg, {plain = true})
+ if #msg_plain > maxwidth then
+ -- windows width is too small? strip the partial message in middle
+ local partlen = math.floor(maxwidth / 2) - 3
+ local sep = msg_plain:sub(partlen + 1, #msg_plain - partlen - 1)
+ local split = msg:split(sep, {plain = true, strict = true})
+ msg = table.concat(split, "...")
+ end
+ return msg
+end
+
+-- show progress with verbose information
+function _show_progress_with_verbose(progress, format, ...)
+ cprint(_get_progress_prefix() .. "${dim}" .. format, progress, ...)
+end
+
+-- show progress with scroll
+function _show_progress_with_scroll(progress, format, ...)
+ cprint(_get_progress_prefix() .. format, progress, ...)
+end
+
+-- build ordered subprocess line infos from progress_lineinfos (internal helper)
+function _build_ordered_subprocess_lineinfos(maxwidth, current_time)
+ local progress_lineinfos = _g.progress_lineinfos
+ if not progress_lineinfos then
+ return {}
+ end
+
+ local order_lineinfos = {}
+ for _, progress_lineinfo in pairs(progress_lineinfos) do
+ local progress_msg = progress_lineinfo.progress_msg
+ if progress_msg then
+ local spent_time = current_time - progress_lineinfo.start_time
+ local time_seconds = spent_time / 1000
+
+ -- determine color based on time (use cached color constants)
+ local timecolor
+ if spent_time > 30000 then
+ timecolor = COLOR_SUPERSLOW
+ elseif spent_time > 1000 then
+ timecolor = COLOR_VERYSLOW
+ elseif spent_time > 500 then
+ timecolor = COLOR_SLOW
+ else
+ timecolor = ""
+ end
+
+ progress_lineinfo.spent_time = spent_time
+ -- use string.format instead of vformat for better performance
+ local time_str = string.format("%s%0.02fs${clear} ", timecolor, time_seconds)
+ local subprogress_line = _strip_progress_line(" > " .. time_str .. progress_msg, maxwidth)
+ progress_lineinfo.progress_line = subprogress_line
+ table.insert(order_lineinfos, progress_lineinfo)
+ else
+ progress_lineinfo.spent_time = 0
+ progress_lineinfo.progress_line = nil
+ end
+ end
+
+ table.sort(order_lineinfos, function (a, b) return a.spent_time > b.spent_time end)
+ return order_lineinfos
+end
+
+-- display subprocess progress lines (internal helper)
+function _display_subprocess_lines(order_lineinfos)
+ local linecount = 0
+ for _, lineinfo in ipairs(order_lineinfos) do
+ -- we need not show it if the progress job is idle in runjobs now
+ local progress_running = lineinfo.running
+ if progress_running and progress_running:data("runjobs.running") == false then
+ lineinfo.progress_line = nil
+ end
+ if lineinfo.progress_line then
+ tty.erase_line().cr()
+ cprint(lineinfo.progress_line)
+ linecount = linecount + 1
+ end
+ end
+ _g.linecount = linecount
+end
+
+-- redraw the multirow progress area (internal helper)
+function _redraw_multirow_progress(maxwidth)
+ local last_total_progress = _g.last_total_progress
+ if not last_total_progress then
+ return
+ end
+
+ -- redraw the total progress line
+ tty.erase_line().cr()
+ cprint(last_total_progress)
+
+ -- build and display the subprocess lines
+ local current_time = os.mclock()
+ local order_lineinfos = _build_ordered_subprocess_lineinfos(maxwidth, current_time)
+ _display_subprocess_lines(order_lineinfos)
+ io.flush()
+end
+
+-- show progress with multi-row refresh
+-- @see https://github.com/xmake-io/xmake/issues/6805
+function _show_progress_with_multirow_refresh(progress, format, ...)
+ local running = scheduler.co_running()
+ if not running then
+ _show_progress_with_scroll(progress, format, ...)
+ return
+ end
+
+ -- get window size and time once
+ local maxwidth = os.getwinsize().width
+ local current_time = os.mclock()
+
+ -- get progress line
+ local is_first = false
+ local is_finished = math.floor(progress) == 100
+ local progress_msg = vformat(format, ...)
+ local progress_line = _strip_progress_line(string.format(_get_progress_prefix(), progress) .. progress_msg, maxwidth)
+
+ local progress_lineinfos = _g.progress_lineinfos
+ if progress_lineinfos == nil then
+ progress_lineinfos = {}
+ _g.progress_lineinfos = progress_lineinfos
+ is_first = true
+ end
+ if is_first then
+ tty.cursor_hide()
+ os.atexit(function (ok, errors)
+ tty.cursor_show()
+ end)
+ end
+
+ -- show the total progress line
+ local linecount = _g.linecount or 0
+ if not is_first and linecount > 0 then
+ tty.cursor_move_up(linecount + 1)
+ end
+ tty.erase_line().cr()
+ cprint(progress_line)
+
+ -- save the total progress line and progress value for potential redraw in show_output
+ _g.last_total_progress = progress_line
+ _g.last_total_progress_value = progress
+
+ -- update the current progress info
+ local current_lineinfo = progress_lineinfos[running]
+ if current_lineinfo == nil then
+ current_lineinfo = {start_time = current_time, spent_time = 0, running = running}
+ progress_lineinfos[running] = current_lineinfo
+ end
+ if is_finished then
+ current_lineinfo.progress_msg = nil
+ else
+ current_lineinfo.progress_msg = progress_msg
+ end
+
+ -- build and display the subprocess lines
+ if not is_finished then
+ local order_lineinfos = _build_ordered_subprocess_lineinfos(maxwidth, current_time)
+ current_lineinfo.start_time = current_time
+ _display_subprocess_lines(order_lineinfos)
+ _g.refresh_mode = "multirow"
+ else
+ -- when finished, clear all subprocess lines without leaving empty lines
+ local old_linecount = _g.linecount or 0
+ if old_linecount > 0 then
+ tty.erase_down()
+ end
+ _g.refresh_mode = nil
+ _g.progress_lineinfos = nil
+ _g.last_total_progress = nil
+ _g.last_total_progress_value = nil
+ _g.linecount = 0
+ tty.cursor_show()
+ end
+ io.flush()
+end
+
+-- show progress with single-row refresh (ninja style)
+function _show_progress_with_singlerow_refresh(progress, format, ...)
+ local maxwidth = os.getwinsize().width
+ local is_finished = math.floor(progress) == 100
+ local progress_msg = vformat(format, ...)
+ local progress_line = _strip_progress_line(string.format(_get_progress_prefix(), progress) .. progress_msg, maxwidth)
+ tty.erase_line().cr()
+ cprintf(progress_line)
+ if is_finished then
+ print("")
+ _g.refresh_mode = nil
+ else
+ _g.refresh_mode = "singlerow"
+ end
+ io.flush()
end
-- show the message with progress
function show(progress, format, ...)
progress = type(progress) == "table" and progress:percent() or math.floor(progress)
- local progress_prefix = "${color.build.progress}" .. theme.get("text.build.progress_format") .. ":${clear} "
if option.get("verbose") then
- cprint(progress_prefix .. "${dim}" .. format, progress, ...)
+ _show_progress_with_verbose(progress, format, ...)
+ elseif _is_scroll() then
+ _show_progress_with_scroll(progress, format, ...)
+ elseif _is_multirow_refresh() then
+ _show_progress_with_multirow_refresh(progress, format, ...)
+ elseif _is_singlerow_refresh() then
+ _show_progress_with_singlerow_refresh(progress, format, ...)
else
- local is_scroll = _g.is_scroll
- if is_scroll == nil then
- is_scroll = theme.get("text.build.progress_style") == "scroll"
- _g.is_scroll = is_scroll
- end
- if is_scroll then
- cprint(progress_prefix .. format, progress, ...)
- else
- tty.erase_line_to_start().cr()
- local msg = vformat(progress_prefix .. format, progress, ...)
- local msg_plain = colors.translate(msg, {plain = true})
- local maxwidth = os.getwinsize().width
- if #msg_plain <= maxwidth then
- cprintf(msg)
- else
- -- windows width is too small? strip the partial message in middle
- local partlen = math.floor(maxwidth / 2) - 3
- local sep = msg_plain:sub(partlen + 1, #msg_plain - partlen - 1)
- local split = msg:split(sep, {plain = true, strict = true})
- cprintf(table.concat(split, "..."))
- end
- if math.floor(progress) == 100 then
- print("")
- _g.showing_without_scroll = false
- else
- _g.showing_without_scroll = true
+ _show_progress_with_scroll(progress, format, ...)
+ end
+end
+
+-- print additional output logs with colors outside the progress log area, such as warning logs.
+-- it's used when the progress style is multirow/singlerow refresh.
+function show_output(format, ...)
+ local refresh_mode = _g.refresh_mode
+ if refresh_mode == "singlerow" then
+ print("")
+ cprint(format, ...)
+ elseif refresh_mode == "multirow" then
+ -- get window size once and the number of fixed progress lines at the bottom
+ -- +1 for the total progress line
+ local maxwidth = os.getwinsize().width
+ local linecount = (_g.linecount or 0) + 1
+
+ -- move to the top of progress area and clear to bottom
+ tty.cursor_move_up(linecount)
+ tty.erase_down()
+ tty.cr()
+
+ -- show the current task's progress line before the log output
+ local progress_lineinfos = _g.progress_lineinfos
+ if progress_lineinfos then
+ local running = scheduler.co_running()
+ if running then
+ local current_lineinfo = progress_lineinfos[running]
+ if current_lineinfo and current_lineinfo.progress_msg then
+ local progress_value = _g.last_total_progress_value or 0
+ local progress_line = _strip_progress_line(string.format(_get_progress_prefix(), math.floor(progress_value)) .. current_lineinfo.progress_msg, maxwidth)
+ tty.erase_line_to_end()
+ cprint(progress_line)
+ end
end
- io.flush()
end
+
+ -- print the log output, which will scroll naturally
+ cprint(format, ...)
+
+ -- redraw the progress area immediately
+ _redraw_multirow_progress(maxwidth)
+ else
+ cprint(format, ...)
end
end
-- get the message text with progress
function text(progress, format, ...)
progress = type(progress) == "table" and progress:percent() or math.floor(progress)
- local progress_prefix = "${color.build.progress}" .. theme.get("text.build.progress_format") .. ":${clear} "
+ local progress_prefix = _get_progress_prefix()
if option.get("verbose") then
return string.format(progress_prefix .. "${dim}" .. format, progress, ...)
else
@@ -128,8 +392,6 @@ end
-- @params opt - options
-- - chars - an array of chars for progress indicator
function new(stream, opt)
-
- -- set default values
stream = stream or io.stdout
opt = opt or {}
if opt.chars == nil or #opt.chars == 0 then