diff options
| author | Stefan Boberg <[email protected]> | 2026-03-30 18:10:54 +0200 |
|---|---|---|
| committer | Stefan Boberg <[email protected]> | 2026-03-30 18:10:54 +0200 |
| commit | 277c107e3991bf4c5ca0a0478188d7c53fe685b5 (patch) | |
| tree | a686723dc1504abaa3f45b86d96619a70bee018f | |
| parent | Simplify pager search to inline incremental mode (diff) | |
| download | zen-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.cpp | 23 | ||||
| -rw-r--r-- | src/zen/cmds/help_cmd.h | 2 | ||||
| -rw-r--r-- | src/zenutil/consoletui.cpp | 80 |
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)) |