rpcconsole.cpp raw
1 // Copyright (c) 2011-2022 The Limenka developers
2 // Distributed under the MIT software license, see the accompanying
3 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
5 #include <limenka-build-config.h> // IWYU pragma: keep
6
7 #include <qt/rpcconsole.h>
8 #include <qt/forms/ui_debugwindow.h>
9
10 #include <chainparams.h>
11 #include <clientversion.h>
12 #include <common/system.h>
13 #include <interfaces/node.h>
14 #include <node/connection_types.h>
15 #include <qt/bantablemodel.h>
16 #include <qt/clientmodel.h>
17 #include <qt/guiutil.h>
18 #include <qt/optionsmodel.h>
19 #include <qt/pairingpage.h>
20 #include <qt/peertablesortproxy.h>
21 #include <qt/platformstyle.h>
22 #ifdef ENABLE_WALLET
23 #include <qt/walletmodel.h>
24 #endif // ENABLE_WALLET
25 #include <rpc/client.h>
26 #include <rpc/server.h>
27 #include <util/strencodings.h>
28 #include <util/string.h>
29 #include <util/time.h>
30 #include <util/threadnames.h>
31
32 #include <univalue.h>
33
34 #include <Qt>
35 #include <QAbstractButton>
36 #include <QAbstractItemModel>
37 #include <QColor>
38 #include <QDateTime>
39 #include <QEvent>
40 #include <QFont>
41 #include <QFontMetrics>
42 #include <QKeyEvent>
43 #include <QKeySequence>
44 #include <QLabel>
45 #include <QLatin1String>
46 #include <QLocale>
47 #include <QMenu>
48 #include <QMessageBox>
49 #include <QScreen>
50 #include <QScrollBar>
51 #include <QSettings>
52 #include <QShortcut>
53 #include <QString>
54 #include <QStringList>
55 #include <QStyledItemDelegate>
56 #include <QTime>
57 #include <QTimer>
58 #include <QVariant>
59
60 #include <cassert>
61 #include <chrono>
62
63 using util::Join;
64
65 const int CONSOLE_HISTORY = 50;
66 const int INITIAL_TRAFFIC_GRAPH_MINS = 30;
67 const QSize FONT_RANGE(4, 40);
68 const char fontSizeSettingsKey[] = "consoleFontSize";
69
70 const struct {
71 const char *url;
72 const char *source;
73 } ICON_MAPPING[] = {
74 {"cmd-request", ":/icons/tx_input"},
75 {"cmd-reply", ":/icons/tx_output"},
76 {"cmd-error", ":/icons/tx_output"},
77 {"misc", ":/icons/tx_inout"},
78 {nullptr, nullptr}
79 };
80
81 static const RPCConsole::ThemeColors LIGHT_THEME_COLORS = {
82 .warning = QColor("#FF0000"),
83 .userinput = QColor("#007D32")
84 };
85
86 static const RPCConsole::ThemeColors DARK_THEME_COLORS = {
87 .warning = QColor("#FF8080"),
88 .userinput = QColor("#45DEB5")
89 };
90
91 namespace {
92
93 // don't add private key handling cmd's to the history
94 const QStringList historyFilter = QStringList()
95 << "backupwallet"
96 << "createwallet"
97 << "createwalletdescriptor"
98 << "deriveaddresses"
99 << "dumpmasterprivkey"
100 << "dumpprivkey"
101 << "dumpwallet"
102 << "encryptwallet"
103 << "getdescriptorinfo"
104 << "importprivkey"
105 << "importmulti"
106 << "listdescriptors"
107 << "migratewallet"
108 << "sethdseed"
109 << "signmessagewithprivkey"
110 << "signrawtransactionwithkey"
111 << "sweepprivkeys"
112 << "walletpassphrase"
113 << "walletpassphrasechange"
114 << "walletprocesspsbt";
115
116 }
117
118 /* Object for executing console RPC commands in a separate thread.
119 */
120 class RPCExecutor : public QObject
121 {
122 Q_OBJECT
123 public:
124 explicit RPCExecutor(interfaces::Node& node) : m_node(node) {}
125
126 public Q_SLOTS:
127 void request(const QString &command, const WalletModel* wallet_model);
128
129 Q_SIGNALS:
130 void reply(int category, const QString &command);
131
132 private:
133 interfaces::Node& m_node;
134 };
135
136 /** Class for handling RPC timers
137 * (used for e.g. re-locking the wallet after a timeout)
138 */
139 class QtRPCTimerBase: public QObject, public RPCTimerBase
140 {
141 Q_OBJECT
142 public:
143 QtRPCTimerBase(std::function<void()>& _func, int64_t millis):
144 func(_func)
145 {
146 timer.setSingleShot(true);
147 connect(&timer, &QTimer::timeout, [this]{ func(); });
148 timer.start(millis);
149 }
150 ~QtRPCTimerBase() = default;
151 private:
152 QTimer timer;
153 std::function<void()> func;
154 };
155
156 class QtRPCTimerInterface: public RPCTimerInterface
157 {
158 public:
159 ~QtRPCTimerInterface() = default;
160 const char *Name() override { return "Qt"; }
161 RPCTimerBase* NewTimer(std::function<void()>& func, int64_t millis) override
162 {
163 return new QtRPCTimerBase(func, millis);
164 }
165 };
166
167 class PeerIdViewDelegate : public QStyledItemDelegate
168 {
169 Q_OBJECT
170 public:
171 explicit PeerIdViewDelegate(QObject* parent = nullptr)
172 : QStyledItemDelegate(parent) {}
173
174 QString displayText(const QVariant& value, const QLocale& locale) const override
175 {
176 // Additional spaces should visually separate right-aligned content
177 // from the next column to the right.
178 return value.toString() + QLatin1String(" ");
179 }
180 };
181
182 #include <qt/rpcconsole.moc>
183
184 /**
185 * Split shell command line into a list of arguments and optionally execute the command(s).
186 * Aims to emulate \c bash and friends.
187 *
188 * - Command nesting is possible with parenthesis; for example: validateaddress(getnewaddress())
189 * - Arguments are delimited with whitespace or comma
190 * - Extra whitespace at the beginning and end and between arguments will be ignored
191 * - Text can be "double" or 'single' quoted
192 * - The backslash \c \ is used as escape character
193 * - Outside quotes, any character can be escaped
194 * - Within double quotes, only escape \c " and backslashes before a \c " or another backslash
195 * - Within single quotes, no escaping is possible and no special interpretation takes place
196 *
197 * @param[in] node optional node to execute command on
198 * @param[out] strResult stringified result from the executed command(chain)
199 * @param[in] strCommand Command line to split
200 * @param[in] fExecute set true if you want the command to be executed
201 * @param[out] pstrFilteredOut Command line, filtered to remove any sensitive data
202 */
203
204 bool RPCConsole::RPCParseCommandLine(interfaces::Node* node, std::string &strResult, const std::string &strCommand, const bool fExecute, std::string * const pstrFilteredOut, const WalletModel* wallet_model)
205 {
206 std::vector< std::vector<std::string> > stack;
207 stack.emplace_back();
208
209 enum CmdParseState
210 {
211 STATE_EATING_SPACES,
212 STATE_EATING_SPACES_IN_ARG,
213 STATE_EATING_SPACES_IN_BRACKETS,
214 STATE_ARGUMENT,
215 STATE_SINGLEQUOTED,
216 STATE_DOUBLEQUOTED,
217 STATE_ESCAPE_OUTER,
218 STATE_ESCAPE_DOUBLEQUOTED,
219 STATE_COMMAND_EXECUTED,
220 STATE_COMMAND_EXECUTED_INNER
221 } state = STATE_EATING_SPACES;
222 std::string curarg;
223 UniValue lastResult;
224 unsigned nDepthInsideSensitive = 0;
225 size_t filter_begin_pos = 0, chpos;
226 std::vector<std::pair<size_t, size_t>> filter_ranges;
227
228 auto add_to_current_stack = [&](const std::string& strArg) {
229 if (stack.back().empty() && (!nDepthInsideSensitive) && historyFilter.contains(QString::fromStdString(strArg), Qt::CaseInsensitive)) {
230 nDepthInsideSensitive = 1;
231 filter_begin_pos = chpos;
232 }
233 // Make sure stack is not empty before adding something
234 if (stack.empty()) {
235 stack.emplace_back();
236 }
237 stack.back().push_back(strArg);
238 };
239
240 auto close_out_params = [&]() {
241 if (nDepthInsideSensitive) {
242 if (!--nDepthInsideSensitive) {
243 assert(filter_begin_pos);
244 filter_ranges.emplace_back(filter_begin_pos, chpos);
245 filter_begin_pos = 0;
246 }
247 }
248 stack.pop_back();
249 };
250
251 std::string strCommandTerminated = strCommand;
252 if (strCommandTerminated.back() != '\n')
253 strCommandTerminated += "\n";
254 for (chpos = 0; chpos < strCommandTerminated.size(); ++chpos)
255 {
256 char ch = strCommandTerminated[chpos];
257 switch(state)
258 {
259 case STATE_COMMAND_EXECUTED_INNER:
260 case STATE_COMMAND_EXECUTED:
261 {
262 bool breakParsing = true;
263 switch(ch)
264 {
265 case '[': curarg.clear(); state = STATE_COMMAND_EXECUTED_INNER; break;
266 default:
267 if (state == STATE_COMMAND_EXECUTED_INNER)
268 {
269 if (ch != ']')
270 {
271 // append char to the current argument (which is also used for the query command)
272 curarg += ch;
273 break;
274 }
275 if (curarg.size() && fExecute)
276 {
277 // if we have a value query, query arrays with index and objects with a string key
278 UniValue subelement;
279 if (lastResult.isArray())
280 {
281 const auto parsed{ToIntegral<size_t>(curarg)};
282 if (!parsed) {
283 throw std::runtime_error("Invalid result query");
284 }
285 subelement = lastResult[parsed.value()];
286 }
287 else if (lastResult.isObject())
288 subelement = lastResult.find_value(curarg);
289 else
290 throw std::runtime_error("Invalid result query"); //no array or object: abort
291 lastResult = subelement;
292 }
293
294 state = STATE_COMMAND_EXECUTED;
295 break;
296 }
297 // don't break parsing when the char is required for the next argument
298 breakParsing = false;
299
300 // pop the stack and return the result to the current command arguments
301 close_out_params();
302
303 // don't stringify the json in case of a string to avoid doublequotes
304 if (lastResult.isStr())
305 curarg = lastResult.get_str();
306 else
307 curarg = lastResult.write(2);
308
309 // if we have a non empty result, use it as stack argument otherwise as general result
310 if (curarg.size())
311 {
312 if (stack.size())
313 add_to_current_stack(curarg);
314 else
315 strResult = curarg;
316 }
317 curarg.clear();
318 // assume eating space state
319 state = STATE_EATING_SPACES;
320 }
321 if (breakParsing)
322 break;
323 [[fallthrough]];
324 }
325 case STATE_ARGUMENT: // In or after argument
326 case STATE_EATING_SPACES_IN_ARG:
327 case STATE_EATING_SPACES_IN_BRACKETS:
328 case STATE_EATING_SPACES: // Handle runs of whitespace
329 switch(ch)
330 {
331 case '"': state = STATE_DOUBLEQUOTED; break;
332 case '\'': state = STATE_SINGLEQUOTED; break;
333 case '\\': state = STATE_ESCAPE_OUTER; break;
334 case '(': case ')': case '\n':
335 if (state == STATE_EATING_SPACES_IN_ARG)
336 throw std::runtime_error("Invalid Syntax");
337 if (state == STATE_ARGUMENT)
338 {
339 if (ch == '(' && stack.size() && stack.back().size() > 0)
340 {
341 if (nDepthInsideSensitive) {
342 ++nDepthInsideSensitive;
343 }
344 stack.emplace_back();
345 }
346
347 // don't allow commands after executed commands on baselevel
348 if (!stack.size())
349 throw std::runtime_error("Invalid Syntax");
350
351 add_to_current_stack(curarg);
352 curarg.clear();
353 state = STATE_EATING_SPACES_IN_BRACKETS;
354 }
355 if ((ch == ')' || ch == '\n') && stack.size() > 0)
356 {
357 if (fExecute) {
358 // Convert argument list to JSON objects in method-dependent way,
359 // and pass it along with the method name to the dispatcher.
360 UniValue params = RPCConvertValues(stack.back()[0], std::vector<std::string>(stack.back().begin() + 1, stack.back().end()));
361 std::string method = stack.back()[0];
362 std::string uri;
363 #ifdef ENABLE_WALLET
364 if (wallet_model) {
365 QByteArray encodedName = QUrl::toPercentEncoding(wallet_model->getWalletName());
366 uri = "/wallet/"+std::string(encodedName.constData(), encodedName.length());
367 }
368 #endif
369 assert(node);
370 lastResult = node->executeRpc(method, params, uri);
371 }
372
373 state = STATE_COMMAND_EXECUTED;
374 curarg.clear();
375 }
376 break;
377 case ' ': case ',': case '\t':
378 if(state == STATE_EATING_SPACES_IN_ARG && curarg.empty() && ch == ',')
379 throw std::runtime_error("Invalid Syntax");
380
381 else if(state == STATE_ARGUMENT) // Space ends argument
382 {
383 add_to_current_stack(curarg);
384 curarg.clear();
385 }
386 if ((state == STATE_EATING_SPACES_IN_BRACKETS || state == STATE_ARGUMENT) && ch == ',')
387 {
388 state = STATE_EATING_SPACES_IN_ARG;
389 break;
390 }
391 state = STATE_EATING_SPACES;
392 break;
393 default: curarg += ch; state = STATE_ARGUMENT;
394 }
395 break;
396 case STATE_SINGLEQUOTED: // Single-quoted string
397 switch(ch)
398 {
399 case '\'': state = STATE_ARGUMENT; break;
400 default: curarg += ch;
401 }
402 break;
403 case STATE_DOUBLEQUOTED: // Double-quoted string
404 switch(ch)
405 {
406 case '"': state = STATE_ARGUMENT; break;
407 case '\\': state = STATE_ESCAPE_DOUBLEQUOTED; break;
408 default: curarg += ch;
409 }
410 break;
411 case STATE_ESCAPE_OUTER: // '\' outside quotes
412 curarg += ch; state = STATE_ARGUMENT;
413 break;
414 case STATE_ESCAPE_DOUBLEQUOTED: // '\' in double-quoted text
415 if(ch != '"' && ch != '\\') curarg += '\\'; // keep '\' for everything but the quote and '\' itself
416 curarg += ch; state = STATE_DOUBLEQUOTED;
417 break;
418 }
419 }
420 if (pstrFilteredOut) {
421 if (STATE_COMMAND_EXECUTED == state) {
422 assert(!stack.empty());
423 close_out_params();
424 }
425 *pstrFilteredOut = strCommand;
426 for (auto i = filter_ranges.rbegin(); i != filter_ranges.rend(); ++i) {
427 pstrFilteredOut->replace(i->first, i->second - i->first, "(…)");
428 }
429 }
430 switch(state) // final state
431 {
432 case STATE_COMMAND_EXECUTED:
433 if (lastResult.isStr())
434 strResult = lastResult.get_str();
435 else
436 strResult = lastResult.write(2);
437 [[fallthrough]];
438 case STATE_ARGUMENT:
439 case STATE_EATING_SPACES:
440 return true;
441 default: // ERROR to end in one of the other states
442 return false;
443 }
444 }
445
446 void RPCExecutor::request(const QString &command, const WalletModel* wallet_model)
447 {
448 try
449 {
450 std::string result;
451 std::string executableCommand = command.toStdString() + "\n";
452
453 // Catch the console-only-help command before RPC call is executed and reply with help text as-if a RPC reply.
454 if(executableCommand == "help-console\n") {
455 Q_EMIT reply(RPCConsole::CMD_REPLY, QString(("\n"
456 "This console accepts RPC commands using the standard syntax.\n"
457 " example: getblockhash 0\n\n"
458
459 "This console can also accept RPC commands using the parenthesized syntax.\n"
460 " example: getblockhash(0)\n\n"
461
462 "Commands may be nested when specified with the parenthesized syntax.\n"
463 " example: getblock(getblockhash(0) 1)\n\n"
464
465 "A space or a comma can be used to delimit arguments for either syntax.\n"
466 " example: getblockhash 0\n"
467 " getblockhash,0\n\n"
468
469 "Named results can be queried with a non-quoted key string in brackets using the parenthesized syntax.\n"
470 " example: getblock(getblockhash(0) 1)[tx]\n\n"
471
472 "Results without keys can be queried with an integer in brackets using the parenthesized syntax.\n"
473 " example: getblock(getblockhash(0),1)[tx][0]\n\n"
474
475 "Console Commands:\n"
476 " /clearhistory Clears the command history and console output.\n\n")));
477 return;
478 }
479 if (!RPCConsole::RPCExecuteCommandLine(m_node, result, executableCommand, nullptr, wallet_model)) {
480 Q_EMIT reply(RPCConsole::CMD_ERROR, QString("Parse error: unbalanced ' or \""));
481 return;
482 }
483
484 Q_EMIT reply(RPCConsole::CMD_REPLY, QString::fromStdString(result));
485 }
486 catch (UniValue& objError)
487 {
488 try // Nice formatting for standard-format error
489 {
490 int code = objError.find_value("code").getInt<int>();
491 std::string message = objError.find_value("message").get_str();
492 Q_EMIT reply(RPCConsole::CMD_ERROR, QString::fromStdString(message) + " (code " + QString::number(code) + ")");
493 }
494 catch (const std::runtime_error&) // raised when converting to invalid type, i.e. missing code or message
495 { // Show raw JSON object
496 Q_EMIT reply(RPCConsole::CMD_ERROR, QString::fromStdString(objError.write()));
497 }
498 }
499 catch (const std::exception& e)
500 {
501 Q_EMIT reply(RPCConsole::CMD_ERROR, QString("Error: ") + QString::fromStdString(e.what()));
502 }
503 }
504
505 RPCConsole::RPCConsole(interfaces::Node& node, const PlatformStyle *_platformStyle, QWidget *parent) :
506 QWidget(parent),
507 m_node(node),
508 ui(new Ui::RPCConsole),
509 platformStyle(_platformStyle)
510 {
511 ui->setupUi(this);
512 updateThemeColors();
513
514 // Default tabs are identified by their UI index
515 for (int i = ui->tabWidget->count(); i--; ) {
516 m_tabs[TabTypes(i)] = ui->tabWidget->widget(i);
517 }
518
519 QSettings settings;
520 #ifdef ENABLE_WALLET
521 if (WalletModel::isWalletEnabled()) {
522 // RPCConsole widget is a window.
523 if (!restoreGeometry(settings.value("RPCConsoleWindowGeometry").toByteArray())) {
524 // Restore failed (perhaps missing setting), center the window
525 move(QGuiApplication::primaryScreen()->availableGeometry().center() - frameGeometry().center());
526 }
527 ui->splitter->restoreState(settings.value("RPCConsoleWindowPeersTabSplitterSizes_Knots23").toByteArray());
528 } else
529 #endif // ENABLE_WALLET
530 {
531 // RPCConsole is a child widget.
532 ui->splitter->restoreState(settings.value("RPCConsoleWidgetPeersTabSplitterSizes_Knots23").toByteArray());
533 }
534
535 m_peer_widget_header_state = settings.value("PeersTabPeerHeaderState_Knots23").toByteArray();
536 m_banlist_widget_header_state = settings.value("PeersTabBanlistHeaderState").toByteArray();
537 m_alternating_row_colors = settings.value("PeersTabAlternatingRowColors").toBool();
538
539 {
540 // Move everything down a row to make room
541 const int colCount = ui->gridLayout->columnCount();
542 for (int row{ui->gridLayout->rowCount()}; row > 6; --row) {
543 for (int col{0}; col < colCount; ++col) {
544 QLayoutItem* const layout_item = ui->gridLayout->itemAtPosition(row - 1, col);
545 if (!layout_item) continue;
546 const int index = ui->gridLayout->indexOf(layout_item);
547 int row_rb, col_rb, rowspan, colspan;
548 ui->gridLayout->getItemPosition(index, &row_rb, &col_rb, &rowspan, &colspan);
549 if (row_rb != row - 1 || col_rb != col) continue;
550 ui->gridLayout->takeAt(index);
551 ui->gridLayout->addItem(layout_item, row, col, rowspan, colspan);
552 }
553 }
554 }
555
556 constexpr QChar nonbreaking_hyphen(8209);
557 const std::vector<QString> CONNECTION_TYPE_DOC{
558 //: Explanatory text for an inbound peer connection.
559 tr("Inbound: initiated by peer"),
560 /*: Explanatory text for an outbound peer connection that
561 relays all network information. This is the default behavior for
562 outbound connections. */
563 tr("Outbound Full Relay: default"),
564 /*: Explanatory text for an outbound peer connection that relays
565 network information about blocks and not transactions or addresses. */
566 tr("Outbound Block Relay: does not relay transactions or addresses"),
567 /*: Explanatory text for an outbound peer connection that was
568 established manually through one of several methods. The numbered
569 arguments are stand-ins for the methods available to establish
570 manual connections. */
571 tr("Outbound Manual: added using RPC %1 or %2/%3 configuration options")
572 .arg("addnode")
573 .arg(QString(nonbreaking_hyphen) + "addnode")
574 .arg(QString(nonbreaking_hyphen) + "connect"),
575 /*: Explanatory text for a short-lived outbound peer connection that
576 is used to test the aliveness of known addresses. */
577 tr("Outbound Feeler: short-lived, for testing addresses"),
578 /*: Explanatory text for a short-lived outbound peer connection that is used
579 to request addresses from a peer. */
580 tr("Outbound Address Fetch: short-lived, for soliciting addresses")};
581 const QString connection_types_list{"<ul><li>" + Join(CONNECTION_TYPE_DOC, QString("</li><li>")) + "</li></ul>"};
582 ui->peerConnectionTypeLabel->setToolTip(ui->peerConnectionTypeLabel->toolTip().arg(connection_types_list));
583 const std::vector<QString> TRANSPORT_TYPE_DOC{
584 //: Explanatory text for "detecting" transport type.
585 tr("detecting: peer could be v1 or v2"),
586 //: Explanatory text for v1 transport type.
587 tr("v1: unencrypted, plaintext transport protocol"),
588 //: Explanatory text for v2 transport type.
589 tr("v2: BIP324 encrypted transport protocol")};
590 const QString transport_types_list{"<ul><li>" + Join(TRANSPORT_TYPE_DOC, QString("</li><li>")) + "</li></ul>"};
591 ui->peerTransportTypeLabel->setToolTip(ui->peerTransportTypeLabel->toolTip().arg(transport_types_list));
592 const QString hb_list{"<ul><li>\""
593 + ts.to + "\" – " + tr("we selected the peer for high bandwidth relay") + "</li><li>\""
594 + ts.from + "\" – " + tr("the peer selected us for high bandwidth relay") + "</li><li>\""
595 + ts.no + "\" – " + tr("no high bandwidth relay selected") + "</li></ul>"};
596 ui->peerHighBandwidthLabel->setToolTip(ui->peerHighBandwidthLabel->toolTip().arg(hb_list));
597 ui->dataDir->setToolTip(ui->dataDir->toolTip().arg(QString(nonbreaking_hyphen) + "datadir"));
598 ui->blocksDir->setToolTip(ui->blocksDir->toolTip().arg(QString(nonbreaking_hyphen) + "blocksdir"));
599 ui->openDebugLogfileButton->setToolTip(ui->openDebugLogfileButton->toolTip().arg(CLIENT_NAME));
600
601 if (platformStyle->getImagesOnButtons()) {
602 ui->openDebugLogfileButton->setIcon(platformStyle->SingleColorIcon(":/icons/export"));
603 }
604 ui->clearButton->setIcon(platformStyle->SingleColorIcon(":/icons/remove"));
605
606 ui->fontBiggerButton->setIcon(platformStyle->SingleColorIcon(":/icons/fontbigger"));
607 //: Main shortcut to increase the RPC console font size.
608 ui->fontBiggerButton->setShortcut(tr("Ctrl++"));
609 //: Secondary shortcut to increase the RPC console font size.
610 GUIUtil::AddButtonShortcut(ui->fontBiggerButton, tr("Ctrl+="));
611
612 ui->fontSmallerButton->setIcon(platformStyle->SingleColorIcon(":/icons/fontsmaller"));
613 //: Main shortcut to decrease the RPC console font size.
614 ui->fontSmallerButton->setShortcut(tr("Ctrl+-"));
615 //: Secondary shortcut to decrease the RPC console font size.
616 GUIUtil::AddButtonShortcut(ui->fontSmallerButton, tr("Ctrl+_"));
617
618 ui->promptIcon->setIcon(platformStyle->SingleColorIcon(QStringLiteral(":/icons/prompticon")));
619
620 // Install event filter for up and down arrow
621 ui->lineEdit->installEventFilter(this);
622 ui->lineEdit->setMaxLength(16 * 1024 * 1024);
623 ui->messagesWidget->installEventFilter(this);
624
625 connect(ui->hidePeersDetailButton, &QAbstractButton::clicked, this, &RPCConsole::clearSelectedNode);
626 connect(ui->clearButton, &QAbstractButton::clicked, [this] { clear(); });
627 connect(ui->fontBiggerButton, &QAbstractButton::clicked, this, &RPCConsole::fontBigger);
628 connect(ui->fontSmallerButton, &QAbstractButton::clicked, this, &RPCConsole::fontSmaller);
629 connect(ui->btnClearTrafficGraph, &QPushButton::clicked, ui->trafficGraph, &TrafficGraphWidget::clear);
630
631 // disable the wallet selector by default
632 ui->WalletSelector->setVisible(false);
633 ui->WalletSelectorLabel->setVisible(false);
634
635 // Register RPC timer interface
636 rpcTimerInterface = new QtRPCTimerInterface();
637 // avoid accidentally overwriting an existing, non QTThread
638 // based timer interface
639 m_node.rpcSetTimerInterfaceIfUnset(rpcTimerInterface);
640
641 setTrafficGraphRange(INITIAL_TRAFFIC_GRAPH_MINS);
642 updateDetailWidget();
643
644 consoleFontSize = settings.value(fontSizeSettingsKey, QFont().pointSize()).toInt();
645 clear();
646
647 // load history
648 QMap<size_t, QString> rewrite_replace;
649 int size = settings.beginReadArray("nRPCConsoleWindowHistory");
650 history.clear();
651 for (int i = 0; i < size; ++i) {
652 settings.setArrayIndex(i);
653 QString cmd = settings.value("cmd").toString();
654 QString filtered_cmd;
655 {
656 std::string strFilteredCmd, dummy;
657 if (RPCParseCommandLine(nullptr, dummy, cmd.toStdString(), false, &strFilteredCmd)) {
658 filtered_cmd = QString::fromStdString(strFilteredCmd);
659 } else {
660 // Failed to parse command, so we cannot even filter it for the history
661 filtered_cmd = cmd;
662 }
663 }
664 if (cmd != filtered_cmd) {
665 // Overwrite this line, and trigger an immediate rewrite of history to purge it
666 cmd = QString(cmd.size(), 'x');
667 rewrite_replace[history.size()] = filtered_cmd;
668 }
669 history.append(cmd);
670 }
671 historyPtr = history.size();
672 settings.endArray();
673 if (!rewrite_replace.empty()) {
674 WriteCommandHistory();
675 for (QMapIterator<size_t, QString> i(rewrite_replace); i.hasNext(); ) {
676 i.next();
677 history[i.key()] = i.value();
678 }
679 WriteCommandHistory();
680 }
681
682 GUIUtil::handleCloseWindowShortcut(this);
683
684 QObject::connect(new QShortcut(QKeySequence(QStringLiteral("Ctrl+D")), ui->tab_console), &QShortcut::activated, this, &QWidget::close);
685
686 updateWindowTitle();
687 }
688
689 void RPCConsole::WriteCommandHistory()
690 {
691 // persist history
692 QSettings settings;
693 settings.beginWriteArray("nRPCConsoleWindowHistory");
694 for (int i = 0; i < history.size(); ++i) {
695 settings.setArrayIndex(i);
696 settings.setValue("cmd", history.at(i));
697 }
698 settings.endArray();
699 }
700
701 void RPCConsole::ClearCommandHistory()
702 {
703 // First pass: read existing commands and overwrite with dummy data of same length
704 QSettings settings;
705 int size = settings.beginReadArray("nRPCConsoleWindowHistory");
706 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
707 history.resize(size);
708 #else
709 if (history.size() > size) {
710 history.erase(history.begin() + size, history.end());
711 } else {
712 history.reserve(size);
713 for (int i = history.size(); i < size; ++i) {
714 history.append(QString());
715 }
716 }
717 #endif
718 for (int i = 0; i < size; ++i) {
719 settings.setArrayIndex(i);
720 QString cmd = settings.value("cmd").toString();
721 // Store dummy command with same length as original (don't leak length info)
722 history[i].fill('x', cmd.size());
723 }
724 settings.endArray();
725
726 // Write dummy data to overwrite the original commands
727 WriteCommandHistory();
728
729 // Clear the dummy data (leaves it intact on disk)
730 for (QStringList::size_type i = history.size(); i; ) {
731 --i;
732 history[i].clear();
733 }
734 WriteCommandHistory();
735
736 // Clear the history list
737 history.clear();
738 WriteCommandHistory();
739
740 historyPtr = 0;
741 cmdBeforeBrowsing.clear();
742 }
743
744 RPCConsole::~RPCConsole()
745 {
746 QSettings settings;
747 #ifdef ENABLE_WALLET
748 if (WalletModel::isWalletEnabled()) {
749 // RPCConsole widget is a window.
750 settings.setValue("RPCConsoleWindowGeometry", saveGeometry());
751 settings.setValue("RPCConsoleWindowPeersTabSplitterSizes_Knots23", ui->splitter->saveState());
752 } else
753 #endif // ENABLE_WALLET
754 {
755 // RPCConsole is a child widget.
756 settings.setValue("RPCConsoleWidgetPeersTabSplitterSizes_Knots23", ui->splitter->saveState());
757 }
758
759 settings.setValue("PeersTabPeerHeaderState_Knots23", m_peer_widget_header_state);
760 settings.setValue("PeersTabBanlistHeaderState", m_banlist_widget_header_state);
761
762 WriteCommandHistory();
763
764 m_node.rpcUnsetTimerInterface(rpcTimerInterface);
765 delete rpcTimerInterface;
766 delete ui;
767 }
768
769 bool RPCConsole::eventFilter(QObject* obj, QEvent *event)
770 {
771 if(event->type() == QEvent::KeyPress) // Special key handling
772 {
773 QKeyEvent *keyevt = static_cast<QKeyEvent*>(event);
774 int key = keyevt->key();
775 Qt::KeyboardModifiers mod = keyevt->modifiers();
776 switch(key)
777 {
778 case Qt::Key_Up: if(obj == ui->lineEdit) { browseHistory(-1); return true; } break;
779 case Qt::Key_Down: if(obj == ui->lineEdit) { browseHistory(1); return true; } break;
780 case Qt::Key_PageUp: /* pass paging keys to messages widget */
781 case Qt::Key_PageDown:
782 if (obj == ui->lineEdit) {
783 QApplication::sendEvent(ui->messagesWidget, keyevt);
784 return true;
785 }
786 break;
787 case Qt::Key_Return:
788 case Qt::Key_Enter:
789 // forward these events to lineEdit
790 if (obj == autoCompleter->popup()) {
791 QApplication::sendEvent(ui->lineEdit, keyevt);
792 autoCompleter->popup()->hide();
793 return true;
794 }
795 break;
796 default:
797 // Typing in messages widget brings focus to line edit, and redirects key there
798 // Exclude most combinations and keys that emit no text, except paste shortcuts
799 if(obj == ui->messagesWidget && (
800 (!mod && !keyevt->text().isEmpty() && key != Qt::Key_Tab) ||
801 ((mod & Qt::ControlModifier) && key == Qt::Key_V) ||
802 ((mod & Qt::ShiftModifier) && key == Qt::Key_Insert)))
803 {
804 ui->lineEdit->setFocus();
805 QApplication::sendEvent(ui->lineEdit, keyevt);
806 return true;
807 }
808 }
809 }
810 return QWidget::eventFilter(obj, event);
811 }
812
813 void RPCConsole::setClientModel(ClientModel *model, int bestblock_height, int64_t bestblock_date, double verification_progress)
814 {
815 clientModel = model;
816
817 bool wallet_enabled{false};
818 #ifdef ENABLE_WALLET
819 wallet_enabled = WalletModel::isWalletEnabled();
820 #endif // ENABLE_WALLET
821 if (model && !wallet_enabled) {
822 // Show warning, for example if this is a prerelease version
823 connect(model, &ClientModel::alertsChanged, this, &RPCConsole::updateAlerts);
824 updateAlerts(model->getStatusBarWarnings());
825 }
826
827 ui->trafficGraph->setClientModel(model);
828 if (m_tab_pairing) m_tab_pairing->setClientModel(model);
829 if (model && clientModel->getPeerTableModel() && clientModel->getBanTableModel()) {
830 // Keep up to date with client
831 setNumConnections(model->getNumConnections());
832 connect(model, &ClientModel::numConnectionsChanged, this, &RPCConsole::setNumConnections);
833
834 setNumBlocks(bestblock_height, QDateTime::fromSecsSinceEpoch(bestblock_date), verification_progress, SyncType::BLOCK_SYNC);
835 connect(model, &ClientModel::numBlocksChanged, this, &RPCConsole::setNumBlocks);
836
837 updateNetworkState();
838 connect(model, &ClientModel::networkActiveChanged, this, &RPCConsole::setNetworkActive);
839
840 interfaces::Node& node = clientModel->node();
841 updateTrafficStats(node.getTotalBytesRecv(), node.getTotalBytesSent());
842 connect(model, &ClientModel::bytesChanged, this, &RPCConsole::updateTrafficStats);
843
844 connect(model, &ClientModel::mempoolSizeChanged, this, &RPCConsole::setMempoolSize);
845
846 connect(model->getOptionsModel(), &OptionsModel::peersTabAlternatingRowColorsChanged, [this](bool alternating_row_colors) {
847 ui->peerWidget->setAlternatingRowColors(alternating_row_colors);
848 ui->banlistWidget->setAlternatingRowColors(alternating_row_colors);
849 });
850
851 // set up peer table
852 clientModel->getPeerTableModel()->updatePalette();
853 ui->peerWidget->setModel(model->peerTableSortProxy());
854 ui->peerWidget->verticalHeader()->hide();
855 ui->peerWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
856 ui->peerWidget->setSelectionMode(QAbstractItemView::ExtendedSelection);
857 ui->peerWidget->setContextMenuPolicy(Qt::CustomContextMenu);
858
859 if (!ui->peerWidget->horizontalHeader()->restoreState(m_peer_widget_header_state)) {
860 const QFontMetrics fm = ui->peerWidget->fontMetrics();
861 ui->peerWidget->setColumnWidth(PeerTableModel::NetNodeId, GUIUtil::TextWidth(fm, QStringLiteral("99999")));
862 ui->peerWidget->setColumnWidth(PeerTableModel::Age, GUIUtil::TextWidth(fm, GUIUtil::FormatPeerAge(std::chrono::hours{23976 /* 999 days */})));
863 ui->peerWidget->setColumnWidth(PeerTableModel::Direction, DIRECTION_COLUMN_WIDTH);
864 ui->peerWidget->setColumnWidth(PeerTableModel::Address, ADDRESS_COLUMN_WIDTH);
865 ui->peerWidget->setColumnWidth(PeerTableModel::ConnectionType, GUIUtil::TextWidth(fm, GUIUtil::ConnectionTypeToQString(ConnectionType::ADDR_FETCH /* TODO: Find the WIDEST string? */, /*prepend_direction=*/false)));
866 const auto bytesize_width = GUIUtil::TextWidth(fm, GUIUtil::formatBytes(999'000'000'000) + QStringLiteral("xx"));
867 ui->peerWidget->setColumnWidth(PeerTableModel::Subversion, SUBVERSION_COLUMN_WIDTH);
868 ui->peerWidget->setColumnWidth(PeerTableModel::Ping, PING_COLUMN_WIDTH);
869 ui->peerWidget->setColumnWidth(PeerTableModel::Sent, bytesize_width);
870 ui->peerWidget->setColumnWidth(PeerTableModel::Received, bytesize_width);
871 }
872 ui->peerWidget->horizontalHeader()->setStretchLastSection(true);
873 ui->peerWidget->setItemDelegateForColumn(PeerTableModel::NetNodeId, new PeerIdViewDelegate(this));
874 ui->peerWidget->setAlternatingRowColors(m_alternating_row_colors);
875
876 // create peer table context menu
877 peersTableContextMenu = new QMenu(this);
878 //: Context menu action to copy the address of a peer.
879 peersTableContextMenu->addAction(tr("&Copy address"), [this] {
880 GUIUtil::copyEntryData(ui->peerWidget, PeerTableModel::Address, Qt::DisplayRole);
881 });
882 peersTableContextMenu->addSeparator();
883 peersTableContextMenu->addAction(tr("&Disconnect"), this, &RPCConsole::disconnectSelectedNode);
884 peersTableContextMenu->addAction(ts.ban_for + " " + tr("1 &hour"), [this] { banSelectedNode(60 * 60); });
885 peersTableContextMenu->addAction(ts.ban_for + " " + tr("1 d&ay"), [this] { banSelectedNode(60 * 60 * 24); });
886 peersTableContextMenu->addAction(ts.ban_for + " " + tr("1 &week"), [this] { banSelectedNode(60 * 60 * 24 * 7); });
887 peersTableContextMenu->addAction(ts.ban_for + " " + tr("1 &year"), [this] { banSelectedNode(60 * 60 * 24 * 365); });
888 connect(ui->peerWidget, &QTableView::customContextMenuRequested, this, &RPCConsole::showPeersTableContextMenu);
889
890 // peer table signal handling - update peer details when selecting new node
891 connect(ui->peerWidget->selectionModel(), &QItemSelectionModel::selectionChanged, [this] {
892 resetDetailWidget();
893 updateDetailWidget();
894 });
895 connect(model->getPeerTableModel(), &QAbstractItemModel::dataChanged, [this] { updateDetailWidget(); });
896
897 // set up ban table
898 ui->banlistWidget->setModel(model->getBanTableModel());
899 ui->banlistWidget->verticalHeader()->hide();
900 ui->banlistWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
901 ui->banlistWidget->setSelectionMode(QAbstractItemView::SingleSelection);
902 ui->banlistWidget->setContextMenuPolicy(Qt::CustomContextMenu);
903
904 if (!ui->banlistWidget->horizontalHeader()->restoreState(m_banlist_widget_header_state)) {
905 ui->banlistWidget->setColumnWidth(BanTableModel::Address, BANSUBNET_COLUMN_WIDTH);
906 ui->banlistWidget->setColumnWidth(BanTableModel::Bantime, BANTIME_COLUMN_WIDTH);
907 }
908 ui->banlistWidget->horizontalHeader()->setStretchLastSection(true);
909 ui->banlistWidget->setAlternatingRowColors(m_alternating_row_colors);
910
911 // create ban table context menu
912 banTableContextMenu = new QMenu(this);
913 /*: Context menu action to copy the IP/Netmask of a banned peer.
914 IP/Netmask is the combination of a peer's IP address and its Netmask.
915 For IP address, see: https://en.wikipedia.org/wiki/IP_address. */
916 banTableContextMenu->addAction(tr("&Copy IP/Netmask"), [this] {
917 GUIUtil::copyEntryData(ui->banlistWidget, BanTableModel::Address, Qt::DisplayRole);
918 });
919 banTableContextMenu->addSeparator();
920 banTableContextMenu->addAction(tr("&Unban"), this, &RPCConsole::unbanSelectedNode);
921 connect(ui->banlistWidget, &QTableView::customContextMenuRequested, this, &RPCConsole::showBanTableContextMenu);
922
923 // ban table signal handling - clear peer details when clicking a peer in the ban table
924 connect(ui->banlistWidget, &QTableView::clicked, this, &RPCConsole::clearSelectedNode);
925 // ban table signal handling - ensure ban table is shown or hidden (if empty)
926 connect(model->getBanTableModel(), &BanTableModel::layoutChanged, this, &RPCConsole::showOrHideBanTableIfRequired);
927 showOrHideBanTableIfRequired();
928
929 // Provide initial values
930 ui->clientVersion->setText(model->formatFullVersion());
931 ui->clientUserAgent->setText(model->formatSubVersion());
932 ui->dataDir->setText(model->dataDir());
933 ui->blocksDir->setText(model->blocksDir());
934 ui->startupTime->setText(model->formatClientStartupTime());
935 ui->networkName->setText(QString::fromStdString(Params().GetChainTypeString()));
936
937 //Setup autocomplete and attach it
938 QStringList wordList;
939 std::vector<std::string> commandList = m_node.listRpcCommands();
940 for (size_t i = 0; i < commandList.size(); ++i)
941 {
942 wordList << commandList[i].c_str();
943 wordList << ("help " + commandList[i]).c_str();
944 }
945
946 wordList << "help-console";
947 wordList << "/clearhistory";
948 wordList.sort();
949 autoCompleter = new QCompleter(wordList, this);
950 autoCompleter->setModelSorting(QCompleter::CaseSensitivelySortedModel);
951 // ui->lineEdit is initially disabled because running commands is only
952 // possible from now on.
953 ui->lineEdit->setEnabled(true);
954 ui->lineEdit->setCompleter(autoCompleter);
955 autoCompleter->popup()->installEventFilter(this);
956 // Start thread to execute RPC commands.
957 startExecutor();
958 }
959 if (!model) {
960 // Client model is being set to 0, this means shutdown() is about to be called.
961 thread.quit();
962 thread.wait();
963 }
964 }
965
966 void RPCConsole::addPairingTab()
967 {
968 assert(!m_tab_pairing);
969 m_tab_pairing = new PairingPage(this);
970 ui->tabWidget->insertTab(1, m_tab_pairing, tr("&Pairing"));
971 m_tabs[TabTypes::PAIRING] = m_tab_pairing;
972 if (clientModel) m_tab_pairing->setClientModel(clientModel);
973 }
974
975 #ifdef ENABLE_WALLET
976 void RPCConsole::addWallet(WalletModel * const walletModel)
977 {
978 // use name for text and wallet model for internal data object (to allow to move to a wallet id later)
979 ui->WalletSelector->addItem(walletModel->getDisplayName(), QVariant::fromValue(walletModel));
980 if (ui->WalletSelector->count() == 2) {
981 // First wallet added, set to default to match wallet RPC behavior
982 ui->WalletSelector->setCurrentIndex(1);
983 }
984 if (ui->WalletSelector->count() > 2) {
985 ui->WalletSelector->setVisible(true);
986 ui->WalletSelectorLabel->setVisible(true);
987 }
988 }
989
990 void RPCConsole::removeWallet(WalletModel * const walletModel)
991 {
992 ui->WalletSelector->removeItem(ui->WalletSelector->findData(QVariant::fromValue(walletModel)));
993 if (ui->WalletSelector->count() == 2) {
994 ui->WalletSelector->setVisible(false);
995 ui->WalletSelectorLabel->setVisible(false);
996 }
997 }
998
999 void RPCConsole::setCurrentWallet(WalletModel* const wallet_model)
1000 {
1001 QVariant data = QVariant::fromValue(wallet_model);
1002 ui->WalletSelector->setCurrentIndex(ui->WalletSelector->findData(data));
1003 }
1004 #endif
1005
1006 static QString categoryClass(int category)
1007 {
1008 switch(category)
1009 {
1010 case RPCConsole::CMD_REQUEST: return "cmd-request"; break;
1011 case RPCConsole::CMD_REPLY: return "cmd-reply"; break;
1012 case RPCConsole::CMD_ERROR: return "cmd-error"; break;
1013 default: return "misc";
1014 }
1015 }
1016
1017 void RPCConsole::fontBigger()
1018 {
1019 setFontSize(consoleFontSize+1);
1020 }
1021
1022 void RPCConsole::fontSmaller()
1023 {
1024 setFontSize(consoleFontSize-1);
1025 }
1026
1027 void RPCConsole::setFontSize(int newSize)
1028 {
1029 QSettings settings;
1030
1031 //don't allow an insane font size
1032 if (newSize < FONT_RANGE.width() || newSize > FONT_RANGE.height())
1033 return;
1034
1035 // temp. store the console content
1036 QString str = ui->messagesWidget->toHtml();
1037
1038 // replace font tags size in current content
1039 str.replace(QString("font-size:%1pt").arg(consoleFontSize), QString("font-size:%1pt").arg(newSize));
1040
1041 // store the new font size
1042 consoleFontSize = newSize;
1043 settings.setValue(fontSizeSettingsKey, consoleFontSize);
1044
1045 // clear console (reset icon sizes, default stylesheet) and re-add the content
1046 float oldPosFactor = 1.0 / ui->messagesWidget->verticalScrollBar()->maximum() * ui->messagesWidget->verticalScrollBar()->value();
1047 clear(/*keep_prompt=*/true);
1048 ui->messagesWidget->setHtml(str);
1049 ui->messagesWidget->verticalScrollBar()->setValue(oldPosFactor * ui->messagesWidget->verticalScrollBar()->maximum());
1050 }
1051
1052 void RPCConsole::clear(bool keep_prompt)
1053 {
1054 ui->messagesWidget->clear();
1055 if (!keep_prompt) ui->lineEdit->clear();
1056 ui->lineEdit->setFocus();
1057
1058 // Add smoothly scaled icon images.
1059 // (when using width/height on an img, Qt uses nearest instead of linear interpolation)
1060 for(int i=0; ICON_MAPPING[i].url; ++i)
1061 {
1062 ui->messagesWidget->document()->addResource(
1063 QTextDocument::ImageResource,
1064 QUrl(ICON_MAPPING[i].url),
1065 platformStyle->SingleColorImage(ICON_MAPPING[i].source).scaled(QSize(consoleFontSize*2, consoleFontSize*2), Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
1066 }
1067
1068 // set default stylesheet
1069 updateConsoleStyleSheet();
1070
1071 static const QString welcome_message =
1072 /*: RPC console welcome message.
1073 Placeholders %7 and %8 are style tags for the warning content, and
1074 they are not space separated from the rest of the text intentionally. */
1075 tr("Welcome to the %1 RPC console.\n"
1076 "Use up and down arrows to navigate history, and %2 to clear screen.\n"
1077 "Use %3 and %4 to increase or decrease the font size.\n"
1078 "Type %5 for an overview of available commands.\n"
1079 "For more information on using this console, type %6.\n"
1080 "\n"
1081 "%7WARNING: Scammers have been active, telling users to type"
1082 " commands here, stealing their wallet contents. Do not use this console"
1083 " without fully understanding the ramifications of a command.%8")
1084 .arg(CLIENT_NAME,
1085 "<b>" + ui->clearButton->shortcut().toString(QKeySequence::NativeText) + "</b>",
1086 "<b>" + ui->fontBiggerButton->shortcut().toString(QKeySequence::NativeText) + "</b>",
1087 "<b>" + ui->fontSmallerButton->shortcut().toString(QKeySequence::NativeText) + "</b>",
1088 "<b>help</b>",
1089 "<b>help-console</b>",
1090 "<span class=\"secwarning\">",
1091 "<span>");
1092
1093 message(CMD_REPLY, welcome_message, true);
1094 }
1095
1096 void RPCConsole::keyPressEvent(QKeyEvent *event)
1097 {
1098 if (windowType() != Qt::Widget && GUIUtil::IsEscapeOrBack(event->key())) {
1099 close();
1100 }
1101 }
1102
1103 void RPCConsole::changeEvent(QEvent* e)
1104 {
1105 if (e->type() == QEvent::PaletteChange) {
1106 ui->clearButton->setIcon(platformStyle->SingleColorIcon(QStringLiteral(":/icons/remove")));
1107 ui->fontBiggerButton->setIcon(platformStyle->SingleColorIcon(QStringLiteral(":/icons/fontbigger")));
1108 ui->fontSmallerButton->setIcon(platformStyle->SingleColorIcon(QStringLiteral(":/icons/fontsmaller")));
1109 ui->promptIcon->setIcon(platformStyle->SingleColorIcon(QStringLiteral(":/icons/prompticon")));
1110
1111 for (int i = 0; ICON_MAPPING[i].url; ++i) {
1112 ui->messagesWidget->document()->addResource(
1113 QTextDocument::ImageResource,
1114 QUrl(ICON_MAPPING[i].url),
1115 platformStyle->SingleColorImage(ICON_MAPPING[i].source).scaled(QSize(consoleFontSize * 2, consoleFontSize * 2), Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
1116 }
1117
1118 if (clientModel && clientModel->getPeerTableModel()) {
1119 clientModel->getPeerTableModel()->updatePalette();
1120 }
1121
1122 updateThemeColors();
1123 }
1124
1125 QWidget::changeEvent(e);
1126 }
1127
1128 void RPCConsole::message(int category, const QString &message, bool html)
1129 {
1130 QTime time = QTime::currentTime();
1131 QString timeString = time.toString();
1132 QString out;
1133 out += "<table><tr><td class=\"time\" width=\"65\">" + timeString + "</td>";
1134 out += "<td class=\"icon\" width=\"32\"><img src=\"" + categoryClass(category) + "\"></td>";
1135 out += "<td class=\"message " + categoryClass(category) + "\" valign=\"middle\">";
1136 if(html)
1137 out += message;
1138 else
1139 out += GUIUtil::HtmlEscape(message, false);
1140 out += "</td></tr></table>";
1141 ui->messagesWidget->append(out);
1142 }
1143
1144 void RPCConsole::updateNetworkState()
1145 {
1146 if (!clientModel) return;
1147 QString connections = QString::number(clientModel->getNumConnections()) + " (";
1148 connections += tr("In:") + " " + QString::number(clientModel->getNumConnections(CONNECTIONS_IN)) + " / ";
1149 connections += tr("Out:") + " " + QString::number(clientModel->getNumConnections(CONNECTIONS_OUT)) + ")";
1150
1151 if(!clientModel->node().getNetworkActive()) {
1152 connections += " (" + tr("Network activity disabled") + ")";
1153 }
1154
1155 ui->numberOfConnections->setText(connections);
1156
1157 QString local_addresses;
1158 std::map<CNetAddr, LocalServiceInfo> hosts = clientModel->getNetLocalAddresses();
1159 for (const auto& [addr, info] : hosts) {
1160 local_addresses += QString::fromStdString(addr.ToStringAddr());
1161 if (!addr.IsI2P()) local_addresses += ":" + QString::number(info.nPort);
1162 local_addresses += ", ";
1163 }
1164 local_addresses.chop(2); // remove last ", "
1165 if (local_addresses.isEmpty()) local_addresses = tr("None");
1166
1167 ui->localAddresses->setText(local_addresses);
1168 }
1169
1170 void RPCConsole::setNumConnections(int count)
1171 {
1172 if (!clientModel)
1173 return;
1174
1175 updateNetworkState();
1176 }
1177
1178 void RPCConsole::setNetworkActive(bool networkActive)
1179 {
1180 updateNetworkState();
1181 }
1182
1183 void RPCConsole::setNumBlocks(int count, const QDateTime& blockDate, double nVerificationProgress, SyncType synctype)
1184 {
1185 if (synctype == SyncType::BLOCK_SYNC) {
1186 ui->numberOfBlocks->setText(QString::number(count));
1187 ui->lastBlockTime->setText(blockDate.toString());
1188 }
1189 }
1190
1191 void RPCConsole::setMempoolSize(long numberOfTxs, size_t dynUsage, size_t maxUsage)
1192 {
1193 ui->mempoolNumberTxs->setText(QString::number(numberOfTxs));
1194
1195 const auto cur_usage_str = dynUsage < 1000000 ?
1196 QObject::tr("%1 kB").arg(dynUsage / 1000.0, 0, 'f', 2) :
1197 QObject::tr("%1 MB").arg(dynUsage / 1000000.0, 0, 'f', 2);
1198 const auto max_usage_str = QObject::tr("%1 MB").arg(maxUsage / 1000000.0, 0, 'f', 2);
1199
1200 ui->mempoolSize->setText(cur_usage_str + " / " + max_usage_str);
1201 }
1202
1203 void RPCConsole::on_lineEdit_returnPressed()
1204 {
1205 QString cmd = ui->lineEdit->text().trimmed();
1206
1207 if (cmd.isEmpty()) {
1208 return;
1209 }
1210
1211 std::string strFilteredCmd;
1212 try {
1213 std::string dummy;
1214 if (!RPCParseCommandLine(nullptr, dummy, cmd.toStdString(), false, &strFilteredCmd)) {
1215 // Failed to parse command, so we cannot even filter it for the history
1216 throw std::runtime_error("Invalid command line");
1217 }
1218 } catch (const std::exception& e) {
1219 QMessageBox::critical(this, "Error", QString("Error: ") + QString::fromStdString(e.what()));
1220 return;
1221 }
1222
1223 // A special case allows to request shutdown even a long-running command is executed.
1224 if (cmd == QLatin1String("stop")) {
1225 std::string dummy;
1226 RPCExecuteCommandLine(m_node, dummy, cmd.toStdString());
1227 return;
1228 }
1229
1230 // Special command to clear command history
1231 if (cmd == QLatin1String("/clearhistory")) {
1232 QMessageBox::StandardButton reply = QMessageBox::question(this,
1233 tr("Clear Command History"),
1234 tr("This will permanently clear your command history and console output.<br><br>"
1235 "While this action is irreversible, complete removal from memory and disk "
1236 "cannot be guaranteed.<br><br>"
1237 "Are you sure you want to proceed?"),
1238 QMessageBox::Yes | QMessageBox::No,
1239 QMessageBox::No);
1240
1241 if (reply == QMessageBox::Yes) {
1242 ClearCommandHistory();
1243 clear(/*keep_prompt=*/false); // Clear console output too
1244 message(CMD_REPLY, tr("Command history and console output cleared."));
1245 }
1246 ui->lineEdit->clear();
1247 return;
1248 }
1249
1250 if (m_is_executing) {
1251 return;
1252 }
1253
1254 ui->lineEdit->clear();
1255
1256 WalletModel* wallet_model{nullptr};
1257 #ifdef ENABLE_WALLET
1258 wallet_model = ui->WalletSelector->currentData().value<WalletModel*>();
1259
1260 if (m_last_wallet_model != wallet_model) {
1261 if (wallet_model) {
1262 message(CMD_REQUEST, tr("Executing command using \"%1\" wallet").arg(wallet_model->getWalletName()));
1263 } else {
1264 message(CMD_REQUEST, tr("Executing command without any wallet"));
1265 }
1266 m_last_wallet_model = wallet_model;
1267 }
1268 #endif // ENABLE_WALLET
1269
1270 message(CMD_REQUEST, QString::fromStdString(strFilteredCmd));
1271 //: A console message indicating an entered command is currently being executed.
1272 message(CMD_REPLY, tr("Executing…"));
1273 m_is_executing = true;
1274
1275 QMetaObject::invokeMethod(m_executor, [this, cmd, wallet_model] {
1276 m_executor->request(cmd, wallet_model);
1277 });
1278
1279 cmd = QString::fromStdString(strFilteredCmd);
1280
1281 // Remove command, if already in history
1282 history.removeOne(cmd);
1283 // Append command to history
1284 history.append(cmd);
1285 // Enforce maximum history size
1286 while (history.size() > CONSOLE_HISTORY) {
1287 history.removeFirst();
1288 }
1289 // Set pointer to end of history
1290 historyPtr = history.size();
1291
1292 // Scroll console view to end
1293 scrollToEnd();
1294 }
1295
1296 void RPCConsole::browseHistory(int offset)
1297 {
1298 // store current text when start browsing through the history
1299 if (historyPtr == history.size()) {
1300 cmdBeforeBrowsing = ui->lineEdit->text();
1301 }
1302
1303 historyPtr += offset;
1304 if(historyPtr < 0)
1305 historyPtr = 0;
1306 if(historyPtr > history.size())
1307 historyPtr = history.size();
1308 QString cmd;
1309 if(historyPtr < history.size())
1310 cmd = history.at(historyPtr);
1311 else if (!cmdBeforeBrowsing.isNull()) {
1312 cmd = cmdBeforeBrowsing;
1313 }
1314 ui->lineEdit->setText(cmd);
1315 }
1316
1317 void RPCConsole::startExecutor()
1318 {
1319 m_executor = new RPCExecutor(m_node);
1320 m_executor->moveToThread(&thread);
1321
1322 // Replies from executor object must go to this object
1323 connect(m_executor, &RPCExecutor::reply, this, [this](int category, const QString& command) {
1324 // Remove "Executing…" message.
1325 ui->messagesWidget->undo();
1326 message(category, command);
1327 scrollToEnd();
1328 m_is_executing = false;
1329 });
1330
1331 // Make sure executor object is deleted in its own thread
1332 connect(&thread, &QThread::finished, m_executor, &RPCExecutor::deleteLater);
1333
1334 // Default implementation of QThread::run() simply spins up an event loop in the thread,
1335 // which is what we want.
1336 thread.start();
1337 QTimer::singleShot(0, m_executor, []() {
1338 util::ThreadRename("qt-rpcconsole");
1339 });
1340 }
1341
1342 void RPCConsole::on_tabWidget_currentChanged(int index)
1343 {
1344 if (ui->tabWidget->widget(index) == ui->tab_console) {
1345 ui->lineEdit->setFocus();
1346 }
1347 }
1348
1349 void RPCConsole::on_openDebugLogfileButton_clicked()
1350 {
1351 GUIUtil::openDebugLogfile();
1352 }
1353
1354 void RPCConsole::scrollToEnd()
1355 {
1356 QScrollBar *scrollbar = ui->messagesWidget->verticalScrollBar();
1357 scrollbar->setValue(scrollbar->maximum());
1358 }
1359
1360 void RPCConsole::on_sldGraphRange_valueChanged(int value)
1361 {
1362 const int multiplier = 5; // each position on the slider represents 5 min
1363 int mins = value * multiplier;
1364 setTrafficGraphRange(mins);
1365 }
1366
1367 void RPCConsole::setTrafficGraphRange(int mins)
1368 {
1369 ui->trafficGraph->setGraphRange(std::chrono::minutes{mins});
1370 ui->lblGraphRange->setText(GUIUtil::formatDurationStr(std::chrono::minutes{mins}));
1371 }
1372
1373 void RPCConsole::updateTrafficStats(quint64 totalBytesIn, quint64 totalBytesOut)
1374 {
1375 ui->lblBytesIn->setText(GUIUtil::formatBytes(totalBytesIn));
1376 ui->lblBytesOut->setText(GUIUtil::formatBytes(totalBytesOut));
1377 }
1378
1379 void RPCConsole::resetDetailWidget()
1380 {
1381 for (int row = 0; QLayoutItem * const item = ui->peerDetailsGrid->itemAtPosition(row, 1); ++row) {
1382 QLabel * const value_label = qobject_cast<QLabel*>(item->widget());
1383 if (!value_label) continue;
1384 value_label->setText(ts.na);
1385 }
1386 }
1387
1388 void RPCConsole::updateDetailWidget()
1389 {
1390 const QList<QModelIndex> selected_peers = GUIUtil::getEntryData(ui->peerWidget, PeerTableModel::NetNodeId);
1391 if (!clientModel || !clientModel->getPeerTableModel() || selected_peers.size() != 1) {
1392 ui->peersTabRightPanel->hide();
1393 ui->peerHeading->setText(tr("Select a peer to view detailed information."));
1394 return;
1395 }
1396 const auto stats = selected_peers.first().data(PeerTableModel::StatsRole).value<CNodeCombinedStats*>();
1397 // update the detail ui with latest node information
1398 QString peerAddrDetails(QString::fromStdString(stats->nodeStats.m_addr_name) + " ");
1399 peerAddrDetails += tr("(peer: %1)").arg(QString::number(stats->nodeStats.nodeid));
1400 if (!stats->nodeStats.addrLocal.empty())
1401 peerAddrDetails += "<br />" + tr("via %1").arg(QString::fromStdString(stats->nodeStats.addrLocal));
1402 ui->peerHeading->setText(peerAddrDetails);
1403 QString bip152_hb_settings;
1404 if (stats->nodeStats.m_bip152_highbandwidth_to) bip152_hb_settings = ts.to;
1405 if (stats->nodeStats.m_bip152_highbandwidth_from) bip152_hb_settings += (bip152_hb_settings.isEmpty() ? ts.from : QLatin1Char('/') + ts.from);
1406 if (bip152_hb_settings.isEmpty()) bip152_hb_settings = ts.no;
1407 ui->peerHighBandwidth->setText(bip152_hb_settings);
1408 const auto time_now{GetTime<std::chrono::seconds>()};
1409 ui->peerConnTime->setText(GUIUtil::formatDurationStr(time_now - stats->nodeStats.m_connected));
1410 ui->peerLastBlock->setText(TimeDurationField(time_now, stats->nodeStats.m_last_block_time));
1411 ui->peerLastTx->setText(TimeDurationField(time_now, stats->nodeStats.m_last_tx_time));
1412 ui->peerLastSend->setText(TimeDurationField(time_now, stats->nodeStats.m_last_send));
1413 ui->peerLastRecv->setText(TimeDurationField(time_now, stats->nodeStats.m_last_recv));
1414 ui->peerBytesSent->setText(GUIUtil::formatBytes(stats->nodeStats.nSendBytes));
1415 ui->peerBytesRecv->setText(GUIUtil::formatBytes(stats->nodeStats.nRecvBytes));
1416 ui->peerPingTime->setText(GUIUtil::formatPingTime(stats->nodeStats.m_last_ping_time));
1417 ui->peerMinPing->setText(GUIUtil::formatPingTime(stats->nodeStats.m_min_ping_time));
1418 if (stats->nodeStats.nVersion) {
1419 ui->peerVersion->setText(QString::number(stats->nodeStats.nVersion));
1420 ui->peerSubversion->setText(QString::fromStdString(stats->nodeStats.cleanSubVer));
1421 }
1422 ui->peerConnectionType->setText(GUIUtil::ConnectionTypeToQString(stats->nodeStats.m_conn_type, /*prepend_direction=*/true));
1423 ui->peerTransportType->setText(QString::fromStdString(TransportTypeAsString(stats->nodeStats.m_transport_type)));
1424 if (stats->nodeStats.m_transport_type == TransportProtocolType::V2) {
1425 ui->peerSessionIdLabel->setVisible(true);
1426 ui->peerSessionId->setVisible(true);
1427 ui->peerSessionId->setText(QString::fromStdString(stats->nodeStats.m_session_id));
1428 } else {
1429 ui->peerSessionIdLabel->setVisible(false);
1430 ui->peerSessionId->setVisible(false);
1431 }
1432 ui->peerNetwork->setText(GUIUtil::NetworkToQString(stats->nodeStats.m_network));
1433 if (stats->nodeStats.m_permission_flags == NetPermissionFlags::None) {
1434 ui->peerPermissions->setText(ts.no_permissions);
1435 } else {
1436 QStringList permissions;
1437 for (const auto& permission : NetPermissions::ToStrings(stats->nodeStats.m_permission_flags)) {
1438 permissions.append(QString::fromStdString(permission));
1439 }
1440 ui->peerPermissions->setText(permissions.join(" & "));
1441 }
1442 ui->peerMappedAS->setText(stats->nodeStats.m_mapped_as != 0 ? QString::number(stats->nodeStats.m_mapped_as) : ts.na);
1443
1444 // This check fails for example if the lock was busy and
1445 // nodeStateStats couldn't be fetched.
1446 if (stats->fNodeStateStatsAvailable) {
1447 ui->timeoffset->setText(GUIUtil::formatTimeOffset(Ticks<std::chrono::seconds>(stats->nodeStateStats.time_offset)));
1448 ui->peerServices->setText(GUIUtil::formatServicesStr(stats->nodeStateStats.their_services));
1449 // Sync height is init to -1
1450 if (stats->nodeStateStats.nSyncHeight > -1) {
1451 ui->peerSyncHeight->setText(QString("%1").arg(stats->nodeStateStats.nSyncHeight));
1452 } else {
1453 ui->peerSyncHeight->setText(ts.unknown);
1454 }
1455 // Common height is init to -1
1456 if (stats->nodeStateStats.nCommonHeight > -1) {
1457 ui->peerCommonHeight->setText(QString("%1").arg(stats->nodeStateStats.nCommonHeight));
1458 } else {
1459 ui->peerCommonHeight->setText(ts.unknown);
1460 }
1461 ui->peerHeight->setText(QString::number(stats->nodeStateStats.m_starting_height));
1462 ui->peerPingWait->setText(GUIUtil::formatPingTime(stats->nodeStateStats.m_ping_wait));
1463 ui->peerAddrRelayEnabled->setText(stats->nodeStateStats.m_addr_relay_enabled ? ts.yes : ts.no);
1464 ui->peerAddrProcessed->setText(QString::number(stats->nodeStateStats.m_addr_processed));
1465 ui->peerAddrRateLimited->setText(QString::number(stats->nodeStateStats.m_addr_rate_limited));
1466 ui->peerRelayTxes->setText(stats->nodeStateStats.m_relay_txs ? ts.yes : ts.no);
1467 }
1468
1469 ui->hidePeersDetailButton->setIcon(platformStyle->SingleColorIcon(QStringLiteral(":/icons/remove")));
1470 ui->peersTabRightPanel->show();
1471 }
1472
1473 void RPCConsole::resizeEvent(QResizeEvent *event)
1474 {
1475 QWidget::resizeEvent(event);
1476 }
1477
1478 void RPCConsole::showEvent(QShowEvent *event)
1479 {
1480 QWidget::showEvent(event);
1481
1482 if (!clientModel || !clientModel->getPeerTableModel())
1483 return;
1484
1485 // start PeerTableModel auto refresh
1486 clientModel->getPeerTableModel()->startAutoRefresh();
1487 }
1488
1489 void RPCConsole::hideEvent(QHideEvent *event)
1490 {
1491 // It is too late to call QHeaderView::saveState() in ~RPCConsole(), as all of
1492 // the columns of QTableView child widgets will have zero width at that moment.
1493 m_peer_widget_header_state = ui->peerWidget->horizontalHeader()->saveState();
1494 m_banlist_widget_header_state = ui->banlistWidget->horizontalHeader()->saveState();
1495
1496 QWidget::hideEvent(event);
1497
1498 if (!clientModel || !clientModel->getPeerTableModel())
1499 return;
1500
1501 // stop PeerTableModel auto refresh
1502 clientModel->getPeerTableModel()->stopAutoRefresh();
1503 }
1504
1505 void RPCConsole::showPeersTableContextMenu(const QPoint& point)
1506 {
1507 QModelIndex index = ui->peerWidget->indexAt(point);
1508 if (index.isValid())
1509 peersTableContextMenu->exec(QCursor::pos());
1510 }
1511
1512 void RPCConsole::showBanTableContextMenu(const QPoint& point)
1513 {
1514 QModelIndex index = ui->banlistWidget->indexAt(point);
1515 if (index.isValid())
1516 banTableContextMenu->exec(QCursor::pos());
1517 }
1518
1519 void RPCConsole::disconnectSelectedNode()
1520 {
1521 // Get selected peer addresses
1522 QList<QModelIndex> nodes = GUIUtil::getEntryData(ui->peerWidget, PeerTableModel::NetNodeId);
1523 for(int i = 0; i < nodes.count(); i++)
1524 {
1525 // Get currently selected peer address
1526 NodeId id = nodes.at(i).data().toLongLong();
1527 // Find the node, disconnect it and clear the selected node
1528 if(m_node.disconnectById(id))
1529 clearSelectedNode();
1530 }
1531 }
1532
1533 void RPCConsole::banSelectedNode(int bantime)
1534 {
1535 if (!clientModel)
1536 return;
1537
1538 for (const QModelIndex& peer : GUIUtil::getEntryData(ui->peerWidget, PeerTableModel::NetNodeId)) {
1539 // Find possible nodes, ban it and clear the selected node
1540 const auto stats = peer.data(PeerTableModel::StatsRole).value<CNodeCombinedStats*>();
1541 if (stats) {
1542 m_node.ban(stats->nodeStats.addr, bantime);
1543 m_node.disconnectByAddress(stats->nodeStats.addr);
1544 }
1545 }
1546 clearSelectedNode();
1547 clientModel->getBanTableModel()->refresh();
1548 }
1549
1550 void RPCConsole::unbanSelectedNode()
1551 {
1552 if (!clientModel)
1553 return;
1554
1555 // Get selected ban addresses
1556 QList<QModelIndex> nodes = GUIUtil::getEntryData(ui->banlistWidget, BanTableModel::Address);
1557 BanTableModel* ban_table_model{clientModel->getBanTableModel()};
1558 bool unbanned{false};
1559 for (const auto& node_index : nodes) {
1560 unbanned |= ban_table_model->unban(node_index);
1561 }
1562 if (unbanned) {
1563 ban_table_model->refresh();
1564 }
1565 }
1566
1567 void RPCConsole::clearSelectedNode()
1568 {
1569 ui->peerWidget->selectionModel()->clearSelection();
1570 cachedNodeids.clear();
1571 updateDetailWidget();
1572 }
1573
1574 void RPCConsole::showOrHideBanTableIfRequired()
1575 {
1576 if (!clientModel)
1577 return;
1578
1579 bool visible = clientModel->getBanTableModel()->shouldShow();
1580 ui->banlistWidget->setVisible(visible);
1581 ui->banHeading->setVisible(visible);
1582 }
1583
1584 std::vector<RPCConsole::TabTypes> RPCConsole::tabs() const
1585 {
1586 std::vector<TabTypes> ret;
1587 ret.reserve(m_tabs.size());
1588
1589 std::map<QWidget*, TabTypes> tabtype_map;
1590 for (const auto& tab : m_tabs) {
1591 tabtype_map[tab.second] = tab.first;
1592 }
1593
1594 for (int i = 0; i < ui->tabWidget->count(); ++i) {
1595 auto tabtype = tabtype_map.find(ui->tabWidget->widget(i));
1596 if (tabtype != tabtype_map.end()) {
1597 ret.push_back(tabtype->second);
1598 }
1599 }
1600 return ret;
1601 }
1602
1603 void RPCConsole::setTabFocus(enum TabTypes tabType)
1604 {
1605 ui->tabWidget->setCurrentWidget(m_tabs[tabType]);
1606 }
1607
1608 QString RPCConsole::tabTitle(TabTypes tab_type) const
1609 {
1610 const int tab_index = ui->tabWidget->indexOf(m_tabs.at(tab_type));
1611 return ui->tabWidget->tabText(tab_index);
1612 }
1613
1614 QKeySequence RPCConsole::tabShortcut(TabTypes tab_type) const
1615 {
1616 switch (tab_type) {
1617 case TabTypes::INFO: return QKeySequence(tr("Ctrl+I"));
1618 case TabTypes::CONSOLE: return QKeySequence(tr("Ctrl+T"));
1619 case TabTypes::GRAPH: return QKeySequence(tr("Ctrl+N"));
1620 case TabTypes::PAIRING: return QKeySequence(QStringLiteral("Alt+5")); // Only used in disablewallet mode - matches wallet GUI's pairing shortcut
1621 case TabTypes::PEERS: return QKeySequence(tr("Ctrl+P"));
1622 } // no default case, so the compiler can warn about missing cases
1623
1624 assert(false);
1625 }
1626
1627 void RPCConsole::updateAlerts(const QString& warnings)
1628 {
1629 this->ui->label_alerts->setVisible(!warnings.isEmpty());
1630 this->ui->label_alerts->setText(warnings);
1631 }
1632
1633 void RPCConsole::updateWindowTitle()
1634 {
1635 const ChainType chain = Params().GetChainType();
1636 if (chain == ChainType::MAIN) return;
1637
1638 const QString chainType = QString::fromStdString(Params().GetChainTypeString());
1639 const QString title = tr("Node window - [%1]").arg(chainType);
1640 this->setWindowTitle(title);
1641 }
1642
1643 void RPCConsole::updateThemeColors()
1644 {
1645 // Detect dark mode for color palette selection
1646 const bool dark_mode = GUIUtil::isDarkMode(palette().color(backgroundRole()));
1647
1648 // Set theme colors pointer based on dark mode
1649 m_theme_colors = dark_mode ? &DARK_THEME_COLORS : &LIGHT_THEME_COLORS;
1650
1651 // Update icons
1652 if (platformStyle->getImagesOnButtons()) {
1653 ui->openDebugLogfileButton->setIcon(platformStyle->SingleColorIcon(":/icons/export"));
1654 }
1655 ui->hidePeersDetailButton->setIcon(platformStyle->SingleColorIcon(QStringLiteral(":/icons/remove")));
1656
1657 // Update console stylesheet with new colors
1658 updateConsoleStyleSheet();
1659 }
1660
1661 void RPCConsole::updateConsoleStyleSheet()
1662 {
1663 assert(m_theme_colors);
1664
1665 #ifdef Q_OS_MACOS
1666 QFontInfo fixedFontInfo(GUIUtil::fixedPitchFont(/*use_embedded_font=*/true));
1667 #else
1668 QFontInfo fixedFontInfo(GUIUtil::fixedPitchFont());
1669 #endif
1670 ui->messagesWidget->document()->setDefaultStyleSheet(
1671 QString(
1672 "table { }"
1673 "td.time { color: #808080; font-size: %2; padding-top: 3px; } "
1674 "td.message { font-family: %1; font-size: %2; white-space:pre-wrap; } "
1675 "td.cmd-request { color: %3; } "
1676 "td.cmd-error { color: %4; } "
1677 ".secwarning { color: %4; }"
1678 "b { color: %3; } "
1679 ).arg(fixedFontInfo.family(), QString("%1pt").arg(consoleFontSize), m_theme_colors->userinput.name(), m_theme_colors->warning.name())
1680 );
1681
1682 #ifdef Q_OS_MACOS
1683 // On macOS, updating the stylesheet doesn't affect existing HTML content
1684 // So we need to manually update the HTML similar to setFontSize()
1685 QString str = ui->messagesWidget->toHtml();
1686
1687 // Replace any theme colors with current theme colors
1688 // Check both themes since we don't know which was used previously
1689 for (const auto* theme : {&LIGHT_THEME_COLORS, &DARK_THEME_COLORS}) {
1690 if (theme != m_theme_colors) {
1691 str.replace(QString("color:%1").arg(theme->warning.name()),
1692 QString("color:%1").arg(m_theme_colors->warning.name()));
1693 str.replace(QString("color:%1").arg(theme->userinput.name()),
1694 QString("color:%1").arg(m_theme_colors->userinput.name()));
1695 }
1696 }
1697
1698 QScrollBar* scrollbar = ui->messagesWidget->verticalScrollBar();
1699 int oldScrollValue = scrollbar->value();
1700
1701 // Set the updated HTML back
1702 ui->messagesWidget->setHtml(str);
1703 scrollbar->setValue(oldScrollValue);
1704 #endif
1705 }
1706