1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
|
--!A cross-platform build utility based on Lua
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015-present, Xmake Open Source Community.
--
-- @author OpportunityLiu
-- @file progress.lua
--
-- imports
import("core.base.option")
import("core.base.colors")
import("core.base.tty")
import("core.base.scheduler")
import("core.theme.theme")
import("core.project.project")
-- cache color strings
local COLOR_SUPERSLOW = "${color.build.progress_superslow}"
local COLOR_VERYSLOW = "${color.build.progress_veryslow}"
local COLOR_SLOW = "${color.build.progress_slow}"
-- is scroll output?
function _is_scroll()
-- if style is forced, use it
if _g.forced_style then
return _g.forced_style == "scroll"
end
local is_scroll = _g.is_scroll
if is_scroll == nil then
local style = project.policy("build.progress_style") or 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()
-- if style is forced, use it
if _g.forced_style then
return _g.forced_style == "multirow"
end
local is_multirow_refresh = _g.is_multirow_refresh
if is_multirow_refresh == nil then
local style = project.policy("build.progress_style") or theme.get("text.build.progress_style")
if style == "multirow" 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()
-- if style is forced, use it
if _g.forced_style then
return _g.forced_style == "singlerow"
end
local is_singlerow_refresh = _g.is_singlerow_refresh
if is_singlerow_refresh == nil then
local style = project.policy("build.progress_style") or theme.get("text.build.progress_style")
if style == "singlerow" 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
-- set progress style (temporarily override the current style)
-- @param style "scroll", "singlerow", or "multirow"
function set_style(style)
-- save the original style if not already saved
if not _g.saved_style then
-- get current effective style
if _is_multirow_refresh() then
_g.saved_style = "multirow"
elseif _is_singlerow_refresh() then
_g.saved_style = "singlerow"
else
_g.saved_style = "scroll"
end
end
-- set forced style
_g.forced_style = style
-- clear cached flags to force recalculation
_g.is_scroll = nil
_g.is_multirow_refresh = nil
_g.is_singlerow_refresh = nil
end
-- restore progress style (restore the original style from project policy)
function restore_style()
_g.forced_style = nil
_g.saved_style = nil
-- clear cached flags to force recalculation
_g.is_scroll = nil
_g.is_multirow_refresh = nil
_g.is_singlerow_refresh = nil
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
-- clear the left lines
local left_linecount = #order_lineinfos - linecount
if left_linecount > 0 then
for i = 1, left_linecount do
tty.erase_line().cr()
print("")
end
tty.cursor_move_up(left_linecount)
end
_g.linecount = linecount
end
-- redraw the multirow progress area (internal helper)
-- @param maxwidth: window width
-- @param current_time: optional current time (to avoid repeated os.mclock() calls)
function _redraw_multirow_progress(maxwidth, current_time)
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
if not current_time then
current_time = os.mclock()
end
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
_g.last_show_time = current_time
-- update the current progress info
local current_lineinfo = progress_lineinfos[running]
if current_lineinfo == nil then
current_lineinfo = {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
current_lineinfo.start_time = current_time
end
-- build and display the subprocess lines
if not is_finished then
local order_lineinfos = _build_ordered_subprocess_lineinfos(maxwidth, 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 current coroutine's progress line (internal helper for multirow mode)
function _show_current_coroutine_progress(maxwidth)
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
end
end
-- show the message with progress
function show(progress, format, ...)
progress = type(progress) == "table" and progress:percent() or math.floor(progress)
if option.get("verbose") then
_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
_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
_show_current_coroutine_progress(maxwidth)
-- print the log output, which will scroll naturally
cprint(format, ...)
-- redraw the progress area immediately
_redraw_multirow_progress(maxwidth)
else
cprint(format, ...)
end
end
-- check if multirow refresh mode is enabled
function is_multirow()
return _is_multirow_refresh()
end
-- refresh the multirow progress display to update elapsed time
-- this is useful for long-running tasks to keep the elapsed time updated
function refresh()
local refresh_mode = _g.refresh_mode
if refresh_mode == "multirow" then
-- get current time once and reuse it
local current_time = os.mclock()
-- only refresh if more than 500ms has passed since last show
-- this avoids too frequent refreshes
local last_show_time = _g.last_show_time
if last_show_time then
local elapsed = current_time - last_show_time
if elapsed <= 500 then
return
end
end
-- move cursor back to the top of progress area to avoid scrolling
local linecount = _g.linecount or 0
if linecount > 0 then
tty.cursor_move_up(linecount + 1)
end
-- redraw the progress area immediately, passing current_time to avoid repeated calls
local maxwidth = os.getwinsize().width
_redraw_multirow_progress(maxwidth, current_time)
end
end
-- abort the progress display mode, used for error exit or early termination
-- this function cleans up the progress display area and restores the terminal state
function show_abort()
local refresh_mode = _g.refresh_mode
if refresh_mode == "multirow" then
-- move to the top of progress area and clear
local linecount = (_g.linecount or 0) + 1
if linecount > 0 then
tty.cursor_move_up(linecount)
tty.erase_down()
tty.cr()
end
-- show the current coroutine's progress line before abort
local maxwidth = os.getwinsize().width
_show_current_coroutine_progress(maxwidth)
tty.cursor_show()
-- reset all global states
_g.refresh_mode = nil
_g.progress_lineinfos = nil
_g.last_total_progress = nil
_g.last_total_progress_value = nil
_g.linecount = 0
io.flush()
elseif refresh_mode == "singlerow" then
-- clear the current progress line and move to next line
print("")
_g.refresh_mode = nil
io.flush()
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 = _get_progress_prefix()
if option.get("verbose") then
return string.format(progress_prefix .. "${dim}" .. format, progress, ...)
else
return string.format(progress_prefix .. format, progress, ...)
end
end
|