From acb20832d15bf50325491ecccd9abe2d19f311e8 Mon Sep 17 00:00:00 2001 From: aristocratos Date: Tue, 10 Aug 2021 20:20:33 +0200 Subject: [PATCH] Added mem and disks --- Makefile | 19 +-- src/btop.cpp | 210 ++++++++------------------- src/btop_config.cpp | 9 +- src/btop_draw.cpp | 314 ++++++++++++++++++++++++++++++++++------- src/btop_input.cpp | 18 ++- src/btop_linux.cpp | 335 ++++++++++++++++++++++++++++++++++++++------ src/btop_shared.hpp | 26 +++- src/btop_theme.cpp | 12 +- src/btop_tools.cpp | 2 +- src/btop_tools.hpp | 8 +- 10 files changed, 686 insertions(+), 267 deletions(-) diff --git a/Makefile b/Makefile index 9d04958..fbbfc20 100644 --- a/Makefile +++ b/Makefile @@ -25,12 +25,13 @@ DEPEXT := d OBJEXT := o #Flags, Libraries and Includes -REQFLAGS := -std=c++20 -pthread -WARNFLAGS := -Wall -Wextra -Wno-stringop-overread -pedantic -OPTFLAGS := -O3 -CXXFLAGS := $(OPTFLAGS) $(WARNFLAGS) -LINKFLAGS += -pthread -INC := -I$(INCDIR) -I$(SRCDIR) +REQFLAGS := -std=c++20 +WARNFLAGS := -Wall -Wextra -Wno-stringop-overread -pedantic -pedantic-errors -Wfatal-errors +OPTFLAGS := -O2 -ftree-loop-vectorize +override LDCXXFLAGS += -pthread -D_FORTIFY_SOURCE=2 -D_GLIBCXX_ASSERTIONS -fexceptions -fstack-protector -fstack-clash-protection -fcf-protection -flto +override CXXFLAGS += $(REQFLAGS) $(LDCXXFLAGS) $(OPTFLAGS) $(WARNFLAGS) +override LDFLAGS += $(LDCXXFLAGS) $(OPTFLAGS) $(WARNFLAGS) +INC := -I$(INCDIR) -I$(SRCDIR) SOURCES := $(shell find $(SRCDIR) -type f -name *.$(SRCEXT)) OBJECTS := $(patsubst $(SRCDIR)/%,$(BUILDDIR)/%,$(SOURCES:.$(SRCEXT)=.$(OBJEXT))) @@ -73,12 +74,12 @@ uninstall: #Link btop: $(OBJECTS) - $(CXX) -o $(TARGETDIR)/btop $^ $(LINKFLAGS) + $(CXX) -o $(TARGETDIR)/btop $^ $(LDFLAGS) #Compile $(BUILDDIR)/%.$(OBJEXT): $(SRCDIR)/%.$(SRCEXT) - $(CXX) $(REQFLAGS) $(CXXFLAGS) $(INC) -c -o $@ $< - @$(CXX) $(REQFLAGS) $(CXXFLAGS) $(INC) -MM $(SRCDIR)/$*.$(SRCEXT) > $(BUILDDIR)/$*.$(DEPEXT) + $(CXX) $(CXXFLAGS) $(INC) -c -o $@ $< + @$(CXX) $(CXXFLAGS) $(INC) -MM $(SRCDIR)/$*.$(SRCEXT) > $(BUILDDIR)/$*.$(DEPEXT) @cp -f $(BUILDDIR)/$*.$(DEPEXT) $(BUILDDIR)/$*.$(DEPEXT).tmp @sed -e 's|.*:|$(BUILDDIR)/$*.$(OBJEXT):|' < $(BUILDDIR)/$*.$(DEPEXT).tmp > $(BUILDDIR)/$*.$(DEPEXT) @sed -e 's/.*://' -e 's/\\$$//' < $(BUILDDIR)/$*.$(DEPEXT).tmp | fmt -1 | sed -e 's/^ *//' -e 's/$$/:/' >> $(BUILDDIR)/$*.$(DEPEXT) diff --git a/src/btop.cpp b/src/btop.cpp index 49af64d..22e9027 100644 --- a/src/btop.cpp +++ b/src/btop.cpp @@ -18,6 +18,8 @@ tab-size = 4 #include #include +#include +#include #include #include #include @@ -85,6 +87,7 @@ namespace Global { uint64_t start_time; atomic resized (false); + atomic resizing (0); atomic quitting (false); bool arg_tty = false; @@ -138,31 +141,33 @@ void argumentParser(const int& argc, char **argv) { //* Handler for SIGWINCH and general resizing events, does nothing if terminal hasn't been resized unless force=true void term_resize(bool force) { - if (auto refreshed = Term::refresh() or force) { + if (auto refreshed = Term::refresh(); refreshed or force) { if (force and refreshed) force = false; - Global::resized = true; - Runner::stop(); } else return; + auto rez_state = ++Global::resizing; + if (rez_state > 1) return; + Global::resized = true; + Runner::stop(); while (not force) { sleep_ms(100); - if (not Term::refresh()) break; + if (rez_state != Global::resizing) rez_state = --Global::resizing; + else if (not Term::refresh()) break; } Input::interrupt = true; - Draw::calcSizes(); + Global::resizing = 0; } //* Exit handler; stops threads, restores terminal and saves config changes -void clean_quit(const int sig) { +void clean_quit(int sig) { if (Global::quitting) return; Global::quitting = true; Runner::stop(); - if (Term::initialized) { - Term::restore(); - } + if (not Global::exit_error_msg.empty()) { + sig = 1; Logger::error(Global::exit_error_msg); std::cerr << "ERROR: " << Global::exit_error_msg << endl; } @@ -170,9 +175,19 @@ void clean_quit(const int sig) { Input::clear(); Logger::info("Quitting! Runtime: " + sec_to_dhms(time_s() - Global::start_time)); - //? Call quick_exit if there is any existing Tools::atomic_lock objects to avoid a segfault from its destructor - //! There's probably a better solution to this... - if (Tools::active_locks > 0) quick_exit((sig != -1 ? sig : 0)); + //? Wait for any remaining Tools::atomic_lock destructors to finish for max 1000ms + for (int i = 0; Tools::active_locks > 0 and i < 100; i++) { + sleep_ms(10); + } + + if (Term::initialized) { + Term::restore(); + } + + //? 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) { + quick_exit((sig != -1 ? sig : 0)); + } if (sig != -1) exit(sig); } @@ -254,11 +269,13 @@ void banner_gen() { namespace Runner { atomic active (false); atomic stopping (false); + atomic waiting (false); string output; string overlay; string clock; sigset_t mask; + pthread_mutex_t mtx; struct runner_conf { vector boxes; @@ -268,14 +285,20 @@ namespace Runner { struct runner_conf current_conf; - //* Secondary thread; run collect, draw and print out + //? -------------------------------------- Single instance secondary thread --------------------------------------- void * _runner(void * _) { (void) _; pthread_sigmask(SIG_BLOCK, &mask, NULL); + int ret = pthread_mutex_lock(&mtx); + if (active or stopping or ret != 0 or Global::resized) { + if (ret == 0) pthread_mutex_unlock(&mtx); + pthread_exit(NULL); + } atomic_lock lck(active); auto timestamp = time_micros(); - output.clear(); - const auto& conf = current_conf; + string output; + output.reserve(Term::height * Term::width); + const auto conf = current_conf; for (const auto& box : conf.boxes) { if (stopping) break; @@ -307,6 +330,7 @@ namespace Runner { } if (stopping) { + pthread_mutex_unlock(&mtx); pthread_exit(NULL); } @@ -320,13 +344,16 @@ namespace Runner { //! DEBUG stats --> cout << Fx::reset << Mv::to(1, 20) << "Runner took: " << rjust(to_string(time_micros() - timestamp), 5) << " μs. " << flush; + pthread_mutex_unlock(&mtx); pthread_exit(NULL); } + //? ------------------------------------------ Secondary thread end ----------------------------------------------- //* Runs collect and draw in a secondary thread, unlocks and locks config to update cached values, box="all": all boxes void run(const string& box, const bool no_update, const bool force_redraw) { + atomic_lock lck(waiting); atomic_wait(active); - if (stopping) return; + if (stopping or Global::resized) return; if (box == "overlay") { cout << Term::sync_start << Global::overlay << Term::sync_end; @@ -357,13 +384,25 @@ namespace Runner { if (pthread_detach(runner_id) != 0) throw std::runtime_error("Failed to detach _runner thread!"); + + for (int i = 0; not active and i < 10; i++) sleep_ms(1); } } //* Stops any secondary thread running void stop() { stopping = true; - atomic_wait(active); + int ret = pthread_mutex_trylock(&mtx); + if (ret == EOWNERDEAD or ret == ENOTRECOVERABLE) { + if (active) active = false; + Global::exit_error_msg = "Runner thread died unexpectedly!"; + if (not Global::quitting) exit(1); + } + else if (ret == EBUSY) + atomic_wait(active); + else if (ret == 0) + pthread_mutex_unlock(&mtx); + sleep_ms(1); stopping = false; } @@ -514,131 +553,6 @@ int main(int argc, char **argv) { cout << Term::sync_start << Cpu::box << Mem::box << Net::box << Proc::box << Term::sync_end << flush; - //* ------------------------------------------------ TESTING ------------------------------------------------------ - - - if (false) { - - cout << "Current: " << std::setlocale(LC_ALL, NULL) << endl; - exit(0); - - } - - //* Test theme - if (false) { - string key; - bool no_redraw = false; - Config::unlock(); - auto theme_index = v_index(Theme::themes, Config::getS("color_theme")); - uint64_t timer = 0; - while (key != "q") { - key.clear(); - - if (not no_redraw) { - - cout << Fx::reset << Term::clear << "Theme: " << Config::getS("color_theme") << ". Generation took " << timer << " μs." << endl; - size_t i = 0; - for(const auto& item : Theme::colors) { - cout << rjust(item.first, 15) << ":" << item.second << "■"s * 10 << Fx::reset << " "; - if (++i == 4) { - i = 0; - cout << endl; - } - } - cout << Fx::reset << endl; - - - cout << "Gradients:"; - for (const auto& [name, cvec] : Theme::gradients) { - cout << endl << rjust(name + ":", 10); - for (auto& color : cvec) { - cout << color << "■"; - } - - cout << Fx::reset << endl; - } - } - - no_redraw = true; - key = Input::wait(); - if (key.empty()) continue; - if (key == "right") { - if (theme_index == Theme::themes.size() - 1) theme_index = 0; - else theme_index++; - } - else if (key == "left") { - if (theme_index == 0) theme_index = Theme::themes.size() - 1; - else theme_index--; - } - else continue; - no_redraw = false; - Config::set("color_theme", Theme::themes.at(theme_index)); - timer = time_micros(); - Theme::setTheme(); - timer = time_micros() - timer; - - } - - - exit(0); - } - - //* Test graphs - if (false) { - - deque mydata; - for (long long i = 0; i <= 100; i++) mydata.push_back(i); - for (long long i = 100; i >= 0; i--) mydata.push_back(i); - // mydata.push_back(50); - deque mydata2 = {2, 3}; - // mydata2.push_back(10); - // for (long long i = 0; i <= 100; i++) mydata2.push_back(i); - // for (long long i = 100; i >= 0; i--) mydata2.push_back(i); - - - - cout << Draw::createBox(5, 10, Term::width - 10, 12, Theme::c("proc_box"), false, "braille", "", 1) << Mv::save; - cout << Draw::createBox(5, 23, Term::width - 10, 12, Theme::c("proc_box"), false, "block", "", 2); - cout << Draw::createBox(5, 36, Term::width - 10, 12, Theme::c("proc_box"), false, "tty", "", 3) << flush; - auto kts = time_micros(); - Draw::Graph kgraph {Term::width - 13, 10, "cpu", mydata, "braille", false, false}; - Draw::Graph kgraph2 {Term::width - 13, 10, "upload", {0}, "braille", false, false}; - Draw::Graph kgraph3 {Term::width - 13, 10, "download", mydata2, "braille", false, false}; - - - cout << Mv::restore << kgraph(mydata, true) - << Mv::restore << Mv::d(13) << kgraph2(mydata, true) - << Mv::restore << Mv::d(26) << kgraph3(mydata, true) << '\n' - << Mv::d(1) << "Init took " << time_micros() - kts << " μs. " << endl; - - // Input::wait(); - - for (;;) { - mydata.push_back(std::rand() % 101); - mydata2.push_back(mydata.back()); - if (mydata.size() > 1000) mydata.pop_front(); - if (mydata2.size() > 1000) mydata2.pop_front(); - kgraph3 = {Term::width - 13, 10, "cpu", mydata2, "braille", false, false}; - kts = time_micros(); - cout << Term::sync_start << Mv::restore << kgraph(mydata) - << Mv::restore << Mv::d(13) << kgraph2(mydata) - << Mv::restore << Mv::d(26) << kgraph3(mydata2) - << Term::sync_end << endl; - cout << Mv::d(1) << "Time: " << time_micros() - kts << " μs. " << flush; - if (Input::poll()) { - if (Input::get() == "space") Input::wait(); - else break; - } - sleep_ms(50); - } - Input::get(); - - exit(0); - - } - - - //? ------------------------------------------------ MAIN LOOP ---------------------------------------------------- uint64_t update_ms = Config::getI("update_ms"); @@ -647,17 +561,17 @@ int main(int argc, char **argv) { try { while (not true not_eq not false) { //? Check for exceptions in secondary thread and exit with fail signal if true - if (Global::thread_exception) clean_quit(1); + if (Global::thread_exception) exit(1); //? Make sure terminal size hasn't changed (in case of SIGWINCH not working properly) term_resize(); - //? Print out boxes outlines and trigger secondary thread to redraw if terminal has been resized + //? Trigger secondary thread to redraw if terminal has been resized if (Global::resized) { - cout << Term::sync_start << Cpu::box << Mem::box << Net::box << Proc::box << Term::sync_end << flush; Global::resized = false; - if (time_ms() < future_time) - Runner::run("all", true, true); + Draw::calcSizes(); + Runner::run("all", true); + atomic_wait(Runner::active); } //? Start secondary collect & draw thread at the interval set by config value @@ -668,7 +582,7 @@ int main(int argc, char **argv) { } //? Loop over input polling and input action processing - for (auto current_time = time_ms(); current_time < future_time and not Global::resized; current_time = time_ms()) { + for (auto current_time = time_ms(); current_time < future_time; current_time = time_ms()) { //? Check for external clock changes and for changes to the update timer if (update_ms != (uint64_t)Config::getI("update_ms")) { diff --git a/src/btop_config.cpp b/src/btop_config.cpp index ba5b53b..77850df 100644 --- a/src/btop_config.cpp +++ b/src/btop_config.cpp @@ -121,8 +121,8 @@ namespace Config { {"custom_cpu_name", "#* Custom cpu model name, empty string to disable."}, - {"disks_filter", "#* Optional filter for shown disks, should be full path of a mountpoint, separate multiple values with a comma \",\".\n" - "#* Begin line with \"exclude=\" to change to exclude filter, otherwise defaults to \"most include\" filter. Example: disks_filter=\"exclude=/boot, /home/user\"."}, + {"disks_filter", "#* Optional filter for shown disks, should be full path of a mountpoint, separate multiple values with whitespace \" \".\n" + "#* Begin line with \"exclude=\" to change to exclude filter, otherwise defaults to \"most include\" filter. Example: disks_filter=\"exclude=/boot /home/user\"."}, {"mem_graphs", "#* Show graphs instead of meters for memory values."}, @@ -144,8 +144,8 @@ namespace Config { {"io_graph_combined", "#* Set to True to show combined read/write io graphs in io mode."}, - {"io_graph_speeds", "#* Set the top speed for the io graphs in MiB/s (10 by default), use format \"device:speed\" separate disks with a comma \",\".\n" - "#* Example: \"/dev/sda:100, /dev/sdb:20\"."}, + {"io_graph_speeds", "#* Set the top speed for the io graphs in MiB/s (10 by default), use format \"mountpoint:speed\" separate disks with whitespace \" \".\n" + "#* Example: \"/mnt/media:100 /:20 /boot:1\"."}, {"net_download", "#* Set fixed values for network graphs, default \"10M\" = 10 Mibibytes, possible units \"K\", \"M\", \"G\", append with \"bit\" for bits instead of bytes, i.e \"100mbit\"."}, @@ -273,6 +273,7 @@ namespace Config { void unlock() { if (not locked) return; + atomic_wait(Runner::active); atomic_lock lck(writelock); try { if (Proc::shown) { diff --git a/src/btop_draw.cpp b/src/btop_draw.cpp index f48cf73..e09f802 100644 --- a/src/btop_draw.cpp +++ b/src/btop_draw.cpp @@ -146,7 +146,7 @@ namespace Draw { else { const string first = uresize(text, --upos); pos = first.size(); - text = first + text.substr(pos); + text = first + luresize(text.substr(pos), ulen(text) - upos - 1); } } else if (key == "delete" and pos < text.size()) { @@ -159,7 +159,7 @@ namespace Draw { } else if (ulen(key) == 1) { if (key.size() == 1) { - text.insert(pos++, 1, key[0]); + text.insert(pos++, 1, key.at(0)); upos++; } else { @@ -201,7 +201,7 @@ namespace Draw { string out; if (line_color.empty()) line_color = Theme::c("div_line"); const auto& tty_mode = Config::getB("tty_mode"); - const string numbering = (num == 0) ? "" : Theme::c("hi_fg") + (tty_mode ? std::to_string(num) : Symbols::superscript[num]); + const string numbering = (num == 0) ? "" : Theme::c("hi_fg") + (tty_mode ? std::to_string(num) : Symbols::superscript.at(clamp(num, 0, 9))); const auto& right_up = (tty_mode ? Symbols::right_up : Symbols::round_right_up); const auto& left_up = (tty_mode ? Symbols::left_up : Symbols::round_left_up); const auto& right_down = (tty_mode ? Symbols::right_down : Symbols::round_right_down); @@ -255,7 +255,7 @@ namespace Draw { for (const int& i : iota(1, width + 1)) { int y = round((double)i * 100.0 / width); if (value >= y) - out += Theme::g(color_gradient)[invert ? 100 - y : y] + Symbols::meter; + out += Theme::g(color_gradient).at(invert ? 100 - y : y) + Symbols::meter; else { out += Theme::c("meter_bg") + Symbols::meter * (width + 1 - i); break; @@ -273,7 +273,7 @@ namespace Draw { const float mod = (height == 1) ? 0.3 : 0.1; long long data_value = 0; if (mult and data_offset > 0) { - last = data[data_offset - 1]; + last = data.at(data_offset - 1); if (max_value > 0) last = clamp((last + offset) * 100 / max_value, 0ll, 100ll); } @@ -286,7 +286,7 @@ namespace Draw { last = 0; } else { - data_value = data[i]; + data_value = data.at(i); if (max_value > 0) data_value = clamp((data_value + offset) * 100 / max_value, 0ll, 100ll); } @@ -296,7 +296,7 @@ namespace Draw { const int cur_low = (height > 1) ? round(100.0 * (height - (horizon + 1)) / height) : 0; //? Calculate previous + current value to fit two values in 1 braille character for (int ai = 0; const auto& value : {last, data_value}) { - const int clamp_min = (no_zero and horizon == height - 1 and not (i == -1 and ai == 0)) ? 1 : 0; + const int clamp_min = (no_zero and horizon == height - 1 and not (mult and i == data_offset and ai == 0)) ? 1 : 0; if (value >= cur_high) result[ai++] = 4; else if (value <= cur_low) @@ -306,7 +306,7 @@ namespace Draw { } } //? Generate graph symbol from 5x5 2D vector - graphs[current][horizon] += (height == 1 and result[0] + result[1] == 0) ? Mv::r(1) : graph_symbol[(result[0] * 5 + result[1])]; + graphs.at(current).at(horizon) += (height == 1 and result.at(0) + result.at(1) == 0) ? Mv::r(1) : graph_symbol.at((result.at(0) * 5 + result.at(1))); } if (mult and i >= 0) last = data_value; } @@ -314,15 +314,15 @@ namespace Draw { out.clear(); if (height == 1) { if (not color_gradient.empty()) - out += (last < 1 and not color_gradient.empty() ? Theme::c("inactive_fg") : Theme::g(color_gradient)[last]); - out += graphs[current][0]; + out += (last < 1 and not color_gradient.empty() ? Theme::c("inactive_fg") : Theme::g(color_gradient).at(last)); + out += graphs.at(current).at(0); } else { for (const int& i : iota(0, height)) { if (i > 0) out += Mv::d(1) + Mv::l(width); if (not color_gradient.empty()) - out += (invert) ? Theme::g(color_gradient)[i * 100 / (height - 1)] : Theme::g(color_gradient)[100 - (i * 100 / (height - 1))]; - out += (invert) ? graphs[current][ (height - 1) - i] : graphs[current][i]; + out += (invert) ? Theme::g(color_gradient).at(i * 100 / (height - 1)) : Theme::g(color_gradient).at(100 - (i * 100 / (height - 1))); + out += (invert) ? graphs.at(current).at((height - 1) - i) : graphs.at(current).at(i); } } if (not color_gradient.empty()) out += Fx::reset; @@ -360,8 +360,8 @@ namespace Draw { //? Make room for new characters on graph if (not tty_mode) current = not current; for (const int& i : iota(0, height)) { - if (graphs[current][i][1] == '[') graphs[current][i].erase(0, 4); - else graphs[current][i].erase(0, 3); + if (graphs.at(current).at(i).at(1) == '[') graphs.at(current).at(i).erase(0, 4); + else graphs.at(current).at(i).erase(0, 3); } this->_create(data, (int)data.size() - 1); return out; @@ -400,7 +400,7 @@ namespace Cpu { auto& graph_lo_field = Config::getS("cpu_graph_lower"); auto& tty_mode = Config::getB("tty_mode"); auto& graph_symbol = (tty_mode ? "tty" : Config::getS("graph_symbol_cpu")); - auto& graph_bg = Symbols::graph_symbols.at((graph_symbol == "default" ? Config::getS("graph_symbol") + "_up" : graph_symbol + "_up"))[1]; + auto& graph_bg = Symbols::graph_symbols.at((graph_symbol == "default" ? Config::getS("graph_symbol") + "_up" : graph_symbol + "_up")).at(1); auto& temp_scale = Config::getS("temp_scale"); string out; out.reserve(width * height); @@ -425,10 +425,10 @@ namespace Cpu { Input::mouse_mappings["+"] = {button_y, x + width - 5, 1, 2}; //? Graphs & meters - graph_upper = Draw::Graph{x + width - b_width - 3, graph_up_height, "cpu", cpu.cpu_percent.at(graph_up_field), graph_symbol}; - cpu_meter = Draw::Meter{b_width - (show_temps ? 23 : 11), "cpu"}; + graph_upper = Draw::Graph{x + width - b_width - 3, graph_up_height, "cpu", cpu.cpu_percent.at(graph_up_field), graph_symbol, false, true}; + cpu_meter = Draw::Meter{b_width - (show_temps ? 23 - (b_column_size <= 1 and b_columns == 1 ? 6 : 0) : 11), "cpu"}; if (not single_graph) - graph_lower = Draw::Graph{x + width - b_width - 3, graph_low_height, "cpu", cpu.cpu_percent.at(graph_lo_field), graph_symbol, Config::getB("cpu_invert_lower")}; + graph_lower = Draw::Graph{x + width - b_width - 3, graph_low_height, "cpu", cpu.cpu_percent.at(graph_lo_field), graph_symbol, Config::getB("cpu_invert_lower"), true}; if (mid_line) { out += Mv::to(y + graph_up_height + 1, x) + Fx::ub + Theme::c("cpu_box") + Symbols::div_left + Theme::c("div_line") + Symbols::h_line * (width - b_width - 2) + Symbols::div_right @@ -444,7 +444,7 @@ namespace Cpu { if (show_temps) { temp_graphs.clear(); temp_graphs.emplace_back(5, 1, "", cpu.temp.at(0), graph_symbol, false, false, cpu.temp_max, -23); - if (not hide_cores) { + if (not hide_cores and b_column_size > 1) { for (const auto& i : iota((size_t)1, cpu.temp.size())) { temp_graphs.emplace_back(5, 1, "", cpu.temp.at(i), graph_symbol, false, false, cpu.temp_max, -23); } @@ -463,11 +463,14 @@ namespace Cpu { + Symbols::title_left + Fx::b + Theme::c("title") + cpuHz + Fx::ub + Theme::c("div_line") + Symbols::title_right; out += Mv::to(b_y + 1, b_x + 1) + Theme::c("main_fg") + Fx::b + "CPU " + cpu_meter(cpu.cpu_percent.at("total").back()) - + Theme::g("cpu")[cpu.cpu_percent.at("total").back()] + rjust(to_string(cpu.cpu_percent.at("total").back()), 4) + Theme::c("main_fg") + '%'; + + Theme::g("cpu").at(cpu.cpu_percent.at("total").back()) + rjust(to_string(cpu.cpu_percent.at("total").back()), 4) + Theme::c("main_fg") + '%'; if (show_temps) { - const auto& [temp, unit] = celsius_to(cpu.temp.at(0).back(), temp_scale); - out += ' ' + Theme::c("inactive_fg") + graph_bg * 5 + Mv::l(5) + Theme::g("temp")[clamp(cpu.temp.at(0).back() * 100 / cpu.temp_max, 0ll, 100ll)] - + temp_graphs.at(0)(cpu.temp.at(0), data_same or redraw) + rjust(to_string(temp), 4) + Theme::c("main_fg") + unit; + const auto [temp, unit] = celsius_to(cpu.temp.at(0).back(), temp_scale); + const auto& temp_color = Theme::g("temp").at(clamp(cpu.temp.at(0).back() * 100 / cpu.temp_max, 0ll, 100ll)); + if (b_column_size > 1 or b_columns > 1) + out += ' ' + Theme::c("inactive_fg") + graph_bg * 5 + Mv::l(5) + temp_color + + temp_graphs.at(0)(cpu.temp.at(0), data_same or redraw); + out += rjust(to_string(temp), 4) + Theme::c("main_fg") + unit; } out += Theme::c("div_line") + Symbols::v_line; @@ -480,15 +483,18 @@ namespace Cpu { + ljust(to_string(n), (b_column_size == 0 ? 2 : 3)); if (b_column_size > 0 or extra_width > 0) out += Theme::c("inactive_fg") + graph_bg * (5 * b_column_size + extra_width) + Mv::l(5 * b_column_size + extra_width) - + Theme::g("cpu")[cpu.core_percent[n].back()] + core_graphs[n](cpu.core_percent[n], data_same or redraw); + + Theme::g("cpu").at(cpu.core_percent.at(n).back()) + core_graphs.at(n)(cpu.core_percent.at(n), data_same or redraw); else - out += Theme::g("cpu")[cpu.core_percent[n].back()]; - out += rjust(to_string(cpu.core_percent[n].back()), (b_column_size < 2 ? 3 : 4)) + Theme::c("main_fg") + '%'; + out += Theme::g("cpu").at(cpu.core_percent.at(n).back()); + out += rjust(to_string(cpu.core_percent.at(n).back()), (b_column_size < 2 ? 3 : 4)) + Theme::c("main_fg") + '%'; if (show_temps and not hide_cores) { - const auto& [temp, unit] = celsius_to(cpu.temp.at(n+1).back(), temp_scale); - out += ' ' + Theme::c("inactive_fg") + graph_bg * 5 + Mv::l(5) + Theme::g("temp")[clamp(cpu.temp.at(n+1).back() * 100 / cpu.temp_max, 0ll, 100ll)] - + temp_graphs.at(n+1)(cpu.temp.at(n+1), data_same or redraw) + rjust(to_string(temp), 4) + Theme::c("main_fg") + unit; + const auto [temp, unit] = celsius_to(cpu.temp.at(n+1).back(), temp_scale); + const auto& temp_color = Theme::g("temp").at(clamp(cpu.temp.at(n+1).back() * 100 / cpu.temp_max, 0ll, 100ll)); + if (b_column_size > 1) + out += ' ' + Theme::c("inactive_fg") + graph_bg * 5 + Mv::l(5) + temp_color + + temp_graphs.at(n+1)(cpu.temp.at(n+1), data_same or redraw); + out += temp_color + rjust(to_string(temp), 4) + Theme::c("main_fg") + unit; } out += Theme::c("div_line") + Symbols::v_line; @@ -525,7 +531,7 @@ namespace Cpu { } redraw = false; - return out; + return out + Fx::reset; } } @@ -535,19 +541,230 @@ namespace Mem { int min_w = 36, min_h = 10; int x = 1, y, width, height; int mem_width, disks_width, divider, item_height, mem_size, mem_meter, graph_height, disk_meter; + int disks_io_h = 0; bool shown = true, redraw = true; string box; + unordered_flat_map mem_meters; + unordered_flat_map mem_graphs; + unordered_flat_map disk_meters_used; + unordered_flat_map disk_meters_free; + unordered_flat_map io_graphs; string draw(const mem_info& mem, const bool force_redraw, const bool data_same) { if (Runner::stopping) return ""; - (void)mem; - (void)data_same; - string out = Mv::to(0, 0); - if (redraw or force_redraw) { - redraw = false; + if (force_redraw) redraw = true; + auto& show_swap = Config::getB("show_swap"); + auto& swap_disk = Config::getB("swap_disk"); + auto& show_disks = Config::getB("show_disks"); + auto& show_io_stat = Config::getB("show_io_stat"); + auto& io_mode = Config::getB("io_mode"); + auto& io_graph_combined = Config::getB("io_graph_combined"); + auto& use_graphs = Config::getB("mem_graphs"); + auto& tty_mode = Config::getB("tty_mode"); + auto& graph_symbol = (tty_mode ? "tty" : Config::getS("graph_symbol_mem")); + // auto& graph_bg = Symbols::graph_symbols.at((graph_symbol == "default" ? Config::getS("graph_symbol") + "_up" : graph_symbol + "_up")).at(1); + string out; + out.reserve(height * width); + + //* Redraw elements not needed to be updated every cycle + if (redraw) { out += box; + mem_meters.clear(); + mem_graphs.clear(); + disk_meters_free.clear(); + disk_meters_used.clear(); + io_graphs.clear(); + + //? Mem graphs and meters + for (const auto& name : mem_names) { + if (use_graphs) + mem_graphs[name] = Draw::Graph{mem_meter, graph_height, name, mem.percent.at(name)}; + else + mem_meters[name] = Draw::Meter{mem_meter, name}; + } + if (show_swap and has_swap) { + for (const auto& name : swap_names) { + if (use_graphs) + mem_graphs[name] = Draw::Graph{mem_meter, graph_height, name.substr(5), mem.percent.at(name)}; + else + mem_meters[name] = Draw::Meter{mem_meter, name.substr(5)}; + } + } + + //? Disk meters and io graphs + if (show_disks) { + if (show_io_stat or io_mode) { + if (io_mode) + disks_io_h = max((int)floor((double)(height - 2 - disk_ios) / max(1, disk_ios)), (io_graph_combined ? 1 : 2)); + else + disks_io_h = 1; + int half_height = ceil((double)disks_io_h / 2); + + unordered_flat_map custom_speeds; + if (not Config::getS("io_graph_speeds").empty()) { + auto split = ssplit(Config::getS("io_graph_speeds")); + for (const auto& entry : split) { + auto vals = ssplit(entry); + if (vals.size() == 2 and mem.disks.contains(vals.at(0)) and isint(vals.at(1))) + custom_speeds[vals.at(0)] = std::stoi(vals.at(1)); + } + } + for (const auto& [name, disk] : mem.disks) { + if (disk.io_read.empty()) continue; + long long speed = (custom_speeds.contains(name) ? custom_speeds.at(name) : 10) << 20; + + //? Create one combined graph for IO read/write if enabled + if (not io_mode or (io_mode and io_graph_combined)) { + deque combined(disk.io_read.size(), 0); + rng::transform(disk.io_read, disk.io_write, combined.begin(), std::plus()); + io_graphs[name] = Draw::Graph{disks_width - (io_mode ? 0 : 6), disks_io_h, "available", combined, graph_symbol, false, true, speed}; + } + else { + io_graphs[name + "_read"] = Draw::Graph{disks_width, half_height, "free", disk.io_read, graph_symbol, false, true, speed}; + io_graphs[name + "_write"] = Draw::Graph{disks_width, disks_io_h - half_height, "used", disk.io_write, graph_symbol, true, true, speed}; + } + } + } + if (disk_meter > 0) { + for (int i = 0; const auto& name : mem.disks_order) { + if (i * 2 > height - 2) break; + disk_meters_used[name] = Draw::Meter{disk_meter, "used"}; + if ((int)mem.disks_order.size() * 3 <= height - 1) + disk_meters_free[name] = Draw::Meter{disk_meter, "free"}; + } + } + } + out += Mv::to(y, x + width - 6) + Fx::ub + Theme::c("mem_box") + Symbols::title_left + (io_mode ? Fx::b : "") + Theme::c("hi_fg") + + 'i' + Theme::c("title") + 'o' + Fx::ub + Theme::c("mem_box") + Symbols::title_right; + Input::mouse_mappings["i"] = {y, x + width - 5, 1, 2}; } - return out; + + //? Mem and swap + int cx = 1, cy = 1; + string divider = (graph_height > 0 ? Mv::l(2) + Theme::c("mem_box") + Symbols::div_left + Theme::c("div_line") + Symbols::h_line * (mem_width - 1) + + (show_disks ? "" : Theme::c("mem_box")) + Symbols::div_right + Mv::l(mem_width - 1) + Theme::c("main_fg") : ""); + string up = (graph_height >= 2 ? Mv::l(mem_width - 2) + Mv::u(graph_height - 1) : ""); + bool big_mem = mem_width > 21; + + out += Mv::to(y + 1, x + 2) + Theme::c("title") + Fx::b + "Total:" + rjust(floating_humanizer(Shared::totalMem), mem_width - 9) + Fx::ub + Theme::c("main_fg"); + vector comb_names (mem_names.begin(), mem_names.end()); + if (show_swap and has_swap and not swap_disk) comb_names.insert(comb_names.end(), swap_names.begin(), swap_names.end()); + for (auto name : comb_names) { + if (cy > height - 4) break; + string title; + if (name == "swap_used") { + if (cy > height - 5) break; + if (height - cy > 6) { + if (graph_height > 0) out += Mv::to(y+1+cy, x+1+cx) + divider; + cy += 1; + } + out += Mv::to(y+1+cy, x+1+cx) + Theme::c("title") + Fx::b + "Swap:" + rjust(floating_humanizer(mem.stats.at("swap_total")), mem_width - 8) + + Theme::c("main_fg") + Fx::ub; + cy += 1; + title = "Used"; + } + else if (name == "swap_free") + title = "Free"; + + 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) { + 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); + cy += (graph_height == 0 ? 2 : graph_height + 1); + } + else { + out += Mv::to(y+1+cy, x+1+cx) + ljust(title, (mem_size > 1 ? 5 : 1)) + (graph_height >= 2 ? "" : " ") + + graphics + Theme::c("title") + rjust(humanized, (mem_size > 1 ? 9 : 7)); + cy += (graph_height == 0 ? 1 : graph_height); + } + } + if (graph_height > 0 and cy < height - 2) + out += Mv::to(y+1+cy, x+1+cx) + divider; + + //? Disks + if (show_disks) { + const auto& disks = mem.disks; + cx = x + mem_width - 1; cy = 0; + const bool big_disk = disks_width >= 25; + divider = Mv::l(1) + Theme::c("div_line") + Symbols::div_left + Symbols::h_line * disks_width + Theme::c("mem_box") + Symbols::div_right + Mv::l(disks_width - 1); + if (io_mode) { + for (const auto& mount : mem.disks_order) { + if (cy > height - 3) break; + const auto& disk = disks.at(mount); + if (disk.io_read.empty()) continue; + const string total = floating_humanizer(disk.total, not big_disk); + out += Mv::to(y+1+cy, x+1+cx) + divider + Theme::c("title") + Fx::b + uresize(disk.name, disks_width - 2) + Mv::to(y+1+cy, x+cx + disks_width - total.size() - 1) + + trans(total) + Fx::ub; + if (big_disk) { + const string used_percent = to_string(disk.used_percent); + out += Mv::to(y+1+cy, x+1+cx + round((double)disks_width / 2) - round((double)used_percent.size() / 2)) + Theme::c("main_fg") + used_percent + '%'; + } + if (++cy > height - 3) break; + if (io_graph_combined) { + auto comb_val = disk.io_read.back() + disk.io_write.back(); + const string humanized = (disk.io_write.back() > 0 ? "▼"s : ""s) + (disk.io_read.back() > 0 ? "▲"s : ""s) + + (comb_val > 0 ? Mv::r(1) + floating_humanizer(comb_val, true) : "RW"); + if (disks_io_h == 1) out += Mv::to(y+1+cy, x+1+cx) + string(5, ' '); + out += Mv::to(y+1+cy, x+1+cx) + io_graphs.at(mount)({comb_val}, redraw or data_same) + + Mv::to(y+1+cy, x+1+cx) + Theme::c("main_fg") + humanized; + cy += disks_io_h; + } + else { + const string human_read = (disk.io_read.back() > 0 ? "▲" + floating_humanizer(disk.io_read.back(), true) : "R"); + const string human_write = (disk.io_write.back() > 0 ? "▼" + floating_humanizer(disk.io_write.back(), true) : "W"); + if (disks_io_h <= 3) out += Mv::to(y+1+cy, x+1+cx) + string(5, ' ') + Mv::to(y+cy + disks_io_h, x+1+cx) + string(5, ' '); + out += Mv::to(y+1+cy, x+1+cx) + io_graphs.at(mount + "_read")(disk.io_read, redraw or data_same) + Mv::l(disks_width) + + Mv::d(1) + io_graphs.at(mount + "_write")(disk.io_write, redraw or data_same) + + Mv::to(y+1+cy, x+1+cx) + human_read + Mv::to(y+cy + disks_io_h, x+1+cx) + human_write; + cy += disks_io_h; + } + } + } + else { + for (const auto& mount : mem.disks_order) { + if (cy > height - 3) break; + const auto& disk = disks.at(mount); + auto comb_val = (not disk.io_read.empty() ? disk.io_read.back() + disk.io_write.back() : 0ll); + const string human_io = (comb_val > 0 and big_disk ? (disk.io_write.back() > 0 ? "▼"s : ""s) + (disk.io_read.back() > 0 ? "▲"s : ""s) + + floating_humanizer(comb_val, true) : ""); + const string human_total = floating_humanizer(disk.total, not big_disk); + const string human_used = floating_humanizer(disk.used, not big_disk); + const string human_free = floating_humanizer(disk.free, not big_disk); + + out += Mv::to(y+1+cy, x+1+cx) + divider + Theme::c("title") + Fx::b + uresize(disk.name, disks_width - 2) + Mv::to(y+1+cy, x+cx + disks_width - human_total.size() - 1) + + trans(human_total) + Fx::ub + Theme::c("main_fg"); + if (big_disk and not human_io.empty()) + out += Mv::to(y+1+cy, x+1+cx + round((double)disks_width / 2) - round((double)human_io.size() / 2)) + Theme::c("main_fg") + human_io; + if (++cy > height - 3) break; + if (show_io_stat and io_graphs.contains(mount)) { + out += Mv::to(y+1+cy, x+1+cx) + (big_disk ? " IO: " : " IO " + Mv::l(2)) + io_graphs.at(mount)({comb_val}, redraw or data_same); + if (not big_disk) out += Mv::to(y+1+cy, x+cx) + Theme::c("main_fg") + human_io; + if (++cy > height - 3) break; + } + + out += Mv::to(y+1+cy, x+1+cx) + (big_disk ? " Used:" + rjust(to_string(disk.used_percent) + '%', 4) : "U") + ' ' + + disk_meters_used.at(mount)(disk.used_percent) + rjust(human_used, (big_disk ? 9 : 7)); + if (++cy > height - 3) break; + + if ((int)disks.size() * 3 + (show_io_stat ? disk_ios : 0) <= height - 1) { + out += Mv::to(y+1+cy, x+1+cx) + (big_disk ? " Free:" + rjust(to_string(disk.free_percent) + '%', 4) : "F") + ' ' + + disk_meters_free.at(mount)(disk.free_percent) + rjust(human_free, (big_disk ? 9 : 7)); + cy++; + if ((int)disks.size() * 4 + (show_io_stat ? disk_ios : 0) <= height - 1) cy++; + } + + } + } + if (cy < height - 2) out += Mv::to(y+1+cy, x+1+cx) + divider; + } + + + + redraw = false; + return out + Fx::reset; } } @@ -570,7 +787,7 @@ namespace Net { redraw = false; out += box; } - return out; + return out + Fx::reset; } } @@ -660,7 +877,7 @@ namespace Proc { auto& proc_colors = Config::getB("proc_colors"); auto& tty_mode = Config::getB("tty_mode"); auto& graph_symbol = (tty_mode ? "tty" : Config::getS("graph_symbol_proc")); - auto& graph_bg = Symbols::graph_symbols.at((graph_symbol == "default" ? Config::getS("graph_symbol") + "_up" : graph_symbol + "_up"))[1]; + auto& graph_bg = Symbols::graph_symbols.at((graph_symbol == "default" ? Config::getS("graph_symbol") + "_up" : graph_symbol + "_up")).at(1); auto& mem_bytes = Config::getB("proc_mem_bytes"); start = Config::getI("proc_start"); selected = Config::getI("proc_selected"); @@ -707,7 +924,7 @@ namespace Proc { //? Draw structure of details box const string pid_str = to_string(detailed.entry.pid); out += Mv::to(y, x) + Theme::c("proc_box") + Symbols::div_left + Symbols::h_line + title_left + Theme::c("hi_fg") + Fx::b - + (tty_mode ? "4" : Symbols::superscript[4]) + Theme::c("title") + "proc" + + (tty_mode ? "4" : Symbols::superscript.at(4)) + Theme::c("title") + "proc" + Fx::ub + title_right + Symbols::h_line * (width - 10) + Symbols::div_right + Mv::to(d_y, dgraph_x + 2) + title_left + Fx::b + Theme::c("title") + pid_str + Fx::ub + title_right + title_left + Fx::b + Theme::c("title") + uresize(detailed.entry.name, dgraph_width - pid_str.size() - 7, true) + Fx::ub + title_right; @@ -911,7 +1128,7 @@ namespace Proc { p_counters.erase(p.pid); } else - p_counters[p.pid] = 0; + p_counters.at(p.pid) = 0; } out += Fx::reset; @@ -931,20 +1148,20 @@ namespace Proc { for (int i = 0; int v : {(int)round(p.cpu_p), (int)round(p.mem * 100 / Shared::totalMem), (int)p.threads / 3}) { if (proc_gradient) { int val = (min(v, 100) + 100) - calc * 100 / select_max; - if (val < 100) colors[i++] = Theme::g("proc_color")[val]; - else colors[i++] = Theme::g("process")[val - 100]; + if (val < 100) colors[i++] = Theme::g("proc_color").at(val); + else colors[i++] = Theme::g("process").at(val - 100); } else - colors[i++] = Theme::g("process")[min(v, 100)]; + colors[i++] = Theme::g("process").at(min(v, 100)); } - c_color = colors[0]; m_color = colors[1]; t_color = colors[2]; + c_color = colors.at(0); m_color = colors.at(1); t_color = colors.at(2); } else { c_color = m_color = t_color = Fx::b; end = Fx::ub; } if (proc_gradient) { - g_color = Theme::g("proc")[calc * 100 / select_max]; + g_color = Theme::g("proc").at(calc * 100 / select_max); } } @@ -985,7 +1202,7 @@ namespace Proc { + g_color + ljust(((int)p.user.size() > user_size ? p.user.substr(0, user_size - 1) + '+' : p.user), user_size) + ' ' + m_color + rjust(mem_str, 5) + end + ' ' + (is_selected ? "" : Theme::c("inactive_fg")) + graph_bg * 5 - + (p_graphs.contains(p.pid) ? Mv::l(5) + c_color + p_graphs[p.pid]({(p.cpu_p >= 0.1 and p.cpu_p < 5 ? 5ll : (long long)round(p.cpu_p))}, data_same) : "") + end + ' ' + + (p_graphs.contains(p.pid) ? Mv::l(5) + c_color + p_graphs.at(p.pid)({(p.cpu_p >= 0.1 and p.cpu_p < 5 ? 5ll : (long long)round(p.cpu_p))}, data_same) : "") + end + ' ' + c_color + rjust(cpu_str, 4) + " " + end; if (lc++ > height - 5) break; } @@ -1109,8 +1326,8 @@ namespace Draw { if (show_disks) { mem_width = ceil((double)(width - 3) / 2); - disks_width = width - mem_width - 3; mem_width += mem_width % 2; + disks_width = width - mem_width - 2; divider = x + mem_width; } else @@ -1124,7 +1341,7 @@ namespace Draw { else mem_size = 1; - mem_meter = max(0, width - (disks_width * show_disks) - (mem_size > 2 ? 9 : 20)); + mem_meter = max(0, mem_width - (mem_size > 2 ? 7 : 17)); if (mem_size == 1) mem_meter += 6; if (mem_graphs) { @@ -1181,6 +1398,5 @@ namespace Draw { select_max = height - 3; box = createBox(x, y, width, height, Theme::c("proc_box"), true, "proc", "", 4); } - } } \ No newline at end of file diff --git a/src/btop_input.cpp b/src/btop_input.cpp index 232af3c..ad90994 100644 --- a/src/btop_input.cpp +++ b/src/btop_input.cpp @@ -81,8 +81,8 @@ namespace Input { bool poll(int timeout) { if (timeout < 1) return cin.rdbuf()->in_avail() > 0; while (timeout > 0) { + if (interrupt) return interrupt = false; if (cin.rdbuf()->in_avail() > 0) return true; - if (interrupt) { interrupt = false; return false; } sleep_ms(timeout < 10 ? timeout : 10); timeout -= 10; } @@ -373,6 +373,22 @@ namespace Input { return; } } + + //? Input actions for mem box + if (Mem::shown) { + bool keep_going = false; + bool no_update = true; + bool redraw = true; + + if (key == "i") { + Config::flip("io_mode"); + } + + if (not keep_going) { + Runner::run("mem", no_update, redraw); + return; + } + } } diff --git a/src/btop_linux.cpp b/src/btop_linux.cpp index b498a32..c926a98 100644 --- a/src/btop_linux.cpp +++ b/src/btop_linux.cpp @@ -24,6 +24,7 @@ tab-size = 4 #include #include #include +#include #include #include @@ -37,16 +38,6 @@ using namespace Tools; //? --------------------------------------------------- FUNCTIONS ----------------------------------------------------- -namespace Tools { - double system_uptime() { - string upstr; - ifstream pread("/proc/uptime"); - getline(pread, upstr, ' '); - pread.close(); - return stod(upstr); - } -} - namespace Cpu { vector core_old_totals; vector core_old_idles; @@ -84,7 +75,6 @@ namespace Cpu { namespace Shared { fs::path procPath, passwd_path; - fs::file_time_type passwd_time; uint64_t totalMem; long pageSize, clkTck, coreCount; @@ -140,6 +130,8 @@ namespace Shared { Cpu::got_sensors = Cpu::get_sensors(); Cpu::core_mapping = Cpu::get_core_mapping(); + //? Init for namespace Mem + Mem::collect(); } @@ -395,7 +387,9 @@ namespace Cpu { auto get_core_mapping() -> unordered_flat_map { unordered_flat_map core_map; - ifstream cpuinfo("/proc/cpuinfo"); + + //? Try to get core mapping from /proc/cpuinfo + ifstream cpuinfo(Shared::procPath / "cpuinfo"); if (cpuinfo.good()) { int cpu, core; for (string instr; cpuinfo >> instr;) { @@ -412,8 +406,9 @@ namespace Cpu { } } + //? If core mapping from cpuinfo was incomplete try to guess remainder, if missing completely map 0-0 1-1 2-2 etc. if (core_map.size() < (size_t)Shared::coreCount) { - if (Shared::coreCount % 2 == 0 and core_map.size() == (size_t)Shared::coreCount / 2) { + if (Shared::coreCount % 2 == 0 and (long)core_map.size() == Shared::coreCount / 2) { for (int i = 0; i < Shared::coreCount / 2; i++) core_map[Shared::coreCount / 2 + i] = i; } @@ -424,6 +419,7 @@ namespace Cpu { } } + //? Apply user set custom mapping if any const auto& custom_map = Config::getS("cpu_core_map"); if (not custom_map.empty()) { try { @@ -433,7 +429,7 @@ namespace Cpu { int change_id = std::stoi(vals.at(0)); int new_id = std::stoi(vals.at(1)); if (not core_map.contains(change_id) or new_id >= Shared::coreCount) continue; - core_map[change_id] = new_id; + core_map.at(change_id) = new_id; } } catch (...) {} @@ -488,7 +484,7 @@ namespace 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 (cpu.cpu_percent.at("total").size() > (size_t)Term::width * 2) cpu.cpu_percent.at("total").pop_front(); + if (cpu.cpu_percent.at("total").size() > (size_t)Term::width * 2) cpu.cpu_percent.at("total").pop_front(); //? Populate cpu.cpu_percent with all fields from stat for (int ii = 0; const auto& val : times) { @@ -496,7 +492,7 @@ namespace Cpu { cpu_old.at(time_names.at(ii)) = val; //? Reduce size if there are more values than needed for graph - while (cpu.cpu_percent.at(time_names.at(ii)).size() > (size_t)Term::width * 2) cpu.cpu_percent.at(time_names.at(ii)).pop_front(); + if (cpu.cpu_percent.at(time_names.at(ii)).size() > (size_t)Term::width * 2) cpu.cpu_percent.at(time_names.at(ii)).pop_front(); if (++ii == 10) break; } @@ -535,12 +531,254 @@ namespace Cpu { namespace Mem { bool has_swap = false; + bool disks_fail = false; + vector fstab; + fs::file_time_type fstab_time; + int disk_ios = 0; - mem_info current_mem; + + mem_info current_mem {}; auto collect(const bool no_update) -> mem_info { - (void)no_update; - return current_mem; + if (Runner::stopping or no_update) return current_mem; + auto& show_swap = Config::getB("show_swap"); + auto& swap_disk = Config::getB("swap_disk"); + auto& show_disks = Config::getB("show_disks"); + auto& mem = current_mem; + + mem.stats.at("swap_total") = 0; + + //? Read memory info from /proc/meminfo + ifstream meminfo(Shared::procPath / "meminfo"); + if (meminfo.good()) { + bool got_avail = false; + for (string label; meminfo >> label;) { + if (label == "MemFree:") { + meminfo >> mem.stats.at("free"); + mem.stats.at("free") <<= 10; + } + else if (label == "MemAvailable:") { + meminfo >> mem.stats.at("available"); + mem.stats.at("available") <<= 10; + got_avail = true; + } + else if (label == "Cached:") { + meminfo >> mem.stats.at("cached"); + mem.stats.at("cached") <<= 10; + if (not show_swap and not swap_disk) break; + } + else if (label == "SwapTotal:") { + meminfo >> mem.stats.at("swap_total"); + mem.stats.at("swap_total") <<= 10; + } + else if (label == "SwapFree:") { + meminfo >> mem.stats.at("swap_free"); + mem.stats.at("swap_free") <<= 10; + break; + } + meminfo.ignore(SSmax, '\n'); + } + if (not got_avail) mem.stats.at("available") = mem.stats.at("free") + mem.stats.at("cached"); + mem.stats.at("used") = Shared::totalMem - mem.stats.at("available"); + if (mem.stats.at("swap_total") > 0) mem.stats.at("swap_used") = mem.stats.at("swap_total") - mem.stats.at("swap_free"); + } + else + throw std::runtime_error("Failed to read /proc/meminfo"); + + meminfo.close(); + + //? Calculate percentages + for (const auto& name : mem_names) { + mem.percent.at(name).push_back(round((double)mem.stats.at(name) * 100 / Shared::totalMem)); + } + + 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"))); + } + has_swap = true; + } + else + has_swap = false; + + //? Remove values beyond whats needed for graph creation + for (auto& [ignored, deq] : mem.percent) { + if (deq.size() > (size_t)Term::width * 2) deq.pop_front(); + } + + //? Get disks stats + if (show_disks and not disks_fail) { + try { + auto& disks_filter = Config::getS("disks_filter"); + bool filter_exclude = false; + auto& use_fstab = Config::getB("use_fstab"); + auto& only_physical = Config::getB("only_physical"); + auto& disks = mem.disks; + ifstream diskread; + + vector 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); + } + } + + //? Get list of "real" filesystems from /proc/filesystems + vector fstypes; + if (only_physical and not use_fstab) { + fstypes = {"zfs", "wslfs", "drvfs"}; + diskread.open(Shared::procPath / "filesystems"); + if (diskread.good()) { + for (string fstype; diskread >> fstype;) { + if (not is_in(fstype, "nodev", "squashfs", "nullfs")) + fstypes.push_back(fstype); + diskread.ignore(SSmax, '\n'); + } + } + else + throw std::runtime_error("Failed to read /proc/filesystems"); + diskread.close(); + } + + //? Get disk list to use from fstab if enabled + if (use_fstab and fs::last_write_time("/etc/fstab") != fstab_time) { + fstab.clear(); + fstab_time = fs::last_write_time("/etc/fstab"); + diskread.open("/etc/fstab"); + if (diskread.good()) { + for (string instr; diskread >> instr;) { + if (not instr.starts_with('#')) { + diskread >> instr; + if (not is_in(instr, "none", "swap")) fstab.push_back(instr); + } + diskread.ignore(SSmax, '\n'); + } + } + else + throw std::runtime_error("Failed to read /etc/fstab"); + diskread.close(); + } + + //? Get mounts from /etc/mtab or /proc/self/mounts + vector found; + diskread.open((fs::exists("/etc/mtab") ? fs::path("/etc/mtab") : Shared::procPath / "self/mounts")); + if (diskread.good()) { + string dev, mountpoint, fstype; + while (not diskread.eof()) { + std::error_code ec; + diskread >> dev >> mountpoint >> fstype; + + //? 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; + } + + if ((not use_fstab and not only_physical) + or (use_fstab and v_contains(fstab, mountpoint)) + or (not use_fstab and only_physical and v_contains(fstypes, fstype))) { + found.push_back(mountpoint); + + //? Save mountpoint, name, dev path and path to /sys/block stat file + if (not disks.contains(mountpoint)) { + disks[mountpoint] = disk_info{fs::canonical(dev, ec), fs::path(mountpoint).filename()}; + if (disks.at(mountpoint).dev.empty()) disks.at(mountpoint).dev = dev; + if (disks.at(mountpoint).name.empty()) disks.at(mountpoint).name = (mountpoint == "/" ? "root" : mountpoint); + string devname = disks.at(mountpoint).dev.filename(); + while (devname.size() >= 2) { + if (fs::exists("/sys/block/" + devname + "/stat")) { + disks.at(mountpoint).stat = "/sys/block/" + devname + "/stat"; + break; + } + devname.resize(devname.size() - 1); + } + } + + } + diskread.ignore(SSmax, '\n'); + } + //? Remove disks no longer mounted or filtered out + if (swap_disk and has_swap) found.push_back("swap"); + for (auto i = disks.begin(); i != disks.end();) { + if (not v_contains(found, i->first)) + i = disks.erase(i); + else + i++; + } + } + else + throw std::runtime_error("Failed to get mounts from /etc/mtab and /proc/self/mounts"); + diskread.close(); + + //? Get disk/partition stats + for (auto& [mountpoint, disk] : disks) { + if (not fs::exists(mountpoint)) 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 (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 : found) + if (not is_in(name, "/", "swap")) mem.disks_order.push_back(name); + + //? Get disks IO + int64_t sectors_read, sectors_write; + disk_ios = 0; + for (auto& [ignored, disk] : disks) { + if (disk.stat.empty()) continue; + diskread.open(disk.stat); + if (diskread.good()) { + disk_ios++; + for (int i = 0; i < 2; i++) { diskread >> std::ws; diskread.ignore(SSmax, ' '); } + diskread >> sectors_read; + if (disk.io_read.empty()) + disk.io_read.push_back(0); + else + disk.io_read.push_back(max(0l, (sectors_read - disk.old_io.at(0)) * 512)); + disk.old_io.at(0) = sectors_read; + if (disk.io_read.size() > (size_t)Term::width * 2) disk.io_read.pop_front(); + + for (int i = 0; i < 3; i++) { diskread >> std::ws; diskread.ignore(SSmax, ' '); } + diskread >> sectors_write; + if (disk.io_write.empty()) + disk.io_write.push_back(0); + else + disk.io_write.push_back(max(0l, (sectors_write - disk.old_io.at(1)) * 512)); + disk.old_io.at(1) = sectors_write; + if (disk.io_write.size() > (size_t)Term::width * 2) disk.io_write.pop_front(); + } + diskread.close(); + } + } + catch (const std::exception& e) { + Logger::warning("Error in Mem::collect() : " + (string)e.what()); + disks_fail = true; + } + } + + return mem; } } @@ -568,6 +806,7 @@ namespace Proc { vector current_procs; unordered_flat_map cache; unordered_flat_map uid_user; + fs::file_time_type passwd_time; uint64_t cputimes; int counter = 0; @@ -657,17 +896,17 @@ namespace Proc { if (pid != detailed.last_pid) { detailed = {}; detailed.last_pid = pid; - detailed.skip_smaps = (not Config::getB("proc_info_smaps")); + detailed.skip_smaps = not Config::getB("proc_info_smaps"); } //? Copy proc_info for process from proc vector - auto p = rng::find(procs, pid, &proc_info::pid); - detailed.entry = *p; + auto p_info = rng::find(procs, pid, &proc_info::pid); + detailed.entry = *p_info; //? Update cpu percent deque for process cpu graph if (not Config::getB("proc_per_core")) detailed.entry.cpu_p *= Shared::coreCount; detailed.cpu_percent.push_back(round(detailed.entry.cpu_p)); - while (detailed.cpu_percent.size() > (size_t)Term::width) detailed.cpu_percent.pop_front(); + if (detailed.cpu_percent.size() > (size_t)Term::width) detailed.cpu_percent.pop_front(); //? Process runtime detailed.elapsed = sec_to_dhms(uptime - (cache.at(pid).cpu_s / Shared::clkTck)); @@ -716,7 +955,7 @@ namespace Proc { redraw = true; } - while (detailed.mem_bytes.size() > (size_t)Term::width) detailed.mem_bytes.pop_front(); + if (detailed.mem_bytes.size() > (size_t)Term::width) detailed.mem_bytes.pop_front(); //? Get bytes read and written from proc/[pid]/io if (fs::exists(pid_path / "io")) { @@ -768,9 +1007,9 @@ namespace Proc { } //? Update uid_user map if /etc/passwd changed since last run - if (not Shared::passwd_path.empty() and fs::last_write_time(Shared::passwd_path) != Shared::passwd_time) { + if (not Shared::passwd_path.empty() and fs::last_write_time(Shared::passwd_path) != passwd_time) { string r_uid, r_user; - Shared::passwd_time = fs::last_write_time(Shared::passwd_path); + passwd_time = fs::last_write_time(Shared::passwd_path); uid_user.clear(); pread.open(Shared::passwd_path); if (pread.good()) { @@ -782,6 +1021,9 @@ namespace Proc { pread.ignore(SSmax, '\n'); } } + else { + Shared::passwd_path.clear(); + } pread.close(); } @@ -842,9 +1084,9 @@ namespace Proc { cache[new_proc.pid] = {name, cmd, user, name_offset}; } - new_proc.name = cache[new_proc.pid].name; - new_proc.cmd = cache[new_proc.pid].cmd; - new_proc.user = cache[new_proc.pid].user; + new_proc.name = cache.at(new_proc.pid).name; + new_proc.cmd = cache.at(new_proc.pid).cmd; + new_proc.user = cache.at(new_proc.pid).user; //* Parse /proc/[pid]/stat pread.open(d.path() / "stat"); @@ -856,16 +1098,17 @@ namespace Proc { size_t x = 0, next_x = 3; uint64_t cpu_t = 0; try { - while (pread.good()) { + for (;;) { while (pread.good() and ++x - offset < next_x) { pread.ignore(SSmax, ' '); } + if (pread.bad()) goto stat_loop_done; getline(pread, short_str, ' '); switch (x-offset) { case 3: { //? Process state - new_proc.state = short_str[0]; + new_proc.state = short_str.at(0); continue; } case 4: { //? Parent pid @@ -888,16 +1131,16 @@ namespace Proc { } case 20: { //? Number of threads new_proc.threads = stoull(short_str); - if (cache[new_proc.pid].cpu_s == 0) { + if (cache.at(new_proc.pid).cpu_s == 0) { next_x = 22; - cache[new_proc.pid].cpu_t = cpu_t; + cache.at(new_proc.pid).cpu_t = cpu_t; } else next_x = 24; continue; } case 22: { //? Save cpu seconds to cache if missing - cache[new_proc.pid].cpu_s = stoull(short_str); + cache.at(new_proc.pid).cpu_s = stoull(short_str); next_x = 24; continue; } @@ -923,13 +1166,13 @@ namespace Proc { if (x-offset < 24) continue; //? Process cpu usage since last update - new_proc.cpu_p = round(cmult * 1000 * (cpu_t - cache[new_proc.pid].cpu_t) / max(1ul, cputimes - old_cputimes)) / 10.0; + new_proc.cpu_p = round(cmult * 1000 * (cpu_t - cache.at(new_proc.pid).cpu_t) / max(1ul, cputimes - old_cputimes)) / 10.0; //? Process cumulative cpu usage since process start - new_proc.cpu_c = (double)cpu_t / max(1.0, (uptime * Shared::clkTck) - cache[new_proc.pid].cpu_s); + new_proc.cpu_c = (double)cpu_t / max(1.0, (uptime * Shared::clkTck) - cache.at(new_proc.pid).cpu_s); //? Update cache with latest cpu times - cache[new_proc.pid].cpu_t = cpu_t; + cache.at(new_proc.pid).cpu_t = cpu_t; if (show_detailed and not got_detailed and new_proc.pid == detailed_pid) { got_detailed = true; @@ -994,13 +1237,13 @@ namespace Proc { if (not tree and not reverse and sorting == "cpu lazy") { double max = 10.0, target = 30.0; for (size_t i = 0, x = 0, offset = 0; i < procs.size(); i++) { - if (i <= 5 and procs[i].cpu_p > max) - max = procs[i].cpu_p; + if (i <= 5 and procs.at(i).cpu_p > max) + max = procs.at(i).cpu_p; else if (i == 6) target = (max > 30.0) ? max : 10.0; - if (i == offset and procs[i].cpu_p > 30.0) + if (i == offset and procs.at(i).cpu_p > 30.0) offset++; - else if (procs[i].cpu_p > target) { + else if (procs.at(i).cpu_p > target) { rotate(procs.begin() + offset, procs.begin() + i, procs.begin() + i + 1); if (++x > 10) break; } @@ -1042,4 +1285,14 @@ namespace Proc { } } +namespace Tools { + double system_uptime() { + string upstr; + ifstream pread(Shared::procPath / "uptime"); + getline(pread, upstr, ' '); + pread.close(); + return stod(upstr); + } +} + #endif \ No newline at end of file diff --git a/src/btop_shared.hpp b/src/btop_shared.hpp index df3b6ac..d31e399 100644 --- a/src/btop_shared.hpp +++ b/src/btop_shared.hpp @@ -28,7 +28,7 @@ tab-size = 4 using std::string, std::vector, std::deque, robin_hood::unordered_flat_map, std::atomic, std::array; -void clean_quit(const int sig=-1); +void clean_quit(int sig=-1); void term_resize(bool force=false); void banner_gen(); @@ -105,16 +105,28 @@ namespace Mem { extern string box; extern int x, y, width, height; extern bool has_swap, shown, redraw; + const array mem_names = {"used", "available", "cached", "free"}; + const array swap_names = {"swap_used", "swap_free"}; + extern int disk_ios; struct disk_info { - uint64_t total = 0, used = 0; + std::filesystem::path dev; + string name; + std::filesystem::path stat = ""; + int64_t total = 0, used = 0, free = 0; + int used_percent = 0, free_percent = 0; + array old_io = {0, 0}; + deque io_read = {}; + deque io_write = {}; }; struct mem_info { - uint64_t total = 0, available = 0, cached = 0, free = 0; - unordered_flat_map> percent; - unordered_flat_map> disks_io; + unordered_flat_map stats = {{"used", 0}, {"available", 0}, {"cached", 0}, {"free", 0}, + {"swap_total", 0}, {"swap_used", 0}, {"swap_free", 0}}; + unordered_flat_map> percent = {{"used", {}}, {"available", {}}, {"cached", {}}, {"free", {}}, + {"swap_total", {}}, {"swap_used", {}}, {"swap_free", {}}}; unordered_flat_map disks; + vector disks_order; }; //* Collect mem & disks stats @@ -177,11 +189,11 @@ namespace Proc { //* Container for process info box struct detail_container { + size_t last_pid = 0; + bool skip_smaps = false; proc_info entry; string elapsed, parent, status, io_read, io_write, memory; long long first_mem = -1; - size_t last_pid = 0; - bool skip_smaps = false; deque cpu_percent; deque mem_bytes; }; diff --git a/src/btop_theme.cpp b/src/btop_theme.cpp index bce4a00..be20790 100644 --- a/src/btop_theme.cpp +++ b/src/btop_theme.cpp @@ -67,22 +67,22 @@ namespace Theme { { "cpu_start", "#77ca9b" }, { "cpu_mid", "#cbc06c" }, { "cpu_end", "#dc4c4c" }, - { "free_start", "#223014" }, + { "free_start", "#384f21" }, { "free_mid", "#b5e685" }, { "free_end", "#dcff85" }, - { "cached_start", "#0b1a29" }, + { "cached_start", "#163350" }, { "cached_mid", "#74e6fc" }, { "cached_end", "#26c5ff" }, - { "available_start", "#292107" }, + { "available_start", "#4e3f0e" }, { "available_mid", "#ffd77a" }, { "available_end", "#ffb814" }, - { "used_start", "#3b1f1c" }, + { "used_start", "#592b26" }, { "used_mid", "#d9626d" }, { "used_end", "#ff4769" }, - { "download_start", "#231a63" }, + { "download_start", "#291f75" }, { "download_mid", "#4f43a3" }, { "download_end", "#b0a9de" }, - { "upload_start", "#510554" }, + { "upload_start", "#620665" }, { "upload_mid", "#7d4180" }, { "upload_end", "#dcafde" }, { "process_start", "#80d0a3" }, diff --git a/src/btop_tools.cpp b/src/btop_tools.cpp index 65a4f62..70ec163 100644 --- a/src/btop_tools.cpp +++ b/src/btop_tools.cpp @@ -73,7 +73,7 @@ namespace Term { bool refresh() { struct winsize w; - ioctl(STDOUT_FILENO, TIOCGWINSZ, &w); + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) < 0) return false; if (width != w.ws_col or height != w.ws_row) { width = w.ws_col; height = w.ws_row; diff --git a/src/btop_tools.hpp b/src/btop_tools.hpp index f94aa4b..acb570b 100644 --- a/src/btop_tools.hpp +++ b/src/btop_tools.hpp @@ -139,6 +139,12 @@ namespace Tools { //* Resize a string consisting of UTF8 characters from left (only reduces size) string luresize(const string str, const size_t len, const bool wide=false); + //* Capatilize + inline string capitalize(string str) { + str.at(0) = toupper(str.at(0)); + return str; + } + //* Return with only uppercase characters inline string str_to_upper(string str) { std::ranges::for_each(str, [](auto& c) { c = ::toupper(c); } ); @@ -171,7 +177,7 @@ namespace Tools { //* Compare with all following values template - bool is_in(First &&first, T && ... t) { + inline bool is_in(const First& first, const T& ... t) { return ((first == t) || ...); }