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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 15 KiB |
@@ -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 }
|
||||
|
||||
4
setup.sh
@@ -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
|
||||
}
|
||||
|
||||
|
||||
20
src/app.cpp
@@ -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
|
||||
|
||||
10
src/app.h
@@ -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
|
||||
|
||||
@@ -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
|
||||
// ============================================================================
|
||||
|
||||
@@ -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)
|
||||
// ===========================================================================
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||