aboutsummaryrefslogtreecommitdiff
path: root/src/zen/cmds/history_cmd.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/zen/cmds/history_cmd.cpp')
-rw-r--r--src/zen/cmds/history_cmd.cpp228
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