libmain: add progress bar with multiple status lines

Add the log-formats `multiline` and `multiline-with-logs` which offer
multiple current active building status lines.

Change-Id: Idd8afe62f8591b5d8b70e258c5cefa09be4cab03
This commit is contained in:
kloenk 2024-06-01 16:06:26 +02:00
parent 8a3d063a49
commit fac5d78034
8 changed files with 132 additions and 38 deletions

View file

@ -60,6 +60,10 @@ jade:
forgejo: jade
github: lf-
kloenk:
forgejo: kloenk
github: kloenk
lovesegfault:
github: lovesegfault

View file

@ -0,0 +1,14 @@
---
synopsis: Add log formats `multiline` and `multiline-with-logs`
cls: [1369]
credits: [kloenk]
category: Improvements
---
Added two new log formats (`multiline` and `multiline-with-logs`) that display
current activities below each other for better visibility.
These formats attempt to use the maximum available lines
(defaulting to 25 if unable to determine) and print up to that many lines.
The status bar is displayed as the first line, with each subsequent
activity on its own line.

View file

@ -78,6 +78,15 @@ Most commands in Lix accept the following command-line options:
Display the raw logs, with the progress bar at the bottom.
- `multiline`
Display a progress bar during the builds and in the lines below that one line per activity.
- `multiline-with-logs`
Displayes the raw logs, with a progress bar and activities each in a new line at the bottom.
- <span id="opt-no-build-output">[`--no-build-output`](#opt-no-build-output)</span> / `-Q`
By default, output written by builders to standard output and standard error is echoed to the Lix command's standard error.

View file

@ -57,7 +57,7 @@ MixCommonArgs::MixCommonArgs(const std::string & programName)
addFlag({
.longName = "log-format",
.description = "Set the format of log output; one of `raw`, `internal-json`, `bar` or `bar-with-logs`.",
.description = "Set the format of log output; one of `raw`, `internal-json`, `bar`, `bar-with-logs`, `multiline` or `multiline-with-logs`.",
.category = loggingCategory,
.labels = {"format"},
.handler = {[](std::string format) { setLogFormat(format); }},

View file

@ -17,6 +17,10 @@ LogFormat parseLogFormat(const std::string & logFormatStr) {
return LogFormat::bar;
else if (logFormatStr == "bar-with-logs")
return LogFormat::barWithLogs;
else if (logFormatStr == "multiline")
return LogFormat::multiline;
else if (logFormatStr == "multiline-with-logs")
return LogFormat::multilineWithLogs;
throw Error("option 'log-format' has an invalid value '%s'", logFormatStr);
}
@ -35,6 +39,17 @@ Logger * makeDefaultLogger() {
logger->setPrintBuildLogs(true);
return logger;
}
case LogFormat::multiline: {
auto logger = makeProgressBar();
logger->setPrintMultiline(true);
return logger;
}
case LogFormat::multilineWithLogs: {
auto logger = makeProgressBar();
logger->setPrintMultiline(true);
logger->setPrintBuildLogs(true);
return logger;
}
default:
abort();
}

View file

@ -11,6 +11,8 @@ enum class LogFormat {
internalJSON,
bar,
barWithLogs,
multiline,
multilineWithLogs,
};
void setLogFormat(const std::string & logFormatStr);

View file

@ -73,6 +73,8 @@ private:
std::map<ActivityType, ActivitiesByType> activitiesByType;
int lastLines = 0;
uint64_t filesLinked = 0, bytesLinked = 0;
uint64_t corruptedPaths = 0, untrustedPaths = 0;
@ -89,6 +91,7 @@ private:
std::condition_variable quitCV, updateCV;
bool printBuildLogs = false;
bool printMultiline = false;
bool isTTY;
public:
@ -103,7 +106,7 @@ public:
while (state->active) {
if (!state->haveUpdate)
state.wait_for(updateCV, nextWakeup);
nextWakeup = draw(*state);
nextWakeup = draw(*state, {});
state.wait_for(quitCV, std::chrono::milliseconds(50));
}
});
@ -165,8 +168,7 @@ public:
void log(State & state, Verbosity lvl, std::string_view s)
{
if (state.active) {
writeToStderr("\r\e[K" + filterANSIEscapes(s, !isTTY) + ANSI_NORMAL "\n");
draw(state);
draw(state, s);
} else {
auto s2 = s + ANSI_NORMAL "\n";
if (!isTTY) s2 = filterANSIEscapes(s2, true);
@ -298,10 +300,14 @@ public:
}
log(*state, lvlInfo, ANSI_FAINT + info.name.value_or("unnamed") + suffix + ANSI_NORMAL + lastLine);
} else {
if (!printMultiline) {
state->activities.erase(i->second);
info.lastLine = lastLine;
state->activities.emplace_back(info);
i->second = std::prev(state->activities.end());
} else {
i->second->lastLine = lastLine;
}
update(*state);
}
}
@ -354,60 +360,97 @@ public:
updateCV.notify_one();
}
std::chrono::milliseconds draw(State & state)
std::chrono::milliseconds draw(State & state, const std::optional<std::string_view> & s)
{
auto nextWakeup = A_LONG_TIME;
state.haveUpdate = false;
if (state.paused || !state.active) return nextWakeup;
std::string line;
auto windowSize = getWindowSize();
auto width = windowSize.second;
if (width <= 0) {
width = std::numeric_limits<decltype(width)>::max();
}
if (printMultiline && (state.lastLines >= 1)) {
// FIXME: make sure this works on windows
writeToStderr(fmt("\e[G\e[%dF\e[J", state.lastLines));
}
state.lastLines = 0;
if (s != std::nullopt)
writeToStderr("\r\e[K" + filterANSIEscapes(s.value(), !isTTY) + ANSI_NORMAL "\n");
std::string line;
std::string status = getStatus(state);
if (!status.empty()) {
line += '[';
line += status;
line += "]";
}
if (printMultiline && !line.empty()) {
writeToStderr(filterANSIEscapes(line, false, width) + "\n");
state.lastLines++;
}
auto height = windowSize.first > 0 ? windowSize.first : 25;
auto moreBuilds = 0;
auto now = std::chrono::steady_clock::now();
std::string activity_line;
if (!state.activities.empty()) {
if (!status.empty()) line += " ";
auto i = state.activities.rbegin();
for (auto i = state.activities.begin(); i != state.activities.end(); ++i) {
if (!(i->visible && (!i->s.empty() || !i->lastLine.empty()))) {
continue;
}
while (i != state.activities.rend()) {
if (i->visible && (!i->s.empty() || !i->lastLine.empty())) {
/* Don't show activities until some time has
passed, to avoid displaying very short
activities. */
auto delay = std::chrono::milliseconds(10);
if (i->startTime + delay < now)
break;
else
nextWakeup = std::min(nextWakeup, std::chrono::duration_cast<std::chrono::milliseconds>(delay - (now - i->startTime)));
}
++i;
if (i->startTime + delay >= now) {
nextWakeup = std::min(
nextWakeup,
std::chrono::duration_cast<std::chrono::milliseconds>(
delay - (now - i->startTime)
)
);
}
if (i != state.activities.rend()) {
line += i->s;
activity_line = i->s;
if (!i->phase.empty()) {
line += " (";
line += i->phase;
line += ")";
activity_line += " (";
activity_line += i->phase;
activity_line += ")";
}
if (!i->lastLine.empty()) {
if (!i->s.empty()) line += ": ";
line += i->lastLine;
if (!i->s.empty()) {
activity_line += ": ";
}
activity_line += i->lastLine;
}
if (printMultiline) {
if (state.lastLines < (height - 1)) {
writeToStderr(filterANSIEscapes(activity_line, false, width) + "\n");
state.lastLines++;
} else {
moreBuilds++;
}
}
}
}
auto width = getWindowSize().second;
if (width <= 0) width = std::numeric_limits<decltype(width)>::max();
if (printMultiline && moreBuilds) {
writeToStderr(fmt("And %d more...", moreBuilds));
}
if (!printMultiline) {
line += " " + activity_line;
writeToStderr("\r" + filterANSIEscapes(line, false, width) + ANSI_NORMAL + "\e[K");
}
return nextWakeup;
}
@ -506,9 +549,8 @@ public:
{
auto state(state_.lock());
if (state->active) {
std::cerr << "\r\e[K";
Logger::writeToStdout(s);
draw(*state);
draw(*state, {});
} else {
Logger::writeToStdout(s);
}
@ -521,7 +563,7 @@ public:
std::cerr << fmt("\r\e[K%s ", msg);
auto s = trim(readLine(STDIN_FILENO));
if (s.size() != 1) return {};
draw(*state);
draw(*state, {});
return s[0];
}
@ -529,6 +571,11 @@ public:
{
this->printBuildLogs = printBuildLogs;
}
void setPrintMultiline(bool printMultiline) override
{
this->printMultiline = printMultiline;
}
};
Logger * makeProgressBar()

View file

@ -114,6 +114,9 @@ public:
virtual void setPrintBuildLogs(bool printBuildLogs)
{ }
virtual void setPrintMultiline(bool printMultiline)
{ }
};
/**