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

@@ -503,6 +503,11 @@ embed_resource(
${CMAKE_BINARY_DIR}/generated/ui_toml_embedded.h
ui_toml
)
embed_resource(
${CMAKE_SOURCE_DIR}/res/default_banlist.txt
${CMAKE_BINARY_DIR}/generated/default_banlist_embedded.h
default_banlist
)
# Note: xmrig is embedded via build.sh (embedded_data.h) for Windows builds,
# following the same pattern as daemon embedding.

View File

@@ -31,11 +31,11 @@ BEGIN
BEGIN
BLOCK "040904B0" // US-English, Unicode
BEGIN
VALUE "CompanyName", "The Hush Developers\0"
VALUE "CompanyName", "DragonX Developers\0"
VALUE "FileDescription", "ObsidianDragon Wallet\0"
VALUE "FileVersion", "@DRAGONX_VERSION@\0"
VALUE "InternalName", "ObsidianDragon\0"
VALUE "LegalCopyright", "Copyright 2024-2026 The Hush Developers. GPLv3.\0"
VALUE "LegalCopyright", "Copyright 2024-2026 DragonX Developers. GPLv3.\0"
VALUE "OriginalFilename", "ObsidianDragon.exe\0"
VALUE "ProductName", "ObsidianDragon\0"
VALUE "ProductVersion", "@DRAGONX_VERSION@\0"

13
res/default_banlist.txt Normal file
View File

@@ -0,0 +1,13 @@
# Default Ban List — DragonX Wallet
# IPs listed here are banned automatically when the wallet connects.
# One IP or subnet per line. Comments start with #. Blank lines are ignored.
#
# Examples:
# 192.168.1.100
# 10.0.0.0/8
# 203.0.113.42
#
# Rebuild the wallet after editing this file.
185.159.129.12
185.228.233.222

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -779,6 +779,8 @@ pool-url-input = { width = 300.0, height = 28.0 }
pool-worker-input = { width = 200.0, height = 28.0 }
log-panel-height = { size = 120.0 }
log-panel-min = { size = 60.0 }
idle-row-height = { size = 28.0 }
idle-combo-width = { size = 64.0 }
[tabs.peers]
refresh-button = { size = 110.0 }
@@ -1249,8 +1251,8 @@ inset-shadow-fade-ratio = { size = 5.0 }
[components.content-area]
padding-x = 0.0
padding-y = 0.0
margin-top = { size = 4.0 }
margin-bottom = { size = -40.0 }
margin-top = { size = 6.0 }
margin-bottom = { size = -39.0 }
edge-fade-zone = { size = 0.0 }
[components.content-area.window]
@@ -1262,10 +1264,10 @@ page-fade-speed = { size = 8.0 }
collapse-hysteresis = { size = 60.0 }
header-icon = { icon-dark = "logos/logo_ObsidianDragon_dark.png", icon-light = "logos/logo_ObsidianDragon_light.png" }
coin-icon = { icon = "logos/logo_dragonx_128.png" }
header-title = { font = "subtitle1", size = 14.0, pad-x = 16.0, pad-y = 12.0, logo-gap = 8.0, opacity = 0.7 }
header-title = { font = "subtitle1", size = 14.0, pad-x = 22.0, pad-y = 6.0, logo-gap = 4.0, opacity = 0.7, offset-y = 4.0 }
[components.main-window.window]
padding = [12, 38]
padding = [12, 36]
[components.shutdown]
content-height = { height = 120.0 }
@@ -1309,6 +1311,8 @@ rpc-label-width = { size = 85.0 }
security-combo-width = { size = 120.0 }
port-input-min-width = { size = 60.0 }
port-input-width-ratio = { size = 0.4 }
idle-combo-width = { size = 64.0 }
about-logo-size = { size = 64.0 }
[components.main-layout]
app-bar-height = { size = 64.0 }

View File

@@ -416,8 +416,8 @@ clone_dragonx_if_needed() {
git clone https://git.dragonx.is/DragonX/dragonx.git "$DRAGONX_SRC"
else
ok "dragonx source already present"
info "Pulling latest dragonx..."
(cd "$DRAGONX_SRC" && git pull --ff-only 2>/dev/null || true)
info "Switching to dragonx branch and pulling latest..."
(cd "$DRAGONX_SRC" && git checkout dragonx 2>/dev/null && git pull --ff-only 2>/dev/null || true)
fi
}

View File

@@ -292,6 +292,9 @@ void App::update()
checkAutoLock();
}
// Mine-when-idle check (runs every frame, internally rate-limited by idle detection)
checkIdleMining();
// P8: Dedup rebuildAddressList — only rebuild once per frame
if (address_list_dirty_) {
address_list_dirty_ = false;
@@ -715,13 +718,14 @@ void App::render()
const float brandPadX = hdrF("pad-x", mainPadX / hdp);
const float brandPadY = hdrF("pad-y", 8.0f);
const float logoGap = hdrF("logo-gap", 8.0f);
const float brandOffY = hdrF("offset-y", 0.0f);
const float hdrOpacity = (hdrElem.opacity >= 0.0f) ? hdrElem.opacity : 0.7f;
// Logo
float logoSize = mainPadTop - brandPadY * 2.0f; // fit within header
if (logoSize < 16.0f * hdp) logoSize = 16.0f * hdp;
float logoX = winPos.x + brandPadX;
float logoY = winPos.y + brandPadY;
float logoY = winPos.y + brandPadY + brandOffY;
if (logo_tex_ != 0) {
float aspect = (logo_h_ > 0) ? (float)logo_w_ / (float)logo_h_ : 1.0f;
float logoW = logoSize * aspect;
@@ -740,7 +744,7 @@ void App::render()
? hdrElem.size * hdp
: titleFont->LegacySize;
if (titleFont) {
float textY = winPos.y + (mainPadTop - titleFontSize) * 0.5f;
float textY = winPos.y + (mainPadTop - titleFontSize) * 0.5f + brandOffY;
ImU32 textCol = ui::material::OnSurface();
// Apply header text opacity
int a = (int)((float)((textCol >> 24) & 0xFF) * hdrOpacity);
@@ -803,7 +807,7 @@ void App::render()
// Build sidebar status for badges + footer
ui::SidebarStatus sbStatus;
sbStatus.peerCount = static_cast<int>(state_.peers.size());
sbStatus.miningActive = state_.mining.generate;
sbStatus.miningActive = state_.mining.generate || state_.pool_mining.xmrig_running;
// Load logo texture lazily on first frame (or after theme change)
// Also reload when dark↔light mode changes so the correct variant shows
@@ -1370,8 +1374,9 @@ void App::renderStatusBar()
}
}
// Mining indicator (if mining)
if (state_.mining.generate) {
// Mining indicator (if mining — solo or pool)
const bool anyMining = state_.mining.generate || state_.pool_mining.xmrig_running;
if (anyMining) {
ImGui::SameLine(0, sbSectionGap);
ImGui::TextDisabled("|");
ImGui::SameLine(0, sbSeparatorGap);
@@ -1379,8 +1384,11 @@ void App::renderStatusBar()
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), ICON_MD_CONSTRUCTION);
ImGui::PopFont();
ImGui::SameLine(0, sbIconTextGap);
double displayHashrate = state_.pool_mining.xmrig_running
? state_.pool_mining.hashrate_10s
: state_.mining.localHashrate;
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%.1f H/s",
state_.mining.localHashrate);
displayHashrate);
}
// Decrypt-import background task indicator

View File

@@ -174,6 +174,9 @@ public:
void startPoolMining(int threads);
void stopPoolMining();
// Mine-when-idle state query
bool isIdleMiningActive() const { return idle_mining_active_; }
// Peers
const std::vector<PeerInfo>& getPeers() const { return state_.peers; }
const std::vector<BannedPeer>& getBannedPeers() const { return state_.bannedPeers; }
@@ -264,6 +267,8 @@ public:
// Logo texture accessor (wallet branding icon)
ImTextureID getLogoTexture() const { return logo_tex_; }
int getLogoWidth() const { return logo_w_; }
int getLogoHeight() const { return logo_h_; }
// Coin logo texture accessor (DragonX currency icon for balance tab)
ImTextureID getCoinLogoTexture() const { return coin_logo_tex_; }
@@ -564,6 +569,9 @@ private:
// Auto-lock on idle
std::chrono::steady_clock::time_point last_interaction_ = std::chrono::steady_clock::now();
// Mine-when-idle: auto-start/stop mining based on system idle state
bool idle_mining_active_ = false; // true when mining was auto-started by idle detection
// Private methods - rendering
void renderStatusBar();
void renderAboutDialog();
@@ -582,6 +590,7 @@ private:
void tryConnect();
void onConnected();
void onDisconnected(const std::string& reason);
void applyDefaultBanlist();
// Private methods - data refresh
void refreshData();
@@ -590,6 +599,7 @@ private:
void refreshPrice();
void refreshWalletEncryptionState();
void checkAutoLock();
void checkIdleMining();
};
} // namespace dragonx

View File

@@ -13,6 +13,7 @@
#include "daemon/embedded_daemon.h"
#include "daemon/xmrig_manager.h"
#include "ui/notifications.h"
#include "default_banlist_embedded.h"
#include "util/platform.h"
#include "util/perf_log.h"
@@ -52,6 +53,7 @@ void App::tryConnect()
if (embedded_daemon_ && embedded_daemon_->externalDaemonDetected()) {
connection_status_ = "Waiting for daemon config...";
VERBOSE_LOGF("[connect #%d] External daemon detected on port, waiting for config file to appear\n", connect_attempt);
refresh_timer_ = REFRESH_INTERVAL - 1.0f;
return;
}
@@ -63,9 +65,11 @@ void App::tryConnect()
if (startEmbeddedDaemon()) {
// Will retry connection after daemon starts
VERBOSE_LOGF("[connect #%d] Embedded daemon starting, will retry connection...\n", connect_attempt);
refresh_timer_ = REFRESH_INTERVAL - 1.0f;
} else if (embedded_daemon_ && embedded_daemon_->externalDaemonDetected()) {
connection_status_ = "Waiting for daemon config...";
VERBOSE_LOGF("[connect #%d] External daemon detected but no config yet, will retry...\n", connect_attempt);
refresh_timer_ = REFRESH_INTERVAL - 1.0f;
} else {
VERBOSE_LOGF("[connect #%d] startEmbeddedDaemon() failed — lastError: %s, binary: %s\n",
connect_attempt,
@@ -148,10 +152,14 @@ void App::tryConnect()
state_.connected = false;
connection_status_ = "Waiting for dragonxd to start...";
VERBOSE_LOGF("[connect #%d] RPC connection failed — daemon still starting, will retry...\n", attempt);
// Fast retry: force the refresh timer to fire on the next cycle
// instead of waiting the full 5-second REFRESH_INTERVAL.
refresh_timer_ = REFRESH_INTERVAL - 1.0f;
} else if (externalDetected) {
state_.connected = false;
connection_status_ = "Connecting to daemon...";
VERBOSE_LOGF("[connect #%d] RPC connection failed — external daemon on port but RPC not ready yet, will retry...\n", attempt);
refresh_timer_ = REFRESH_INTERVAL - 1.0f;
} else {
onDisconnected("Connection failed");
VERBOSE_LOGF("[connect #%d] RPC connection failed — no daemon starting, no external detected\n", attempt);
@@ -286,6 +294,9 @@ void App::onConnected()
// Initial data refresh
refreshData();
refreshMarketData();
// Apply compiled-in default ban list
applyDefaultBanlist();
}
void App::onDisconnected(const std::string& reason)
@@ -1403,6 +1414,57 @@ void App::clearBans()
});
}
void App::applyDefaultBanlist()
{
if (!state_.connected || !rpc_ || !worker_) return;
// Parse the embedded default_banlist.txt (compiled from res/default_banlist.txt)
std::string data(reinterpret_cast<const char*>(embedded::default_banlist_data),
embedded::default_banlist_size);
std::vector<std::string> ips;
size_t pos = 0;
while (pos < data.size()) {
size_t eol = data.find('\n', pos);
if (eol == std::string::npos) eol = data.size();
std::string line = data.substr(pos, eol - pos);
pos = eol + 1;
// Strip carriage return (Windows line endings)
if (!line.empty() && line.back() == '\r') line.pop_back();
// Strip leading/trailing whitespace
size_t start = line.find_first_not_of(" \t");
if (start == std::string::npos) continue;
line = line.substr(start, line.find_last_not_of(" \t") - start + 1);
// Skip empty lines and comments
if (line.empty() || line[0] == '#') continue;
ips.push_back(line);
}
if (ips.empty()) return;
// Apply bans on the worker thread to avoid blocking the UI
worker_->post([this, ips]() -> rpc::RPCWorker::MainCb {
int applied = 0;
for (const auto& ip : ips) {
try {
// 0 = permanent ban (until node restart or manual unban)
// Using a very long duration (10 years) for effectively permanent bans
rpc_->call("setban", {ip, "add", 315360000});
applied++;
} catch (...) {
// Already banned or invalid — skip silently
}
}
return [applied]() {
if (applied > 0) {
DEBUG_LOGF("[Banlist] Applied %d default bans\n", applied);
}
};
});
}
// ============================================================================
// Address Operations
// ============================================================================

View File

@@ -413,6 +413,67 @@ void App::checkAutoLock() {
}
}
// ===========================================================================
// Mine when idle — auto-start/stop mining based on system idle state
// ===========================================================================
void App::checkIdleMining() {
if (!settings_ || !settings_->getMineWhenIdle()) {
// Feature disabled — if we previously auto-started, stop now
if (idle_mining_active_) {
idle_mining_active_ = false;
if (settings_ && settings_->getPoolMode()) {
if (xmrig_manager_ && xmrig_manager_->isRunning())
stopPoolMining();
} else {
if (state_.mining.generate)
stopMining();
}
}
return;
}
int idleSec = util::Platform::getSystemIdleSeconds();
int delay = settings_->getMineIdleDelay();
bool isPool = settings_->getPoolMode();
// Check if mining is already running (manually started by user)
bool miningActive = isPool
? (xmrig_manager_ && xmrig_manager_->isRunning())
: state_.mining.generate;
if (idleSec >= delay) {
// System is idle — start mining if not already running
if (!miningActive && !idle_mining_active_ && !mining_toggle_in_progress_.load()) {
// For solo mining, need daemon connected and synced
if (!isPool && (!state_.connected || state_.sync.syncing)) return;
int threads = settings_->getPoolThreads();
if (threads <= 0) threads = std::max(1, (int)std::thread::hardware_concurrency() / 2);
idle_mining_active_ = true;
if (isPool)
startPoolMining(threads);
else
startMining(threads);
DEBUG_LOGF("[App] Idle mining started after %d seconds idle\n", idleSec);
}
} else {
// User is active — stop mining if we auto-started it
if (idle_mining_active_) {
idle_mining_active_ = false;
if (isPool) {
if (xmrig_manager_ && xmrig_manager_->isRunning())
stopPoolMining();
} else {
if (state_.mining.generate)
stopMining();
}
DEBUG_LOGF("[App] Idle mining stopped — user returned\n");
}
}
}
// ===========================================================================
// Restart Setup Wizard (from Settings)
// ===========================================================================

View File

@@ -148,6 +148,8 @@ bool Settings::load(const std::string& path)
if (j.contains("pool_tls")) pool_tls_ = j["pool_tls"].get<bool>();
if (j.contains("pool_hugepages")) pool_hugepages_ = j["pool_hugepages"].get<bool>();
if (j.contains("pool_mode")) pool_mode_ = j["pool_mode"].get<bool>();
if (j.contains("mine_when_idle")) mine_when_idle_ = j["mine_when_idle"].get<bool>();
if (j.contains("mine_idle_delay")) mine_idle_delay_= std::max(30, j["mine_idle_delay"].get<int>());
if (j.contains("saved_pool_urls") && j["saved_pool_urls"].is_array()) {
saved_pool_urls_.clear();
for (const auto& u : j["saved_pool_urls"])
@@ -231,6 +233,8 @@ bool Settings::save(const std::string& path)
j["pool_tls"] = pool_tls_;
j["pool_hugepages"] = pool_hugepages_;
j["pool_mode"] = pool_mode_;
j["mine_when_idle"] = mine_when_idle_;
j["mine_idle_delay"]= mine_idle_delay_;
j["saved_pool_urls"] = json::array();
for (const auto& u : saved_pool_urls_)
j["saved_pool_urls"].push_back(u);

View File

@@ -208,6 +208,12 @@ public:
bool getPoolMode() const { return pool_mode_; }
void setPoolMode(bool v) { pool_mode_ = v; }
// Mine when idle (auto-start mining when system is idle)
bool getMineWhenIdle() const { return mine_when_idle_; }
void setMineWhenIdle(bool v) { mine_when_idle_ = v; }
int getMineIdleDelay() const { return mine_idle_delay_; }
void setMineIdleDelay(int seconds) { mine_idle_delay_ = std::max(30, seconds); }
// Saved pool URLs (user-managed favorites dropdown)
const std::vector<std::string>& getSavedPoolUrls() const { return saved_pool_urls_; }
void addSavedPoolUrl(const std::string& url) {
@@ -289,6 +295,8 @@ private:
bool pool_tls_ = false;
bool pool_hugepages_ = true;
bool pool_mode_ = false; // false=solo, true=pool
bool mine_when_idle_ = false; // auto-start mining when system idle
int mine_idle_delay_= 120; // seconds of idle before mining starts
std::vector<std::string> saved_pool_urls_; // user-saved pool URL favorites
std::vector<std::string> saved_pool_workers_; // user-saved worker address favorites

View File

@@ -87,7 +87,7 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
curl_easy_setopt(impl_->curl, CURLOPT_HTTPHEADER, impl_->headers);
curl_easy_setopt(impl_->curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, 30L);
curl_easy_setopt(impl_->curl, CURLOPT_CONNECTTIMEOUT, 3L);
curl_easy_setopt(impl_->curl, CURLOPT_CONNECTTIMEOUT, 1L); // localhost — fails fast if not listening
// Test connection with getinfo
try {

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();

View File

@@ -30,6 +30,7 @@
#include <unistd.h>
#include <pwd.h>
#include <dirent.h>
#include <dlfcn.h>
#ifdef __APPLE__
#include <mach-o/dyld.h>
#include <sys/sysctl.h>
@@ -596,5 +597,94 @@ double Platform::getDaemonMemoryUsageMB()
#endif
}
// ============================================================================
// System-wide idle time detection
// ============================================================================
int Platform::getSystemIdleSeconds()
{
#ifdef _WIN32
LASTINPUTINFO lii;
lii.cbSize = sizeof(lii);
if (GetLastInputInfo(&lii)) {
DWORD elapsed = GetTickCount() - lii.dwTime;
return (int)(elapsed / 1000);
}
return 0;
#elif defined(__APPLE__)
// macOS: use IOKit HIDSystem idle time
// Fallback to 0 (feature not supported) to keep it simple
return 0;
#else
// Linux: dynamically load libXss to query X11 screensaver idle time
// This avoids a hard link dependency on X11/Xss libraries.
typedef struct {
int type;
unsigned long serial;
int/*Bool*/ send_event;
void* /*Display**/ display;
unsigned long/*Drawable*/ window;
int state;
int kind;
unsigned long til_or_since;
unsigned long idle;
unsigned long eventMask;
} XScreenSaverInfo;
// Function pointer types
typedef void* (*XOpenDisplay_t)(const char*);
typedef unsigned long (*XDefaultRootWindow_t)(void*);
typedef XScreenSaverInfo* (*XScreenSaverAllocInfo_t)(void);
typedef int (*XScreenSaverQueryInfo_t)(void*, unsigned long, XScreenSaverInfo*);
typedef int (*XCloseDisplay_t)(void*);
typedef void (*XFree_t)(void*);
static bool s_tried = false;
static void* s_x11 = nullptr;
static void* s_xss = nullptr;
static XOpenDisplay_t s_XOpenDisplay = nullptr;
static XDefaultRootWindow_t s_XDefaultRootWindow = nullptr;
static XScreenSaverAllocInfo_t s_XScreenSaverAllocInfo = nullptr;
static XScreenSaverQueryInfo_t s_XScreenSaverQueryInfo = nullptr;
static XCloseDisplay_t s_XCloseDisplay = nullptr;
(void)s_XCloseDisplay; // retained for potential cleanup
static XFree_t s_XFree = nullptr;
static void* s_display = nullptr;
if (!s_tried) {
s_tried = true;
s_x11 = dlopen("libX11.so.6", RTLD_LAZY);
s_xss = dlopen("libXss.so.1", RTLD_LAZY);
if (s_x11 && s_xss) {
s_XOpenDisplay = (XOpenDisplay_t)dlsym(s_x11, "XOpenDisplay");
s_XDefaultRootWindow = (XDefaultRootWindow_t)dlsym(s_x11, "XDefaultRootWindow");
s_XCloseDisplay = (XCloseDisplay_t)dlsym(s_x11, "XCloseDisplay"); // NOLINT
s_XFree = (XFree_t)dlsym(s_x11, "XFree");
s_XScreenSaverAllocInfo = (XScreenSaverAllocInfo_t)dlsym(s_xss, "XScreenSaverAllocInfo");
s_XScreenSaverQueryInfo = (XScreenSaverQueryInfo_t)dlsym(s_xss, "XScreenSaverQueryInfo");
if (s_XOpenDisplay && s_XDefaultRootWindow && s_XScreenSaverAllocInfo && s_XScreenSaverQueryInfo) {
s_display = s_XOpenDisplay(nullptr);
}
}
}
if (!s_display || !s_XScreenSaverAllocInfo || !s_XScreenSaverQueryInfo || !s_XDefaultRootWindow) {
return 0;
}
XScreenSaverInfo* info = s_XScreenSaverAllocInfo();
if (!info) return 0;
int idleSec = 0;
unsigned long root = s_XDefaultRootWindow(s_display);
if (s_XScreenSaverQueryInfo(s_display, root, info)) {
idleSec = (int)(info->idle / 1000); // idle is in milliseconds
}
if (s_XFree) s_XFree(info);
return idleSec;
#endif
}
} // namespace util
} // namespace dragonx

View File

@@ -123,6 +123,14 @@ public:
* @return Combined daemon RSS in MB, or 0 if no daemon found
*/
static double getDaemonMemoryUsageMB();
/**
* @brief Get system-wide idle time in seconds
* Uses platform-specific APIs: GetLastInputInfo (Windows),
* XScreenSaverQueryInfo via dlopen (Linux), IOKit (macOS).
* @return Seconds since last user input, or 0 on failure
*/
static int getSystemIdleSeconds();
};
/**