// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "platform.h" #include #include #include #include #include #include // Embedded ui.toml (generated at build time) #if __has_include("ui_toml_embedded.h") #include "ui_toml_embedded.h" #define HAS_EMBEDDED_UI_TOML 1 #else #define HAS_EMBEDDED_UI_TOML 0 #endif #include #ifdef _WIN32 #include #include #include #include #include #else #include #include #include #include #ifdef __APPLE__ #include #include #include #endif #endif #include "../util/logger.h" namespace dragonx { namespace util { bool Platform::openUrl(const std::string& url) { if (url.empty()) return false; #ifdef _WIN32 // Windows: Use ShellExecute HINSTANCE result = ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL); return (reinterpret_cast(result) > 32); #elif defined(__APPLE__) // macOS: Use 'open' command std::string cmd = "open \"" + url + "\" &"; return (system(cmd.c_str()) == 0); #else // Linux: Use xdg-open std::string cmd = "xdg-open \"" + url + "\" >/dev/null 2>&1 &"; return (system(cmd.c_str()) == 0); #endif } bool Platform::openFolder(const std::string& path, bool createIfMissing) { if (path.empty()) return false; // Create directory if it doesn't exist if (createIfMissing) { #ifdef _WIN32 // Windows: Create directory recursively std::string cmd = "mkdir \"" + path + "\" 2>nul"; (void)system(cmd.c_str()); // Ignore return value - dir may already exist #else // Linux/macOS: Create directory with parents std::string cmd = "mkdir -p \"" + path + "\" 2>/dev/null"; (void)system(cmd.c_str()); // Ignore return value - dir may already exist #endif } #ifdef _WIN32 // Windows: Use explorer.exe to open folder HINSTANCE result = ShellExecuteA(nullptr, "explore", path.c_str(), nullptr, nullptr, SW_SHOWNORMAL); return (reinterpret_cast(result) > 32); #elif defined(__APPLE__) // macOS: Use 'open' command (works for folders too) std::string cmd = "open \"" + path + "\" &"; return (system(cmd.c_str()) == 0); #else // Linux: Use xdg-open (works for folders too) std::string cmd = "xdg-open \"" + path + "\" >/dev/null 2>&1 &"; return (system(cmd.c_str()) == 0); #endif } uint64_t Platform::getFileSize(const std::string& path) { struct stat st; if (stat(path.c_str(), &st) == 0) { return static_cast(st.st_size); } return 0; } std::string Platform::formatFileSize(uint64_t bytes) { char buf[64]; if (bytes >= 1024ULL * 1024ULL * 1024ULL) { snprintf(buf, sizeof(buf), "%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0)); } else if (bytes >= 1024ULL * 1024ULL) { snprintf(buf, sizeof(buf), "%.2f MB", bytes / (1024.0 * 1024.0)); } else if (bytes >= 1024ULL) { snprintf(buf, sizeof(buf), "%.2f KB", bytes / 1024.0); } else { snprintf(buf, sizeof(buf), "%llu bytes", static_cast(bytes)); } return std::string(buf); } std::string Platform::getHomeDir() { #ifdef _WIN32 char path[MAX_PATH]; if (SUCCEEDED(SHGetFolderPathA(nullptr, CSIDL_PROFILE, nullptr, 0, path))) { return std::string(path); } // Fallback const char* home = getenv("USERPROFILE"); return home ? home : "C:\\"; #else const char* home = getenv("HOME"); if (home) return home; // Fallback to passwd entry struct passwd* pw = getpwuid(getuid()); if (pw && pw->pw_dir) return pw->pw_dir; return "/tmp"; #endif } std::string Platform::getDragonXDataDir() { #ifdef _WIN32 char path[MAX_PATH]; if (SUCCEEDED(SHGetFolderPathA(nullptr, CSIDL_APPDATA, nullptr, 0, path))) { return std::string(path) + "\\Hush\\DRAGONX\\"; } return getHomeDir() + "\\AppData\\Roaming\\Hush\\DRAGONX\\"; #elif defined(__APPLE__) return getHomeDir() + "/Library/Application Support/Hush/DRAGONX/"; #else return getHomeDir() + "/.hush/DRAGONX/"; #endif } std::string Platform::getDataDir() { return getDragonXDataDir(); } std::string Platform::getConfigDir() { #ifdef _WIN32 char path[MAX_PATH]; if (SUCCEEDED(SHGetFolderPathA(nullptr, CSIDL_APPDATA, nullptr, 0, path))) { return std::string(path) + "\\ObsidianDragon\\"; } return getHomeDir() + "\\AppData\\Roaming\\ObsidianDragon\\"; #else return getHomeDir() + "/.config/ObsidianDragon/"; #endif } bool Platform::deleteFile(const std::string& path) { #ifdef _WIN32 return DeleteFileA(path.c_str()) != 0 || GetLastError() == ERROR_FILE_NOT_FOUND; #else return (remove(path.c_str()) == 0 || errno == ENOENT); #endif } std::string Platform::getExecutableDirectory() { #ifdef _WIN32 char path[MAX_PATH]; DWORD len = GetModuleFileNameA(nullptr, path, MAX_PATH); if (len > 0 && len < MAX_PATH) { std::string fullPath(path); size_t pos = fullPath.find_last_of("\\/"); if (pos != std::string::npos) { return fullPath.substr(0, pos); } } return "."; #elif defined(__APPLE__) char path[1024]; uint32_t size = sizeof(path); if (_NSGetExecutablePath(path, &size) == 0) { char* resolved = realpath(path, nullptr); if (resolved) { std::string fullPath(resolved); free(resolved); size_t pos = fullPath.find_last_of('/'); if (pos != std::string::npos) { return fullPath.substr(0, pos); } } } return "."; #else // Linux: read /proc/self/exe char path[1024]; ssize_t len = readlink("/proc/self/exe", path, sizeof(path) - 1); if (len > 0) { path[len] = '\0'; std::string fullPath(path); size_t pos = fullPath.find_last_of('/'); if (pos != std::string::npos) { return fullPath.substr(0, pos); } } return "."; #endif } std::string getExecutableDirectory() { return Platform::getExecutableDirectory(); } std::string Platform::getObsidianDragonDir() { #ifdef _WIN32 const char* appdata = std::getenv("APPDATA"); if (appdata) { return (std::filesystem::path(appdata) / "ObsidianDragon").string(); } return (std::filesystem::path(getHomeDir()) / "AppData" / "Roaming" / "ObsidianDragon").string(); #elif defined(__APPLE__) return (std::filesystem::path(getHomeDir()) / "Library" / "Application Support" / "ObsidianDragon").string(); #else const char* xdg_config = std::getenv("XDG_CONFIG_HOME"); if (xdg_config) { return (std::filesystem::path(xdg_config) / "ObsidianDragon").string(); } return (std::filesystem::path(getHomeDir()) / ".config" / "ObsidianDragon").string(); #endif } void Platform::ensureObsidianDragonSetup() { namespace fs = std::filesystem; fs::path base = getObsidianDragonDir(); fs::path themes_dir = base / "themes"; fs::path example_dir = base / "example" / "example_theme"; fs::path example_img = example_dir / "img"; // Create directory structure std::error_code ec; fs::create_directories(themes_dir, ec); fs::create_directories(example_img, ec); if (ec) { DEBUG_LOGF("Warning: Could not create ObsidianDragon directories: %s\n", ec.message().c_str()); return; } DEBUG_LOGF("ObsidianDragon: Config directory: %s\n", base.string().c_str()); // Write example theme.toml — a full copy of ui.toml so users can see // and modify the complete layout+theme structure. UISchema can load // themes from TOML format. // Regenerate whenever the binary is newer than the example file, so // layout changes in ui.toml are always reflected. fs::path example_toml = example_dir / "theme.toml"; bool needsWrite = !fs::exists(example_toml); if (!needsWrite) { // Regenerate if the running binary is newer than the example file fs::path exePath = fs::path(getExecutableDirectory()) / #ifdef _WIN32 "ObsidianDragon.exe"; #else "ObsidianDragon"; #endif std::error_code tec; auto exeTime = fs::last_write_time(exePath, tec); auto fileTime = fs::last_write_time(example_toml, tec); if (!tec && exeTime > fileTime) needsWrite = true; } if (needsWrite) { std::ofstream f(example_toml); if (f.is_open()) { // Use the build-time embedded copy first; fall back to the // bundled file on disk. std::string content; #if HAS_EMBEDDED_UI_TOML content.assign(reinterpret_cast(embedded::ui_toml_data), embedded::ui_toml_size); #else { fs::path bundled = fs::path(getExecutableDirectory()) / "res" / "themes" / "ui.toml"; std::ifstream src(bundled); if (src.is_open()) { std::ostringstream ss; ss << src.rdbuf(); content = ss.str(); } } #endif if (!content.empty()) { // Patch the theme metadata so the example is clearly // distinguishable from the bundled default. // In TOML: name = "DragonX" → name = "Example Theme" auto replaceFirst = [&](const std::string& key, const std::string& oldVal, const std::string& newVal) { // Find 'key = "oldVal"' pattern and replace oldVal std::string needle = key + " = "; auto pos = content.find(needle); if (pos == std::string::npos) return; auto vpos = content.find(oldVal, pos); if (vpos != std::string::npos && vpos - pos < 80) content.replace(vpos, oldVal.size(), newVal); }; replaceFirst("name", "\"DragonX\"", "\"Example Theme\""); replaceFirst("author", "\"The Hush Developers\"", "\"Your Name\""); // Prepend comment block (TOML uses # for comments natively) std::string header = "# === DragonX Wallet Theme File ===\n" "# Copy this entire folder into the 'themes' folder to use it.\n" "# The folder name becomes the theme ID.\n" "#\n" "# === Images ===\n" "# Place image files in the 'img' subfolder.\n" "# Specify custom filenames below, or use the defaults:\n" "# backgrounds/gradient/dark_gradient.png - background gradient overlay\n" "# logos/logo_ObsidianDragon_dark.png - sidebar logo (128x128 recommended)\n" "#\n" "# === Colors ===\n" "# Colors use CSS-style formats: #RRGGBB, #RRGGBBAA, or rgba(r,g,b,a)\n" "# Palette variables can be referenced elsewhere with var(--name)\n\n"; content = header + content; } else { DEBUG_LOGF("ObsidianDragon: Warning — ui.toml not available, skipping example theme\n"); } f << content; f.close(); DEBUG_LOGF("ObsidianDragon: Created example theme: %s\n", example_toml.string().c_str()); } } // Copy bundled images into example/example_theme/img/ for reference fs::path exe_dir = getExecutableDirectory(); fs::path res_img = exe_dir / "res" / "img"; auto copyIfMissing = [&](const char* srcRelPath, const char* dstFilename) { fs::path src = res_img / srcRelPath; fs::path dst = example_img / dstFilename; if (fs::exists(src) && !fs::exists(dst)) { std::error_code cpec; fs::copy_file(src, dst, cpec); if (!cpec) { DEBUG_LOGF("ObsidianDragon: Copied %s to example theme\n", dstFilename); } } }; copyIfMissing("backgrounds/gradient/dark_gradient.png", "dark_gradient.png"); copyIfMissing("logos/logo_ObsidianDragon_dark.png", "logo_ObsidianDragon_dark.png"); copyIfMissing("logos/logo_ObsidianDragon_light.png", "logo_ObsidianDragon_light.png"); } double Platform::getTotalSystemRAM_MB() { static double cached = 0.0; if (cached > 0.0) return cached; #ifdef _WIN32 MEMORYSTATUSEX memInfo; memInfo.dwLength = sizeof(memInfo); if (GlobalMemoryStatusEx(&memInfo)) { cached = (double)memInfo.ullTotalPhys / (1024.0 * 1024.0); } #elif defined(__APPLE__) int64_t memSize = 0; size_t len = sizeof(memSize); if (sysctlbyname("hw.memsize", &memSize, &len, nullptr, 0) == 0) { cached = (double)memSize / (1024.0 * 1024.0); } #else // Linux: use sysconf long pages = sysconf(_SC_PHYS_PAGES); long pageSize = sysconf(_SC_PAGE_SIZE); if (pages > 0 && pageSize > 0) { cached = (double)pages * (double)pageSize / (1024.0 * 1024.0); } #endif return cached; } double Platform::getUsedSystemRAM_MB() { #ifdef _WIN32 MEMORYSTATUSEX memInfo; memInfo.dwLength = sizeof(memInfo); if (GlobalMemoryStatusEx(&memInfo)) { double total = (double)memInfo.ullTotalPhys / (1024.0 * 1024.0); double avail = (double)memInfo.ullAvailPhys / (1024.0 * 1024.0); return total - avail; } return 0.0; #elif defined(__APPLE__) // macOS: vm_statistics64 for free/active/inactive/wired pages vm_size_t pageSize = 0; host_page_size(mach_host_self(), &pageSize); vm_statistics64_data_t vmStats; mach_msg_type_number_t count = HOST_VM_INFO64_COUNT; if (host_statistics64(mach_host_self(), HOST_VM_INFO64, (host_info64_t)&vmStats, &count) == KERN_SUCCESS) { // "used" = total - free - inactive (matches Activity Monitor "Memory Used") double total = getTotalSystemRAM_MB(); double freeMB = (double)(vmStats.free_count + vmStats.inactive_count) * (double)pageSize / (1024.0 * 1024.0); return total - freeMB; } return 0.0; #else // Linux: parse /proc/meminfo for MemAvailable FILE* f = fopen("/proc/meminfo", "r"); if (!f) return 0.0; double totalMB = 0.0, availMB = 0.0; char line[256]; int found = 0; while (fgets(line, sizeof(line), f) && found < 2) { long val = 0; if (strncmp(line, "MemTotal:", 9) == 0) { if (sscanf(line + 9, "%ld", &val) == 1) { totalMB = (double)val / 1024.0; found++; } } else if (strncmp(line, "MemAvailable:", 13) == 0) { if (sscanf(line + 13, "%ld", &val) == 1) { availMB = (double)val / 1024.0; found++; } } } fclose(f); if (found == 2) return totalMB - availMB; return 0.0; #endif } double Platform::getSelfMemoryUsageMB() { #ifdef _WIN32 PROCESS_MEMORY_COUNTERS pmc; if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) { return (double)pmc.WorkingSetSize / (1024.0 * 1024.0); } return 0.0; #elif defined(__APPLE__) struct mach_task_basic_info info; mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT; if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&info, &count) == KERN_SUCCESS) { return (double)info.resident_size / (1024.0 * 1024.0); } return 0.0; #else // Linux: read /proc/self/status for VmRSS FILE* f = fopen("/proc/self/status", "r"); if (!f) return 0.0; char line[256]; while (fgets(line, sizeof(line), f)) { if (strncmp(line, "VmRSS:", 6) == 0) { long val = 0; if (sscanf(line + 6, "%ld", &val) == 1) { fclose(f); return (double)val / 1024.0; // kB -> MB } } } fclose(f); return 0.0; #endif } double Platform::getDaemonMemoryUsageMB() { #ifdef _WIN32 // Windows: use CreateToolhelp32Snapshot to find dragonxd.exe processes // and query their working set size. HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snap == INVALID_HANDLE_VALUE) return 0.0; PROCESSENTRY32 entry; entry.dwSize = sizeof(entry); double totalMB = 0.0; if (Process32First(snap, &entry)) { do { if (_stricmp(entry.szExeFile, "dragonxd.exe") == 0) { // GetProcessMemoryInfo requires PROCESS_QUERY_INFORMATION + PROCESS_VM_READ. // Try full access first; fall back to limited for elevated processes. HANDLE hProc = OpenProcess( PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, entry.th32ProcessID); if (!hProc) { hProc = OpenProcess( PROCESS_QUERY_LIMITED_INFORMATION, FALSE, entry.th32ProcessID); } if (hProc) { PROCESS_MEMORY_COUNTERS pmc; ZeroMemory(&pmc, sizeof(pmc)); pmc.cb = sizeof(pmc); if (GetProcessMemoryInfo(hProc, &pmc, sizeof(pmc))) totalMB += (double)pmc.WorkingSetSize / (1024.0 * 1024.0); CloseHandle(hProc); } } } while (Process32Next(snap, &entry)); } CloseHandle(snap); return totalMB; #elif defined(__APPLE__) // macOS: use pgrep to find dragonxd PIDs, then read RSS via proc_pidinfo FILE* fp = popen("pgrep -x dragonxd", "r"); if (!fp) return 0.0; double totalMB = 0.0; char line[64]; while (fgets(line, sizeof(line), fp)) { pid_t pid = (pid_t)atoi(line); if (pid <= 0) continue; struct mach_task_basic_info info; mach_msg_type_number_t cnt = MACH_TASK_BASIC_INFO_COUNT; // We can't directly task_for_pid without entitlements, so fall back to ps char cmd[128]; snprintf(cmd, sizeof(cmd), "ps -o rss= -p %d", pid); FILE* ps = popen(cmd, "r"); if (ps) { char rssLine[64]; if (fgets(rssLine, sizeof(rssLine), ps)) { long rssKB = atol(rssLine); if (rssKB > 0) totalMB += (double)rssKB / 1024.0; } pclose(ps); } } pclose(fp); return totalMB; #else // Linux: iterate /proc//comm looking for "dragonxd" DIR* procDir = opendir("/proc"); if (!procDir) return 0.0; double totalMB = 0.0; pid_t selfPid = getpid(); struct dirent* entry; while ((entry = readdir(procDir)) != nullptr) { // Only numeric directories (PIDs) if (entry->d_name[0] < '0' || entry->d_name[0] > '9') continue; pid_t pid = (pid_t)atoi(entry->d_name); if (pid <= 0 || pid == selfPid) continue; // Read comm (process name) char commPath[64]; snprintf(commPath, sizeof(commPath), "/proc/%d/comm", pid); FILE* cf = fopen(commPath, "r"); if (!cf) continue; char comm[256] = {}; if (fgets(comm, sizeof(comm), cf)) { // Strip trailing newline size_t len = strlen(comm); if (len > 0 && comm[len - 1] == '\n') comm[len - 1] = '\0'; } fclose(cf); if (strcmp(comm, "dragonxd") != 0) continue; // Read VmRSS from /proc//status char statusPath[64]; snprintf(statusPath, sizeof(statusPath), "/proc/%d/status", pid); FILE* sf = fopen(statusPath, "r"); if (!sf) continue; char sline[256]; while (fgets(sline, sizeof(sline), sf)) { if (strncmp(sline, "VmRSS:", 6) == 0) { long val = 0; if (sscanf(sline + 6, "%ld", &val) == 1) totalMB += (double)val / 1024.0; // kB -> MB break; } } fclose(sf); } closedir(procDir); return totalMB; #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 } // ============================================================================ // GPU utilization detection // ============================================================================ int Platform::getGpuUtilization() { #ifdef _WIN32 // Windows: read GPU utilization via SetupAPI / D3DKMT // Not all GPUs expose this; return -1 if unavailable. // Use a popen fallback: nvidia-smi for NVIDIA, or return -1. static bool s_tried_nvidia = false; static bool s_has_nvidia = false; if (!s_tried_nvidia) { s_tried_nvidia = true; FILE* f = _popen("where nvidia-smi 2>nul", "r"); if (f) { char buf[256]; s_has_nvidia = (fgets(buf, sizeof(buf), f) != nullptr); _pclose(f); } } if (s_has_nvidia) { FILE* f = _popen("nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits 2>nul", "r"); if (f) { char buf[64]; int util = -1; if (fgets(buf, sizeof(buf), f)) { util = atoi(buf); if (util < 0 || util > 100) util = -1; } _pclose(f); return util; } } return -1; #elif defined(__APPLE__) return -1; #else // Linux: try multiple GPU sysfs paths // AMD: /sys/class/drm/card*/device/gpu_busy_percent { // Try card0 through card3 char path[128]; for (int i = 0; i < 4; i++) { snprintf(path, sizeof(path), "/sys/class/drm/card%d/device/gpu_busy_percent", i); std::ifstream ifs(path); if (ifs.is_open()) { int val = -1; ifs >> val; if (val >= 0 && val <= 100) return val; } } } // NVIDIA: nvidia-smi (binary may exist even without sysfs) { static bool s_tried = false; static bool s_has_nvidia_smi = false; if (!s_tried) { s_tried = true; FILE* f = popen("which nvidia-smi 2>/dev/null", "r"); if (f) { char buf[256]; s_has_nvidia_smi = (fgets(buf, sizeof(buf), f) != nullptr); pclose(f); } } if (s_has_nvidia_smi) { FILE* f = popen("nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits 2>/dev/null", "r"); if (f) { char buf[64]; int util = -1; if (fgets(buf, sizeof(buf), f)) { util = atoi(buf); if (util < 0 || util > 100) util = -1; } pclose(f); return util; } } } // Intel: compare current vs max freq as a rough proxy { std::ifstream curF("/sys/class/drm/card0/gt_cur_freq_mhz"); std::ifstream maxF("/sys/class/drm/card0/gt_max_freq_mhz"); if (curF.is_open() && maxF.is_open()) { int cur = 0, mx = 0; curF >> cur; maxF >> mx; if (mx > 0) return std::min(100, (cur * 100) / mx); } } return -1; #endif } } // namespace util } // namespace dragonx