Add mine-when-idle, default banlist, and console parsing improvements

Mine-when-idle:
- Auto-start/stop mining based on system idle time detection
- Platform::getSystemIdleSeconds() via XScreenSaver (Linux) / GetLastInputInfo (Win)
- Settings: mine_when_idle toggle + configurable delay (30s–10m)
- Settings page UI with checkbox and delay combo

Console tab:
- Shell-like argument parsing with quote and JSON bracket support
- Pass JSON objects/arrays directly as RPC params
- Fix selection indices when lines are evicted from buffer

Connection & status bar:
- Reduce RPC connect timeout to 1s for localhost fast-fail
- Fast retry timer on daemon startup and external daemon detection
- Show pool mining hashrate in status bar; sidebar badge reflects pool state

UI polish:
- Add logo to About card in settings; expose logo dimensions on App
- Header title offset-y support; adjust content-area margins
- Fix banned peers row cursor position (rawRowPosB.x)

Branding:
- Update copyright to "DragonX Developers" in RC and About section
- Replace logo/icon assets with updated versions

Misc:
- setup.sh: checkout dragonx branch before pulling
- Remove stale prebuilt-binaries/xmrig/.gitkeep
This commit is contained in:
dan_s
2026-03-07 13:42:31 -06:00
parent 653a90de62
commit cc617dd5be
22 changed files with 431 additions and 41 deletions

View File

@@ -104,6 +104,10 @@ static LowSpecSnapshot s_lowSpecSnap;
// Daemon — keep running on close
static bool sp_keep_daemon_running = false;
static bool sp_stop_external_daemon = false;
// Mining — mine when idle
static bool sp_mine_when_idle = false;
static int sp_mine_idle_delay = 120;
static bool sp_verbose_logging = false;
// Debug logging categories
@@ -165,6 +169,8 @@ static void loadSettingsPageState(config::Settings* settings) {
Layout::setUserFontScale(sp_font_scale); // sync with Layout on load
sp_keep_daemon_running = settings->getKeepDaemonRunning();
sp_stop_external_daemon = settings->getStopExternalDaemon();
sp_mine_when_idle = settings->getMineWhenIdle();
sp_mine_idle_delay = settings->getMineIdleDelay();
sp_verbose_logging = settings->getVerboseLogging();
sp_debug_categories = settings->getDebugCategories();
sp_debug_cats_dirty = false;
@@ -212,6 +218,8 @@ static void saveSettingsPageState(config::Settings* settings) {
settings->setFontScale(sp_font_scale);
settings->setKeepDaemonRunning(sp_keep_daemon_running);
settings->setStopExternalDaemon(sp_stop_external_daemon);
settings->setMineWhenIdle(sp_mine_when_idle);
settings->setMineIdleDelay(sp_mine_idle_delay);
settings->setVerboseLogging(sp_verbose_logging);
settings->setDebugCategories(sp_debug_categories);
@@ -1075,6 +1083,46 @@ void RenderSettingsPage(App* app) {
if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f);
}
// Mine when idle — checkbox + delay combo
{
if (ImGui::Checkbox("Mine when idle", &sp_mine_when_idle)) {
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Automatically start mining when the\nsystem is idle (no keyboard/mouse input)");
if (sp_mine_when_idle) {
ImGui::SameLine(0, Layout::spacingMd());
ImGui::AlignTextToFramePadding();
ImGui::TextColored(ImVec4(1, 1, 1, 0.5f), "after");
ImGui::SameLine(0, Layout::spacingSm());
struct DelayOption { int seconds; const char* label; };
static const DelayOption delays[] = {
{30, "30s"}, {60, "1m"}, {120, "2m"}, {300, "5m"}, {600, "10m"}
};
const char* previewLabel = "2m";
for (const auto& d : delays) {
if (d.seconds == sp_mine_idle_delay) { previewLabel = d.label; break; }
}
ImGui::SetNextItemWidth(schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f));
if (ImGui::BeginCombo("##IdleDelay", previewLabel, ImGuiComboFlags_NoArrowButton)) {
for (const auto& d : delays) {
bool selected = (d.seconds == sp_mine_idle_delay);
if (ImGui::Selectable(d.label, selected)) {
sp_mine_idle_delay = d.seconds;
saveSettingsPageState(app->settings());
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("How long to wait before starting mining");
}
}
// Bottom row — Keys & Data left-aligned, Setup Wizard right-aligned
{
const char* r1[] = {"Import Key...", "Export Key...", "Export All...", "Backup...", "Export CSV..."};
@@ -1583,7 +1631,23 @@ void RenderSettingsPage(App* app) {
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
ImGui::Indent(pad);
float contentW = availWidth - pad * 2;
// Logo on the left side of the about card
ImTextureID logoTex = app->getLogoTexture();
float logoAreaW = 0;
if (logoTex != 0) {
float logoMaxH = schema::UI().drawElement("components.settings-page", "about-logo-size").sizeOr(64.0f);
float logoH = logoMaxH;
float aspect = (app->getLogoHeight() > 0) ? (float)app->getLogoWidth() / (float)app->getLogoHeight() : 1.0f;
float logoW = logoH * aspect;
ImVec2 logoPos = ImGui::GetCursorScreenPos();
dl->AddImage(logoTex,
ImVec2(logoPos.x, logoPos.y),
ImVec2(logoPos.x + logoW, logoPos.y + logoH));
logoAreaW = logoW + Layout::spacingLg();
ImGui::Indent(logoAreaW);
}
float contentW = availWidth - pad * 2 - logoAreaW;
// App name + version on same line
ImGui::PushFont(sub1);
@@ -1601,7 +1665,7 @@ void RenderSettingsPage(App* app) {
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::PushFont(body2);
ImGui::PushTextWrapPos(cardMin.x + availWidth - pad);
ImGui::PushTextWrapPos(cardMin.x + availWidth - pad - logoAreaW);
ImGui::TextUnformatted(
"A shielded cryptocurrency wallet for DragonX (DRGX), "
"built with Dear ImGui for a lightweight, portable experience.");
@@ -1611,14 +1675,18 @@ void RenderSettingsPage(App* app) {
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::PushFont(capFont);
ImGui::TextColored(ImVec4(1,1,1,0.5f), "Copyright 2024-2026 The Hush Developers | GPLv3 License");
ImGui::TextColored(ImVec4(1,1,1,0.5f), "Copyright 2024-2026 DragonX Developers | GPLv3 License");
ImGui::PopFont();
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
// Buttons — consistent equal-width row
// Buttons — consistent equal-width row (full card width)
if (logoAreaW > 0) {
ImGui::Unindent(logoAreaW);
}
{
float aboutBtnW = (contentW - Layout::spacingMd() * 3) / 4.0f;
float fullContentW = availWidth - pad * 2;
float aboutBtnW = (fullContentW - Layout::spacingMd() * 3) / 4.0f;
if (TactileButton("Website", ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
util::Platform::openUrl("https://dragonx.is");

View File

@@ -1630,12 +1630,39 @@ void ConsoleTab::executeCommand(const std::string& cmd, rpc::RPCClient* rpc, rpc
return;
}
// Parse command and arguments
// Parse command and arguments (shell-like: handles quotes and JSON brackets)
std::vector<std::string> args;
std::istringstream iss(cmd);
std::string token;
while (iss >> token) {
args.push_back(token);
{
size_t i = 0;
size_t len = cmd.size();
while (i < len) {
// Skip whitespace
while (i < len && (cmd[i] == ' ' || cmd[i] == '\t')) i++;
if (i >= len) break;
std::string tok;
if (cmd[i] == '"' || cmd[i] == '\'') {
// Quoted string — collect until matching close quote
char quote = cmd[i++];
while (i < len && cmd[i] != quote) tok += cmd[i++];
if (i < len) i++; // skip closing quote
} else if (cmd[i] == '[' || cmd[i] == '{') {
// JSON array/object — collect until matching bracket
char open = cmd[i];
char close = (open == '[') ? ']' : '}';
int depth = 0;
while (i < len) {
if (cmd[i] == open) depth++;
else if (cmd[i] == close) depth--;
tok += cmd[i++];
if (depth == 0) break;
}
} else {
// Unquoted token — collect until whitespace
while (i < len && cmd[i] != ' ' && cmd[i] != '\t') tok += cmd[i++];
}
if (!tok.empty()) args.push_back(tok);
}
}
if (args.empty()) return;
@@ -1647,24 +1674,31 @@ void ConsoleTab::executeCommand(const std::string& cmd, rpc::RPCClient* rpc, rpc
for (size_t i = 1; i < args.size(); i++) {
const std::string& arg = args[i];
// Try to parse as number
try {
if (arg.find('.') != std::string::npos) {
params.push_back(std::stod(arg));
} else {
// Check for bool
if (arg == "true") {
params.push_back(true);
} else if (arg == "false") {
params.push_back(false);
// Try to parse as JSON first (handles objects, arrays, etc.)
if (!arg.empty() && (arg[0] == '{' || arg[0] == '[')) {
auto parsed = nlohmann::json::parse(arg, nullptr, false);
if (!parsed.is_discarded()) {
params.push_back(parsed);
continue;
}
}
// Try to parse as number or bool
if (arg == "true") {
params.push_back(true);
} else if (arg == "false") {
params.push_back(false);
} else {
try {
if (arg.find('.') != std::string::npos) {
params.push_back(std::stod(arg));
} else {
// Try as integer
params.push_back(std::stoll(arg));
}
} catch (...) {
// Keep as string
params.push_back(arg);
}
} catch (...) {
// Keep as string
params.push_back(arg);
}
}
@@ -1757,9 +1791,24 @@ void ConsoleTab::addLine(const std::string& line, ImU32 color)
lines_.push_back({line, color});
// Limit buffer size
// Limit buffer size — adjust selection indices when lines are removed
// from the front so the highlight stays on the text the user selected.
int popped = 0;
while (lines_.size() > 10000) {
lines_.pop_front();
popped++;
}
if (popped > 0 && has_selection_) {
sel_anchor_.line -= popped;
sel_end_.line -= popped;
if (sel_anchor_.line < 0 && sel_end_.line < 0) {
// Entire selection was in the removed range
has_selection_ = false;
is_selecting_ = false;
} else {
if (sel_anchor_.line < 0) { sel_anchor_.line = 0; sel_anchor_.col = 0; }
if (sel_end_.line < 0) { sel_end_.line = 0; sel_end_.col = 0; }
}
}
// Track new output while user is scrolled up

View File

@@ -907,7 +907,7 @@ void RenderPeersTab(App* app)
effects::ImGuiAcrylic::EndAcrylicPopup();
}
ImGui::SetCursorScreenPos(ImVec2(rowPos.x, rowEnd.y));
ImGui::SetCursorScreenPos(ImVec2(rawRowPosB.x, rowEnd.y));
if (i < state.bannedPeers.size() - 1) {
ImVec2 divStart = ImGui::GetCursorScreenPos();