From 5a677f620225b52099d2a2a71306f601160bda49 Mon Sep 17 00:00:00 2001 From: DanS Date: Wed, 1 Jul 2026 16:40:38 -0500 Subject: [PATCH] feat(console): right-click "Copy value" for hashes/addresses under the cursor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Right-clicking the output now offers "Copy " when a long alphanumeric token (txid/blockhash/address, >=16 chars) is under the cursor — resolved via screenToTextPos. Uses right-click so it never conflicts with the left-drag text selection. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ui/windows/console_tab.cpp | 37 ++++++++++++++++++++++++++++++++++ src/ui/windows/console_tab.h | 1 + src/util/i18n.cpp | 1 + 3 files changed, 39 insertions(+) diff --git a/src/ui/windows/console_tab.cpp b/src/ui/windows/console_tab.cpp index f31609e..c1a3114 100644 --- a/src/ui/windows/console_tab.cpp +++ b/src/ui/windows/console_tab.cpp @@ -549,6 +549,24 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec) } } +namespace { +// Extract a hash/address-like token (a long alphanumeric run, >= 16 chars) around byte +// offset `col` in `text`, for the right-click "Copy value" action. Returns "" if none. +std::string extractCopyableToken(const std::string& text, int col) +{ + const int n = static_cast(text.size()); + auto isTok = [](unsigned char c) { return std::isalnum(c) != 0; }; + int at = col; + if (at >= n || (at >= 0 && !isTok(static_cast(text[at])))) at--; + if (at < 0 || at >= n || !isTok(static_cast(text[at]))) return {}; + int s = at, e = at; + while (s > 0 && isTok(static_cast(text[s - 1]))) s--; + while (e + 1 < n && isTok(static_cast(text[e + 1]))) e++; + std::string tok = text.substr(s, e - s + 1); + return tok.size() >= 16 ? tok : std::string(); // txids/blockhashes/addresses are long +} +} // namespace + void ConsoleTab::renderOutput() { using namespace material; @@ -929,8 +947,27 @@ void ConsoleTab::renderOutput() ImGui::Dummy(ImVec2(0, ImGui::GetTextLineHeight())); } + // Capture the hash/address under the cursor on right-click, for "Copy value". + if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { + TextPos tp = screenToTextPos(ImGui::GetMousePos(), + output_line_height_ > 0.0f ? output_line_height_ : line_height); + context_token_.clear(); + if (tp.line >= 0 && tp.line < static_cast(lines_.size())) + context_token_ = extractCopyableToken(lines_[tp.line].text, tp.col); + } + // Right-click context menu if (ImGui::BeginPopupContextWindow("ConsoleContextMenu")) { + if (!context_token_.empty()) { + std::string shortTok = context_token_.size() > 22 + ? context_token_.substr(0, 12) + "\xE2\x80\xA6" + context_token_.substr(context_token_.size() - 6) + : context_token_; + std::string label = std::string(TR("console_copy_value")) + " \"" + shortTok + "\""; + if (ImGui::MenuItem(label.c_str())) { + ImGui::SetClipboardText(context_token_.c_str()); + } + ImGui::Separator(); + } if (ImGui::MenuItem(TR("copy"), "Ctrl+C", false, has_selection_)) { std::string selected = getSelectedText(); if (!selected.empty()) { diff --git a/src/ui/windows/console_tab.h b/src/ui/windows/console_tab.h index 78e7270..56d5f9c 100644 --- a/src/ui/windows/console_tab.h +++ b/src/ui/windows/console_tab.h @@ -153,6 +153,7 @@ private: // Output filter char filter_text_[128] = {0}; mutable int filter_match_count_ = 0; // lines matching the text filter (for the toolbar) + std::string context_token_; // hash/address under the cursor at right-click mutable std::vector visible_indices_; // Cached for selection mapping // Wrapped line height caching (for variable-height text wrapping) diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index d13ec43..9e415a1 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -983,6 +983,7 @@ void I18n::loadBuiltinEnglish() strings_["console_help_stop"] = " stop - Stop the daemon"; strings_["console_line_count"] = "%zu lines"; strings_["console_matches"] = "matches"; + strings_["console_copy_value"] = "Copy"; strings_["console_new_lines"] = "%d new lines"; strings_["console_no_daemon"] = "No daemon"; strings_["console_not_connected"] = "Error: Not connected to daemon";