summaryrefslogtreecommitdiff
path: root/xmake/modules/utils/progress.lua
blob: 0da5b4cbba85f5f47358ce7694712659ea51dcb3 (plain)
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
--!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.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" } }

-- stop the progress indicator, clear written frames
function progress:stop()
    if self._RUNNING ~= 0 then
        self:clear()
        self._RUNNING = 0
        self._INDEX = 0
    end
end

function progress:_clear()
    if self._RUNNING == 1 then
        tty.erase_line_to_end()
        self._RUNNING = 2
        return true
    end
end

-- clear previous frame of the progress indicator
function progress:clear()
    if self:_clear() then
        self._STREAM:flush()
    end
end

-- write next frame of the progress indicator
function progress:write()
    local chars = self._OPT.chars[self._INDEX % #self._OPT.chars + 1]
    tty.cursor_and_attrs_save()
    self._STREAM:write(chars)
    self._STREAM:flush()
    tty.cursor_and_attrs_restore()
    self._INDEX = self._INDEX + 1
    self._RUNNING = 1
end

-- check if the progress indicator is running
function progress:running()
    return self._RUNNING and true or false
end

-- 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

-- strip progress line
function _strip_progress_line(msg)
    local msg_plain = colors.translate(msg, {plain = true})
    local maxwidth = os.getwinsize().width
    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, ...)
    local progress_prefix = "${color.build.progress}" .. theme.get("text.build.progress_format") .. ":${clear} "
    cprint(progress_prefix .. "${dim}" .. format, progress, ...)
end

-- show progress with scroll
function _show_progress_with_scroll(progress, format, ...)
    local progress_prefix = "${color.build.progress}" .. theme.get("text.build.progress_format") .. ":${clear} "
    cprint(progress_prefix .. format, progress, ...)
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 progress line
    local is_first = false
    local is_finished = math.floor(progress) == 100
    local progress_prefix = "${color.build.progress}" .. theme.get("text.build.progress_format") .. ":${clear} "
    local progress_msg = vformat(format, ...)
    local progress_line = _strip_progress_line(vformat(progress_prefix, progress) .. progress_msg)
    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
    local maxwidth = os.getwinsize().width
    if not is_first and linecount > 0 then
        tty.cursor_move_to_col(maxwidth)
        tty.cursor_move_up(linecount + 1)
    end
    tty.erase_line_to_start().cr()
    cprint(progress_line)

    -- update the current progress info
    local current_time = os.mclock()
    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

    -- sort the progress lines by the spent time
    local order_lineinfos = {}
    for _, progress_lineinfo in pairs(progress_lineinfos) do
        local progress_msg = progress_lineinfo.progress_msg
        if progress_msg then
            local timecolor = ""
            local spent_time = current_time - progress_lineinfo.start_time
            if spent_time > 1000 then
                timecolor = "${color.build.progress_veryslow}"
            elseif spent_time > 500 then
                timecolor = "${color.build.progress_slow}"
            end
            progress_lineinfo.spent_time = spent_time
            local subprogress_line = _strip_progress_line(vformat("  > %s%0.02fs${clear} ", timecolor, spent_time / 1000) .. progress_msg)
            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)
    current_lineinfo.start_time = current_time

    -- show the subprocess lines
    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
        tty.cursor_move_to_col(maxwidth)
        tty.erase_line_to_start().cr()
        if lineinfo.progress_line then
            cprint(lineinfo.progress_line)
        else
            print("")
        end
        linecount = linecount + 1
    end

    if is_finished then
        _g.is_refreshing = false
        _g.progress_lineinfos = nil
        _g.linecount = 0
        tty.cursor_show()
    else
        _g.is_refreshing = true
        _g.linecount = linecount
    end
    io.flush()
end

-- show progress with single-row refresh (ninja style)
function _show_progress_with_singlerow_refresh(progress, format, ...)
    local is_finished = math.floor(progress) == 100
    local progress_prefix = "${color.build.progress}" .. theme.get("text.build.progress_format") .. ":${clear} "
    tty.erase_line_to_start().cr()
    cprintf(progress_prefix .. format, progress, ...)
    if is_finished then
        print("")
        _g.is_refreshing = false
    else
        _g.is_refreshing = true
    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
        _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, ...)
    if not _g.is_refreshing then
        return
    end

    print("")
    cprint(format, ...)
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} "
    if option.get("verbose") then
        return string.format(progress_prefix .. "${dim}" .. format, progress, ...)
    else
        return string.format(progress_prefix .. format, progress, ...)
    end
end

-- build a progress indicator
-- @params stream - stream to write to, will use io.stdout if not provided
-- @params opt - options
--               - chars - an array of chars for progress indicator
function new(stream, opt)
    stream = stream or io.stdout
    opt = opt or {}
    if opt.chars == nil or #opt.chars == 0 then
        opt.chars = theme.get("text.spinner.chars")
    end
    return progress {_OPT = opt, _STREAM = stream, _RUNNING = 0, _INDEX = 0}
end