aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2026-03-30 18:10:54 +0200
committerStefan Boberg <[email protected]>2026-03-30 18:10:54 +0200
commit277c107e3991bf4c5ca0a0478188d7c53fe685b5 (patch)
treea686723dc1504abaa3f45b86d96619a70bee018f
parentSimplify pager search to inline incremental mode (diff)
downloadzen-277c107e3991bf4c5ca0a0478188d7c53fe685b5.tar.xz
zen-277c107e3991bf4c5ca0a0478188d7c53fe685b5.zip
Highlight command and subcommand names in help TUI
- Render command names in bright white in the picker list - Render subcommand names in bright white in the pager help text - Add ANSI-aware VisibleWidth() and TruncateAnsi() helpers so line truncation in TuiPickOne and TuiPager correctly handles escape codes - Only emit ANSI codes in interactive mode; non-interactive output remains plain text
-rw-r--r--src/zen/cmds/help_cmd.cpp23
-rw-r--r--src/zen/cmds/help_cmd.h2
-rw-r--r--src/zenutil/consoletui.cpp80
3 files changed, 87 insertions, 18 deletions
diff --git a/src/zen/cmds/help_cmd.cpp b/src/zen/cmds/help_cmd.cpp
index 98961abcc..6dfe7bed5 100644
--- a/src/zen/cmds/help_cmd.cpp
+++ b/src/zen/cmds/help_cmd.cpp
@@ -48,7 +48,7 @@ SplitLines(const std::string& Text)
static std::string
FormatPickerLabel(const CommandInfo& Cmd)
{
- return fmt::format("{:<20s} {}", Cmd.CmdName, Cmd.CmdSummary);
+ return fmt::format("\033[1;37m{:<20s}\033[0m {}", Cmd.CmdName, Cmd.CmdSummary);
}
// Print all commands grouped by category (same format as zen --help)
@@ -94,12 +94,16 @@ GlobalOptionsHelpText(cxxopts::Options* GlobalOptions)
return GlobalOptions->help(Groups);
}
-// Build the full help text for a command, including subcommands and global options
+// Build the full help text for a command, including subcommands and global options.
+// When UseAnsi is true, command/subcommand names are highlighted with bright white.
std::string
-HelpCommand::BuildFullHelpText(const CommandInfo& Cmd) const
+HelpCommand::BuildFullHelpText(const CommandInfo& Cmd, bool UseAnsi) const
{
std::string Text = Cmd.Cmd->HelpText();
+ const char* NameStart = UseAnsi ? "\033[1;37m" : "";
+ const char* NameEnd = UseAnsi ? "\033[0m" : "";
+
// If this command has subcommands, append their listing and individual options
ZenCmdWithSubCommands* CmdWithSubs = dynamic_cast<ZenCmdWithSubCommands*>(Cmd.Cmd);
if (CmdWithSubs != nullptr)
@@ -117,7 +121,7 @@ HelpCommand::BuildFullHelpText(const CommandInfo& Cmd) const
Text += "\nSubcommands:\n";
for (ZenSubCmdBase* Sub : SubCmds)
{
- Text += fmt::format(" {:<{}} {}\n", Sub->SubOptions().program(), MaxNameLen, Sub->Description());
+ Text += fmt::format(" {}{:<{}}{} {}\n", NameStart, Sub->SubOptions().program(), MaxNameLen, NameEnd, Sub->Description());
}
// Detailed options for each subcommand
@@ -170,17 +174,16 @@ HelpCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
{
if (StrCaseCompare(m_CommandName.c_str(), Cmd.CmdName) == 0)
{
- std::string HelpText = BuildFullHelpText(Cmd);
-
if (IsTuiAvailable())
{
- std::vector<std::string> Lines = SplitLines(HelpText);
- std::string Title = fmt::format("zen {} -- {}", Cmd.CmdName, Cmd.CmdSummary);
+ std::string HelpText = BuildFullHelpText(Cmd, true);
+ std::vector<std::string> Lines = SplitLines(HelpText);
+ std::string Title = fmt::format("zen {} -- {}", Cmd.CmdName, Cmd.CmdSummary);
TuiPager(Title, Lines);
}
else
{
- ZEN_CONSOLE("{}", HelpText);
+ ZEN_CONSOLE("{}", BuildFullHelpText(Cmd, false));
}
return;
}
@@ -242,7 +245,7 @@ HelpCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv)
continue; // Category header selected, ignore
}
- std::string HelpText = BuildFullHelpText(*SelectedCmd);
+ std::string HelpText = BuildFullHelpText(*SelectedCmd, true);
std::vector<std::string> Lines = SplitLines(HelpText);
std::string Title = fmt::format("zen {} -- {}", SelectedCmd->CmdName, SelectedCmd->CmdSummary);
TuiPager(Title, Lines);
diff --git a/src/zen/cmds/help_cmd.h b/src/zen/cmds/help_cmd.h
index 29018e803..289abdece 100644
--- a/src/zen/cmds/help_cmd.h
+++ b/src/zen/cmds/help_cmd.h
@@ -25,7 +25,7 @@ public:
virtual ZenCmdCategory& CommandCategory() const override { return g_UtilitiesCategory; }
private:
- std::string BuildFullHelpText(const CommandInfo& Cmd) const;
+ std::string BuildFullHelpText(const CommandInfo& Cmd, bool UseAnsi = false) const;
cxxopts::Options m_Options{Name, Description};
std::span<const CommandInfo> m_Commands;
diff --git a/src/zenutil/consoletui.cpp b/src/zenutil/consoletui.cpp
index 02488248d..d3d73814d 100644
--- a/src/zenutil/consoletui.cpp
+++ b/src/zenutil/consoletui.cpp
@@ -210,6 +210,71 @@ IsTuiAvailable()
return Cached;
}
+// Compute visible width of a string, skipping ANSI escape sequences.
+static int
+VisibleWidth(const std::string& Text)
+{
+ int Width = 0;
+ bool InEscape = false;
+ for (char c : Text)
+ {
+ if (InEscape)
+ {
+ if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))
+ {
+ InEscape = false; // Final byte of escape sequence
+ }
+ }
+ else if (c == '\033')
+ {
+ InEscape = true;
+ }
+ else
+ {
+ ++Width;
+ }
+ }
+ return Width;
+}
+
+// Truncate a string that may contain ANSI escape sequences to a visible width.
+// Appends "..." if truncated. Ensures ANSI state is reset after truncation.
+static std::string
+TruncateAnsi(const std::string& Text, int MaxVisible)
+{
+ if (MaxVisible <= 0)
+ {
+ return {};
+ }
+
+ int Visible = 0;
+ bool InEscape = false;
+ for (size_t i = 0; i < Text.size(); ++i)
+ {
+ char c = Text[i];
+ if (InEscape)
+ {
+ if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))
+ {
+ InEscape = false;
+ }
+ }
+ else if (c == '\033')
+ {
+ InEscape = true;
+ }
+ else
+ {
+ ++Visible;
+ if (Visible >= MaxVisible - 2) // Leave room for "..."
+ {
+ return Text.substr(0, i + 1) + "\033[0m...";
+ }
+ }
+ }
+ return Text;
+}
+
// Case-insensitive substring match
static bool
ContainsCaseInsensitive(const std::string& Haystack, const std::string& Needle)
@@ -342,11 +407,12 @@ TuiPickOne(std::string_view Title, std::span<const std::string> Items, int Initi
printf("\033[1;7m");
}
- const char* Indicator = IsSelected ? " \xe2\x96\xb6 " : " ";
- const std::string& ItemText = Items[Visible[i]];
- if (MaxTextLen > 0 && static_cast<int>(ItemText.size()) > MaxTextLen)
+ const char* Indicator = IsSelected ? " \xe2\x96\xb6 " : " ";
+ const std::string& ItemText = Items[Visible[i]];
+ int ItemVisible = VisibleWidth(ItemText);
+ if (MaxTextLen > 0 && ItemVisible > MaxTextLen)
{
- printf("%s%.*s...", Indicator, MaxTextLen - 3, ItemText.c_str());
+ printf("%s%s", Indicator, TruncateAnsi(ItemText, MaxTextLen).c_str());
}
else
{
@@ -886,14 +952,14 @@ TuiPager(std::string_view Title, const std::vector<std::string>& Lines)
printf("\033[43;30m"); // yellow background, black text
}
- // Truncate to terminal width
- if (Line.size() <= Cols)
+ // Truncate to terminal width (ANSI-aware)
+ if (VisibleWidth(Line) <= static_cast<int>(Cols))
{
printf("%s", Line.c_str());
}
else
{
- printf("%.*s", static_cast<int>(Cols), Line.c_str());
+ printf("%s", TruncateAnsi(Line, static_cast<int>(Cols)).c_str());
}
if (SearchMatchLine >= 0 && LineIdx == static_cast<uint32_t>(SearchMatchLine))