From 09ab8d52c532f70a10cdde225c7cdad741824776 Mon Sep 17 00:00:00 2001 From: DanS Date: Sat, 13 Jun 2026 12:07:01 -0500 Subject: [PATCH] feat(explorer): live (debounced) search as the user types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The explorer search only ran on Enter or the Search button. Now it also fires automatically ~350ms after the user stops typing, once the query is resolvable — a block height (all digits) or a complete 64-char hash/txid. Partial hex is ignored so it won't flash "invalid query" mid-type, per-keystroke RPC spam is avoided via the debounce, and the same query isn't re-run. Enter/button still work for an immediate search. Co-Authored-By: Claude Opus 4.8 --- src/ui/windows/explorer_tab.cpp | 39 +++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/ui/windows/explorer_tab.cpp b/src/ui/windows/explorer_tab.cpp index e22a472..26bf134 100644 --- a/src/ui/windows/explorer_tab.cpp +++ b/src/ui/windows/explorer_tab.cpp @@ -41,6 +41,20 @@ using namespace material; static char s_search_buf[128] = {}; static bool s_search_loading = false; static std::string s_search_error; +static double s_search_change_time = -1.0; // GetTime() of the last edit (debounce for live search) +static std::string s_search_last_query; // last query actually run (avoids re-running it) +static constexpr double kSearchDebounceSeconds = 0.35; + +// A query that performSearch() can resolve to a concrete result: a block height (all digits) or a +// complete 64-char block hash / txid. Partial hex is intentionally NOT resolvable, so live search +// stays quiet (no "invalid query") until the user finishes typing or pasting a full hash. +static bool isResolvableQuery(const std::string& q) { + if (q.empty()) return false; + if (std::all_of(q.begin(), q.end(), ::isdigit)) return true; + return q.size() == 64 && std::all_of(q.begin(), q.end(), [](char c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); + }); +} using BlockSummary = ExplorerBlockSummary; static ExplorerBlockCache s_recent_block_cache; @@ -462,6 +476,7 @@ static void renderSearchBar(App* app, float availWidth) { ImGui::SetNextItemWidth(inputW); bool enter = ImGui::InputText("##ExplorerSearch", s_search_buf, sizeof(s_search_buf), ImGuiInputTextFlags_EnterReturnsTrue); + bool edited = ImGui::IsItemEdited(); // must read immediately after the input item ImGui::PopStyleVar(); ImGui::SameLine(); @@ -471,10 +486,26 @@ static void renderSearchBar(App* app, float availWidth) { bool clicked = material::StyledButton(TR("explorer_search"), ImVec2(btnW, barH)); - if ((enter || clicked) && app->rpc() && app->rpc()->isConnected()) { - std::string query(s_search_buf); - while (!query.empty() && query.front() == ' ') query.erase(query.begin()); - while (!query.empty() && query.back() == ' ') query.pop_back(); + std::string query(s_search_buf); + while (!query.empty() && query.front() == ' ') query.erase(query.begin()); + while (!query.empty() && query.back() == ' ') query.pop_back(); + + if (edited) { + s_search_change_time = ImGui::GetTime(); // (re)start the debounce on each keystroke + s_search_error.clear(); // don't show a stale error mid-type + } + + bool doSearch = enter || clicked; + // Live search: once the user pauses and the query is resolvable, run it without waiting for Enter. + if (!doSearch && s_search_change_time >= 0.0 && + (ImGui::GetTime() - s_search_change_time) >= kSearchDebounceSeconds && + isResolvableQuery(query) && query != s_search_last_query) { + doSearch = true; + } + + if (doSearch && app->rpc() && app->rpc()->isConnected() && !query.empty()) { + s_search_change_time = -1.0; // consume the debounce + s_search_last_query = query; performSearch(app, query); }