From da4e46dd1fc04067b5ba4bc16dd68134fa7efad2 Mon Sep 17 00:00:00 2001 From: Finn Behrens Date: Sat, 1 Jun 2024 16:06:26 +0200 Subject: [PATCH] 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 --- doc/manual/change-authors.yml | 4 + doc/manual/rl-next/multiline-log-format.md | 14 +++ doc/manual/src/command-ref/opt-common.md | 10 ++ src/libmain/common-args.cc | 2 +- src/libmain/loggers.cc | 15 +++ src/libmain/loggers.hh | 2 + src/libmain/progress-bar.cc | 112 ++++++++++++++------- src/libmain/progress-bar.hh | 9 +- src/libutil/logging.hh | 3 + 9 files changed, 132 insertions(+), 39 deletions(-) create mode 100644 doc/manual/rl-next/multiline-log-format.md diff --git a/doc/manual/change-authors.yml b/doc/manual/change-authors.yml index c56f588ca..5a7b8117e 100644 --- a/doc/manual/change-authors.yml +++ b/doc/manual/change-authors.yml @@ -60,6 +60,10 @@ jade: forgejo: jade github: lf- +kloenk: + forgejo: kloenk + github: kloenk + lovesegfault: github: lovesegfault diff --git a/doc/manual/rl-next/multiline-log-format.md b/doc/manual/rl-next/multiline-log-format.md new file mode 100644 index 000000000..62bb9f1e7 --- /dev/null +++ b/doc/manual/rl-next/multiline-log-format.md @@ -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. diff --git a/doc/manual/src/command-ref/opt-common.md b/doc/manual/src/command-ref/opt-common.md index cd289b7ea..7740da953 100644 --- a/doc/manual/src/command-ref/opt-common.md +++ b/doc/manual/src/command-ref/opt-common.md @@ -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. + - `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. + + - [`--no-build-output`](#opt-no-build-output) / `-Q` By default, output written by builders to standard output and standard error is echoed to the Lix command's standard error. diff --git a/src/libmain/common-args.cc b/src/libmain/common-args.cc index 8fcb10325..0b756301c 100644 --- a/src/libmain/common-args.cc +++ b/src/libmain/common-args.cc @@ -58,7 +58,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); }}, diff --git a/src/libmain/loggers.cc b/src/libmain/loggers.cc index 80080d616..8c3c4e355 100644 --- a/src/libmain/loggers.cc +++ b/src/libmain/loggers.cc @@ -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(); } diff --git a/src/libmain/loggers.hh b/src/libmain/loggers.hh index e5721420c..6064b6160 100644 --- a/src/libmain/loggers.hh +++ b/src/libmain/loggers.hh @@ -11,6 +11,8 @@ enum class LogFormat { internalJSON, bar, barWithLogs, + multiline, + multilineWithLogs, }; void setLogFormat(const std::string & logFormatStr); diff --git a/src/libmain/progress-bar.cc b/src/libmain/progress-bar.cc index b3466a860..b3b46fc21 100644 --- a/src/libmain/progress-bar.cc +++ b/src/libmain/progress-bar.cc @@ -95,8 +95,7 @@ void ProgressBar::logEI(const ErrorInfo & ei) void ProgressBar::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); @@ -234,10 +233,14 @@ void ProgressBar::result(ActivityId act, ResultType type, const std::vectoractivities.erase(i->second); - info.lastLine = lastLine; - state->activities.emplace_back(info); - i->second = std::prev(state->activities.end()); + 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); } } @@ -290,60 +293,93 @@ void ProgressBar::update(State & state) updateCV.notify_one(); } -std::chrono::milliseconds ProgressBar::draw(State & state) +std::chrono::milliseconds ProgressBar::draw(State & state, const std::optional & 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::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 moreActivities = 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(); - - 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(delay - (now - i->startTime))); + for (auto i = state.activities.begin(); i != state.activities.end(); ++i) { + if (!(i->visible && (!i->s.empty() || !i->lastLine.empty()))) { + continue; + } + /* 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) { + nextWakeup = std::min( + nextWakeup, + std::chrono::duration_cast( + delay - (now - i->startTime) + ) + ); } - ++i; - } - 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 moreActivities++; } } } - auto width = getWindowSize().second; - if (width <= 0) width = std::numeric_limits::max(); + if (printMultiline && moreActivities) + writeToStderr(fmt("And %d more...", moreActivities)); - writeToStderr("\r" + filterANSIEscapes(line, false, width) + ANSI_NORMAL + "\e[K"); + if (!printMultiline) { + line += " " + activity_line; + writeToStderr("\r" + filterANSIEscapes(line, false, width) + ANSI_NORMAL + "\e[K"); + } return nextWakeup; } @@ -442,9 +478,8 @@ void ProgressBar::writeToStdout(std::string_view s) { auto state(state_.lock()); if (state->active) { - std::cerr << "\r\e[K"; Logger::writeToStdout(s); - draw(*state); + draw(*state, {}); } else { Logger::writeToStdout(s); } @@ -457,7 +492,7 @@ std::optional ProgressBar::ask(std::string_view msg) 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]; } @@ -466,6 +501,11 @@ void ProgressBar::setPrintBuildLogs(bool printBuildLogs) this->printBuildLogs = printBuildLogs; } +void ProgressBar::setPrintMultiline(bool printMultiline) +{ + this->printMultiline = printMultiline; +} + Logger * makeProgressBar() { return new ProgressBar(shouldANSI()); diff --git a/src/libmain/progress-bar.hh b/src/libmain/progress-bar.hh index 76e2ed4ff..176e941e8 100644 --- a/src/libmain/progress-bar.hh +++ b/src/libmain/progress-bar.hh @@ -47,6 +47,8 @@ struct ProgressBar : public Logger std::map activitiesByType; + int lastLines = 0; + uint64_t filesLinked = 0, bytesLinked = 0; uint64_t corruptedPaths = 0, untrustedPaths = 0; @@ -63,6 +65,7 @@ struct ProgressBar : public Logger std::condition_variable quitCV, updateCV; bool printBuildLogs = false; + bool printMultiline = false; bool isTTY; ProgressBar(bool isTTY) @@ -75,7 +78,7 @@ struct ProgressBar : public Logger 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)); } }); @@ -114,7 +117,7 @@ struct ProgressBar : public Logger void update(State & state); - std::chrono::milliseconds draw(State & state); + std::chrono::milliseconds draw(State & state, const std::optional & s); std::string getStatus(State & state); @@ -123,6 +126,8 @@ struct ProgressBar : public Logger std::optional ask(std::string_view msg) override; void setPrintBuildLogs(bool printBuildLogs) override; + + void setPrintMultiline(bool printMultiline) override; }; Logger * makeProgressBar(); diff --git a/src/libutil/logging.hh b/src/libutil/logging.hh index 64be8bc62..115b979f8 100644 --- a/src/libutil/logging.hh +++ b/src/libutil/logging.hh @@ -114,6 +114,9 @@ public: virtual void setPrintBuildLogs(bool printBuildLogs) { } + + virtual void setPrintMultiline(bool printMultiline) + { } }; /**