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 21865ccce0
commit da4e46dd1f
9 changed files with 132 additions and 39 deletions

View file

@ -60,6 +60,10 @@ jade:
forgejo: jade forgejo: jade
github: lf- github: lf-
kloenk:
forgejo: kloenk
github: kloenk
lovesegfault: lovesegfault:
github: 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,16 @@ Most commands in Lix accept the following command-line options:
Display the raw logs, with the progress bar at the bottom. 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` - <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. By default, output written by builders to standard output and standard error is echoed to the Lix command's standard error.

View file

@ -58,7 +58,7 @@ MixCommonArgs::MixCommonArgs(const std::string & programName)
addFlag({ addFlag({
.longName = "log-format", .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, .category = loggingCategory,
.labels = {"format"}, .labels = {"format"},
.handler = {[](std::string format) { setLogFormat(format); }}, .handler = {[](std::string format) { setLogFormat(format); }},

View file

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

View file

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

View file

@ -95,8 +95,7 @@ void ProgressBar::logEI(const ErrorInfo & ei)
void ProgressBar::log(State & state, Verbosity lvl, std::string_view s) void ProgressBar::log(State & state, Verbosity lvl, std::string_view s)
{ {
if (state.active) { if (state.active) {
writeToStderr("\r\e[K" + filterANSIEscapes(s, !isTTY) + ANSI_NORMAL "\n"); draw(state, s);
draw(state);
} else { } else {
auto s2 = s + ANSI_NORMAL "\n"; auto s2 = s + ANSI_NORMAL "\n";
if (!isTTY) s2 = filterANSIEscapes(s2, true); if (!isTTY) s2 = filterANSIEscapes(s2, true);
@ -234,10 +233,14 @@ void ProgressBar::result(ActivityId act, ResultType type, const std::vector<Fiel
} }
log(*state, lvlInfo, ANSI_FAINT + info.name.value_or("unnamed") + suffix + ANSI_NORMAL + lastLine); log(*state, lvlInfo, ANSI_FAINT + info.name.value_or("unnamed") + suffix + ANSI_NORMAL + lastLine);
} else { } else {
if (!printMultiline) {
state->activities.erase(i->second); state->activities.erase(i->second);
info.lastLine = lastLine; info.lastLine = lastLine;
state->activities.emplace_back(info); state->activities.emplace_back(info);
i->second = std::prev(state->activities.end()); i->second = std::prev(state->activities.end());
} else {
i->second->lastLine = lastLine;
}
update(*state); update(*state);
} }
} }
@ -290,60 +293,93 @@ void ProgressBar::update(State & state)
updateCV.notify_one(); updateCV.notify_one();
} }
std::chrono::milliseconds ProgressBar::draw(State & state) std::chrono::milliseconds ProgressBar::draw(State & state, const std::optional<std::string_view> & s)
{ {
auto nextWakeup = A_LONG_TIME; auto nextWakeup = A_LONG_TIME;
state.haveUpdate = false; state.haveUpdate = false;
if (state.paused || !state.active) return nextWakeup; 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); std::string status = getStatus(state);
if (!status.empty()) { if (!status.empty()) {
line += '['; line += '[';
line += status; line += status;
line += "]"; line += "]";
} }
if (printMultiline && !line.empty()) {
writeToStderr(filterANSIEscapes(line, false, width) + "\n");
state.lastLines++;
}
auto height = windowSize.first > 0 ? windowSize.first : 25;
auto moreActivities = 0;
auto now = std::chrono::steady_clock::now(); auto now = std::chrono::steady_clock::now();
std::string activity_line;
if (!state.activities.empty()) { if (!state.activities.empty()) {
if (!status.empty()) line += " "; for (auto i = state.activities.begin(); i != state.activities.end(); ++i) {
auto i = state.activities.rbegin(); 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 /* Don't show activities until some time has
passed, to avoid displaying very short passed, to avoid displaying very short
activities. */ activities. */
auto delay = std::chrono::milliseconds(10); auto delay = std::chrono::milliseconds(10);
if (i->startTime + delay < now) if (i->startTime + delay >= now) {
break; nextWakeup = std::min(
else nextWakeup,
nextWakeup = std::min(nextWakeup, std::chrono::duration_cast<std::chrono::milliseconds>(delay - (now - i->startTime))); std::chrono::duration_cast<std::chrono::milliseconds>(
} delay - (now - i->startTime)
++i; )
);
} }
if (i != state.activities.rend()) { activity_line = i->s;
line += i->s;
if (!i->phase.empty()) { if (!i->phase.empty()) {
line += " ("; activity_line += " (";
line += i->phase; activity_line += i->phase;
line += ")"; activity_line += ")";
} }
if (!i->lastLine.empty()) { if (!i->lastLine.empty()) {
if (!i->s.empty()) line += ": "; if (!i->s.empty())
line += i->lastLine; activity_line += ": ";
activity_line += i->lastLine;
}
if (printMultiline) {
if (state.lastLines < (height -1)) {
writeToStderr(filterANSIEscapes(activity_line, false, width) + "\n");
state.lastLines++;
} else moreActivities++;
} }
} }
} }
auto width = getWindowSize().second; if (printMultiline && moreActivities)
if (width <= 0) width = std::numeric_limits<decltype(width)>::max(); writeToStderr(fmt("And %d more...", moreActivities));
if (!printMultiline) {
line += " " + activity_line;
writeToStderr("\r" + filterANSIEscapes(line, false, width) + ANSI_NORMAL + "\e[K"); writeToStderr("\r" + filterANSIEscapes(line, false, width) + ANSI_NORMAL + "\e[K");
}
return nextWakeup; return nextWakeup;
} }
@ -442,9 +478,8 @@ void ProgressBar::writeToStdout(std::string_view s)
{ {
auto state(state_.lock()); auto state(state_.lock());
if (state->active) { if (state->active) {
std::cerr << "\r\e[K";
Logger::writeToStdout(s); Logger::writeToStdout(s);
draw(*state); draw(*state, {});
} else { } else {
Logger::writeToStdout(s); Logger::writeToStdout(s);
} }
@ -457,7 +492,7 @@ std::optional<char> ProgressBar::ask(std::string_view msg)
std::cerr << fmt("\r\e[K%s ", msg); std::cerr << fmt("\r\e[K%s ", msg);
auto s = trim(readLine(STDIN_FILENO)); auto s = trim(readLine(STDIN_FILENO));
if (s.size() != 1) return {}; if (s.size() != 1) return {};
draw(*state); draw(*state, {});
return s[0]; return s[0];
} }
@ -466,6 +501,11 @@ void ProgressBar::setPrintBuildLogs(bool printBuildLogs)
this->printBuildLogs = printBuildLogs; this->printBuildLogs = printBuildLogs;
} }
void ProgressBar::setPrintMultiline(bool printMultiline)
{
this->printMultiline = printMultiline;
}
Logger * makeProgressBar() Logger * makeProgressBar()
{ {
return new ProgressBar(shouldANSI()); return new ProgressBar(shouldANSI());

View file

@ -47,6 +47,8 @@ struct ProgressBar : public Logger
std::map<ActivityType, ActivitiesByType> activitiesByType; std::map<ActivityType, ActivitiesByType> activitiesByType;
int lastLines = 0;
uint64_t filesLinked = 0, bytesLinked = 0; uint64_t filesLinked = 0, bytesLinked = 0;
uint64_t corruptedPaths = 0, untrustedPaths = 0; uint64_t corruptedPaths = 0, untrustedPaths = 0;
@ -63,6 +65,7 @@ struct ProgressBar : public Logger
std::condition_variable quitCV, updateCV; std::condition_variable quitCV, updateCV;
bool printBuildLogs = false; bool printBuildLogs = false;
bool printMultiline = false;
bool isTTY; bool isTTY;
ProgressBar(bool isTTY) ProgressBar(bool isTTY)
@ -75,7 +78,7 @@ struct ProgressBar : public Logger
while (state->active) { while (state->active) {
if (!state->haveUpdate) if (!state->haveUpdate)
state.wait_for(updateCV, nextWakeup); state.wait_for(updateCV, nextWakeup);
nextWakeup = draw(*state); nextWakeup = draw(*state, {});
state.wait_for(quitCV, std::chrono::milliseconds(50)); state.wait_for(quitCV, std::chrono::milliseconds(50));
} }
}); });
@ -114,7 +117,7 @@ struct ProgressBar : public Logger
void update(State & state); void update(State & state);
std::chrono::milliseconds draw(State & state); std::chrono::milliseconds draw(State & state, const std::optional<std::string_view> & s);
std::string getStatus(State & state); std::string getStatus(State & state);
@ -123,6 +126,8 @@ struct ProgressBar : public Logger
std::optional<char> ask(std::string_view msg) override; std::optional<char> ask(std::string_view msg) override;
void setPrintBuildLogs(bool printBuildLogs) override; void setPrintBuildLogs(bool printBuildLogs) override;
void setPrintMultiline(bool printMultiline) override;
}; };
Logger * makeProgressBar(); Logger * makeProgressBar();

View file

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