From 51522d424d5bba78c4d916f5ddbedf3fd86e1542 Mon Sep 17 00:00:00 2001 From: danipiza Date: Mon, 11 May 2026 09:38:19 +0200 Subject: [PATCH] [#24495] Initial approach Signed-off-by: danipiza --- .../cpp_utils/event/StdinEventHandler.hpp | 18 +++++ cpp_utils/src/cpp/event/StdinEventHandler.cpp | 66 +++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/cpp_utils/include/cpp_utils/event/StdinEventHandler.hpp b/cpp_utils/include/cpp_utils/event/StdinEventHandler.hpp index e5dbd85..9bcde9f 100644 --- a/cpp_utils/include/cpp_utils/event/StdinEventHandler.hpp +++ b/cpp_utils/include/cpp_utils/event/StdinEventHandler.hpp @@ -97,6 +97,21 @@ class StdinEventHandler : public EventHandler CPP_UTILS_DllAPI bool is_ignoring_input() const noexcept; + /** + * @brief Set a callback invoked when the user presses Tab. + * + * The callback receives the full current input line and returns a list of + * candidate completions for the last whitespace-delimited token. The handler + * then performs standard completion behavior: if a single match (or a longer + * common prefix) exists, the token is extended in place; if several matches + * exist, they are listed below the prompt, which is then redrawn. + * + * Pass an empty std::function to disable Tab completion. + */ + CPP_UTILS_DllAPI + void set_tab_completion_callback( + std::function(const std::string&)> callback) noexcept; + protected: /** @@ -167,6 +182,9 @@ class StdinEventHandler : public EventHandler //! Whether to ignore input (e.g. during interactive commands like echo) std::atomic ignore_input_{false}; + //! Optional callback invoked on Tab to obtain completion candidates for the current line + std::function(const std::string&)> tab_completion_callback_; + }; } /* namespace event */ diff --git a/cpp_utils/src/cpp/event/StdinEventHandler.cpp b/cpp_utils/src/cpp/event/StdinEventHandler.cpp index 0f03e18..8a53471 100644 --- a/cpp_utils/src/cpp/event/StdinEventHandler.cpp +++ b/cpp_utils/src/cpp/event/StdinEventHandler.cpp @@ -550,6 +550,66 @@ void StdinEventHandler::stdin_listener_thread_routine_() noexcept cursor_index = 0; break; } + else if (c == '\t') + { + if (!tab_completion_callback_) + { + continue; + } + + std::vector suggestions = tab_completion_callback_(read_str); + if (suggestions.empty()) + { + continue; + } + + // Locate the last whitespace-delimited token in the line + size_t last_space = read_str.find_last_of(" \t"); + size_t token_start = (last_space == std::string::npos) ? 0 : last_space + 1; + std::string current_token = read_str.substr(token_start); + + // Compute the longest common prefix of all suggestions + std::string lcp = suggestions[0]; + for (size_t i = 1; i < suggestions.size(); i++) + { + size_t j = 0; + while (j < lcp.size() && j < suggestions[i].size() && lcp[j] == suggestions[i][j]) + { + j++; + } + lcp.resize(j); + } + + if (suggestions.size() == 1 || lcp.size() > current_token.size()) + { + // Replace the last token with the unique match or the extended common prefix + const std::string completion = (suggestions.size() == 1) ? suggestions[0] : lcp; + std::string new_line = read_str.substr(0, token_start) + completion; + update_line(">> ", read_str, cursor_index, + [new_line](std::string& line, size_t& index) + { + line = new_line; + index = line.size(); + }); + } + else + { + // Several matches: move cursor to end of line, list candidates, redraw prompt + update_line(">> ", read_str, cursor_index, + [](std::string& line, size_t& index) + { + index = line.size(); + }); + + std::cout << "\n"; + for (const auto& s : suggestions) + { + std::cout << " " << s << "\n"; + } + std::cout << "\033[38;5;82m" << ">> " << "\033[0m" << read_str << std::flush; + cursor_index = read_str.size(); + } + } else { char ch = static_cast(c); @@ -592,6 +652,12 @@ bool StdinEventHandler::is_ignoring_input() const noexcept return ignore_input_.load(std::memory_order_relaxed); } +void StdinEventHandler::set_tab_completion_callback( + std::function(const std::string&)> callback) noexcept +{ + tab_completion_callback_ = std::move(callback); +} + } /* namespace event */ } /* namespace utils */ } /* namespace eprosima */