diff options
Diffstat (limited to 'src/zen/cmds/history_cmd.cpp')
| -rw-r--r-- | src/zen/cmds/history_cmd.cpp | 228 |
1 files changed, 228 insertions, 0 deletions
diff --git a/src/zen/cmds/history_cmd.cpp b/src/zen/cmds/history_cmd.cpp new file mode 100644 index 000000000..27faae1eb --- /dev/null +++ b/src/zen/cmds/history_cmd.cpp @@ -0,0 +1,228 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "history_cmd.h" + +#include <zenbase/zenbase.h> +#include <zencore/except_fmt.h> +#include <zencore/filesystem.h> +#include <zencore/fmtutils.h> +#include <zencore/logging.h> +#include <zencore/process.h> +#include <zenutil/consoletui.h> +#include <zenutil/invocationhistory.h> + +#include <algorithm> + +namespace zen { + +HistoryCommand::HistoryCommand() +{ + m_Options.add_options()("h,help", "Print help"); + m_Options.add_options()("filter", + "Filter by executable ('zen' or 'zenserver' / 'server')", + cxxopts::value<std::string>(m_Filter)->default_value("")); + m_Options.add_options()("print", "Print the selected command line instead of running it", cxxopts::value<bool>(m_Print)); + m_Options.add_options()("l,list", "List all invocations to stdout instead of showing the picker", cxxopts::value<bool>(m_List)); +} + +HistoryCommand::~HistoryCommand() +{ +} + +namespace { + + bool KeepRecord(const HistoryRecord& Rec, std::string_view Filter) + { + if (Filter.empty()) + { + return true; + } + if (Filter == "zen") + { + return Rec.Exe == "zen"; + } + if (Filter == "zenserver" || Filter == "server") + { + return Rec.Exe == "zenserver"; + } + return true; + } + + std::string BuildLabel(const HistoryRecord& Rec, int32_t TerminalCols) + { + constexpr int32_t kIndicator = 3; // " > " or " " prefix from TuiPickOne + constexpr int32_t kEllipsis = 3; // "..." + + std::string Exe = Rec.Exe.empty() ? std::string("?") : Rec.Exe; + std::string Lbl = fmt::format("{} {:<9} pid {:<7}", Rec.Ts, Exe, Rec.Pid); + + if (!Rec.CmdLine.empty()) + { + int32_t Available = TerminalCols - kIndicator - 2 - static_cast<int32_t>(Lbl.size()); + if (Available > kEllipsis) + { + Lbl += " "; + if (static_cast<int32_t>(Rec.CmdLine.size()) <= Available) + { + Lbl += Rec.CmdLine; + } + else + { + Lbl.append(Rec.CmdLine, 0, static_cast<size_t>(Available - kEllipsis)); + Lbl += "..."; + } + } + } + return Lbl; + } + + int SpawnZen(const HistoryRecord& Rec) + { + const std::filesystem::path TargetPath = GetRunningExecutablePath(); + ZEN_CONSOLE("Running: {}", Rec.CmdLine); + + CreateProcOptions Opts{}; + CreateProcResult Result = CreateProc(TargetPath, Rec.CmdLine, Opts); + if (!Result) + { + throw zen::runtime_error("failed to launch '{}'", TargetPath); + } + + ProcessHandle Proc; + Proc.Initialize(Result); + return Proc.WaitExitCode(); + } + + void SpawnZenServerDetached(const HistoryRecord& Rec) + { + const std::filesystem::path TargetPath = GetRunningExecutablePath().parent_path() / ("zenserver" ZEN_EXE_SUFFIX_LITERAL); + + ZEN_CONSOLE("Launching detached: {}", Rec.CmdLine); + + CreateProcOptions Opts{}; + Opts.Flags = CreateProcOptions::Flag_NoConsole | CreateProcOptions::Flag_NewProcessGroup; + CreateProcResult Result = CreateProc(TargetPath, Rec.CmdLine, Opts); + if (!Result) + { + throw zen::runtime_error("failed to launch '{}'", TargetPath); + } + +#if ZEN_PLATFORM_WINDOWS + // Take ownership of the handle just to release it; we do not wait. + ProcessHandle Proc; + Proc.Initialize(Result); + ZEN_CONSOLE("Launched {} (pid {})", TargetPath, Proc.Pid()); +#else + ZEN_CONSOLE("Launched {} (pid {})", TargetPath, static_cast<int>(Result)); +#endif + } + + void PrintPlainTable(const std::vector<HistoryRecord>& Records) + { + if (Records.empty()) + { + ZEN_CONSOLE("No invocation history available."); + return; + } + for (const HistoryRecord& Rec : Records) + { + std::string Line = fmt::format("{} {:<9} pid {:<7}", Rec.Ts, Rec.Exe, Rec.Pid); + if (!Rec.CmdLine.empty()) + { + Line += " "; + Line += Rec.CmdLine; + } + ZEN_CONSOLE("{}", Line); + } + } + +} // namespace + +void +HistoryCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) +{ + ZEN_UNUSED(GlobalOptions); + + if (!ParseOptions(argc, argv)) + { + return; + } + + if (m_List && m_Print) + { + throw OptionParseException("'--list' conflicts with '--print'", m_Options.help()); + } + + std::vector<HistoryRecord> All = ReadInvocationHistory(); + + std::vector<HistoryRecord> Filtered; + Filtered.reserve(All.size()); + for (HistoryRecord& Rec : All) + { + if (KeepRecord(Rec, m_Filter)) + { + Filtered.push_back(std::move(Rec)); + } + } + + std::reverse(Filtered.begin(), Filtered.end()); + + if (Filtered.empty()) + { + ZEN_CONSOLE("No invocation history available{}.", m_Filter.empty() ? "" : " for this filter"); + return; + } + + if (m_List || !IsTuiAvailable()) + { + PrintPlainTable(Filtered); + return; + } + + const int32_t Cols = static_cast<int32_t>(TuiConsoleColumns()); + std::vector<std::string> Labels; + Labels.reserve(Filtered.size()); + for (const HistoryRecord& Rec : Filtered) + { + Labels.push_back(BuildLabel(Rec, Cols)); + } + + int Selected = TuiPickOne("Recent invocations. Select one to re-run:", Labels); + if (Selected < 0) + { + return; + } + + const HistoryRecord& Pick = Filtered[Selected]; + + if (m_Print) + { + ZEN_CONSOLE("{}", Pick.CmdLine); + return; + } + + if (Pick.CmdLine.empty()) + { + throw zen::runtime_error("selected record has no command line"); + } + + if (Pick.Exe == "zenserver") + { + SpawnZenServerDetached(Pick); + return; + } + + if (Pick.Exe == "zen") + { + int ExitCode = SpawnZen(Pick); + if (ExitCode != 0) + { + throw ErrorWithReturnCode(fmt::format("zen exited with code {}", ExitCode), ExitCode); + } + return; + } + + throw zen::runtime_error("unknown executable '{}' in history record", Pick.Exe); +} + +} // namespace zen |