summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile21
-rw-r--r--src/btop.cpp6
-rw-r--r--src/btop_draw.cpp19
-rw-r--r--src/btop_input.cpp6
-rw-r--r--src/btop_tools.hpp7
-rw-r--r--src/osx/btop_collect.cpp1193
6 files changed, 1238 insertions, 14 deletions
diff --git a/Makefile b/Makefile
index c614b93..4a43e33 100644
--- a/Makefile
+++ b/Makefile
@@ -82,13 +82,26 @@ OBJEXT := o
override REQFLAGS := -std=c++20
WARNFLAGS := -Wall -Wextra -pedantic
OPTFLAGS ?= -O2 -ftree-loop-vectorize -flto=$(THREADS)
-LDCXXFLAGS := -pthread -D_FORTIFY_SOURCE=2 -D_GLIBCXX_ASSERTIONS -fexceptions -fstack-protector -fstack-clash-protection $(ADDFLAGS)
+LDCXXFLAGS := -pthread -D_FORTIFY_SOURCE=2 -D_GLIBCXX_ASSERTIONS -fexceptions $(ADDFLAGS)
override CXXFLAGS += $(REQFLAGS) $(LDCXXFLAGS) $(OPTFLAGS) $(WARNFLAGS)
override LDFLAGS += $(LDCXXFLAGS) $(OPTFLAGS) $(WARNFLAGS)
INC := -I$(INCDIR) -I$(SRCDIR)
SU_USER := root
SU_GROUP := root
+ifdef DEBUG
+ override OPTFLAGS := -O0 -g
+endif
+
+ifeq ($(PLATFORM), OSX)
+ override LDCXXFLAGS += -framework IOKit -framework CoreFoundation
+endif
+ifneq ($(ARCH),arm64)
+ifneq ($(PLATFORM),OSX)
+ override LDCXXFLAGS += -fstack-protector -fstack-clash-protection
+endif
+endif
+
SOURCES := $(shell find $(SRCDIR) -maxdepth 1 -type f -name *.$(SRCEXT))
SOURCES += $(shell find $(SRCDIR)/$(PLATFORM_DIR) -type f -name *.$(SRCEXT))
@@ -124,7 +137,9 @@ help:
@printf " clean Remove built objects\n"
@printf " distclean Remove built objects and binaries\n"
@printf " install Install btop++ to \$$PREFIX ($(PREFIX))\n"
+ifneq ($(PLATFORM),OSX)
@printf " setuid Set installed binary owner/group to \$$SU_USER/\$$SU_GROUP ($(SU_USER)/$(SU_GROUP)) and set SUID bit\n"
+endif
@printf " uninstall Uninstall btop++ from \$$PREFIX\n"
@printf " info Display information about Environment,compiler and linker flags\n"
@@ -154,6 +169,7 @@ install:
@printf "\033[1;92mInstalling themes to: \033[1;97m$(DESTDIR)$(PREFIX)/share/btop/themes\033[0m\n"
@cp -pr themes $(DESTDIR)$(PREFIX)/share/btop
+ifneq ($(PLATFORM),OSX)
#? Set SUID bit for btop as $SU_USER in $SU_GROUP
setuid:
@printf "\033[1;97mFile: $(DESTDIR)$(PREFIX)/bin/btop\n"
@@ -161,6 +177,7 @@ setuid:
@chown $(SU_USER):$(SU_GROUP) $(DESTDIR)$(PREFIX)/bin/btop
@printf "\033[1;92mSetting SUID bit\033[0m\n"
@chmod u+s $(DESTDIR)$(PREFIX)/bin/btop
+endif
uninstall:
@printf "\033[1;91mRemoving: \033[1;97m$(DESTDIR)$(PREFIX)/bin/btop\033[0m\n"
@@ -179,7 +196,7 @@ btop: $(OBJECTS)
@$(QUIET) || printf "\n\033[1;92mLinking and optimizing binary\033[37m...\033[0m\n"
@$(CXX) -o $(TARGETDIR)/btop $^ $(LDFLAGS) || exit 1
@printf "\033[1;92m-> \033[1;37m$(TARGETDIR)/btop \033[100D\033[35C\033[1;93m(\033[1;97m$$(du -ah $(TARGETDIR)/btop | cut -f1)iB\033[1;93m) \033[92m(\033[97m$$(date -d @$$(expr $$(date +%s 2>/dev/null || echo "0") - $${TSTAMP} 2>/dev/null) -u +%Mm:%Ss 2>/dev/null | sed 's/^00m://' || echo '')\033[92m)\033[0m\n"
- printf "\n\033[1;92mBuild complete in \033[92m(\033[97m$$(date -d @$$(expr $$(date +%s 2>/dev/null || echo "0") - $(TIMESTAMP) 2>/dev/null) -u +%Mm:%Ss 2>/dev/null | sed 's/^00m://' || echo "unknown")\033[92m)\033[0m\n"
+ @printf "\n\033[1;92mBuild complete in \033[92m(\033[97m$$(date -d @$$(expr $$(date +%s 2>/dev/null || echo "0") - $(TIMESTAMP) 2>/dev/null) -u +%Mm:%Ss 2>/dev/null | sed 's/^00m://' || echo "unknown")\033[92m)\033[0m\n"
#? Compile
.ONESHELL:
diff --git a/src/btop.cpp b/src/btop.cpp
index f6a5978..2e0b01b 100644
--- a/src/btop.cpp
+++ b/src/btop.cpp
@@ -225,7 +225,11 @@ void clean_quit(int sig) {
//? Assume error if still not cleaned up and call quick_exit to avoid a segfault from Tools::atomic_lock destructor
if (Tools::active_locks > 0) {
+#ifdef __APPLE__
+ exit((sig != -1 ? sig : 0));
+#else
quick_exit((sig != -1 ? sig : 0));
+#endif
}
if (sig != -1) exit(sig);
@@ -292,7 +296,7 @@ namespace Runner {
pthread_mutex_t& pt_mutex;
public:
int status;
- thread_lock(pthread_mutex_t& mtx) : pt_mutex(mtx) { status = pthread_mutex_lock(&pt_mutex); }
+ thread_lock(pthread_mutex_t& mtx) : pt_mutex(mtx) { pthread_mutex_init(&mtx, NULL); status = pthread_mutex_lock(&pt_mutex); }
~thread_lock() { if (status == 0) pthread_mutex_unlock(&pt_mutex); }
};
diff --git a/src/btop_draw.cpp b/src/btop_draw.cpp
index d290832..609a336 100644
--- a/src/btop_draw.cpp
+++ b/src/btop_draw.cpp
@@ -815,7 +815,7 @@ namespace Mem {
if (title.empty()) title = capitalize(name);
const string humanized = floating_humanizer(mem.stats.at(name));
const string graphics = (use_graphs ? mem_graphs.at(name)(mem.percent.at(name), redraw or data_same) : mem_meters.at(name)(mem.percent.at(name).back()));
- if (mem_size > 2) {
+ if (mem_size > 2 && mem.percent.at(name).size() > 0) {
out += Mv::to(y+1+cy, x+1+cx) + divider + ljust(title, 4, false, false, not big_mem) + ljust(":", (big_mem ? 1 : 6))
+ Mv::to(y+1+cy, x+cx + mem_width - 2 - humanized.size()) + trans(humanized)
+ Mv::to(y+2+cy, x+cx + (graph_height >= 2 ? 0 : 1)) + graphics + up + rjust(to_string(mem.percent.at(name).back()) + "%", 4);
@@ -1313,14 +1313,15 @@ namespace Proc {
if (item_fit >= 7) out += cjust(to_string(detailed.entry.threads), item_width);
if (item_fit >= 8) out += cjust(to_string(detailed.entry.p_nice), item_width);
-
- const double mem_p = (double)detailed.mem_bytes.back() * 100 / totalMem;
- string mem_str = to_string(mem_p);
- mem_str.resize((mem_p < 10 or mem_p >= 100 ? 3 : 4));
- out += Mv::to(d_y + 4, d_x + 1) + Theme::c("title") + Fx::b + rjust((item_fit > 4 ? "Memory: " : "M:") + mem_str + "% ", (d_width / 3) - 2)
- + Theme::c("inactive_fg") + Fx::ub + graph_bg * (d_width / 3) + Mv::l(d_width / 3)
- + Theme::c("proc_misc") + detailed_mem_graph(detailed.mem_bytes, (redraw or data_same or not alive)) + ' '
- + Theme::c("title") + Fx::b + detailed.memory;
+ if (detailed.mem_bytes.size() > 0) {
+ const double mem_p = (double)detailed.mem_bytes.back() * 100 / totalMem;
+ string mem_str = to_string(mem_p);
+ mem_str.resize((mem_p < 10 or mem_p >= 100 ? 3 : 4));
+ out += Mv::to(d_y + 4, d_x + 1) + Theme::c("title") + Fx::b + rjust((item_fit > 4 ? "Memory: " : "M:") + mem_str + "% ", (d_width / 3) - 2)
+ + Theme::c("inactive_fg") + Fx::ub + graph_bg * (d_width / 3) + Mv::l(d_width / 3)
+ + Theme::c("proc_misc") + detailed_mem_graph(detailed.mem_bytes, (redraw or data_same or not alive)) + ' '
+ + Theme::c("title") + Fx::b + detailed.memory;
+ }
}
//? Check bounds of current selection and view
diff --git a/src/btop_input.cpp b/src/btop_input.cpp
index 2056bea..6e4fc02 100644
--- a/src/btop_input.cpp
+++ b/src/btop_input.cpp
@@ -32,6 +32,8 @@ using std::cin, std::vector, std::string_literals::operator""s;
using namespace Tools;
namespace rng = std::ranges;
+extern void clean_quit(int sig);
+
namespace Input {
//* Map for translating key codes to readable values
@@ -187,7 +189,7 @@ namespace Input {
if (not filtering) {
bool keep_going = false;
if (str_to_lower(key) == "q") {
- exit(0);
+ clean_quit(0);
}
else if (is_in(key, "escape", "m")) {
Menu::show(Menu::Menus::Main);
@@ -483,4 +485,4 @@ namespace Input {
}
}
-} \ No newline at end of file
+}
diff --git a/src/btop_tools.hpp b/src/btop_tools.hpp
index 97e010a..ea3b543 100644
--- a/src/btop_tools.hpp
+++ b/src/btop_tools.hpp
@@ -32,6 +32,13 @@ tab-size = 4
using std::string, std::vector, std::atomic, std::to_string, std::regex, std::tuple, std::array;
+#ifndef HOST_NAME_MAX
+#if defined(__APPLE__)
+#define HOST_NAME_MAX 255
+#else
+#define HOST_NAME_MAX 64
+#endif /* __APPLE__ */
+#endif /* HOST_NAME_MAX */
//? ------------------------------------------------- NAMESPACES ------------------------------------------------------
diff --git a/src/osx/btop_collect.cpp b/src/osx/btop_collect.cpp
new file mode 100644
index 0000000..789158a
--- /dev/null
+++ b/src/osx/btop_collect.cpp
@@ -0,0 +1,1193 @@
+/* Copyright 2021 Aristocratos (jakob@qvantnet.com)
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+indent = tab
+tab-size = 4
+*/
+
+#include <IOKit/ps/IOPSKeys.h>
+#include <IOKit/ps/IOPowerSources.h>
+#include <arpa/inet.h>
+#include <ifaddrs.h>
+#include <libproc.h>
+#include <mach/mach_host.h>
+#include <mach/mach_init.h>
+#include <mach/mach_types.h>
+#include <mach/processor_info.h>
+#include <mach/vm_statistics.h>
+#include <net/if.h>
+#include <net/if_dl.h>
+#include <netdb.h>
+#include <netinet/tcp_fsm.h>
+#include <pwd.h>
+#include <sys/socket.h>
+#include <sys/statvfs.h>
+#include <sys/sysctl.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <btop_config.hpp>
+#include <btop_shared.hpp>
+#include <btop_tools.hpp>
+#include <cmath>
+#include <fstream>
+#include <numeric>
+#include <ranges>
+#include <regex>
+#include <string>
+
+using std::clamp, std::string_literals::operator""s, std::cmp_equal, std::cmp_less, std::cmp_greater;
+using std::ifstream, std::numeric_limits, std::streamsize, std::round, std::max, std::min;
+namespace fs = std::filesystem;
+namespace rng = std::ranges;
+using namespace Tools;
+
+//? --------------------------------------------------- FUNCTIONS -----------------------------------------------------
+
+namespace Cpu {
+ vector<long long> core_old_totals;
+ vector<long long> core_old_idles;
+ vector<string> available_fields;
+ vector<string> available_sensors = {"Auto"};
+ cpu_info current_cpu;
+ fs::path freq_path = "/sys/devices/system/cpu/cpufreq/policy0/scaling_cur_freq";
+ bool got_sensors = false, cpu_temp_only = false;
+
+ //* Populate found_sensors map
+ bool get_sensors();
+
+ //* Get current cpu clock speed
+ string get_cpuHz();
+
+ //* Search /proc/cpuinfo for a cpu name
+ string get_cpuName();
+
+ struct Sensor {
+ fs::path path;
+ string label;
+ int64_t temp = 0;
+ int64_t high = 0;
+ int64_t crit = 0;
+ };
+
+ unordered_flat_map<string, Sensor> found_sensors;
+ string cpu_sensor;
+ vector<string> core_sensors;
+ unordered_flat_map<int, int> core_mapping;
+} // namespace Cpu
+
+namespace Mem {
+ double old_uptime;
+}
+
+namespace Shared {
+
+ fs::path passwd_path;
+ uint64_t totalMem;
+ long pageSize, clkTck, coreCount;
+ int totalMem_len;
+
+ void init() {
+ //? Shared global variables init
+
+ // passwd_path = (fs::is_regular_file(fs::path("/etc/passwd")) and access("/etc/passwd", R_OK) != -1) ? "/etc/passwd" : "";
+ // if (passwd_path.empty())
+ // Logger::warning("Could not read /etc/passwd, will show UID instead of username.");
+
+ coreCount = sysconf(_SC_NPROCESSORS_ONLN);
+ if (coreCount < 1) {
+ coreCount = 1;
+ Logger::warning("Could not determine number of cores, defaulting to 1.");
+ }
+
+ pageSize = sysconf(_SC_PAGE_SIZE);
+ if (pageSize <= 0) {
+ pageSize = 4096;
+ Logger::warning("Could not get system page size. Defaulting to 4096, processes memory usage might be incorrect.");
+ }
+
+ clkTck = sysconf(_SC_CLK_TCK);
+ if (clkTck <= 0) {
+ clkTck = 100;
+ Logger::warning("Could not get system clock ticks per second. Defaulting to 100, processes cpu usage might be incorrect.");
+ }
+
+ int64_t memsize = 0;
+ size_t size = sizeof(memsize);
+ if (sysctlbyname("hw.memsize", &memsize, &size, NULL, 0) < 0) {
+ Logger::warning("Could not get memory size");
+ }
+ totalMem = memsize;
+
+ //? Init for namespace Cpu
+ if (not fs::exists(Cpu::freq_path) or access(Cpu::freq_path.c_str(), R_OK) == -1) Cpu::freq_path.clear();
+ Cpu::current_cpu.core_percent.insert(Cpu::current_cpu.core_percent.begin(), Shared::coreCount, {});
+ Cpu::current_cpu.temp.insert(Cpu::current_cpu.temp.begin(), Shared::coreCount + 1, {});
+ Cpu::core_old_totals.insert(Cpu::core_old_totals.begin(), Shared::coreCount, 0);
+ Cpu::core_old_idles.insert(Cpu::core_old_idles.begin(), Shared::coreCount, 0);
+ Cpu::collect();
+ for (auto &[field, vec] : Cpu::current_cpu.cpu_percent) {
+ if (not vec.empty()) Cpu::available_fields.push_back(field);
+ }
+ Cpu::cpuName = Cpu::get_cpuName();
+ Cpu::got_sensors = Cpu::get_sensors();
+ for (const auto &[sensor, ignored] : Cpu::found_sensors) {
+ Cpu::available_sensors.push_back(sensor);
+ }
+ Cpu::core_mapping = Cpu::get_core_mapping();
+
+ //? Init for namespace Mem
+ Mem::old_uptime = system_uptime();
+ Mem::collect();
+ }
+
+} // namespace Shared
+
+namespace Cpu {
+ string cpuName;
+ string cpuHz;
+ bool has_battery = true;
+ tuple<int, long, string> current_bat;
+
+ const array<string, 10> time_names = {"user", "nice", "system", "idle", "iowait", "irq", "softirq", "steal", "guest", "guest_nice"};
+
+ unordered_flat_map<string, long long> cpu_old = {
+ {"totals", 0},
+ {"idles", 0},
+ {"user", 0},
+ {"nice", 0},
+ {"system", 0},
+ {"idle", 0},
+ {"iowait", 0},
+ {"irq", 0},
+ {"softirq", 0},
+ {"steal", 0},
+ {"guest", 0},
+ {"guest_nice", 0}};
+
+ string get_cpuName() {
+ string name;
+ char buffer[1024];
+ size_t size = sizeof(buffer);
+ if (sysctlbyname("machdep.cpu.brand_string", &buffer, &size, NULL, 0) < 0) {
+ Logger::error("Failed to get CPU name");
+ return name;
+ }
+ name = string(buffer);
+
+ auto name_vec = ssplit(name);
+
+ if ((s_contains(name, "Xeon"s) or v_contains(name_vec, "Duo"s)) and v_contains(name_vec, "CPU"s)) {
+ auto cpu_pos = v_index(name_vec, "CPU"s);
+ if (cpu_pos < name_vec.size() - 1 and not name_vec.at(cpu_pos + 1).ends_with(')'))
+ name = name_vec.at(cpu_pos + 1);
+ else
+ name.clear();
+ } else if (v_contains(name_vec, "Ryzen"s)) {
+ auto ryz_pos = v_index(name_vec, "Ryzen"s);
+ name = "Ryzen" + (ryz_pos < name_vec.size() - 1 ? ' ' + name_vec.at(ryz_pos + 1) : "") + (ryz_pos < name_vec.size() - 2 ? ' ' + name_vec.at(ryz_pos + 2) : "");
+ } else if (s_contains(name, "Intel"s) and v_contains(name_vec, "CPU"s)) {
+ auto cpu_pos = v_index(name_vec, "CPU"s);
+ if (cpu_pos < name_vec.size() - 1 and not name_vec.at(cpu_pos + 1).ends_with(')') and name_vec.at(cpu_pos + 1) != "@")
+ name = name_vec.at(cpu_pos + 1);
+ else
+ name.clear();
+ } else
+ name.clear();
+
+ if (name.empty() and not name_vec.empty()) {
+ for (const auto &n : name_vec) {
+ if (n == "@") break;
+ name += n + ' ';
+ }
+ name.pop_back();
+ for (const auto &reg : {regex("Processor"), regex("CPU"), regex("\\(R\\)"), regex("\\(TM\\)"), regex("Intel"),
+ regex("AMD"), regex("Core"), regex("\\d?\\.?\\d+[mMgG][hH][zZ]")}) {
+ name = std::regex_replace(name, reg, "");
+ }
+ name = trim(name);
+ }
+
+ return name;
+ }
+
+ bool get_sensors() {
+ return not found_sensors.empty();
+ }
+
+ void update_sensors() {
+ if (cpu_sensor.empty())
+ return;
+
+ const auto &cpu_sensor = (not Config::getS("cpu_sensor").empty() and found_sensors.contains(Config::getS("cpu_sensor")) ? Config::getS("cpu_sensor") : Cpu::cpu_sensor);
+
+ found_sensors.at(cpu_sensor).temp = stol(readfile(found_sensors.at(cpu_sensor).path, "0")) / 1000;
+ current_cpu.temp.at(0).push_back(found_sensors.at(cpu_sensor).temp);
+ current_cpu.temp_max = found_sensors.at(cpu_sensor).crit;
+ if (current_cpu.temp.at(0).size() > 20)
+ current_cpu.temp.at(0).pop_front();
+
+ if (Config::getB("show_coretemp") and not cpu_temp_only) {
+ vector<string> done;
+ for (const auto &sensor : core_sensors) {
+ if (v_contains(done, sensor))
+ continue;
+ found_sensors.at(sensor).temp = stol(readfile(found_sensors.at(sensor).path, "0")) / 1000;
+ done.push_back(sensor);
+ }
+ for (const auto &[core, temp] : core_mapping) {
+ if (cmp_less(core + 1, current_cpu.temp.size()) and cmp_less(temp, core_sensors.size())) {
+ current_cpu.temp.at(core + 1).push_back(found_sensors.at(core_sensors.at(temp)).temp);
+ if (current_cpu.temp.at(core + 1).size() > 20)
+ current_cpu.temp.at(core + 1).pop_front();
+ }
+ }
+ }
+ }
+
+ string get_cpuHz() {
+ unsigned int freq = 1;
+ size_t size = sizeof(freq);
+
+ int mib[2];
+ mib[0] = CTL_HW;
+ mib[1] = HW_CPU_FREQ;
+
+ if (sysctl(mib, 2, &freq, &size, NULL, 0) < 0) {
+ // this fails on Apple Silicon macs. Apparently you're not allowed to know
+ return "";
+ }
+ return std::to_string(freq / 1000.0 / 1000.0 / 1000.0).substr(0, 3);
+ }
+
+ auto get_core_mapping() -> unordered_flat_map<int, int> {
+ unordered_flat_map<int, int> core_map;
+ if (cpu_temp_only) return core_map;
+
+ natural_t cpu_count;
+ natural_t i;
+ processor_info_array_t info_array;
+ mach_msg_type_number_t info_count;
+ kern_return_t error;
+
+ mach_port_t host_port = mach_host_self();
+ error = host_processor_info(host_port, PROCESSOR_CPU_LOAD_INFO, &cpu_count, &info_array, &info_count);
+ if (error != KERN_SUCCESS) {
+ Logger::error("Failed getting CPU info");
+ return core_map;
+ }
+ for (i = 0; i < cpu_count; i++) {
+ core_map[i] = i;
+ }
+
+ //? If core mapping from cpuinfo was incomplete try to guess remainder, if missing completely, map 0-0 1-1 2-2 etc.
+ if (cmp_less(core_map.size(), Shared::coreCount)) {
+ if (Shared::coreCount % 2 == 0 and (long) core_map.size() == Shared::coreCount / 2) {
+ for (int i = 0, n = 0; i < Shared::coreCount / 2; i++) {
+ if (std::cmp_greater_equal(n, core_sensors.size())) n = 0;
+ core_map[Shared::coreCount / 2 + i] = n++;
+ }
+ } else {
+ core_map.clear();
+ for (int i = 0, n = 0; i < Shared::coreCount; i++) {
+ if (std::cmp_greater_equal(n, core_sensors.size())) n = 0;
+ core_map[i] = n++;
+ }
+ }
+ }
+
+ //? Apply user set custom mapping if any
+ const auto &custom_map = Config::getS("cpu_core_map");
+ if (not custom_map.empty()) {
+ try {
+ for (const auto &split : ssplit(custom_map)) {
+ const auto vals = ssplit(split, ':');
+ if (vals.size() != 2) continue;
+ int change_id = std::stoi(vals.at(0));
+ int new_id = std::stoi(vals.at(1));
+ if (not core_map.contains(change_id) or cmp_greater(new_id, core_sensors.size())) continue;
+ core_map.at(change_id) = new_id;
+ }
+ } catch (...) {
+ }
+ }
+
+ return core_map;
+ }
+
+ auto get_battery() -> tuple<int, long, string> {
+ if (not has_battery) return {0, 0, ""};
+
+ uint32_t percent = -1;
+ long seconds = -1;
+ string status = "discharging";
+ CFTypeRef ps_info = IOPSCopyPowerSourcesInfo();
+ if (ps_info) {
+ CFArrayRef one_ps_descriptor = IOPSCopyPowerSourcesList(ps_info);
+ if (one_ps_descriptor) {
+ if (CFArrayGetCount(one_ps_descriptor)) {
+ CFDictionaryRef one_ps = IOPSGetPowerSourceDescription(ps_info, CFArrayGetValueAtIndex(one_ps_descriptor, 0));
+ has_battery = true;
+ CFNumberRef remaining = (CFNumberRef)CFDictionaryGetValue(one_ps, CFSTR(kIOPSTimeToEmptyKey));
+ int32_t estimatedMinutesRemaining;
+ if (remaining) {
+ CFNumberGetValue(remaining, kCFNumberSInt32Type, &estimatedMinutesRemaining);
+ seconds = estimatedMinutesRemaining * 60;
+ }
+ CFNumberRef charge = (CFNumberRef)CFDictionaryGetValue(one_ps, CFSTR(kIOPSCurrentCapacityKey));
+ if (charge) {
+ CFNumberGetValue(charge, kCFNumberSInt32Type, &percent);
+ }
+ CFBooleanRef charging = (CFBooleanRef)CFDictionaryGetValue(one_ps, CFSTR(kIOPSIsChargingKey));
+ if (charging) {
+ bool isCharging = CFBooleanGetValue(charging);
+ if (isCharging) {
+ status = "charging";
+ }
+ }
+ if (percent == 100) {
+ status = "full";
+ }
+ } else {
+ has_battery = false;
+ }
+ CFRelease(one_ps_descriptor);
+ } else {
+ has_battery = false;
+ }
+ CFRelease(ps_info);
+ }
+ return {percent, seconds, status};
+ }
+
+ auto collect(const bool no_update) -> cpu_info & {
+ if (Runner::stopping or (no_update and not current_cpu.cpu_percent.at("total").empty()))
+ return current_cpu;
+ auto &cpu = current_cpu;
+
+ double avg[3];
+
+ if (getloadavg(avg, sizeof(avg)) < 0) {
+ Logger::error("failed to get load averages");
+ }
+
+ cpu.load_avg[0] = avg[0];
+ cpu.load_avg[1] = avg[1];
+ cpu.load_avg[2] = avg[2];
+ natural_t cpu_count;
+ natural_t i;
+ processor_info_array_t info_array;
+ mach_msg_type_number_t info_count;
+ kern_return_t error;
+ processor_cpu_load_info_data_t *cpu_load_info = NULL;
+
+ mach_port_t host_port = mach_host_self();
+ error = host_processor_info(host_port, PROCESSOR_CPU_LOAD_INFO, &cpu_count, &info_array, &info_count);
+ if (error != KERN_SUCCESS) {
+ Logger::error("Failed getting CPU load info");
+ }
+ cpu_load_info = (processor_cpu_load_info_data_t *)info_array;
+ long long global_totals = 0;
+ long long global_idles = 0;
+ for (i = 0; i < cpu_count; i++) {
+ vector<long long> times;
+ long long total_sum = 0;
+ //? 0=user, 1=nice, 2=system, 3=idle, 4=iowait, 5=irq, 6=softirq, 7=steal, 8=guest, 9=guest_nice
+ times.push_back(cpu_load_info[i].cpu_ticks[CPU_STATE_USER]);
+ times.push_back(cpu_load_info[i].cpu_ticks[CPU_STATE_NICE]);
+ times.push_back(cpu_load_info[i].cpu_ticks[CPU_STATE_SYSTEM]);
+ times.push_back(cpu_load_info[i].cpu_ticks[CPU_STATE_IDLE]);
+ times.push_back(0);
+ times.push_back(0);
+ times.push_back(0);
+ times.push_back(0);
+ times.push_back(0);
+ times.push_back(0);
+ for (long long t : times) {
+ total_sum += t;
+ }
+ try {
+ //? Subtract fields 8-9 and any future unknown fields
+ const long long totals = max(0ll, total_sum - (times.size() > 8 ? std::accumulate(times.begin() + 8, times.end(), 0) : 0));
+
+ //? Add iowait field if present
+ const long long idles = max(0ll, times.at(3) + (times.size() > 4 ? times.at(4) : 0));
+
+ global_totals += totals;
+ global_idles += idles;
+ //? Calculate cpu total for each core
+ if (i > Shared::coreCount) break;
+ const long long calc_totals = max(0ll, totals - core_old_totals.at(i));
+ const long long calc_idles = max(0ll, idles - core_old_idles.at(i));
+ core_old_totals.at(i) = totals;
+ core_old_idles.at(i) = idles;
+
+ cpu.core_percent.at(i).push_back(clamp((long long)round((double)(calc_totals - calc_idles) * 100 / calc_totals), 0ll, 100ll));
+
+ //? Reduce size if there are more values than needed for graph
+ if (cpu.core_percent.at(i).size() > 40) cpu.core_percent.at(i).pop_front();
+
+ //? Populate cpu.cpu_percent with all fields from syscall
+ for (int ii = 0; const auto &val : times) {
+ cpu.cpu_percent.at(time_names.at(ii)).push_back(clamp((long long)round((double)(val - cpu_old.at(time_names.at(ii))) * 100 / calc_totals), 0ll, 100ll));
+ cpu_old.at(time_names.at(ii)) = val;
+ }
+ } catch (const std::exception &e) {
+ Logger::error("get_cpuHz() : " + (string)e.what());
+ throw std::runtime_error("collect() : " + (string)e.what());
+ }
+ }
+ const long long calc_totals = max(1ll, global_totals - cpu_old.at("totals"));
+ const long long calc_idles = max(1ll, global_idles - cpu_old.at("idles"));
+ cpu_old.at("totals") = global_totals;
+ cpu_old.at("idles") = global_idles;
+
+ //? Total usage of cpu
+ cpu.cpu_percent.at("total").push_back(clamp((long long)round((double)(calc_totals - calc_idles) * 100 / calc_totals), 0ll, 100ll));
+
+ //? Reduce size if there are more values than needed for graph
+ while (cmp_greater(cpu.cpu_percent.at("total").size(), width * 2)) cpu.cpu_percent.at("total").pop_front();
+
+ if (Config::getB("show_cpu_freq")) {
+ auto hz = get_cpuHz();
+ if (hz != "") {
+ cpuHz = hz;
+ }
+ }
+
+ if (Config::getB("check_temp") and got_sensors)
+ update_sensors();
+
+ if (Config::getB("show_battery") and has_battery)
+ current_bat = get_battery();
+
+ return cpu;
+ }
+} // namespace Cpu
+
+namespace Mem {
+ bool has_swap = false;
+ vector<string> fstab;
+ fs::file_time_type fstab_time;
+ int disk_ios = 0;
+ vector<string> last_found;
+
+ mem_info current_mem{};
+
+ uint64_t get_totalMem() {
+ return Shared::totalMem;
+ }
+
+ auto collect(const bool no_update) -> mem_info & {
+ if (Runner::stopping or (no_update and not current_mem.percent.at("used").empty()))
+ return current_mem;
+
+ auto &show_swap = Config::getB("show_swap");
+ auto &show_disks = Config::getB("show_disks");
+ auto &swap_disk = Config::getB("swap_disk");
+ auto &mem = current_mem;
+ static const bool snapped = (getenv("BTOP_SNAPPED") != NULL);
+
+ vm_statistics64 p;
+ mach_msg_type_number_t info_size = HOST_VM_INFO64_COUNT;
+ if (host_statistics64(mach_host_self(), HOST_VM_INFO64, (host_info64_t)&p, &info_size) == 0) {
+ mem.stats.at("available") = p.free_count * Shared::pageSize;
+ mem.stats.at("free") = p.free_count * Shared::pageSize;
+ mem.stats.at("cached") = p.external_page_count * Shared::pageSize;
+ mem.stats.at("used") = (p.active_count + p.inactive_count + p.wire_count) * Shared::pageSize;
+ }
+
+ int mib[2] = {CTL_VM, VM_SWAPUSAGE};
+
+ struct xsw_usage swap;
+ size_t len = sizeof(struct xsw_usage);
+ if (sysctl(mib, 2, &swap, &len, NULL, 0) == 0) {
+ mem.stats.at("swap_total") = swap.xsu_total;
+ mem.stats.at("swap_free") = swap.xsu_avail;
+ mem.stats.at("swap_used") = swap.xsu_used;
+ }
+
+ if (show_swap and mem.stats.at("swap_total") > 0) {
+ for (const auto &name : swap_names) {
+ mem.percent.at(name).push_back(round((double)mem.stats.at(name) * 100 / mem.stats.at("swap_total")));
+ while (cmp_greater(mem.percent.at(name).size(), width * 2))
+ mem.percent.at(name).pop_front();
+ }
+ has_swap = true;
+ } else
+ has_swap = false;
+ //? Calculate percentages
+ for (const auto &name : mem_names) {
+ mem.percent.at(name).push_back(round((double)mem.stats.at(name) * 100 / Shared::totalMem));
+ while (cmp_greater(mem.percent.at(name).size(), width * 2))
+ mem.percent.at(name).pop_front();
+ }
+
+ if (show_disks) {
+ double uptime = system_uptime();
+ auto &disks_filter = Config::getS("disks_filter");
+ bool filter_exclude = false;
+ // auto &only_physical = Config::getB("only_physical");
+ auto &disks = mem.disks;
+ vector<string> filter;
+ if (not disks_filter.empty()) {
+ filter = ssplit(disks_filter);
+ if (filter.at(0).starts_with("exclude=")) {
+ filter_exclude = true;
+ filter.at(0) = filter.at(0).substr(8);
+ }
+ }
+
+ struct statfs *stfs;
+ int count = getmntinfo(&stfs, MNT_WAIT);
+ vector<string> found;
+ found.reserve(last_found.size());
+ for (int i = 0; i < count; i++) {
+ std::error_code ec;
+ string mountpoint = stfs[i].f_mntonname;
+ string dev = stfs[i].f_mntfromname;
+ disks[mountpoint] = disk_info{fs::canonical(dev, ec), fs::path(mountpoint).filename()};
+
+ //? Match filter if not empty
+ if (not filter.empty()) {
+ bool match = v_contains(filter, mountpoint);
+ if ((filter_exclude and match) or (not filter_exclude and not match))
+ continue;
+ }
+
+ found.push_back(mountpoint);
+ if (not v_contains(last_found, mountpoint))
+ redraw = true;
+
+ if (disks.at(mountpoint).dev.empty())
+ disks.at(mountpoint).dev = dev;
+ if (disks.at(mountpoint).name.empty())
+ disks.at(mountpoint).name = (mountpoint == "/" ? "root" : mountpoint);
+ disks.at(mountpoint).free = stfs[i].f_bfree;
+ disks.at(mountpoint).total = stfs[i].f_iosize;
+ }
+
+ //? Remove disks no longer mounted or filtered out
+ if (swap_disk and has_swap) found.push_back("swap");
+ for (auto it = disks.begin(); it != disks.end();) {
+ if (not v_contains(found, it->first))
+ it = disks.erase(it);
+ else
+ it++;
+ }
+ if (found.size() != last_found.size()) redraw = true;
+ last_found = std::move(found);
+
+ //? Get disk/partition stats
+ for (auto &[mountpoint, disk] : disks) {
+ if (std::error_code ec; not fs::exists(mountpoint, ec))
+ continue;
+ struct statvfs vfs;
+ if (statvfs(mountpoint.c_str(), &vfs) < 0) {
+ Logger::warning("Failed to get disk/partition stats with statvfs() for: " + mountpoint);
+ continue;
+ }
+ disk.total = vfs.f_blocks * vfs.f_frsize;
+ disk.free = vfs.f_bfree * vfs.f_frsize;
+ disk.used = disk.total - disk.free;
+ disk.used_percent = round((double)disk.used * 100 / disk.total);
+ disk.free_percent = 100 - disk.used_percent;
+ }
+
+ //? Setup disks order in UI and add swap if enabled
+ mem.disks_order.clear();
+ if (snapped and disks.contains("/mnt"))
+ mem.disks_order.push_back("/mnt");
+ else if (disks.contains("/"))
+ mem.disks_order.push_back("/");
+ if (swap_disk and has_swap) {
+ mem.disks_order.push_back("swap");
+ if (not disks.contains("swap"))
+ disks["swap"] = {"", "swap"};
+ disks.at("swap").total = mem.stats.at("swap_total");
+ disks.at("swap").used = mem.stats.at("swap_used");
+ disks.at("swap").free = mem.stats.at("swap_free");
+ disks.at("swap").used_percent = mem.percent.at("swap_used").back();
+ disks.at("swap").free_percent = mem.percent.at("swap_free").back();
+ }
+ for (const auto &name : last_found)
+ if (not is_in(name, "/", "swap", "/dev"))
+ mem.disks_order.push_back(name);
+
+ old_uptime = uptime;
+ }
+ return mem;
+ }
+
+} // namespace Mem
+
+namespace Net {
+ unordered_flat_map<string, net_info> current_net;
+ net_info empty_net = {};
+ vector<string> interfaces;
+ string selected_iface;
+ int errors = 0;
+ unordered_flat_map<string, uint64_t> graph_max = {{"download", {}}, {"upload", {}}};
+ unordered_flat_map<string, array<int, 2>> max_count = {{"download", {}}, {"upload", {}}};
+ bool rescale = true;
+ uint64_t timestamp = 0;
+
+ //* RAII wrapper for getifaddrs
+ class getifaddr_wrapper {
+ struct ifaddrs *ifaddr;
+
+ public:
+ int status;
+ getifaddr_wrapper() { status = getifaddrs(&ifaddr); }
+ ~getifaddr_wrapper() { freeifaddrs(ifaddr); }
+ auto operator()() -> struct ifaddrs * { return ifaddr; }
+ };
+
+ auto collect(const bool no_update) -> net_info & {
+ auto &net = current_net;
+ auto &config_iface = Config::getS("net_iface");
+ auto &net_sync = Config::getB("net_sync");
+ auto &net_auto = Config::getB("net_auto");
+ auto new_timestamp = time_ms();
+
+ if (not no_update and errors < 3) {
+ //? Get interface list using getifaddrs() wrapper
+ getifaddr_wrapper if_wrap{};
+ if (if_wrap.status != 0) {
+ errors++;
+ Logger::error("Net::collect() -> getifaddrs() failed with id " + to_string(if_wrap.status));
+ redraw = true;
+ return empty_net;
+ }
+ int family = 0;
+ char ip[NI_MAXHOST];
+ interfaces.clear();
+ string ipv4, ipv6;
+
+ //? Iteration over all items in getifaddrs() list
+ for (auto *ifa = if_wrap(); ifa != NULL; ifa = ifa->ifa_next) {
+ if (ifa->ifa_addr == NULL) continue;
+ family = ifa->ifa_addr->sa_family;
+ const auto &iface = ifa->ifa_name;
+ //? Get IPv4 address
+ if (family == AF_INET) {
+ if (getnameinfo(ifa->ifa_addr, sizeof(struct sockaddr_in), ip, NI_MAXHOST, NULL, 0, NI_NUMERICHOST) == 0)
+ net[iface].ipv4 = ip;
+ }
+ //? Get IPv6 address
+ else if (family == AF_INET6) {
+ if (getnameinfo(ifa->ifa_addr, sizeof(struct sockaddr_in6), ip, NI_MAXHOST, NULL, 0, NI_NUMERICHOST) == 0)
+ net[iface].ipv6 = ip;
+ }
+
+ //? Update available interfaces vector and get status of interface
+ if (not v_contains(interfaces, iface)) {
+ interfaces.push_back(iface);
+ net[iface].connected = (ifa->ifa_flags & IFF_RUNNING);
+ }
+ }
+
+ unordered_flat_map<string, std::tuple<uint64_t, uint64_t>> ifstats;
+ int mib[] = {CTL_NET, PF_ROUTE, 0, 0, NET_RT_IFLIST2, 0};
+ size_t len;
+ if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) {
+ Logger::error("failed getting network interfaces");
+ }
+ char *buf = (char *)malloc(len);
+ if (sysctl(mib, 6, buf, &len, NULL, 0) < 0) {
+ Logger::error("failed getting network interfaces");
+ }
+ char *lim = buf + len;
+ char *next = NULL;
+ for (next = buf; next < lim;) {
+ struct if_msghdr *ifm = (struct if_msghdr *)next;
+ next += ifm->ifm_msglen;
+ if (ifm->ifm_type == RTM_IFINFO2) {
+ struct if_msghdr2 *if2m = (struct if_msghdr2 *)ifm;
+ struct sockaddr_dl *sdl = (struct sockaddr_dl *)(if2m + 1);
+ char iface[32];
+ strncpy(iface, sdl->sdl_data, sdl->sdl_nlen);
+ iface[sdl->sdl_nlen] = 0;
+ ifstats[iface] = std::tuple(if2m->ifm_data.ifi_ibytes, if2m->ifm_data.ifi_obytes);
+ }
+ }
+
+ //? Get total recieved and transmitted bytes + device address if no ip was found
+ for (const auto &iface : interfaces) {
+ for (const string dir : {"download", "upload"}) {
+ auto &saved_stat = net.at(iface).stat.at(dir);
+ auto &bandwidth = net.at(iface).bandwidth.at(dir);
+ auto dirval = dir == "download" ? std::get<0>(ifstats[iface]) : std::get<1>(ifstats[iface]);
+ uint64_t val = saved_stat.last;
+ try {
+ val = max(dirval, val);
+ } catch (const std::invalid_argument &) {
+ } catch (const std::out_of_range &) {
+ }
+
+ //? Update speed, total and top values
+ saved_stat.speed = round((double)(val - saved_stat.last) / ((double)(new_timestamp - timestamp) / 1000));
+ if (saved_stat.speed > saved_stat.top) saved_stat.top = saved_stat.speed;
+ if (saved_stat.offset > val) saved_stat.offset = 0;
+ saved_stat.total = val - saved_stat.offset;
+ saved_stat.last = val;
+
+ //? Add values to graph
+ bandwidth.push_back(saved_stat.speed);
+ while (cmp_greater(bandwidth.size(), width * 2)) bandwidth.pop_front();
+
+ //? Set counters for auto scaling
+ if (net_auto and selected_iface == iface) {
+ if (saved_stat.speed > graph_max[dir]) {
+ ++max_count[dir][0];
+ if (max_count[dir][1] > 0) --max_count[dir][1];
+ } else if (graph_max[dir] > 10 << 10 and saved_stat.speed < graph_max[dir] / 10) {
+ ++max_count[dir][1];
+ if (max_count[dir][0] > 0) --max_count[dir][0];
+ }
+ }
+ }
+ }
+
+ //? Clean up net map if needed
+ if (net.size() > interfaces.size()) {
+ for (auto it = net.begin(); it != net.end();) {
+ if (not v_contains(interfaces, it->first))
+ it = net.erase(it);
+ else
+ it++;
+ }
+ net.compact();
+ }
+
+ timestamp = new_timestamp;
+ }
+ //? Return empty net_info struct if no interfaces was found
+ if (net.empty())
+ return empty_net;
+
+ //? Find an interface to display if selected isn't set or valid
+ if (selected_iface.empty() or not v_contains(interfaces, selected_iface)) {
+ max_count["download"][0] = max_count["download"][1] = max_count["upload"][0] = max_count["upload"][1] = 0;
+ redraw = true;
+ if (net_auto) rescale = true;
+ if (not config_iface.empty() and v_contains(interfaces, config_iface))
+ selected_iface = config_iface;
+ else {
+ //? Sort interfaces by total upload + download bytes
+ auto sorted_interfaces = interfaces;
+ rng::sort(sorted_interfaces, [&](const auto &a, const auto &b) {
+ return cmp_greater(net.at(a).stat["download"].total + net.at(a).stat["upload"].total,
+ net.at(b).stat["download"].total + net.at(b).stat["upload"].total);
+ });
+ selected_iface.clear();
+ //? Try to set to a connected interface
+ for (const auto &iface : sorted_interfaces) {
+ if (net.at(iface).connected) selected_iface = iface;
+ brea