File reorganization and more efficient build

This commit is contained in:
aristocratos 2021-06-19 14:57:27 +02:00
parent ba481d042c
commit d459d088a0
14 changed files with 2043 additions and 1510 deletions

View file

@ -1,14 +1,46 @@
PREFIX ?= /usr/local
DOCDIR ?= $(PREFIX)/share/btop/doc
CPP = g++
override CPPFLAGS += -std=c++20 -pthread
OPTFLAG = -O3
INFOFLAGS += -Wall -Wextra -Wno-stringop-overread -pedantic
INCLUDES = -Isrc -Iinclude
btop: btop.cpp
@mkdir -p bin
$(CPP) $(CPPFLAGS) $(INCLUDES) $(OPTFLAG) $(INFOFLAGS) -o bin/btop btop.cpp
#Compiler and Linker
CXX := g++
#The Target Binary Program
TARGET := btop
#The Directories, Source, Includes, Objects and Binary
SRCDIR := src
INCDIR := include
BUILDDIR := obj
TARGETDIR := bin
SRCEXT := cpp
DEPEXT := d
OBJEXT := o
#Flags, Libraries and Includes
CXXFLAGS := -std=c++20 -pthread -O3 -Wall -Wextra -Wno-stringop-overread -pedantic
INC := -I$(INCDIR) -I$(SRCDIR)
#---------------------------------------------------------------------------------
#DO NOT EDIT BELOW THIS LINE
#---------------------------------------------------------------------------------
SOURCES := $(shell find $(SRCDIR) -type f -name *.$(SRCEXT))
OBJECTS := $(patsubst $(SRCDIR)/%,$(BUILDDIR)/%,$(SOURCES:.$(SRCEXT)=.$(OBJEXT)))
#Default Make
all: directories $(TARGET)
#Make the Directories
directories:
@mkdir -p $(TARGETDIR)
@mkdir -p $(BUILDDIR)
#Clean only Objecst
clean:
@rm -rf $(BUILDDIR)
#Full Clean, Objects and Binaries
dist-clean: clean
@rm -rf $(TARGETDIR)
install:
@mkdir -p $(DESTDIR)$(PREFIX)/bin
@ -23,5 +55,21 @@ uninstall:
@rm -rf $(DESTDIR)$(DOCDIR)
@rm -rf $(DESTDIR)$(PREFIX)/share/btop
clean:
rm -rf bin
#Pull in dependency info for *existing* .o files
-include $(OBJECTS:.$(OBJEXT)=.$(DEPEXT))
#Link
$(TARGET): $(OBJECTS)
$(CXX) -o $(TARGETDIR)/$(TARGET) $^
#Compile
$(BUILDDIR)/%.$(OBJEXT): $(SRCDIR)/%.$(SRCEXT)
$(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)
@rm -f $(BUILDDIR)/$*.$(DEPEXT).tmp
#Non-File Targets
.PHONY: all clean dist-clean uninstall

View file

@ -30,19 +30,7 @@ tab-size = 4
#include <filesystem>
#include <unistd.h>
#include <robin_hood.h>
namespace Global {
const std::vector<std::array<std::string, 2>> Banner_src = {
{"#E62525", "██████╗ ████████╗ ██████╗ ██████╗"},
{"#CD2121", "██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗ ██╗ ██╗"},
{"#B31D1D", "██████╔╝ ██║ ██║ ██║██████╔╝ ██████╗██████╗"},
{"#9A1919", "██╔══██╗ ██║ ██║ ██║██╔═══╝ ╚═██╔═╝╚═██╔═╝"},
{"#801414", "██████╔╝ ██║ ╚██████╔╝██║ ╚═╝ ╚═╝"},
{"#000000", "╚═════╝ ╚═╝ ╚═════╝ ╚═╝"},
};
const std::string Version = "0.0.20";
int coreCount;
}
#include <cmath>
#include <btop_tools.h>
#include <btop_config.h>
@ -71,9 +59,23 @@ namespace Global {
#error Platform not supported!
#endif
namespace Global {
const std::vector<std::array<std::string, 2>> Banner_src = {
{"#E62525", "██████╗ ████████╗ ██████╗ ██████╗"},
{"#CD2121", "██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗ ██╗ ██╗"},
{"#B31D1D", "██████╔╝ ██║ ██║ ██║██████╔╝ ██████╗██████╗"},
{"#9A1919", "██╔══██╗ ██║ ██║ ██║██╔═══╝ ╚═██╔═╝╚═██╔═╝"},
{"#801414", "██████╔╝ ██║ ╚██████╔╝██║ ╚═╝ ╚═╝"},
{"#000000", "╚═════╝ ╚═╝ ╚═════╝ ╚═╝"},
};
std::string Version = "0.0.21";
int coreCount;
}
using std::string, std::vector, std::array, robin_hood::unordered_flat_map, std::atomic, std::endl, std::cout, std::views::iota, std::list, std::accumulate;
using std::flush, std::endl, std::future, std::string_literals::operator""s, std::future_status;
using std::flush, std::endl, std::future, std::string_literals::operator""s, std::future_status, std::to_string, std::round;
namespace fs = std::filesystem;
namespace rng = std::ranges;
using namespace Tools;
@ -89,6 +91,8 @@ namespace Global {
uint64_t start_time;
bool quitting = false;
bool arg_tty = false;
}
@ -102,10 +106,26 @@ void argumentParser(int argc, char **argv){
exit(0);
}
else if (argument == "-h" || argument == "--help") {
cout << "help here" << endl;
cout << "usage: btop [-h] [-v] [-/+t] [--debug]\n\n"
<< "optional arguments:\n"
<< " -h, --help show this help message and exit\n"
<< " -v, --version show version info and exit\n"
<< " -t, --tty_on force (ON) tty mode, max 16 colors and tty friendly graph symbols\n"
<< " +t, --tty_off force (OFF) tty mode\n"
<< " --debug start with loglevel set to DEBUG, overriding value set in config\n"
<< endl;
exit(0);
}
else if (argument == "--debug") Global::debug = true;
else if (argument == "--debug")
Global::debug = true;
else if (argument == "-t" || argument == "--tty_on") {
Config::set("tty_mode", true);
Global::arg_tty = true;
}
else if (argument == "+t" || argument == "--tty_off") {
Config::set("tty_mode", false);
Global::arg_tty = true;
}
else {
cout << " Unknown argument: " << argument << "\n" <<
" Use -h or --help for help." << endl;
@ -163,18 +183,21 @@ void banner_gen() {
int bg_i;
Global::banner.clear();
Global::banner_width = 0;
auto tty_mode = (Config::getB("tty_mode"));
for (auto line: Global::Banner_src) {
if (auto w = ulen(line[1]); w > Global::banner_width) Global::banner_width = w;
fg = Theme::hex_to_color(line[0], !truecolor);
bg_i = 120-z*12;
bg = Theme::dec_to_color(bg_i, bg_i, bg_i, !truecolor);
for (size_t i = 0; i < line[1].size(); i += 3) {
if (line[1][i] == ' '){
if (line[1][i] == ' ') {
letter = ' ';
i -= 2;
} else{
letter = line[1].substr(i, 3);
}
else
letter = line[1].substr(i, 3);
if (tty_mode && letter != "" && letter != " ") letter = "";
b_color = (letter == "") ? fg : bg;
if (b_color != oc) Global::banner += b_color;
Global::banner += letter;
@ -260,10 +283,10 @@ int main(int argc, char **argv){
{ vector<string> load_errors;
Config::load(Config::conf_file, load_errors);
if (Global::debug) Logger::loglevel = 4;
else Logger::loglevel = v_index(Logger::log_levels, Config::getS("log_level"));
if (Global::debug) Logger::set("DEBUG");
else Logger::set(Config::getS("log_level"));
Logger::info("Log level set to " + Config::getS("log_level") + ".");
Logger::debug("Logger set to DEBUG");
for (auto& err_str : load_errors) Logger::warning(err_str);
}
@ -283,6 +306,17 @@ int main(int argc, char **argv){
clean_quit(1);
}
Logger::debug("Running on " + Term::current_tty);
if (!Global::arg_tty && Config::getB("force_tty")) {
Config::set("tty_mode", true);
Logger::info("Forcing tty mode: setting 16 color mode and using tty friendly graph symbols");
}
else if (!Global::arg_tty && Term::current_tty.starts_with("/dev/tty")) {
Config::set("tty_mode", true);
Logger::info("Real tty detected, setting 16 color mode and using tty friendly graph symbols");
}
#if defined(LINUX)
//? Linux init
Proc::init();
@ -329,7 +363,7 @@ int main(int argc, char **argv){
cout << "Colors:" << endl;
uint i = 0;
for(auto& item : Theme::colors) {
for(auto& item : Theme::test_colors()) {
cout << rjust(item.first, 15) << ":" << item.second << ""s * 10 << Fx::reset << " ";
// << Theme::dec(item.first)[0] << ":" << Theme::dec(item.first)[1] << ":" << Theme::dec(item.first)[2] << ;
if (++i == 4) {
@ -341,7 +375,7 @@ int main(int argc, char **argv){
cout << "Gradients:";
for (auto& [name, cvec] : Theme::gradients) {
for (auto& [name, cvec] : Theme::test_gradients()) {
cout << endl << rjust(name + ":", 10);
for (auto& color : cvec) {
cout << color << "";
@ -388,13 +422,21 @@ int main(int argc, char **argv){
// for (long long i = 100; i >= 0; i--) mydata.push_back(i);
Draw::Graph kgraph {};
cout << Draw::createBox({.x = 5, .y = 10, .width = Term::width - 10, .height = 12, .line_color = Theme::c("proc_box"), .title = "graph", .fill = false, .num = 7}) << Mv::save << flush;
Draw::Graph kgraph2 {};
Draw::Graph kgraph3 {};
cout << Draw::createBox({.x = 5, .y = 10, .width = Term::width - 10, .height = 12, .line_color = Theme::c("proc_box"), .title = "braille", .fill = false, .num = 1}) << Mv::save;
cout << Draw::createBox({.x = 5, .y = 23, .width = Term::width - 10, .height = 12, .line_color = Theme::c("proc_box"), .title = "block", .fill = false, .num = 2});
cout << Draw::createBox({.x = 5, .y = 36, .width = Term::width - 10, .height = 12, .line_color = Theme::c("proc_box"), .title = "tty", .fill = false, .num = 3}) << flush;
// Draw::Meter kmeter {};
// Draw::Graph kgraph2 {};
// Draw::Graph kgraph3 {};
auto kts = time_micros();
kgraph(Term::width - 12, 10, "cpu", mydata, false, false);
kgraph(Term::width - 12, 10, "cpu", mydata, "braille", false, false);
kgraph2(Term::width - 12, 10, "cpu", mydata, "block", false, false);
kgraph3(Term::width - 12, 10, "cpu", mydata, "tty", false, false);
// kmeter(Term::width - 12, "process");
// cout << Mv::save << kgraph(mydata) << "\n\nInit took " << time_micros() - kts << " μs. " << endl;
@ -405,7 +447,10 @@ int main(int argc, char **argv){
// cout << kgraph2() << endl;
// exit(0);
cout << Mv::restore << kgraph(mydata, true) << "\n\n" << Mv::d(1) << "Init took " << time_micros() - kts << " μs. " << endl;
cout << Mv::restore << kgraph(mydata, true)
<< Mv::restore << Mv::d(13) << kgraph2(mydata, true)
<< Mv::restore << Mv::d(26) << kgraph3(mydata, true) << endl
<< Mv::d(1) << "Init took " << time_micros() - kts << " μs. " << endl;
// cout << Mv::save << kgraph(mydata, true) << "\n" << kgraph2(mydata, true) << "\n" << kgraph3(mydata, true) << "\n" << kmeter(mydata.back()) << "\n\nInit took " << time_micros() - kts << " μs. " << endl;
// sleep_ms(1000);
// mydata.push_back(50);
@ -420,7 +465,10 @@ int main(int argc, char **argv){
// mydata.back() = y;
kts = time_micros();
// cout << Mv::restore << " "s * Term::width << "\n" << " "s * Term::width << endl;
cout << Mv::restore << kgraph(mydata) << endl;
cout << Mv::restore << kgraph(mydata)
<< Mv::restore << Mv::d(13) << kgraph2(mydata)
<< Mv::restore << Mv::d(26) << kgraph3(mydata)
<< endl;
// cout << Mv::restore << kgraph(mydata) << "\n" << kgraph2(mydata) << "\n" << " "s * Term::width << Mv::l(Term::width) << kgraph3(mydata) << "\n" << kmeter(mydata.back()) << endl;
ktavg.push_front(time_micros() - kts);
if (ktavg.size() > 100) ktavg.pop_back();

337
src/btop_config.cpp Normal file
View file

@ -0,0 +1,337 @@
/* 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 <array>
#include <robin_hood.h>
#include <ranges>
#include <atomic>
#include <fstream>
#include <btop_config.h>
#include <btop_tools.h>
using robin_hood::unordered_flat_map, std::map, std::array, std::atomic;
namespace fs = std::filesystem;
namespace rng = std::ranges;
using namespace Tools;
//* Functions and variables for reading and writing the btop config file
namespace Config {
namespace {
atomic<bool> locked (false);
atomic<bool> writelock (false);
bool write_new;
vector<array<string, 2>> descriptions = {
{"color_theme", "#* Color theme, looks for a .theme file in \"/usr/[local/]share/bpytop/themes\" and \"~/.config/bpytop/themes\", \"Default\" for builtin default theme.\n"
"#* Prefix name by a plus sign (+) for a theme located in user themes folder, i.e. color_theme=\"+monokai\"." },
{"theme_background", "#* If the theme set background should be shown, set to False if you want terminal background transparency."},
{"truecolor", "#* Sets if 24-bit truecolor should be used, will convert 24-bit colors to 256 color (6x6x6 color cube) if false."},
{"graph_symbol", "#* Default symbols to use for graph creation, \"braille\", \"block\" or \"tty\".\n"
"#* \"braille\" offers the highest resolution but might not be included in all fonts.\n"
"#* \"block\" has half the resolution of braille but uses more common characters.\n"
"#* \"tty\" uses only 3 different symbols but will work with most fonts and should work in a real TTY.\n"
"#* Note that \"tty\" only has half the horizontal resolution of the other two, so will show a shorter historical view."},
{"graph_symbol_cpu", "# Graph symbol to use for graphs in cpu box, \"default\", \"braille\", \"block\" or \"tty\"."},
{"graph_symbol_mem", "# Graph symbol to use for graphs in cpu box, \"default\", \"braille\", \"block\" or \"tty\"."},
{"graph_symbol_net", "# Graph symbol to use for graphs in cpu box, \"default\", \"braille\", \"block\" or \"tty\"."},
{"graph_symbol_proc", "# Graph symbol to use for graphs in cpu box, \"default\", \"braille\", \"block\" or \"tty\"."},
{"force_tty", "#* Set to true to true to force tty mode regardless if a real tty has been detected or not."},
{"shown_boxes", "#* Manually set which boxes to show. Available values are \"cpu mem net proc\", separate values with whitespace."},
{"update_ms", "#* Update time in milliseconds, increases automatically if set below internal loops processing time, recommended 2000 ms or above for better sample times for graphs."},
{"proc_update_mult", "#* Processes update multiplier, sets how often the process list is updated as a multiplier of \"update_ms\".\n"
"#* Set to 2 or higher to greatly decrease bpytop cpu usage. (Only integers)."},
{"proc_sorting", "#* Processes sorting, \"pid\" \"program\" \"arguments\" \"threads\" \"user\" \"memory\" \"cpu lazy\" \"cpu responsive\",\n"
"#* \"cpu lazy\" updates top process over time, \"cpu responsive\" updates top process directly."},
{"proc_reversed", "#* Reverse sorting order, True or False."},
{"proc_tree", "#* Show processes as a tree."},
{"proc_colors", "#* Use the cpu graph colors in the process list."},
{"proc_gradient", "#* Use a darkening gradient in the process list."},
{"proc_per_core", "#* If process cpu usage should be of the core it's running on or usage of the total available cpu power."},
{"proc_mem_bytes", "#* Show process memory as bytes instead of percent."},
{"cpu_graph_upper", "#* Sets the CPU stat shown in upper half of the CPU graph, \"total\" is always available.\n"
"#* Select from a list of detected attributes from the options menu."},
{"cpu_graph_lower", "#* Sets the CPU stat shown in lower half of the CPU graph, \"total\" is always available.\n"
"#* Select from a list of detected attributes from the options menu."},
{"cpu_invert_lower", "#* Toggles if the lower CPU graph should be inverted."},
{"cpu_single_graph", "#* Set to True to completely disable the lower CPU graph."},
{"show_uptime", "#* Shows the system uptime in the CPU box."},
{"check_temp", "#* Show cpu temperature."},
{"cpu_sensor", "#* Which sensor to use for cpu temperature, use options menu to select from list of available sensors."},
{"show_coretemp", "#* Show temperatures for cpu cores also if check_temp is True and sensors has been found."},
{"temp_scale", "#* Which temperature scale to use, available values: \"celsius\", \"fahrenheit\", \"kelvin\" and \"rankine\"."},
{"show_cpu_freq", "#* Show CPU frequency."},
{"draw_clock", "#* Draw a clock at top of screen, formatting according to strftime, empty string to disable."},
{"background_update", "#* Update main ui in background when menus are showing, set this to false if the menus is flickering too much for comfort."},
{"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\"."},
{"mem_graphs", "#* Show graphs instead of meters for memory values."},
{"show_swap", "#* If swap memory should be shown in memory box."},
{"swap_disk", "#* Show swap as a disk, ignores show_swap value above, inserts itself after first disk."},
{"show_disks", "#* If mem box should be split to also show disks info."},
{"only_physical", "#* Filter out non physical disks. Set this to False to include network disks, RAM disks and similar."},
{"use_fstab", "#* Read disks list from /etc/fstab. This also disables only_physical."},
{"show_io_stat", "#* Toggles if io stats should be shown in regular disk usage view."},
{"io_mode", "#* Toggles io mode for disks, showing only big graphs for disk read/write speeds."},
{"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\"."},
{"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\"."},
{"net_upload", ""},
{"net_auto", "#* Start in network graphs auto rescaling mode, ignores any values set above and rescales down to 10 Kibibytes at the lowest."},
{"net_sync", "#* Sync the scaling for download and upload to whichever currently has the highest scale."},
{"net_color_fixed", "#* If the network graphs color gradient should scale to bandwidth usage or auto scale, bandwidth usage is based on \"net_download\" and \"net_upload\" values."},
{"net_iface", "#* Starts with the Network Interface specified here."},
{"show_battery", "#* Show battery stats in top right if battery is present."},
{"log_level", "#* Set loglevel for \"~/.config/bpytop/error.log\" levels are: \"ERROR\" \"WARNING\" \"INFO\" \"DEBUG\".\n"
"#* The level set includes all lower levels, i.e. \"DEBUG\" will show all logging info."}
};
unordered_flat_map<string, string> strings = {
{"color_theme", "Default"},
{"shown_boxes", "cpu mem net proc"},
{"graph_symbol", "braille"},
{"graph_symbol_cpu", "default"},
{"graph_symbol_mem", "default"},
{"graph_symbol_net", "default"},
{"graph_symbol_proc", "default"},
{"proc_sorting", "cpu lazy"},
{"cpu_graph_upper", "total"},
{"cpu_graph_lower", "total"},
{"cpu_sensor", "Auto"},
{"temp_scale", "celsius"},
{"draw_clock", "%X"},
{"custom_cpu_name", ""},
{"disks_filter", ""},
{"io_graph_speeds", ""},
{"net_download", "10M"},
{"net_upload", "10M"},
{"net_iface", ""},
{"log_level", "WARNING"},
{"proc_filter", ""}
};
unordered_flat_map<string, string> stringsTmp;
unordered_flat_map<string, bool> bools = {
{"theme_background", true},
{"truecolor", true},
{"proc_reversed", false},
{"proc_tree", false},
{"proc_colors", true},
{"proc_gradient", true},
{"proc_per_core", false},
{"proc_mem_bytes", true},
{"cpu_invert_lower", true},
{"cpu_single_graph", false},
{"show_uptime", true},
{"check_temp", true},
{"show_coretemp", true},
{"show_cpu_freq", true},
{"background_update", true},
{"mem_graphs", true},
{"show_swap", true},
{"swap_disk", true},
{"show_disks", true},
{"only_physical", true},
{"use_fstab", false},
{"show_io_stat", true},
{"io_mode", false},
{"io_graph_combined", false},
{"net_color_fixed", false},
{"net_auto", true},
{"net_sync", false},
{"show_battery", true},
{"tty_mode", false},
{"force_tty", false},
};
unordered_flat_map<string, bool> boolsTmp;
unordered_flat_map<string, int> ints = {
{"update_ms", 2000},
{"proc_update_mult", 2},
};
unordered_flat_map<string, int> intsTmp;
bool _locked(const string& name){
atomic_wait(writelock);
if (!write_new && rng::find_if(descriptions, [&name](const auto& a){ return a.at(0) == name; }) != descriptions.end())
write_new = true;
return locked.load();
}
}
fs::path conf_dir;
fs::path conf_file;
vector<string> valid_graph_symbols = { "braille", "block", "tty" };
//* Return bool config value <name>
const bool& getB(string name){
return bools.at(name);
}
//* Return integer config value <name>
const int& getI(string name){
return ints.at(name);
}
//* Return string config value <name>
const string& getS(string name){
return strings.at(name);
}
//* Set config value <name> to bool <value>
void set(string name, bool value){
if (_locked(name)) boolsTmp.insert_or_assign(name, value);
else bools.at(name) = value;
}
//* Set config value <name> to int <value>
void set(string name, int value){
if (_locked(name)) intsTmp.insert_or_assign(name, value);
ints.at(name) = value;
}
//* Set config value <name> to string <value>
void set(string name, string value){
if (_locked(name)) stringsTmp.insert_or_assign(name, value);
else strings.at(name) = value;
}
//* Flip config bool <name>
void flip(string name){
if (_locked(name)) {
if (boolsTmp.contains(name)) boolsTmp.at(name) = !boolsTmp.at(name);
else boolsTmp.insert_or_assign(name, (!bools.at(name)));
}
else bools.at(name) = !bools.at(name);
}
//* Wait if locked then lock config and cache changes until unlock
void lock(){
atomic_wait_set(locked);
}
//* Unlock config and write any cached values to config
void unlock(){
atomic_wait_set(writelock);
for (auto& item : stringsTmp){
strings.at(item.first) = item.second;
}
stringsTmp.clear();
for (auto& item : intsTmp){
ints.at(item.first) = item.second;
}
intsTmp.clear();
for (auto& item : boolsTmp){
bools.at(item.first) = item.second;
}
boolsTmp.clear();
locked = false;
writelock = false;
}
//* Load the config file from disk
void load(fs::path conf_file, vector<string>& load_errors){
if (conf_file.empty())
return;
else if (!fs::exists(conf_file)) {
write_new = true;
return;
}
std::ifstream cread(conf_file);
if (cread.good()) {
vector<string> valid_names;
for (auto &n : descriptions)
valid_names.push_back(n[0]);
string v_string;
getline(cread, v_string, '\n');
if (!v_string.ends_with(Global::Version))
write_new = true;
while (!cread.eof()) {
cread >> std::ws;
if (cread.peek() == '#') {
cread.ignore(SSmax, '\n');
continue;
}
string name, value;
getline(cread, name, '=');
if (!v_contains(valid_names, name)) {
cread.ignore(SSmax, '\n');
continue;
}
if (bools.contains(name)) {
cread >> value;
if (!isbool(value))
load_errors.push_back("Got an invalid bool value for config name: " + name);
else
bools.at(name) = stobool(value);
}
else if (ints.contains(name)) {
cread >> value;
if (!isint(value))
load_errors.push_back("Got an invalid integer value for config name: " + name);
else
ints.at(name) = stoi(value);
}
else if (strings.contains(name)) {
cread >> std::ws;
if (cread.peek() == '"') {
cread.ignore(1);
getline(cread, value, '"');
}
else cread >> value;
if (name == "log_level" && !v_contains(Logger::log_levels, value))
load_errors.push_back("Invalid log_level: " + value);
else if (name == "graph_symbol" && !v_contains(valid_graph_symbols, value))
load_errors.push_back("Invalid graph symbol identifier: " + value);
else
strings.at(name) = value;
}
cread.ignore(SSmax, '\n');
}
cread.close();
if (!load_errors.empty()) write_new = true;
}
}
//* Write the config file to disk
void write(){
if (conf_file.empty() || !write_new) return;
Logger::debug("Writing new config file");
std::ofstream cwrite(conf_file, std::ios::trunc);
if (cwrite.good()) {
cwrite << "#? Config file for btop v. " << Global::Version;
for (auto [name, description] : descriptions) {
cwrite << "\n\n" << (description.empty() ? "" : description + "\n") << name << "=";
if (strings.contains(name)) cwrite << "\"" << strings.at(name) << "\"";
else if (ints.contains(name)) cwrite << ints.at(name);
else if (bools.contains(name)) cwrite << (bools.at(name) ? "True" : "False");
}
cwrite.close();
}
}
}

View file

@ -16,303 +16,52 @@ indent = tab
tab-size = 4
*/
#ifndef _btop_config_included_
#define _btop_config_included_
#pragma once
#include <string>
#include <vector>
#include <array>
#include <robin_hood.h>
#include <filesystem>
#include <btop_tools.h>
using std::string, std::vector, robin_hood::unordered_flat_map, std::map;
namespace fs = std::filesystem;
using namespace Tools;
using std::string, std::vector;
//* Functions and variables for reading and writing the btop config file
namespace Config {
namespace {
fs::path conf_dir;
fs::path conf_file;
extern std::filesystem::path conf_dir;
extern std::filesystem::path conf_file;
atomic<bool> locked (false);
atomic<bool> writelock (false);
bool write_new;
vector<array<string, 2>> descriptions = {
{"color_theme", "#* Color theme, looks for a .theme file in \"/usr/[local/]share/bpytop/themes\" and \"~/.config/bpytop/themes\", \"Default\" for builtin default theme.\n"
"#* Prefix name by a plus sign (+) for a theme located in user themes folder, i.e. color_theme=\"+monokai\"." },
{"theme_background", "#* If the theme set background should be shown, set to False if you want terminal background transparency."},
{"truecolor", "#* Sets if 24-bit truecolor should be used, will convert 24-bit colors to 256 color (6x6x6 color cube) if false."},
{"shown_boxes", "#* Manually set which boxes to show. Available values are \"cpu mem net proc\", separate values with whitespace."},
{"update_ms", "#* Update time in milliseconds, increases automatically if set below internal loops processing time, recommended 2000 ms or above for better sample times for graphs."},
{"proc_update_mult", "#* Processes update multiplier, sets how often the process list is updated as a multiplier of \"update_ms\".\n"
"#* Set to 2 or higher to greatly decrease bpytop cpu usage. (Only integers)."},
{"proc_sorting", "#* Processes sorting, \"pid\" \"program\" \"arguments\" \"threads\" \"user\" \"memory\" \"cpu lazy\" \"cpu responsive\",\n"
"#* \"cpu lazy\" updates top process over time, \"cpu responsive\" updates top process directly."},
{"proc_reversed", "#* Reverse sorting order, True or False."},
{"proc_tree", "#* Show processes as a tree."},
{"proc_colors", "#* Use the cpu graph colors in the process list."},
{"proc_gradient", "#* Use a darkening gradient in the process list."},
{"proc_per_core", "#* If process cpu usage should be of the core it's running on or usage of the total available cpu power."},
{"proc_mem_bytes", "#* Show process memory as bytes instead of percent."},
{"cpu_graph_upper", "#* Sets the CPU stat shown in upper half of the CPU graph, \"total\" is always available.\n"
"#* Select from a list of detected attributes from the options menu."},
{"cpu_graph_lower", "#* Sets the CPU stat shown in lower half of the CPU graph, \"total\" is always available.\n"
"#* Select from a list of detected attributes from the options menu."},
{"cpu_invert_lower", "#* Toggles if the lower CPU graph should be inverted."},
{"cpu_single_graph", "#* Set to True to completely disable the lower CPU graph."},
{"show_uptime", "#* Shows the system uptime in the CPU box."},
{"check_temp", "#* Show cpu temperature."},
{"cpu_sensor", "#* Which sensor to use for cpu temperature, use options menu to select from list of available sensors."},
{"show_coretemp", "#* Show temperatures for cpu cores also if check_temp is True and sensors has been found."},
{"temp_scale", "#* Which temperature scale to use, available values: \"celsius\", \"fahrenheit\", \"kelvin\" and \"rankine\"."},
{"show_cpu_freq", "#* Show CPU frequency."},
{"draw_clock", "#* Draw a clock at top of screen, formatting according to strftime, empty string to disable."},
{"background_update", "#* Update main ui in background when menus are showing, set this to false if the menus is flickering too much for comfort."},
{"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\"."},
{"mem_graphs", "#* Show graphs instead of meters for memory values."},
{"show_swap", "#* If swap memory should be shown in memory box."},
{"swap_disk", "#* Show swap as a disk, ignores show_swap value above, inserts itself after first disk."},
{"show_disks", "#* If mem box should be split to also show disks info."},
{"only_physical", "#* Filter out non physical disks. Set this to False to include network disks, RAM disks and similar."},
{"use_fstab", "#* Read disks list from /etc/fstab. This also disables only_physical."},
{"show_io_stat", "#* Toggles if io stats should be shown in regular disk usage view."},
{"io_mode", "#* Toggles io mode for disks, showing only big graphs for disk read/write speeds."},
{"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\"."},
{"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\"."},
{"net_upload", ""},
{"net_auto", "#* Start in network graphs auto rescaling mode, ignores any values set above and rescales down to 10 Kibibytes at the lowest."},
{"net_sync", "#* Sync the scaling for download and upload to whichever currently has the highest scale."},
{"net_color_fixed", "#* If the network graphs color gradient should scale to bandwidth usage or auto scale, bandwidth usage is based on \"net_download\" and \"net_upload\" values."},
{"net_iface", "#* Starts with the Network Interface specified here."},
{"show_battery", "#* Show battery stats in top right if battery is present."},
{"log_level", "#* Set loglevel for \"~/.config/bpytop/error.log\" levels are: \"ERROR\" \"WARNING\" \"INFO\" \"DEBUG\".\n"
"#* The level set includes all lower levels, i.e. \"DEBUG\" will show all logging info."}
};
unordered_flat_map<string, string> strings = {
{"color_theme", "Default"},
{"shown_boxes", "cpu mem net proc"},
{"proc_sorting", "cpu lazy"},
{"cpu_graph_upper", "total"},
{"cpu_graph_lower", "total"},
{"cpu_sensor", "Auto"},
{"temp_scale", "celsius"},
{"draw_clock", "%X"},
{"custom_cpu_name", ""},
{"disks_filter", ""},
{"io_graph_speeds", ""},
{"net_download", "10M"},
{"net_upload", "10M"},
{"net_iface", ""},
{"log_level", "WARNING"},
{"proc_filter", ""}
};
unordered_flat_map<string, string> stringsTmp;
unordered_flat_map<string, bool> bools = {
{"theme_background", true},
{"truecolor", true},
{"proc_reversed", false},
{"proc_tree", false},
{"proc_colors", true},
{"proc_gradient", true},
{"proc_per_core", false},
{"proc_mem_bytes", true},
{"cpu_invert_lower", true},
{"cpu_single_graph", false},
{"show_uptime", true},
{"check_temp", true},
{"show_coretemp", true},
{"show_cpu_freq", true},
{"background_update", true},
{"mem_graphs", true},
{"show_swap", true},
{"swap_disk", true},
{"show_disks", true},
{"only_physical", true},
{"use_fstab", false},
{"show_io_stat", true},
{"io_mode", false},
{"io_graph_combined", false},
{"net_color_fixed", false},
{"net_auto", true},
{"net_sync", false},
{"show_battery", true},
};
unordered_flat_map<string, bool> boolsTmp;
unordered_flat_map<string, int> ints = {
{"update_ms", 2000},
{"proc_update_mult", 2},
};
unordered_flat_map<string, int> intsTmp;
bool _locked(){
atomic_wait(writelock);
if (!write_new) write_new = true;
return locked.load();
}
}
extern vector<string> valid_graph_symbols;
//* Return bool config value <name>
const bool& getB(string name){
return bools.at(name);
}
const bool& getB(string name);
//* Return integer config value <name>
const int& getI(string name){
return ints.at(name);
}
const int& getI(string name);
//* Return string config value <name>
const string& getS(string name){
return strings.at(name);
}
const string& getS(string name);
//* Set config value <name> to bool <value>
void set(string name, bool value){
if (_locked()) boolsTmp.insert_or_assign(name, value);
else bools.at(name) = value;
}
void set(string name, bool value);
//* Set config value <name> to int <value>
void set(string name, int value){
if (_locked()) intsTmp.insert_or_assign(name, value);
ints.at(name) = value;
}
void set(string name, int value);
//* Set config value <name> to string <value>
void set(string name, string value){
if (_locked()) stringsTmp.insert_or_assign(name, value);
else strings.at(name) = value;
}
void set(string name, string value);
//* Flip config bool <name>
void flip(string name){
if (_locked()) {
if (boolsTmp.contains(name)) boolsTmp.at(name) = !boolsTmp.at(name);
else boolsTmp.insert_or_assign(name, (!bools.at(name)));
}
else bools.at(name) = !bools.at(name);
}
void flip(string name);
//* Wait if locked then lock config and cache changes until unlock
void lock(){
atomic_wait_set(locked);
}
void lock();
//* Unlock config and write any cached values to config
void unlock(){
atomic_wait_set(writelock);
for (auto& item : stringsTmp){
strings.at(item.first) = item.second;
}
stringsTmp.clear();
for (auto& item : intsTmp){
ints.at(item.first) = item.second;
}
intsTmp.clear();
for (auto& item : boolsTmp){
bools.at(item.first) = item.second;
}
boolsTmp.clear();
locked = false;
writelock = false;
}
void unlock();
//* Load the config file from disk
void load(fs::path conf_file, vector<string>& load_errors){
if (conf_file.empty())
return;
else if (!fs::exists(conf_file)) {
write_new = true;
return;
}
std::ifstream cread(conf_file);
if (cread.good()) {
vector<string> valid_names;
for (auto &n : descriptions)
valid_names.push_back(n[0]);
string v_string;
getline(cread, v_string, '\n');
if (!v_string.ends_with(Global::Version))
write_new = true;
while (!cread.eof()) {
cread >> std::ws;
if (cread.peek() == '#') {
cread.ignore(SSmax, '\n');
continue;
}
string name, value;
getline(cread, name, '=');
if (!v_contains(valid_names, name)) {
cread.ignore(SSmax, '\n');
continue;
}
if (bools.contains(name)) {
cread >> value;
if (!isbool(value))
load_errors.push_back("Got an invalid bool value for config name: " + name);
else
bools.at(name) = stobool(value);
}
else if (ints.contains(name)) {
cread >> value;
if (!isint(value))
load_errors.push_back("Got an invalid integer value for config name: " + name);
else
ints.at(name) = stoi(value);
}
else if (strings.contains(name)) {
cread >> std::ws;
if (cread.peek() == '"') {
cread.ignore(1);
getline(cread, value, '"');
}
else cread >> value;
if (name == "log_level" && !v_contains(Logger::log_levels, value)) load_errors.push_back("Invalid log_level: " + value);
else strings.at(name) = value;
}
cread.ignore(SSmax, '\n');
}
cread.close();
if (!load_errors.empty()) write_new = true;
}
}
void load(std::filesystem::path conf_file, vector<string>& load_errors);
//* Write the config file to disk
void write(){
if (conf_file.empty() || !write_new) return;
Logger::debug("Writing new config file");
std::ofstream cwrite(conf_file, std::ios::trunc);
if (cwrite.good()) {
cwrite << "#? Config file for btop v. " << Global::Version;
for (auto [name, description] : descriptions) {
cwrite << "\n\n" << (description.empty() ? "" : description + "\n") << name << "=";
if (strings.contains(name)) cwrite << "\"" << strings.at(name) << "\"";
else if (ints.contains(name)) cwrite << ints.at(name);
else if (bools.contains(name)) cwrite << (bools.at(name) ? "True" : "False");
}
cwrite.close();
}
}
}
#endif
void write();
}

281
src/btop_draw.cpp Normal file
View file

@ -0,0 +1,281 @@
/* 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 <array>
#include <map>
#include <ranges>
#include <algorithm>
#include <cmath>
#include <btop_draw.h>
#include <btop_config.h>
#include <btop_theme.h>
#include <btop_tools.h>
using robin_hood::unordered_flat_map, std::round, std::views::iota,
std::string_literals::operator""s, std::clamp, std::array, std::floor;
namespace rng = std::ranges;
namespace Symbols {
const string h_line = "";
const string v_line = "";
const string left_up = "";
const string right_up = "";
const string left_down = "";
const string right_down = "";
const string title_left = "";
const string title_right = "";
const string div_up = "";
const string div_down = "";
const string meter = "";
const array<string, 10> superscript = { "", "¹", "²", "³", "", "", "", "", "", "" };
const unordered_flat_map<string, vector<string>> graph_symbols = {
{ "braille_up", {
"", "", "", "", "",
"", "", "", "", "",
"", "", "", "", "",
"", "", "", "", "",
"", "", "", "", ""
}},
{"braille_down", {
"", "", "", "", "",
"", "", "", "", "",
"", "", "", "", "",
"", "", "", "", "",
"", "", "", "", ""
}},
{"block_up", {
"", "", "", "", "",
"", "", "", "", "",
"", "", "", "", "",
"", "", "", "", "",
"", "", "", "", ""
}},
{"block_down", {
"", "", "", "", "",
"", "", "", "", "",
"", "", "", "", "",
"", "", "", "", "",
"", "", "", "", ""
}},
{"tty_up", {
"", "", "", "", "",
"", "", "", "", "",
"", "", "", "", "",
"", "", "", "", "",
"", "", "", "", ""
}},
{"tty_down", {
"", "", "", "", "",
"", "", "", "", "",
"", "", "", "", "",
"", "", "", "", "",
"", "", "", "", ""
}}
};
}
namespace Draw {
using namespace Tools;
//* Create a box using values from a BoxConf struct and return as a string
string createBox(BoxConf c){
string out;
string lcolor = (c.line_color.empty()) ? Theme::c("div_line") : c.line_color;
string numbering = (c.num == 0) ? "" : Theme::c("hi_fg") + Symbols::superscript[c.num];
out = Fx::reset + lcolor;
//* Draw horizontal lines
for (uint hpos : {c.y, c.y + c.height - 1}){
out += Mv::to(hpos, c.x) + Symbols::h_line * (c.width - 1);
}
//* Draw vertical lines and fill if enabled
for (uint hpos : iota(c.y + 1, c.y + c.height - 1)){
out += Mv::to(hpos, c.x) + Symbols::v_line +
((c.fill) ? string(c.width - 2, ' ') : Mv::r(c.width - 2)) +
Symbols::v_line;
}
//* Draw corners
out += Mv::to(c.y, c.x) + Symbols::left_up +
Mv::to(c.y, c.x + c.width - 1) + Symbols::right_up +
Mv::to(c.y + c.height - 1, c.x) + Symbols::left_down +
Mv::to(c.y + c.height - 1, c.x + c.width - 1) + Symbols::right_down;
//* Draw titles if defined
if (!c.title.empty()){
out += Mv::to(c.y, c.x + 2) + Symbols::title_left + Fx::b + numbering + Theme::c("title") + c.title +
Fx::ub + lcolor + Symbols::title_right;
}
if (!c.title2.empty()){
out += Mv::to(c.y + c.height - 1, c.x + 2) + Symbols::title_left + Theme::c("title") + c.title2 +
Fx::ub + lcolor + Symbols::title_right;
}
return out + Fx::reset + Mv::to(c.y + 1, c.x + 1);
}
void Meter::operator()(int width, string color_gradient, bool invert) {
this->width = width;
this->color_gradient = color_gradient;
this->invert = invert;
cache.clear();
cache.insert(cache.begin(), 101, "");
}
string Meter::operator()(int value) {
if (width < 1) return "";
value = clamp(value, 0, 100);
if (!cache.at(value).empty()) return cache.at(value);
string& out = cache.at(value);
for (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;
else {
out += Theme::c("meter_bg") + Symbols::meter * (width + 1 - i);
break;
}
}
out += Fx::reset;
return out;
}
void Graph::_create(const vector<long long>& data, int data_offset) {
const bool mult = (data.size() - data_offset > 1);
if (mult && (data.size() - data_offset) % 2 != 0) data_offset--;
auto& graph_symbol = Symbols::graph_symbols.at(symbol + '_' + (invert ? "down" : "up"));
array<int, 2> result;
const float mod = (height == 1) ? 0.3 : 0.1;
long long data_value = 0;
if (mult && data_offset > 0) {
last = data[data_offset - 1];
if (max_value > 0) last = clamp((last + offset) * 100 / max_value, 0ll, 100ll);
}
//? Horizontal iteration over values in <data>
for (int i : iota(data_offset, (int)data.size())) {
if (tty_mode && mult && i % 2 != 0) continue;
else if (!tty_mode) current = !current;
if (i == -1) { data_value = 0; last = 0; }
else data_value = data[i];
if (max_value > 0) data_value = clamp((data_value + offset) * 100 / max_value, 0ll, 100ll);
//? Vertical iteration over height of graph
for (int horizon : iota(0, height)){
int cur_high = (height > 1) ? round(100.0 * (height - horizon) / height) : 100;
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
int ai = 0;
for (auto value : {last, data_value}) {
if (value >= cur_high)
result[ai++] = 4;
else if (value <= cur_low)
result[ai++] = 0;
else {
result[ai++] = round((float)(value - cur_low) * 4 / (cur_high - cur_low) + mod);
if (no_zero && horizon == height - 1 && i != -1 && result[ai] == 0) result[ai] = 1;
}
}
//? Generate braille symbol from 5x5 2D vector
graphs[current][horizon] += (height == 1 && result[0] + result[1] == 0) ? Mv::r(1) : graph_symbol[(result[0] * 5 + result[1])];
}
if (mult && i > data_offset) last = data_value;
}
last = data_value;
if (height == 1)
out = (last < 1 ? Theme::c("inactive_fg") : Theme::g(color_gradient)[last]) + graphs[current][0];
else {
out.clear();
for (int i : iota(0, height)) {
if (i > 0) out += Mv::d(1) + Mv::l(width);
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 += Fx::reset;
}
void Graph::operator()(int width, int height, string color_gradient, const vector<long long>& data, string symbol, bool invert, bool no_zero, long long max_value, long long offset) {
graphs[true].clear(); graphs[false].clear();
this->width = width; this->height = height;
this->invert = invert; this->offset = offset;
this->no_zero = no_zero;
this->color_gradient = color_gradient;
if (Config::getB("tty_mode") || symbol == "tty") {
tty_mode = true;
this->symbol = "tty";
}
else if (symbol != "default" && v_contains(Config::valid_graph_symbols, symbol)) this->symbol = symbol;
else this->symbol = Config::getS("graph_symbol");
if (max_value == 0 && offset > 0) max_value = 100;
this->max_value = max_value;
int value_width = ceil((float)data.size() / 2);
int data_offset = 0;
if (value_width > width) data_offset = data.size() - width * 2;
//? Populate the two switching graph vectors and fill empty space if data size < width
for (int i : iota(0, height * 2)) {
graphs[(i % 2 != 0)].push_back((value_width < width) ? ((height == 1) ? Mv::r(1) : ""s) * (width - value_width) : "");
}
if (data.size() == 0) return;
this->_create(data, data_offset);
}
string& Graph::operator()(const vector<long long>& data, bool data_same) {
if (data_same) return out;
//? Make room for new characters on graph
bool select_graph = (tty_mode ? current : !current);
for (int i : iota(0, height)) {
if (graphs[select_graph][i].starts_with(Fx::e)) graphs[current][i].erase(0, 4);
else graphs[select_graph][i].erase(0, 3);
}
this->_create(data, (int)data.size() - 1);
return out;
}
string& Graph::operator()() {
return out;
}
}
namespace Box {
}
namespace Proc {
// Draw::BoxConf box;
}

View file

@ -16,63 +16,32 @@ indent = tab
tab-size = 4
*/
#pragma once
#include <string>
#include <vector>
#include <array>
#include <map>
#include <robin_hood.h>
#include <ranges>
#include <algorithm>
#include <cmath>
#include <btop_config.h>
#include <btop_tools.h>
#ifndef _btop_draw_included_
#define _btop_draw_included_
using std::string, std::vector, robin_hood::unordered_flat_map, std::round, std::views::iota,
std::string_literals::operator""s, std::clamp, std::array, std::floor;
using std::string, std::vector, robin_hood::unordered_flat_map;
namespace Symbols {
const string h_line = "";
const string v_line = "";
const string left_up = "";
const string right_up = "";
const string left_down = "";
const string right_down = "";
const string title_left = "";
const string title_right = "";
const string div_up = "";
const string div_down = "";
const string meter = "";
const array<string, 10> superscript = { "", "¹", "²", "³", "", "", "", "", "", "" };
const vector<string> graph_up = {
"", "", "", "", "",
"", "", "", "", "",
"", "", "", "", "",
"", "", "", "", "",
"", "", "", "", ""
};
const vector<string> graph_down = {
"", "", "", "", "",
"", "", "", "", "",
"", "", "", "", "",
"", "", "", "", "",
"", "", "", "", ""
};
extern const string h_line;
extern const string v_line;
extern const string left_up;
extern const string right_up;
extern const string left_down;
extern const string right_down;
extern const string title_left;
extern const string title_right;
extern const string div_up;
extern const string div_down;
}
namespace Draw {
using namespace Tools;
struct BoxConf {
uint x=0, y=0;
uint width=0, height=0;
@ -82,43 +51,7 @@ namespace Draw {
};
//* Create a box using values from a BoxConf struct and return as a string
string createBox(BoxConf c){
string out;
string lcolor = (c.line_color.empty()) ? Theme::c("div_line") : c.line_color;
string numbering = (c.num == 0) ? "" : Theme::c("hi_fg") + Symbols::superscript[c.num];
out = Fx::reset + lcolor;
//* Draw horizontal lines
for (uint hpos : {c.y, c.y + c.height - 1}){
out += Mv::to(hpos, c.x) + Symbols::h_line * (c.width - 1);
}
//* Draw vertical lines and fill if enabled
for (uint hpos : iota(c.y + 1, c.y + c.height - 1)){
out += Mv::to(hpos, c.x) + Symbols::v_line +
((c.fill) ? string(c.width - 2, ' ') : Mv::r(c.width - 2)) +
Symbols::v_line;
}
//* Draw corners
out += Mv::to(c.y, c.x) + Symbols::left_up +
Mv::to(c.y, c.x + c.width - 1) + Symbols::right_up +
Mv::to(c.y + c.height - 1, c.x) + Symbols::left_down +
Mv::to(c.y + c.height - 1, c.x + c.width - 1) + Symbols::right_down;
//* Draw titles if defined
if (!c.title.empty()){
out += Mv::to(c.y, c.x + 2) + Symbols::title_left + Fx::b + numbering + Theme::c("title") + c.title +
Fx::ub + lcolor + Symbols::title_right;
}
if (!c.title2.empty()){
out += Mv::to(c.y + c.height - 1, c.x + 2) + Symbols::title_left + Theme::c("title") + c.title2 +
Fx::ub + lcolor + Symbols::title_right;
}
return out + Fx::reset + Mv::to(c.y + 1, c.x + 1);
}
string createBox(BoxConf c);
//* Class holding a percentage meter
class Meter {
@ -128,139 +61,32 @@ namespace Draw {
vector<string> cache;
public:
//* Set meter options
void operator()(int width, string color_gradient, bool invert = false) {
this->width = width;
this->color_gradient = color_gradient;
this->invert = invert;
cache.clear();
cache.insert(cache.begin(), 101, "");
}
void operator()(int width, string color_gradient, bool invert = false);
//* Return a string representation of the meter with given value
string operator()(int value) {
if (width < 1) return "";
value = clamp(value, 0, 100);
if (!cache.at(value).empty()) return cache.at(value);
string& out = cache.at(value);
for (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;
else {
out += Theme::c("meter_bg") + Symbols::meter * (width + 1 - i);
break;
}
}
out += Fx::reset;
return out;
}
string operator()(int value);
};
//* Class holding a graph
class Graph {
string out, color_gradient;
string out, color_gradient, symbol = "default";
int width = 0, height = 0;
long long last = 0, max_value = 0, offset = 0;
bool current = true, no_zero = false, invert = false;
bool current = true, no_zero = false, invert = false, tty_mode = false;
unordered_flat_map<bool, vector<string>> graphs = { {true, {}}, {false, {}}};
//* Create two representations of the graph to switch between to represent two values for each braille character
void _create(const vector<long long>& data, int data_offset) {
const bool mult = (data.size() - data_offset > 1);
if (mult && (data.size() - data_offset) % 2 != 0) data_offset--;
auto& graph_symbol = (invert) ? Symbols::graph_down : Symbols::graph_up;
array<int, 2> result;
const float mod = (height == 1) ? 0.3 : 0.1;
long long data_value = 0;
if (mult && data_offset > 0) {
last = data[data_offset - 1];
if (max_value > 0) last = clamp((last + offset) * 100 / max_value, 0ll, 100ll);
}
//? Horizontal iteration over values in <data>
for (int i : iota(data_offset, (int)data.size())) {
current = !current;
if (i == -1) { data_value = 0; last = 0; }
else data_value = data[i];
if (max_value > 0) data_value = clamp((data_value + offset) * 100 / max_value, 0ll, 100ll);
//? Vertical iteration over height of graph
for (int horizon : iota(0, height)){
int cur_high = (height > 1) ? round(100.0 * (height - horizon) / height) : 100;
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
int ai = 0;
for (auto value : {last, data_value}) {
if (value >= cur_high)
result[ai++] = 4;
else if (value <= cur_low)
result[ai++] = 0;
else {
result[ai++] = round((float)(value - cur_low) * 4 / (cur_high - cur_low) + mod);
if (no_zero && horizon == height - 1 && i != -1 && result[ai] == 0) result[ai] = 1;
}
}
//? Generate braille symbol from 5x5 2D vector
graphs[current][horizon] += (height == 1 && result[0] + result[1] == 0) ? Mv::r(1) : graph_symbol[(result[0] * 5 + result[1])];
}
if (mult && i > data_offset) last = data_value;
}
last = data_value;
if (height == 1)
out = (last < 1 ? Theme::c("inactive_fg") : Theme::g(color_gradient)[last]) + graphs[current][0];
else {
out.clear();
for (int i : iota(0, height)) {
if (i > 0) out += Mv::d(1) + Mv::l(width);
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 += Fx::reset;
}
void _create(const vector<long long>& data, int data_offset);
public:
//* Set graph options and initialize with data
void operator()(int width, int height, string color_gradient, const vector<long long>& data, bool invert = false, bool no_zero = false, long long max_value = 0, long long offset = 0) {
graphs[true].clear(); graphs[false].clear();
this->width = width; this->height = height;
this->invert = invert; this->offset = offset;
this->no_zero = no_zero;
this->color_gradient = color_gradient;
if (max_value == 0 && offset > 0) max_value = 100;
this->max_value = max_value;
int value_width = ceil((float)data.size() / 2);
int data_offset = 0;
if (value_width > width) data_offset = data.size() - width * 2;
//? Populate the two switching graph vectors and fill empty space if data size < width
auto& graph_symbol = (invert) ? Symbols::graph_down : Symbols::graph_up;
for (int i : iota(0, height * 2)) {
graphs[(i % 2 != 0)].push_back((value_width < width) ? ((height == 1) ? Mv::r(1) : graph_symbol[0]) * (width - value_width) : "");
}
if (data.size() == 0) return;
this->_create(data, data_offset);
}
void operator()(int width, int height, string color_gradient, const vector<long long>& data, string symbol = "default", bool invert = false, bool no_zero = false, long long max_value = 0, long long offset = 0);
//* Add last value from back of <data> and return string representation of graph
string& operator()(const vector<long long>& data, bool data_same = false) {
if (data_same) return out;
//? Make room for new characters on graph
for (int i : iota(0, height)) {
if (graphs[(!current)][i].starts_with(Fx::e)) graphs[current][i].erase(0, 4);
else graphs[(!current)][i].erase(0, 3);
}
this->_create(data, (int)data.size() - 1);
return out;
}
string& operator()(const vector<long long>& data, bool data_same = false);
//* Return string representation of graph
string& operator()() {
return out;
}
string& operator()();
};
}
@ -276,8 +102,4 @@ namespace Proc {
// Draw::BoxConf box;
}
#endif
}

View file

@ -16,8 +16,7 @@ indent = tab
tab-size = 4
*/
#ifndef _btop_input_included_
#define _btop_input_included_
#pragma once
#include <string>
#include <robin_hood.h>
@ -31,7 +30,7 @@ using namespace Tools;
/* The input functions relies on the following std::cin options being set:
cin.sync_with_stdio(false);
cin.tie(NULL);
These will automatically be set when running Term::init() from btop_tools.h
These will automatically be set when running Term::init() from btop_tools.cpp
*/
//* Functions and variables for handling keyboard and mouse input
@ -113,6 +112,4 @@ namespace Input {
last.clear();
}
}
#endif
}

411
src/btop_linux.cpp Normal file
View file

@ -0,0 +1,411 @@
/* 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
*/
#if defined(__linux__)
#include <string>
#include <vector>
#include <atomic>
#include <fstream>
#include <filesystem>
#include <ranges>
#include <list>
#include <robin_hood.h>
#include <cmath>
#include <iostream>
#include <cmath>
#include <unistd.h>
#include <btop_linux.h>
#include <btop_config.h>
#include <btop_tools.h>
using std::string, std::vector, std::ifstream, std::atomic, std::numeric_limits, std::streamsize,
std::round, std::string_literals::operator""s, robin_hood::unordered_flat_map;
namespace fs = std::filesystem;
namespace rng = std::ranges;
using namespace Tools;
//? --------------------------------------------------- FUNCTIONS -----------------------------------------------------
namespace Tools {
double system_uptime(){
string upstr;
ifstream pread("/proc/uptime");
getline(pread, upstr, ' ');
pread.close();
return stod(upstr);
}
}
namespace Proc {
namespace {
struct p_cache {
string name, cmd, user;
uint64_t cpu_t = 0, cpu_s = 0;
string prefix = "";
size_t depth = 0;
bool collapsed = false;
};
unordered_flat_map<uint, p_cache> cache;
unordered_flat_map<string, string> uid_user;
fs::path passwd_path;
fs::file_time_type passwd_time;
uint counter = 0;
long page_size;
long clk_tck;
}
fs::path proc_path;
uint64_t old_cputimes = 0;
size_t numpids = 500;
atomic<bool> stop (false);
atomic<bool> collecting (false);
vector<string> sort_vector = {
"pid",
"name",
"command",
"threads",
"user",
"memory",
"cpu direct",
"cpu lazy",
};
//* Generate process tree list
void _tree_gen(proc_info& cur_proc, vector<proc_info>& in_procs, vector<proc_info>& out_procs, int cur_depth=0, bool collapsed=false){
auto cur_pos = out_procs.size();
if (!collapsed)
out_procs.push_back(cur_proc);
int children = 0;
for (auto& p : in_procs) {
if (p.ppid == (int)cur_proc.pid) {
children++;
if (collapsed) {
out_procs.back().cpu_p += p.cpu_p;
out_procs.back().mem += p.mem;
out_procs.back().threads += p.threads;
_tree_gen(p, in_procs, out_procs, cur_depth + 1, collapsed);
}
else _tree_gen(p, in_procs, out_procs, cur_depth + 1, cache.at(cur_proc.pid).collapsed);
}
}
if (collapsed) return;
if (out_procs.size() > cur_pos + 1 && !out_procs.back().prefix.ends_with("] ")) {
std::string_view n_prefix = out_procs.back().prefix;
n_prefix.remove_suffix(8);
out_procs.back().prefix = (string)n_prefix + " └─ ";
}
string prefix = " ├─ ";
if (children > 0) prefix = (cache.at(cur_proc.pid).collapsed) ? "[+] " : "[-] ";
out_procs.at(cur_pos).prefix = ""s * cur_depth + prefix;
}
vector<proc_info> current_procs;
//* Collects and sorts process information from /proc, saves to and returns reference to Proc::current_procs;
vector<proc_info>& collect(){
atomic_wait_set(collecting);
auto& sorting = Config::getS("proc_sorting");
auto& reverse = Config::getB("proc_reversed");
auto& filter = Config::getS("proc_filter");
auto& per_core = Config::getB("proc_per_core");
auto& tree = Config::getB("proc_tree");
ifstream pread;
auto uptime = system_uptime();
vector<proc_info> procs;
vector<uint> pid_list;
procs.reserve((numpids + 10));
pid_list.reserve(numpids + 10);
int npids = 0;
int cmult = (per_core) ? Global::coreCount : 1;
(void)tree;
//* Update uid_user map if /etc/passwd changed since last run
if (!passwd_path.empty() && fs::last_write_time(passwd_path) != passwd_time) {
string r_uid, r_user;
passwd_time = fs::last_write_time(passwd_path);
uid_user.clear();
pread.open(passwd_path);
if (pread.good()) {
while (!pread.eof()){
getline(pread, r_user, ':');
pread.ignore(SSmax, ':');
getline(pread, r_uid, ':');
uid_user[r_uid] = r_user;
pread.ignore(SSmax, '\n');
}
}
pread.close();
}
//* Get cpu total times from /proc/stat
uint64_t cputimes = 0;
pread.open(proc_path / "stat");
if (pread.good()) {
pread.ignore(SSmax, ' ');
for (uint64_t times; pread >> times; cputimes += times);
pread.close();
}
else return current_procs;
//* Iterate over all pids in /proc
for (auto& d: fs::directory_iterator(proc_path)){
if (pread.is_open()) pread.close();
if (stop.load()) {
collecting.store(false);
stop.store(false);
return current_procs;
}
bool new_cache = false;
string pid_str = d.path().filename();
if (d.is_directory() && isdigit(pid_str[0])) {
npids++;
proc_info new_proc (stoul(pid_str));
pid_list.push_back(new_proc.pid);
//* Cache program name, command and username
if (!cache.contains(new_proc.pid)) {
string name, cmd, user;
new_cache = true;
pread.open(d.path() / "comm");
if (pread.good()) {
getline(pread, name);
pread.close();
}
else continue;
pread.open(d.path() / "cmdline");
if (pread.good()) {
string tmpstr = "";
while(getline(pread, tmpstr, '\0')) cmd += tmpstr + " ";
pread.close();
if (!cmd.empty()) cmd.pop_back();
}
else continue;
pread.open(d.path() / "status");
if (pread.good()) {
string uid;
while (!pread.eof()){
string line;
getline(pread, line, ':');
if (line == "Uid") {
pread.ignore();
getline(pread, uid, '\t');
break;
} else {
pread.ignore(SSmax, '\n');
}
}
pread.close();
user = (!uid.empty() && uid_user.contains(uid)) ? uid_user.at(uid) : uid;
}
else continue;
cache[new_proc.pid] = {name, cmd, user};
}
//* Match filter if defined
if (!filter.empty()
&& pid_str.find(filter) == string::npos
&& cache[new_proc.pid].name.find(filter) == string::npos
&& cache[new_proc.pid].cmd.find(filter) == string::npos
&& cache[new_proc.pid].user.find(filter) == string::npos) {
if (new_cache) cache.erase(new_proc.pid);
continue;
}
new_proc.name = cache[new_proc.pid].name;
new_proc.cmd = cache[new_proc.pid].cmd;
new_proc.user = cache[new_proc.pid].user;
//* Parse /proc/[pid]/stat
pread.open(d.path() / "stat");
if (pread.good()) {
string instr;
getline(pread, instr);
pread.close();
size_t s_pos = 0, c_pos = 0, s_count = 0;
uint64_t cpu_t = 0;
//? Skip pid and comm field and find comm fields closing ')'
s_pos = instr.find_last_of(')') + 2;
if (s_pos == string::npos) continue;
do {
c_pos = instr.find(' ', s_pos);
if (c_pos == string::npos) break;
switch (s_count) {
case 0: { //? Process state
new_proc.state = instr[s_pos];
break;
}
case 1: { //? Process parent pid
new_proc.ppid = stoi(instr.substr(s_pos, c_pos - s_pos));
break;
}
case 11: { //? Process utime
cpu_t = stoull(instr.substr(s_pos, c_pos - s_pos));
break;
}
case 12: { //? Process stime
cpu_t += stoull(instr.substr(s_pos, c_pos - s_pos));
break;
}
case 16: { //? Process nice value
new_proc.p_nice = stoi(instr.substr(s_pos, c_pos - s_pos));
break;
}
case 17: { //? Process number of threads
new_proc.threads = stoul(instr.substr(s_pos, c_pos - s_pos));
break;
}
case 19: { //? Cache cpu seconds
if (new_cache) cache[new_proc.pid].cpu_s = stoull(instr.substr(s_pos, c_pos - s_pos));
break;
}
case 36: { //? CPU number last executed on
new_proc.cpu_n = stoi(instr.substr(s_pos, c_pos - s_pos));
break;
}
}
s_pos = c_pos + 1;
} while (s_count++ < 36);
if (s_count < 19) continue;
//? Process cpu usage since last update
new_proc.cpu_p = round(cmult * 1000 * (cpu_t - cache[new_proc.pid].cpu_t) / (cputimes - old_cputimes)) / 10.0;
//? Process cumulative cpu usage since process start
new_proc.cpu_c = ((double)cpu_t / clk_tck) / (uptime - (cache[new_proc.pid].cpu_s / clk_tck));
//? Update cache with latest cpu times
cache[new_proc.pid].cpu_t = cpu_t;
}
else continue;
//* Get RSS memory in bytes from /proc/[pid]/statm
pread.open(d.path() / "statm");
if (pread.good()) {
pread.ignore(SSmax, ' ');
pread >> new_proc.mem;
pread.close();
new_proc.mem *= page_size;
}
//* Create proc_info
procs.push_back(new_proc);
}
}
//* Sort processes
rng::sort(procs, [sortint = v_index(sort_vector, sorting), &reverse](proc_info& a, proc_info& b) {
switch (sortint) {
case 0: return (reverse) ? a.pid < b.pid : a.pid > b.pid;
case 1: return (reverse) ? a.name < b.name : a.name > b.name;
case 2: return (reverse) ? a.cmd < b.cmd : a.cmd > b.cmd;
case 3: return (reverse) ? a.threads < b.threads : a.threads > b.threads;
case 4: return (reverse) ? a.user < b.user : a.user > b.user;
case 5: return (reverse) ? a.mem < b.mem : a.mem > b.mem;
case 6: return (reverse) ? a.cpu_p < b.cpu_p : a.cpu_p > b.cpu_p;
case 7: return (reverse) ? a.cpu_c < b.cpu_c : a.cpu_c > b.cpu_c;
}
return false;
});
//* When sorting with "cpu lazy" push processes over threshold cpu usage to the front regardless of cumulative usage
if (sorting == "cpu lazy" && !tree && !reverse) {
double max = 10.0, target = 30.0;
for (size_t i = 0, offset = 0; i < procs.size(); i++) {
if (i <= 5 && procs[i].cpu_p > max)
max = procs[i].cpu_p;
else if (i == 6)
target = (max > 30.0) ? max : 10.0;
if (i == offset && procs[i].cpu_p > 30.0)
offset++;
else if
(procs[i].cpu_p > target) rotate(procs.begin() + offset, procs.begin() + i, procs.begin() + i + 1);
}
}
//* Generate tree view if enabled
if (tree) {
vector<proc_info> tree_procs;
auto min_pid = rng::min(procs, rng::less{}, &proc_info::ppid).ppid;
for (auto& p : procs) {
if (p.ppid == min_pid) _tree_gen(p, procs, tree_procs);
}
procs.swap(tree_procs);
}
//* Clear dead processes from cache at a regular interval
if (++counter >= 10000 || ((int)cache.size() > npids + 100)) {
counter = 0;
unordered_flat_map<uint, p_cache> r_cache;
r_cache.reserve(pid_list.size());
for (auto& p : pid_list) {
if (cache.contains(p)) r_cache[p] = cache.at(p);
}
cache.swap(r_cache);
}
old_cputimes = cputimes;
current_procs.swap(procs);
numpids = npids;
collecting.store(false);
return current_procs;
}
//* Initialize needed variables for collect
void init(){
proc_path = (fs::is_directory(fs::path("/proc")) && access("/proc", R_OK) != -1) ? "/proc" : "";
if (proc_path.empty()) {
string errmsg = "Proc filesystem not found or no permission to read from it!";
Logger::error(errmsg);
std::cout << "ERROR: " << errmsg << std::endl;
exit(1);
}
passwd_path = (access("/etc/passwd", R_OK) != -1) ? fs::path("/etc/passwd") : passwd_path;
if (passwd_path.empty()) Logger::warning("Could not read /etc/passwd, will show UID instead of username.");
page_size = sysconf(_SC_PAGE_SIZE);
if (page_size <= 0) {
page_size = 4096;
Logger::warning("Could not get system page size. Defaulting to 4096, processes memory usage might be incorrect.");
}
clk_tck = sysconf(_SC_CLK_TCK);
if (clk_tck <= 0) {
clk_tck = 100;
Logger::warning("Could not get system clocks per second. Defaulting to 100, processes cpu usage might be incorrect.");
}
}
}
#endif

View file

@ -16,82 +16,30 @@ indent = tab
tab-size = 4
*/
#ifndef _btop_linux_included_
#define _btop_linux_included_
#pragma once
#include <string>
#include <vector>
#include <deque>
#include <array>
#include <atomic>
#include <future>
#include <thread>
#include <fstream>
#include <streambuf>
#include <filesystem>
#include <ranges>
#include <list>
#include <robin_hood.h>
#include <unistd.h>
using std::string, std::vector, std::atomic;
#include <btop_config.h>
#include <btop_tools.h>
using std::string, std::vector, std::array, std::ifstream, std::atomic, std::numeric_limits, std::streamsize;
using std::cout, std::flush, std::endl, std::string_literals::operator""s;
namespace fs = std::filesystem;
using namespace Tools;
//? --------------------------------------------------- FUNCTIONS -----------------------------------------------------
namespace Global {
extern int coreCount;
}
namespace Tools {
double system_uptime(){
string upstr;
ifstream pread("/proc/uptime");
getline(pread, upstr, ' ');
pread.close();
return stod(upstr);
}
double system_uptime();
}
namespace Proc {
namespace {
struct p_cache {
string name, cmd, user;
uint64_t cpu_t = 0, cpu_s = 0;
string prefix = "";
size_t depth = 0;
int collapsed = -1;
};
unordered_flat_map<uint, p_cache> cache;
unordered_flat_map<string, string> uid_user;
fs::path passwd_path;
fs::file_time_type passwd_time;
uint counter = 0;
long page_size;
long clk_tck;
}
fs::path proc_path;
uint64_t old_cputimes = 0;
size_t numpids = 500;
atomic<bool> stop (false);
atomic<bool> collecting (false);
atomic<bool> drawing (false);
vector<string> sort_vector = {
"pid",
"name",
"command",
"threads",
"user",
"memory",
"cpu direct",
"cpu lazy",
};
extern std::filesystem::path proc_path;
extern size_t numpids;
extern atomic<bool> stop;
extern atomic<bool> collecting;
extern vector<string> sort_vector;
//* Container for process information
struct proc_info {
@ -107,321 +55,11 @@ namespace Proc {
string prefix = "";
};
//* Generate process tree list
void _tree_gen(proc_info& cur_proc, vector<proc_info>& in_procs, vector<proc_info>& out_procs, int cur_depth=0, bool collapsed=false){
auto cur_pos = out_procs.size();
if (!collapsed)
out_procs.push_back(cur_proc);
int children = 0;
for (auto& p : in_procs) {
if (p.ppid == (int)cur_proc.pid) {
children++;
if (collapsed) {
out_procs.back().cpu_p += p.cpu_p;
out_procs.back().mem += p.mem;
out_procs.back().threads += p.threads;
_tree_gen(p, in_procs, out_procs, cur_depth + 1, collapsed);
}
else _tree_gen(p, in_procs, out_procs, cur_depth + 1, (cache.at(cur_proc.pid).collapsed == 1));
}
}
if (collapsed) return;
if (out_procs.size() > cur_pos + 1 && !out_procs.back().prefix.ends_with("] ")) {
std::string_view n_prefix = out_procs.back().prefix;
n_prefix.remove_suffix(8);
out_procs.back().prefix = (string)n_prefix + " └─ ";
}
string prefix = " ├─ ";
if (children > 0) prefix = (cache.at(cur_proc.pid).collapsed == 1) ? "[+] " : "[-] ";
out_procs.at(cur_pos).prefix = ""s * cur_depth + prefix;
}
vector<proc_info> current_procs;
extern vector<proc_info> current_procs;
//* Collects and sorts process information from /proc, saves to and returns reference to Proc::current_procs;
auto& collect(){
atomic_wait_set(collecting);
auto& sorting = Config::getS("proc_sorting");
auto& reverse = Config::getB("proc_reversed");
auto& filter = Config::getS("proc_filter");
auto& per_core = Config::getB("proc_per_core");
auto& tree = Config::getB("proc_tree");
ifstream pread;
auto uptime = system_uptime();
vector<proc_info> procs;
vector<uint> pid_list;
procs.reserve((numpids + 10));
pid_list.reserve(numpids + 10);
int npids = 0;
int cmult = (per_core) ? Global::coreCount : 1;
(void)tree;
//* Update uid_user map if /etc/passwd changed since last run
if (!passwd_path.empty() && fs::last_write_time(passwd_path) != passwd_time) {
string r_uid, r_user;
passwd_time = fs::last_write_time(passwd_path);
uid_user.clear();
pread.open(passwd_path);
if (pread.good()) {
while (!pread.eof()){
getline(pread, r_user, ':');
pread.ignore(SSmax, ':');
getline(pread, r_uid, ':');
uid_user[r_uid] = r_user;
pread.ignore(SSmax, '\n');
}
}
pread.close();
}
//* Get cpu total times from /proc/stat
uint64_t cputimes = 0;
pread.open(proc_path / "stat");
if (pread.good()) {
pread.ignore(SSmax, ' ');
for (uint64_t times; pread >> times; cputimes += times);
pread.close();
}
else return current_procs;
//* Iterate over all pids in /proc
for (auto& d: fs::directory_iterator(proc_path)){
if (pread.is_open()) pread.close();
if (stop.load()) {
collecting.store(false);
stop.store(false);
return current_procs;
}
bool new_cache = false;
string pid_str = d.path().filename();
if (d.is_directory() && isdigit(pid_str[0])) {
npids++;
proc_info new_proc (stoul(pid_str));
pid_list.push_back(new_proc.pid);
//* Cache program name, command and username
if (!cache.contains(new_proc.pid)) {
string name, cmd, user;
new_cache = true;
pread.open(d.path() / "comm");
if (pread.good()) {
getline(pread, name);
pread.close();
}
else continue;
pread.open(d.path() / "cmdline");
if (pread.good()) {
string tmpstr = "";
while(getline(pread, tmpstr, '\0')) cmd += tmpstr + " ";
pread.close();
if (!cmd.empty()) cmd.pop_back();
}
else continue;
pread.open(d.path() / "status");
if (pread.good()) {
string uid;
while (!pread.eof()){
string line;
getline(pread, line, ':');
if (line == "Uid") {
pread.ignore();
getline(pread, uid, '\t');
break;
} else {
pread.ignore(SSmax, '\n');
}
}
pread.close();
user = (!uid.empty() && uid_user.contains(uid)) ? uid_user.at(uid) : uid;
}
else continue;
cache[new_proc.pid] = {name, cmd, user};
}
//* Match filter if defined
if (!filter.empty()
&& pid_str.find(filter) == string::npos
&& cache[new_proc.pid].name.find(filter) == string::npos
&& cache[new_proc.pid].cmd.find(filter) == string::npos
&& cache[new_proc.pid].user.find(filter) == string::npos) {
if (new_cache) cache.erase(new_proc.pid);
continue;
}
new_proc.name = cache[new_proc.pid].name;
new_proc.cmd = cache[new_proc.pid].cmd;
new_proc.user = cache[new_proc.pid].user;
//* Parse /proc/[pid]/stat
pread.open(d.path() / "stat");
if (pread.good()) {
string instr;
getline(pread, instr);
pread.close();
size_t s_pos = 0, c_pos = 0, s_count = 0;
uint64_t cpu_t = 0;
//? Skip pid and comm field and find comm fields closing ')'
s_pos = instr.find_last_of(')') + 2;
if (s_pos == string::npos) continue;
do {
c_pos = instr.find(' ', s_pos);
if (c_pos == string::npos) break;
switch (s_count) {
case 0: { //? Process state
new_proc.state = instr[s_pos];
break;
}
case 1: { //? Process parent pid
new_proc.ppid = stoi(instr.substr(s_pos, c_pos - s_pos));
break;
}
case 11: { //? Process utime
cpu_t = stoull(instr.substr(s_pos, c_pos - s_pos));
break;
}
case 12: { //? Process stime
cpu_t += stoull(instr.substr(s_pos, c_pos - s_pos));
break;
}
case 16: { //? Process nice value
new_proc.p_nice = stoi(instr.substr(s_pos, c_pos - s_pos));
break;
}
case 17: { //? Process number of threads
new_proc.threads = stoul(instr.substr(s_pos, c_pos - s_pos));
break;
}
case 19: { //? Cache cpu seconds
if (new_cache) cache[new_proc.pid].cpu_s = stoull(instr.substr(s_pos, c_pos - s_pos));
break;
}
case 36: { //? CPU number last executed on
new_proc.cpu_n = stoi(instr.substr(s_pos, c_pos - s_pos));
break;
}
}
s_pos = c_pos + 1;
} while (s_count++ < 36);
if (s_count < 19) continue;
//? Process cpu usage since last update
new_proc.cpu_p = round(cmult * 1000 * (cpu_t - cache[new_proc.pid].cpu_t) / (cputimes - old_cputimes)) / 10.0;
//? Process cumulative cpu usage since process start
new_proc.cpu_c = ((double)cpu_t / clk_tck) / (uptime - (cache[new_proc.pid].cpu_s / clk_tck));
//? Update cache with latest cpu times
cache[new_proc.pid].cpu_t = cpu_t;
}
else continue;
//* Get RSS memory in bytes from /proc/[pid]/statm
pread.open(d.path() / "statm");
if (pread.good()) {
pread.ignore(SSmax, ' ');
pread >> new_proc.mem;
pread.close();
new_proc.mem *= page_size;
}
//* Create proc_info
procs.push_back(new_proc);
}
}
//* Sort processes
std::ranges::sort(procs, [sortint = v_index(sort_vector, sorting), &reverse](proc_info& a, proc_info& b) {
switch (sortint) {
case 0: return (reverse) ? a.pid < b.pid : a.pid > b.pid;
case 1: return (reverse) ? a.name < b.name : a.name > b.name;
case 2: return (reverse) ? a.cmd < b.cmd : a.cmd > b.cmd;
case 3: return (reverse) ? a.threads < b.threads : a.threads > b.threads;
case 4: return (reverse) ? a.user < b.user : a.user > b.user;
case 5: return (reverse) ? a.mem < b.mem : a.mem > b.mem;
case 6: return (reverse) ? a.cpu_p < b.cpu_p : a.cpu_p > b.cpu_p;
case 7: return (reverse) ? a.cpu_c < b.cpu_c : a.cpu_c > b.cpu_c;
}
return false;
});
//* When using "cpu lazy" sorting push processes with high cpu usage to the front regardless of cumulative usage
if (sorting == "cpu lazy" && !reverse) {
double max = 10.0, target = 30.0;
for (size_t i = 0, offset = 0; i < procs.size(); i++) {
if (i <= 5 && procs[i].cpu_p > max) max = procs[i].cpu_p;
else if (i == 6) target = (max > 30.0) ? max : 10.0;
if (i == offset && procs[i].cpu_p > 30.0) offset++;
else if (procs[i].cpu_p > target) rotate(procs.begin() + offset, procs.begin() + i, procs.begin() + i + 1);
}
}
//* Generate tree view if enabled
if (tree) {
auto min_ppid = std::ranges::min(procs, [](proc_info& a, proc_info& b) { return a.ppid < b.ppid; }).ppid;
vector<proc_info> tree_procs;
for (auto& p : procs) {
if (p.ppid == min_ppid) _tree_gen(p, procs, tree_procs);
}
procs.swap(tree_procs);
}
//* Clear dead processes from cache at a regular interval
if (++counter >= 10000 || ((int)cache.size() > npids + 100)) {
counter = 0;
unordered_flat_map<uint, p_cache> r_cache;
r_cache.reserve(pid_list.size());
for (auto& p : pid_list) {
if (cache.contains(p)) r_cache[p] = cache.at(p);
}
cache.swap(r_cache);
}
old_cputimes = cputimes;
atomic_wait(drawing);
current_procs.swap(procs);
numpids = npids;
collecting.store(false);
return current_procs;
}
vector<proc_info>& collect();
//* Initialize needed variables for collect
void init(){
proc_path = (fs::is_directory(fs::path("/proc")) && access("/proc", R_OK) != -1) ? "/proc" : "";
if (proc_path.empty()) {
string errmsg = "Proc filesystem not found or no permission to read from it!";
Logger::error(errmsg);
cout << "ERROR: " << errmsg << endl;
exit(1);
}
passwd_path = (access("/etc/passwd", R_OK) != -1) ? fs::path("/etc/passwd") : passwd_path;
if (passwd_path.empty()) Logger::warning("Could not read /etc/passwd, will show UID instead of username.");
page_size = sysconf(_SC_PAGE_SIZE);
if (page_size <= 0) {
page_size = 4096;
Logger::warning("Could not get system page size. Defaulting to 4096, processes memory usage might be incorrect.");
}
clk_tck = sysconf(_SC_CLK_TCK);
if (clk_tck <= 0) {
clk_tck = 100;
Logger::warning("Could not get system clocks per second. Defaulting to 100, processes cpu usage might be incorrect.");
}
}
}
#endif
void init();
}

View file

@ -16,8 +16,7 @@ indent = tab
tab-size = 4
*/
#ifndef _btop_menu_included_
#define _btop_menu_included_ 1
#pragma once
#include <string>
#include <vector>
@ -73,7 +72,3 @@ namespace Menu {
}
#endif

288
src/btop_theme.cpp Normal file
View file

@ -0,0 +1,288 @@
/* 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 <cmath>
#include <vector>
#include <unordered_map>
#include <ranges>
#include <algorithm>
#include <btop_tools.h>
#include <btop_config.h>
#include <btop_theme.h>
using std::round, std::vector, robin_hood::unordered_flat_map, std::stoi, std::views::iota, std::array,
std::clamp, std::max, std::min, std::ceil, std::to_string;
using namespace Tools;
namespace rng = std::ranges;
namespace fs = std::filesystem;
namespace Theme {
fs::path theme_dir;
fs::path user_theme_dir;
const unordered_flat_map<string, string> Default_theme = {
{ "main_bg", "#00" },
{ "main_fg", "#cc" },
{ "title", "#ee" },
{ "hi_fg", "#969696" },
{ "selected_bg", "#7e2626" },
{ "selected_fg", "#ee" },
{ "inactive_fg", "#40" },
{ "graph_text", "#60" },
{ "meter_bg", "#40" },
{ "proc_misc", "#0de756" },
{ "cpu_box", "#3d7b46" },
{ "mem_box", "#8a882e" },
{ "net_box", "#423ba5" },
{ "proc_box", "#923535" },
{ "div_line", "#30" },
{ "temp_start", "#4897d4" },
{ "temp_mid", "#5474e8" },
{ "temp_end", "#ff40b6" },
{ "cpu_start", "#50f095" },
{ "cpu_mid", "#f2e266" },
{ "cpu_end", "#fa1e1e" },
{ "free_start", "#223014" },
{ "free_mid", "#b5e685" },
{ "free_end", "#dcff85" },
{ "cached_start", "#0b1a29" },
{ "cached_mid", "#74e6fc" },
{ "cached_end", "#26c5ff" },
{ "available_start", "#292107" },
{ "available_mid", "#ffd77a" },
{ "available_end", "#ffb814" },
{ "used_start", "#3b1f1c" },
{ "used_mid", "#d9626d" },
{ "used_end", "#ff4769" },
{ "download_start", "#231a63" },
{ "download_mid", "#4f43a3" },
{ "download_end", "#b0a9de" },
{ "upload_start", "#510554" },
{ "upload_mid", "#7d4180" },
{ "upload_end", "#dcafde" },
{ "process_start", "#80d0a3" },
{ "process_mid", "#dcd179" },
{ "process_end", "#d45454" }
};
namespace {
//* Convert 24-bit colors to 256 colors using 6x6x6 color cube
int truecolor_to_256(int r, int g, int b){
if (round((double)r / 11) == round((double)g / 11) && round((double)g / 11) == round((double)b / 11)) {
return 232 + round((double)r / 11);
} else {
return round((double)r / 51) * 36 + round((double)g / 51) * 6 + round((double)b / 51) + 16;
}
}
}
string hex_to_color(string hexa, bool t_to_256, string depth){
if (hexa.size() > 1){
hexa.erase(0, 1);
for (auto& c : hexa) if (!isxdigit(c)) {
Logger::error("Invalid hex value: " + hexa);
return "";
}
string pre = Fx::e + (depth == "fg" ? "38" : "48") + ";" + (t_to_256 ? "5;" : "2;");
if (hexa.size() == 2){
int h_int = stoi(hexa, 0, 16);
if (t_to_256){
return pre + to_string(truecolor_to_256(h_int, h_int, h_int)) + "m";
} else {
string h_str = to_string(h_int);
return pre + h_str + ";" + h_str + ";" + h_str + "m";
}
}
else if (hexa.size() == 6){
if (t_to_256){
return pre + to_string(truecolor_to_256(
stoi(hexa.substr(0, 2), 0, 16),
stoi(hexa.substr(2, 2), 0, 16),
stoi(hexa.substr(4, 2), 0, 16))) + "m";
} else {
return pre +
to_string(stoi(hexa.substr(0, 2), 0, 16)) + ";" +
to_string(stoi(hexa.substr(2, 2), 0, 16)) + ";" +
to_string(stoi(hexa.substr(4, 2), 0, 16)) + "m";
}
}
else Logger::error("Invalid size of hex value: " + hexa);
}
else Logger::error("Hex value missing: " + hexa);
return "";
}
string dec_to_color(int r, int g, int b, bool t_to_256, string depth){
string pre = Fx::e + (depth == "fg" ? "38" : "48") + ";" + (t_to_256 ? "5;" : "2;");
r = std::clamp(r, 0, 255);
g = std::clamp(g, 0, 255);
b = std::clamp(b, 0, 255);
if (t_to_256) return pre + to_string(truecolor_to_256(r, g, b)) + "m";
else return pre + to_string(r) + ";" + to_string(g) + ";" + to_string(b) + "m";
}
array<int, 3> esc_to_rgb(string c_string){
array<int, 3> rgb = {-1, -1, -1};
if (c_string.size() >= 14){
c_string.erase(0, 7);
auto c_split = ssplit(c_string, ';');
if (c_split.size() == 3){
rgb[0] = stoi(c_split[0]);
rgb[1] = stoi(c_split[1]);
rgb[2] = stoi(c_split[2].erase(c_split[2].size()));
}
}
return rgb;
}
namespace {
unordered_flat_map<string, string> colors;
unordered_flat_map<string, array<int, 3>> rgbs;
unordered_flat_map<string, array<string, 101>> gradients;
//* Convert hex color to a array of decimals
array<int, 3> hex_to_dec(string hexa){
if (hexa.size() > 1){
hexa.erase(0, 1);
for (auto& c : hexa) if (!isxdigit(c)) return array<int, 3>{-1, -1, -1};
if (hexa.size() == 2){
int h_int = stoi(hexa, 0, 16);
return array<int, 3>{h_int, h_int, h_int};
}
else if (hexa.size() == 6){
return array<int, 3>{
stoi(hexa.substr(0, 2), 0, 16),
stoi(hexa.substr(2, 2), 0, 16),
stoi(hexa.substr(4, 2), 0, 16)
};
}
}
return {-1 ,-1 ,-1};
}
//* Generate colors and rgb decimal vectors for the theme
void generateColors(unordered_flat_map<string, string>& source){
vector<string> t_rgb;
string depth;
bool t_to_256 = !Config::getB("truecolor");
colors.clear(); rgbs.clear();
for (auto& [name, color] : Default_theme) {
depth = (name.ends_with("bg") && name != "meter_bg") ? "bg" : "fg";
if (source.contains(name)) {
if (source.at(name)[0] == '#') {
colors[name] = hex_to_color(source.at(name), t_to_256, depth);
rgbs[name] = hex_to_dec(source.at(name));
}
else {
t_rgb = ssplit(source.at(name));
if (t_rgb.size() != 3)
Logger::error("Invalid RGB decimal value: \"" + source.at(name) + "\"");
else {
colors[name] = dec_to_color(stoi(t_rgb[0]), stoi(t_rgb[1]), stoi(t_rgb[2]), t_to_256, depth);
rgbs[name] = array<int, 3>{stoi(t_rgb[0]), stoi(t_rgb[1]), stoi(t_rgb[2])};
}
}
}
if (colors[name].empty()) {
Logger::info("Missing color value for \"" + name + "\". Using value from default.");
colors[name] = hex_to_color(color, t_to_256, depth);
rgbs[name] = array<int, 3>{-1, -1, -1};
}
}
}
//* Generate color gradients from two or three colors, 101 values indexed 0-100
void generateGradients(){
gradients.clear();
array<string, 101> c_gradient;
bool t_to_256 = !Config::getB("truecolor");
for (auto& [name, source_arr] : rgbs) {
if (!name.ends_with("_start")) continue;
array<array<int, 3>, 101> dec_arr;
dec_arr[0][0] = -1;
string wname = rtrim(name, "_start");
array<array<int, 3>, 3> rgb_arr = {source_arr, rgbs[wname + "_mid"], rgbs[wname + "_end"]};
//? Only start iteration if gradient has a _end color value defined
if (rgb_arr[2][0] >= 0) {
//? Split iteration in two passes of 50 + 51 instead of 101 if gradient has _start, _mid and _end values defined
int rng = (rgb_arr[1][0] >= 0) ? 50 : 100;
for (int rgb : iota(0, 3)){
int arr1 = 0, offset = 0;
int arr2 = (rng == 50) ? 1 : 2;
for (int i : iota(0, 101)) {
dec_arr[i][rgb] = rgb_arr[arr1][rgb] + (i - offset) * (rgb_arr[arr2][rgb] - rgb_arr[arr1][rgb]) / rng;
//? Switch source arrays from _start/_mid to _mid/_end at 50 passes if _mid is defined
if (i == rng) { ++arr1; ++arr2; offset = 50;}
}
}
}
if (dec_arr[0][0] != -1) {
int y = 0;
for (auto& arr : dec_arr) c_gradient[y++] = dec_to_color(arr[0], arr[1], arr[2], t_to_256);
}
else {
//? If only _start was defined fill array with _start color
c_gradient.fill(colors[name]);
}
gradients[wname].swap(c_gradient);
}
}
}
//* Set current theme using <source> map
void set(unordered_flat_map<string, string> source){
generateColors(source);
generateGradients();
Term::fg = colors.at("main_fg");
Term::bg = colors.at("main_bg");
Fx::reset = Fx::reset_base + Term::fg + Term::bg;
}
//* Return escape code for color <name>
const string& c(string name){
return colors.at(name);
}
//* Return array of escape codes for color gradient <name>
const array<string, 101>& g(string name){
return gradients.at(name);
}
//* Return array of red, green and blue in decimal for color <name>
const std::array<int, 3>& dec(string name){
return rgbs.at(name);
}
robin_hood::unordered_flat_map<string, string>& test_colors(){
return colors;
}
robin_hood::unordered_flat_map<string, std::array<string, 101>>& test_gradients(){
return gradients;
}
}

View file

@ -16,277 +16,49 @@ indent = tab
tab-size = 4
*/
#ifndef _btop_theme_included_
#define _btop_theme_included_
#pragma once
#include <string>
#include <cmath>
#include <vector>
#include <unordered_map>
#include <robin_hood.h>
#include <ranges>
#include <algorithm>
#include <filesystem>
#include <btop_tools.h>
#include <btop_config.h>
using std::string, std::round, std::vector, robin_hood::unordered_flat_map, std::stoi, std::views::iota, std::array, std::clamp, std::max, std::min, std::ceil;
using namespace Tools;
using std::string;
namespace Theme {
extern std::filesystem::path theme_dir;
extern std::filesystem::path user_theme_dir;
fs::path theme_dir;
fs::path user_theme_dir;
const unordered_flat_map<string, string> Default_theme = {
{ "main_bg", "#00" },
{ "main_fg", "#cc" },
{ "title", "#ee" },
{ "hi_fg", "#969696" },
{ "selected_bg", "#7e2626" },
{ "selected_fg", "#ee" },
{ "inactive_fg", "#40" },
{ "graph_text", "#60" },
{ "meter_bg", "#40" },
{ "proc_misc", "#0de756" },
{ "cpu_box", "#3d7b46" },
{ "mem_box", "#8a882e" },
{ "net_box", "#423ba5" },
{ "proc_box", "#923535" },
{ "div_line", "#30" },
{ "temp_start", "#4897d4" },
{ "temp_mid", "#5474e8" },
{ "temp_end", "#ff40b6" },
{ "cpu_start", "#50f095" },
{ "cpu_mid", "#f2e266" },
{ "cpu_end", "#fa1e1e" },
{ "free_start", "#223014" },
{ "free_mid", "#b5e685" },
{ "free_end", "#dcff85" },
{ "cached_start", "#0b1a29" },
{ "cached_mid", "#74e6fc" },
{ "cached_end", "#26c5ff" },
{ "available_start", "#292107" },
{ "available_mid", "#ffd77a" },
{ "available_end", "#ffb814" },
{ "used_start", "#3b1f1c" },
{ "used_mid", "#d9626d" },
{ "used_end", "#ff4769" },
{ "download_start", "#231a63" },
{ "download_mid", "#4f43a3" },
{ "download_end", "#b0a9de" },
{ "upload_start", "#510554" },
{ "upload_mid", "#7d4180" },
{ "upload_end", "#dcafde" },
{ "process_start", "#80d0a3" },
{ "process_mid", "#dcd179" },
{ "process_end", "#d45454" }
};
namespace {
//* Convert 24-bit colors to 256 colors using 6x6x6 color cube
int truecolor_to_256(int r, int g, int b){
if (round((double)r / 11) == round((double)g / 11) && round((double)g / 11) == round((double)b / 11)) {
return 232 + round((double)r / 11);
} else {
return round((double)r / 51) * 36 + round((double)g / 51) * 6 + round((double)b / 51) + 16;
}
}
}
extern const robin_hood::unordered_flat_map<string, string> Default_theme;
//* Generate escape sequence for 24-bit or 256 color and return as a string
//* Args hexa: ["#000000"-"#ffffff"] for color, ["#00"-"#ff"] for greyscale
//* t_to_256: [true|false] convert 24bit value to 256 color value
//* depth: ["fg"|"bg"] for either a foreground color or a background color
string hex_to_color(string hexa, bool t_to_256=false, string depth="fg"){
if (hexa.size() > 1){
hexa.erase(0, 1);
for (auto& c : hexa) if (!isxdigit(c)) {
Logger::error("Invalid hex value: " + hexa);
return "";
}
string pre = Fx::e + (depth == "fg" ? "38" : "48") + ";" + (t_to_256 ? "5;" : "2;");
if (hexa.size() == 2){
int h_int = stoi(hexa, 0, 16);
if (t_to_256){
return pre + to_string(truecolor_to_256(h_int, h_int, h_int)) + "m";
} else {
string h_str = to_string(h_int);
return pre + h_str + ";" + h_str + ";" + h_str + "m";
}
}
else if (hexa.size() == 6){
if (t_to_256){
return pre + to_string(truecolor_to_256(
stoi(hexa.substr(0, 2), 0, 16),
stoi(hexa.substr(2, 2), 0, 16),
stoi(hexa.substr(4, 2), 0, 16))) + "m";
} else {
return pre +
to_string(stoi(hexa.substr(0, 2), 0, 16)) + ";" +
to_string(stoi(hexa.substr(2, 2), 0, 16)) + ";" +
to_string(stoi(hexa.substr(4, 2), 0, 16)) + "m";
}
}
else Logger::error("Invalid size of hex value: " + hexa);
}
else Logger::error("Hex value missing: " + hexa);
return "";
}
string hex_to_color(string hexa, bool t_to_256=false, string depth="fg");
//* Generate escape sequence for 24-bit or 256 color and return as a string
//* Args r: [0-255], g: [0-255], b: [0-255]
//* t_to_256: [true|false] convert 24bit value to 256 color value
//* depth: ["fg"|"bg"] for either a foreground color or a background color
string dec_to_color(int r, int g, int b, bool t_to_256=false, string depth="fg"){
string pre = Fx::e + (depth == "fg" ? "38" : "48") + ";" + (t_to_256 ? "5;" : "2;");
r = std::clamp(r, 0, 255);
g = std::clamp(g, 0, 255);
b = std::clamp(b, 0, 255);
if (t_to_256) return pre + to_string(truecolor_to_256(r, g, b)) + "m";
else return pre + to_string(r) + ";" + to_string(g) + ";" + to_string(b) + "m";
}
string dec_to_color(int r, int g, int b, bool t_to_256=false, string depth="fg");
//* Return an array of red, green and blue, 0-255 values for a 24-bit color escape string
auto esc_to_rgb(string c_string){
array<int, 3> rgb = {-1, -1, -1};
if (c_string.size() >= 14){
c_string.erase(0, 7);
auto c_split = ssplit(c_string, ";");
if (c_split.size() == 3){
rgb[0] = stoi(c_split[0]);
rgb[1] = stoi(c_split[1]);
rgb[2] = stoi(c_split[2].erase(c_split[2].size()));
}
}
return rgb;
}
namespace {
unordered_flat_map<string, string> colors;
unordered_flat_map<string, array<int, 3>> rgbs;
unordered_flat_map<string, array<string, 101>> gradients;
//* Convert hex color to a array of decimals
array<int, 3> hex_to_dec(string hexa){
if (hexa.size() > 1){
hexa.erase(0, 1);
for (auto& c : hexa) if (!isxdigit(c)) return array<int, 3>{-1, -1, -1};
if (hexa.size() == 2){
int h_int = stoi(hexa, 0, 16);
return array<int, 3>{h_int, h_int, h_int};
}
else if (hexa.size() == 6){
return array<int, 3>{
stoi(hexa.substr(0, 2), 0, 16),
stoi(hexa.substr(2, 2), 0, 16),
stoi(hexa.substr(4, 2), 0, 16)
};
}
}
return {-1 ,-1 ,-1};
}
//* Generate colors and rgb decimal vectors for the theme
void generateColors(unordered_flat_map<string, string>& source){
vector<string> t_rgb;
string depth;
bool t_to_256 = !Config::getB("truecolor");
colors.clear(); rgbs.clear();
for (auto& [name, color] : Default_theme) {
depth = (name.ends_with("bg") && name != "meter_bg") ? "bg" : "fg";
if (source.contains(name)) {
if (source.at(name)[0] == '#') {
colors[name] = hex_to_color(source.at(name), t_to_256, depth);
rgbs[name] = hex_to_dec(source.at(name));
}
else {
t_rgb = ssplit(source.at(name), " ");
if (t_rgb.size() != 3)
Logger::error("Invalid RGB decimal value: \"" + source.at(name) + "\"");
else {
colors[name] = dec_to_color(stoi(t_rgb[0]), stoi(t_rgb[1]), stoi(t_rgb[2]), t_to_256, depth);
rgbs[name] = array<int, 3>{stoi(t_rgb[0]), stoi(t_rgb[1]), stoi(t_rgb[2])};
}
}
}
if (colors[name].empty()) {
Logger::info("Missing color value for \"" + name + "\". Using value from default.");
colors[name] = hex_to_color(color, t_to_256, depth);
rgbs[name] = array<int, 3>{-1, -1, -1};
}
}
}
//* Generate color gradients from two or three colors, 101 values indexed 0-100
void generateGradients(){
gradients.clear();
array<string, 101> c_gradient;
bool t_to_256 = !Config::getB("truecolor");
for (auto& [name, source_arr] : rgbs) {
if (!name.ends_with("_start")) continue;
array<array<int, 3>, 101> dec_arr;
dec_arr[0][0] = -1;
string wname = rtrim(name, "_start");
array<array<int, 3>, 3> rgb_arr = {source_arr, rgbs[wname + "_mid"], rgbs[wname + "_end"]};
//? Only start iteration if gradient has a _end color value defined
if (rgb_arr[2][0] >= 0) {
//? Split iteration in two passes of 50 + 51 instead of 101 if gradient has _start, _mid and _end values defined
int rng = (rgb_arr[1][0] >= 0) ? 50 : 100;
for (int rgb : iota(0, 3)){
int arr1 = 0, offset = 0;
int arr2 = (rng == 50) ? 1 : 2;
for (int i : iota(0, 101)) {
dec_arr[i][rgb] = rgb_arr[arr1][rgb] + (i - offset) * (rgb_arr[arr2][rgb] - rgb_arr[arr1][rgb]) / rng;
//? Switch source arrays from _start/_mid to _mid/_end at 50 passes if _mid is defined
if (i == rng) { ++arr1; ++arr2; offset = 50;}
}
}
}
if (dec_arr[0][0] != -1) {
int y = 0;
for (auto& arr : dec_arr) c_gradient[y++] = dec_to_color(arr[0], arr[1], arr[2], t_to_256);
}
else {
//? If only _start was defined fill array with _start color
c_gradient.fill(colors[name]);
}
gradients[wname].swap(c_gradient);
}
}
}
std::array<int, 3> esc_to_rgb(string c_string);
//* Set current theme using <source> map
void set(unordered_flat_map<string, string> source){
generateColors(source);
generateGradients();
Term::fg = colors.at("main_fg");
Term::bg = colors.at("main_bg");
Fx::reset = Fx::reset_base + Term::fg + Term::bg;
}
void set(robin_hood::unordered_flat_map<string, string> source);
//* Return escape code for color <name>
auto& c(string name){
return colors.at(name);
}
const string& c(string name);
//* Return array of escape codes for color gradient <name>
auto& g(string name){
return gradients.at(name);
}
const std::array<string, 101>& g(string name);
//* Return array of red, green and blue in decimal for color <name>
auto& dec(string name){
return rgbs.at(name);
}
const std::array<int, 3>& dec(string name);
}
//? Testing
robin_hood::unordered_flat_map<string, string>& test_colors();
robin_hood::unordered_flat_map<string, std::array<string, 101>>& test_gradients();
#endif
}

432
src/btop_tools.cpp Normal file
View file

@ -0,0 +1,432 @@
/* 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 <cmath>
#include <iostream>
#include <fstream>
#include <chrono>
#include <ctime>
#include <sstream>
#include <iomanip>
#include <utility>
#include <robin_hood.h>
#include <thread>
#include <unistd.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <btop_tools.h>
using std::string_view, std::array, std::regex, std::max, std::to_string, std::cin,
std::atomic, robin_hood::unordered_flat_map;
namespace fs = std::filesystem;
namespace rng = std::ranges;
//? ------------------------------------------------- NAMESPACES ------------------------------------------------------
//* Collection of escape codes for text style and formatting
namespace Fx {
const string e = "\x1b[";
const string b = e + "1m";
const string ub = e + "22m";
const string d = e + "2m";
const string ud = e + "22m";
const string i = e + "3m";
const string ui = e + "23m";
const string ul = e + "4m";
const string uul = e + "24m";
const string bl = e + "5m";
const string ubl = e + "25m";
const string s = e + "9m";
const string us = e + "29m";
const string reset_base = e + "0m";
string reset = reset_base;
const regex escape_regex("\033\\[\\d+;?\\d?;?\\d*;?\\d*;?\\d*(m|f|s|u|C|D|A|B){1}");
const regex color_regex("\033\\[\\d+;?\\d?;?\\d*;?\\d*;?\\d*(m){1}");
string uncolor(string& s){
return regex_replace(s, color_regex, "");
}
}
//* Collection of escape codes and functions for cursor manipulation
namespace Mv {
const string to(int line, int col){ return Fx::e + to_string(line) + ";" + to_string(col) + "f";}
const string r(int x){ return Fx::e + to_string(x) + "C";}
const string l(int x){ return Fx::e + to_string(x) + "D";}
const string u(int x){ return Fx::e + to_string(x) + "A";}
const string d(int x) { return Fx::e + to_string(x) + "B";}
const string save = Fx::e + "s";
const string restore = Fx::e + "u";
}
//* Collection of escape codes and functions for terminal manipulation
namespace Term {
bool initialized = false;
bool resized = false;
uint width = 0;
uint height = 0;
string fg, bg, current_tty;
const string hide_cursor = Fx::e + "?25l";
const string show_cursor = Fx::e + "?25h";
const string alt_screen = Fx::e + "?1049h";
const string normal_screen = Fx::e + "?1049l";
const string clear = Fx::e + "2J" + Fx::e + "0;0f";
const string clear_end = Fx::e + "0J";
const string clear_begin = Fx::e + "1J";
const string mouse_on = Fx::e + "?1002h" + Fx::e + "?1015h" + Fx::e + "?1006h";
const string mouse_off = Fx::e + "?1002l";
const string mouse_direct_on = Fx::e + "?1003h";
const string mouse_direct_off = Fx::e + "?1003l";
namespace {
struct termios initial_settings;
//* Toggle terminal input echo
bool echo(bool on=true){
struct termios settings;
if (tcgetattr(STDIN_FILENO, &settings)) return false;
if (on) settings.c_lflag |= ECHO;
else settings.c_lflag &= ~(ECHO);
return 0 == tcsetattr(STDIN_FILENO, TCSANOW, &settings);
}
//* Toggle need for return key when reading input
bool linebuffered(bool on=true){
struct termios settings;
if (tcgetattr(STDIN_FILENO, &settings)) return false;
if (on) settings.c_lflag |= ICANON;
else settings.c_lflag &= ~(ICANON);
if (tcsetattr(STDIN_FILENO, TCSANOW, &settings)) return false;
if (on) setlinebuf(stdin);
else setbuf(stdin, NULL);
return true;
}
}
bool refresh(){
struct winsize w;
ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);
if (width != w.ws_col || height != w.ws_row) {
width = w.ws_col;
height = w.ws_row;
resized = true;
}
return resized;
}
bool init(){
if (!initialized){
initialized = (bool)isatty(STDIN_FILENO);
if (initialized) {
tcgetattr(STDIN_FILENO, &initial_settings);
current_tty = (string)ttyname(STDIN_FILENO);
cin.sync_with_stdio(false);
cin.tie(NULL);
echo(false);
linebuffered(false);
refresh();
resized = false;
}
}
return initialized;
}
void restore(){
if (initialized) {
echo(true);
linebuffered(true);
tcsetattr(STDIN_FILENO, TCSANOW, &initial_settings);
initialized = false;
}
}
}
//? --------------------------------------------------- FUNCTIONS -----------------------------------------------------
namespace Tools {
namespace {
//? Units for floating_humanizer function
const array<string, 11> Units_bit = {"bit", "Kib", "Mib", "Gib", "Tib", "Pib", "Eib", "Zib", "Yib", "Bib", "GEb"};
const array<string, 11> Units_byte = {"Byte", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "BiB", "GEB"};
}
size_t ulen(string str, const bool escape){
if (escape) str = std::regex_replace(str, Fx::escape_regex, "");
return rng::count_if(str, [](char c) { return (static_cast<unsigned char>(c) & 0xC0) != 0x80; } );
}
string uresize(string str, const size_t len){
if (str.size() < 1) return str;
if (len < 1) return "";
for (size_t x = 0, i = 0; i < str.size(); i++) {
if ((static_cast<unsigned char>(str.at(i)) & 0xC0) != 0x80) x++;
if (x == len + 1) {
str.resize(i);
str.shrink_to_fit();
break;
}
}
return str;
}
uint64_t time_s(){
return std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
}
uint64_t time_ms(){
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
}
uint64_t time_micros(){
return std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
}
bool isbool(string& str){
return (str == "true") || (str == "false") || (str == "True") || (str == "False");
}
bool stobool(string& str){
return (str == "true" || str == "True");
}
bool isint(string& str){
if (str.empty()) return false;
size_t offset = (str[0] == '-' ? 1 : 0);
return all_of(str.begin() + offset, str.end(), ::isdigit);
}
string ltrim(const string& str, const string t_str){
string_view str_v = str;
while (str_v.starts_with(t_str)) str_v.remove_prefix(t_str.size());
return (string)str_v;
}
string rtrim(const string& str, const string t_str){
string_view str_v = str;
while (str_v.ends_with(t_str)) str_v.remove_suffix(t_str.size());
return (string)str_v;
}
string trim(const string& str, const string t_str){
return ltrim(rtrim(str, t_str), t_str);
}
vector<string> ssplit(const string& str, const char delim){
vector<string> out;
for (const auto& s : str | rng::views::split(delim)
| rng::views::transform([](auto &&rng) {
return string_view(&*rng.begin(), rng::distance(rng));
})) {
if (!s.empty()) out.emplace_back(s);
}
return out;
}
void sleep_ms(const uint& ms) {
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
}
string ljust(string str, const size_t x, bool utf, bool escape, bool lim){
if (utf || escape) {
if (!escape && lim && ulen(str) > x) str = uresize(str, x);
return str + string(max((int)(x - ulen(str, escape)), 0), ' ');
}
else {
if (lim && str.size() > x) str.resize(x);
return str + string(max((int)(x - str.size()), 0), ' ');
}
}
string rjust(string str, const size_t x, bool utf, bool escape, bool lim){
if (utf || escape) {
if (!escape && lim && ulen(str) > x) str = uresize(str, x);
return string(max((int)(x - ulen(str, escape)), 0), ' ') + str;
}
else {
if (lim && str.size() > x) str.resize(x);
return string(max((int)(x - str.size()), 0), ' ') + str;
}
}
string trans(const string& str){
size_t pos;
string_view oldstr = str;
string newstr;
newstr.reserve(str.size());
while ((pos = oldstr.find(' ')) != string::npos){
newstr.append(oldstr.substr(0, pos));
oldstr.remove_prefix(pos+1);
pos = 1;
while (pos < oldstr.size() && oldstr.at(pos) == ' ') pos++;
newstr.append(Mv::r(pos));
oldstr.remove_suffix(pos-1);
}
return (newstr.empty()) ? str : newstr + (string)oldstr;
}
string sec_to_dhms(uint sec){
string out;
uint d, h, m;
d = sec / (3600 * 24);
sec %= 3600 * 24;
h = sec / 3600;
sec %= 3600;
m = sec / 60;
sec %= 60;
if (d>0) out = to_string(d) + "d ";
out += ((h<10) ? "0" : "") + to_string(h) + ":";
out += ((m<10) ? "0" : "") + to_string(m) + ":";
out += ((sec<10) ? "0" : "") + to_string(sec);
return out;
}
string floating_humanizer(uint64_t value, bool shorten, uint start, bool bit, bool per_second){
string out;
uint mult = (bit) ? 8 : 1;
auto& units = (bit) ? Units_bit : Units_byte;
value *= 100 * mult;
while (value >= 102400){
value >>= 10;
if (value < 100){
out = to_string(value);
break;
}
start++;
}
if (out.empty()) {
out = to_string(value);
if (out.size() == 4 && start > 0) { out.pop_back(); out.insert(2, ".");}
else if (out.size() == 3 && start > 0) out.insert(1, ".");
else if (out.size() >= 2) out.resize(out.size() - 2);
}
if (shorten){
if (out.find('.') != string::npos) out = to_string((int)round(stof(out)));
if (out.size() > 3) { out = to_string((int)(out[0] - '0') + 1); start++;}
out.push_back(units[start][0]);
}
else out += " " + units[start];
if (per_second) out += (bit) ? "ps" : "/s";
return out;
}
std::string operator*(string str, size_t n){
string out;
out.reserve(str.size() * n);
while (n-- > 0) out += str;
return out;
}
string strf_time(string strf){
auto now = std::chrono::system_clock::now();
auto in_time_t = std::chrono::system_clock::to_time_t(now);
std::tm bt {};
std::stringstream ss;
ss << std::put_time(localtime_r(&in_time_t, &bt), strf.c_str());
return ss.str();
}
#if (__GNUC__ > 10)
//* Redirects to atomic wait
void atomic_wait(atomic<bool>& atom, bool val){
atom.wait(val);
}
#else
//* Crude implementation of atomic wait for GCC 10
void atomic_wait(atomic<bool>& atom, bool val){
while (atom.load() == val) sleep_ms(1);
}
#endif
void atomic_wait_set(atomic<bool>& atom, bool val){
atomic_wait(atom, val);
atom.store(val);
}
}
namespace Logger {
using namespace Tools;
namespace {
std::atomic<bool> busy (false);
bool first = true;
string tdf = "%Y/%m/%d (%T) | ";
}
vector<string> log_levels = {
"DISABLED",
"ERROR",
"WARNING",
"INFO",
"DEBUG",
};
size_t loglevel;
fs::path logfile;
void set(string level){
loglevel = v_index(log_levels, level);
}
void log_write(uint level, string& msg){
if (loglevel < level || logfile.empty()) return;
atomic_wait_set(busy, true);
std::error_code ec;
if (fs::exists(logfile) && fs::file_size(logfile, ec) > 1024 << 10 && !ec) {
auto old_log = logfile;
old_log += ".1";
if (fs::exists(old_log)) fs::remove(old_log, ec);
if (!ec) fs::rename(logfile, old_log, ec);
}
if (!ec) {
std::ofstream lwrite(logfile, std::ios::app);
if (first) { first = false; lwrite << "\n" << strf_time(tdf) << "===> btop++ v." << Global::Version << "\n";}
lwrite << strf_time(tdf) << log_levels.at(level) << ": " << msg << "\n";
lwrite.close();
}
else logfile.clear();
busy.store(false);
}
void error(string msg){
log_write(1, msg);
}
void warning(string msg){
log_write(2, msg);
}
void info(string msg){
log_write(3, msg);
}
void debug(string msg){
log_write(4, msg);
}
}

View file

@ -16,238 +16,142 @@ indent = tab
tab-size = 4
*/
#ifndef _btop_tools_included_
#define _btop_tools_included_
#pragma once
#include <string>
#include <cmath>
#include <vector>
#include <iostream>
#include <fstream>
#include <chrono>
#include <ctime>
#include <sstream>
#include <iomanip>
#include <regex>
#include <utility>
#include <atomic>
#include <filesystem>
#include <robin_hood.h>
#include <ranges>
#include <unistd.h>
#include <termios.h>
#include <sys/ioctl.h>
using std::string, std::string_view, std::vector, std::array, std::regex, std::max, std::to_string, std::cin, std::atomic, robin_hood::unordered_flat_map;
namespace fs = std::filesystem;
using std::string, std::vector;
//? ------------------------------------------------- NAMESPACES ------------------------------------------------------
namespace Global {
extern string Version;
}
//* Collection of escape codes for text style and formatting
namespace Fx {
//* Escape sequence start
const string e = "\x1b[";
//* Bold on/off
const string b = e + "1m";
const string ub = e + "22m";
//* Dark on/off
const string d = e + "2m";
const string ud = e + "22m";
//* Italic on/off
const string i = e + "3m";
const string ui = e + "23m";
//* Underline on/off
const string ul = e + "4m";
const string uul = e + "24m";
//* Blink on/off
const string bl = e + "5m";
const string ubl = e + "25m";
//* Strike / crossed-out on/off
const string s = e + "9m";
const string us = e + "29m";
extern const string e; //* Escape sequence start
extern const string b; //* Bold on/off
extern const string ub; //* Bold off
extern const string d; //* Dark on
extern const string ud; //* Dark off
extern const string i; //* Italic on
extern const string ui; //* Italic off
extern const string ul; //* Underline on
extern const string uul; //* Underline off
extern const string bl; //* Blink on
extern const string ubl; //* Blink off
extern const string s; //* Strike/crossed-out on
extern const string us; //* Strike/crossed-out on/off
//* Reset foreground/background color and text effects
const string reset_base = e + "0m";
extern const string reset_base;
//* Reset text effects and restore default foregrund and background color < Changed by C_Theme
string reset = reset_base;
//* Reset text effects and restore theme foregrund and background color
extern string reset;
//* Regex for matching color, style and curse move escape sequences
const regex escape_regex("\033\\[\\d+;?\\d?;?\\d*;?\\d*;?\\d*(m|f|s|u|C|D|A|B){1}");
extern const std::regex escape_regex;
//* Regex for matching only color and style escape sequences
const regex color_regex("\033\\[\\d+;?\\d?;?\\d*;?\\d*;?\\d*(m){1}");
extern const std::regex color_regex;
//* Return a string with all colors and text styling removed
string uncolor(string& s){
return regex_replace(s, color_regex, "");
}
string uncolor(string& s);
}
//* Collection of escape codes and functions for cursor manipulation
namespace Mv {
//* Move cursor to <line>, <column>
string to(int line, int col){ return Fx::e + to_string(line) + ";" + to_string(col) + "f";}
const string to(int line, int col);
//* Move cursor right <x> columns
string r(int x){ return Fx::e + to_string(x) + "C";}
const string r(int x);
//* Move cursor left <x> columns
string l(int x){ return Fx::e + to_string(x) + "D";}
const string l(int x);
//* Move cursor up x lines
string u(int x){ return Fx::e + to_string(x) + "A";}
const string u(int x);
//* Move cursor down x lines
string d(int x) { return Fx::e + to_string(x) + "B";}
const string d(int x);
//* Save cursor position
const string save = Fx::e + "s";
extern const string save;
//* Restore saved cursor postion
const string restore = Fx::e + "u";
extern const string restore;
}
//* Collection of escape codes and functions for terminal manipulation
namespace Term {
bool initialized = false;
bool resized = false;
uint width = 0;
uint height = 0;
string fg, bg;
namespace {
struct termios initial_settings;
//* Toggle terminal input echo
bool echo(bool on=true){
struct termios settings;
if (tcgetattr(STDIN_FILENO, &settings)) return false;
if (on) settings.c_lflag |= ECHO;
else settings.c_lflag &= ~(ECHO);
return 0 == tcsetattr(STDIN_FILENO, TCSANOW, &settings);
}
//* Toggle need for return key when reading input
bool linebuffered(bool on=true){
struct termios settings;
if (tcgetattr(STDIN_FILENO, &settings)) return false;
if (on) settings.c_lflag |= ICANON;
else settings.c_lflag &= ~(ICANON);
if (tcsetattr(STDIN_FILENO, TCSANOW, &settings)) return false;
if (on) setlinebuf(stdin);
else setbuf(stdin, NULL);
return true;
}
}
extern bool initialized;
extern bool resized;
extern uint width;
extern uint height;
extern string fg, bg, current_tty;
//* Hide terminal cursor
const string hide_cursor = Fx::e + "?25l";
extern const string hide_cursor;
//* Show terminal cursor
const string show_cursor = Fx::e + "?25h";
extern const string show_cursor;
//* Switch to alternate screen
const string alt_screen = Fx::e + "?1049h";
extern const string alt_screen;
//* Switch to normal screen
const string normal_screen = Fx::e + "?1049l";
extern const string normal_screen;
//* Clear screen and set cursor to position 0,0
const string clear = Fx::e + "2J" + Fx::e + "0;0f";
extern const string clear;
//* Clear from cursor to end of screen
const string clear_end = Fx::e + "0J";
extern const string clear_end;
//* Clear from cursor to beginning of screen
const string clear_begin = Fx::e + "1J";
extern const string clear_begin;
//* Enable reporting of mouse position on click and release
const string mouse_on = Fx::e + "?1002h" + Fx::e + "?1015h" + Fx::e + "?1006h";
extern const string mouse_on;
//* Disable mouse reporting
const string mouse_off = Fx::e + "?1002l";
extern const string mouse_off;
//* Enable reporting of mouse position at any movement
const string mouse_direct_on = Fx::e + "?1003h";
extern const string mouse_direct_on;
//* Disable direct mouse reporting
const string mouse_direct_off = Fx::e + "?1003l";
extern const string mouse_direct_off;
//* Refresh variables holding current terminal width and height and return true if resized
bool refresh(){
struct winsize w;
ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);
if (width != w.ws_col || height != w.ws_row) {
width = w.ws_col;
height = w.ws_row;
resized = true;
}
return resized;
}
bool refresh();
//* Check for a valid tty, save terminal options and set new options
bool init(){
if (!initialized){
initialized = (bool)isatty(STDIN_FILENO);
if (initialized) {
initialized = (0 == tcgetattr(STDIN_FILENO, &initial_settings));
cin.sync_with_stdio(false);
cin.tie(NULL);
echo(false);
linebuffered(false);
refresh();
resized = false;
}
}
return initialized;
}
bool init();
//* Restore terminal options
void restore(){
if (initialized) {
echo(true);
linebuffered(true);
tcsetattr(STDIN_FILENO, TCSANOW, &initial_settings);
initialized = false;
}
}
void restore();
}
//? --------------------------------------------------- FUNCTIONS -----------------------------------------------------
namespace Tools {
const auto SSmax = std::numeric_limits<std::streamsize>::max();
//* Return number of UTF8 characters in a string with option to disregard escape sequences
size_t ulen(string str, const bool escape=false){
if (escape) str = std::regex_replace(str, Fx::escape_regex, "");
return std::count_if(str.begin(), str.end(),
[](char c) { return (static_cast<unsigned char>(c) & 0xC0) != 0x80; } );
}
size_t ulen(string str, const bool escape=false);
//* Resize a string consisting of UTF8 characters (only reduces size)
string uresize(string str, const size_t len){
if (str.size() < 1) return str;
if (len < 1) return "";
for (size_t x = 0, i = 0; i < str.size(); i++) {
if ((static_cast<unsigned char>(str.at(i)) & 0xC0) != 0x80) x++;
if (x == len + 1) {
str.resize(i);
str.shrink_to_fit();
break;
}
}
return str;
}
string uresize(string str, const size_t len);
//* Check if vector <vec> contains value <find_val>
template <typename T>
@ -262,269 +166,80 @@ namespace Tools {
}
//* Return current time since epoch in seconds
uint64_t time_s(){
return std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
}
uint64_t time_s();
//* Return current time since epoch in milliseconds
uint64_t time_ms(){
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
}
uint64_t time_ms();
//* Return current time since epoch in microseconds
uint64_t time_micros(){
return std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
}
uint64_t time_micros();
//* Check if a string is a valid bool value
bool isbool(string& str){
return (str == "true") || (str == "false") || (str == "True") || (str == "False");
}
bool isbool(string& str);
//* Convert string to bool, returning any value not equal to "true" or "True" as false
bool stobool(string& str){
return (str == "true" || str == "True") ? true : false;
}
bool stobool(string& str);
//* Check if a string is a valid integer value
bool isint(string& str){
if (str.empty()) return false;
size_t offset = (str[0] == '-' ? 1 : 0);
return all_of(str.begin() + offset, str.end(), ::isdigit);
}
bool isint(string& str);
//* Left-trim <t_str> from <str> and return new string
string ltrim(const string& str, const string t_str = " "){
string_view str_v = str;
while (str_v.starts_with(t_str)) str_v.remove_prefix(t_str.size());
return (string)str_v;
}
string ltrim(const string& str, const string t_str = " ");
//* Right-trim <t_str> from <str> and return new string
string rtrim(const string& str, const string t_str = " "){
string_view str_v = str;
while (str_v.ends_with(t_str)) str_v.remove_suffix(t_str.size());
return (string)str_v;
}
string rtrim(const string& str, const string t_str = " ");
//* Left-right-trim <t_str> from <str> and return new string
string trim(const string& str, const string t_str = " "){
return ltrim(rtrim(str, t_str), t_str);
}
string trim(const string& str, const string t_str = " ");
//* Split <string> at <delim> <time> number of times (0 for unlimited) and return vector
vector<string> ssplit(const string& str, const string delim = " ", const int times = 0, const bool ignore_remainder=false){
vector<string> out;
string_view str_v = str;
if (times > 0) out.reserve(times);
if (!str_v.empty() && !delim.empty()){
size_t pos = 0;
int x = 0;
while ((pos = str_v.find(delim)) != string::npos){
if (str_v.substr(0, pos) != delim) out.emplace_back(str_v.substr(0, pos));
str_v.remove_prefix(pos + delim.size());
if (times > 0 && ++x >= times) break;
}
}
if (!ignore_remainder) out.emplace_back(str_v);
return out;
}
//* Split <string> at all occurrences of <delim> and return as vector of strings
vector<string> ssplit(const string& str, const char delim = ' ');
//* Put current thread to sleep for <ms> milliseconds
void sleep_ms(const uint& ms) {
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
}
void sleep_ms(const uint& ms);
//* Left justify string <str> if <x> is greater than <str> length, limit return size to <x> by default
string ljust(string str, const size_t x, bool utf=false, bool escape=false, bool lim=true){
if (utf || escape) {
if (!escape && lim && ulen(str) > x) str = uresize(str, x);
return str + string(max((int)(x - ulen(str, escape)), 0), ' ');
}
else {
if (lim && str.size() > x) str.resize(x);
return str + string(max((int)(x - str.size()), 0), ' ');
}
}
string ljust(string str, const size_t x, bool utf=false, bool escape=false, bool lim=true);
//* Right justify string <str> if <x> is greater than <str> length, limit return size to <x> by default
string rjust(string str, const size_t x, bool utf=false, bool escape=false, bool lim=true){
if (utf || escape) {
if (!escape && lim && ulen(str) > x) str = uresize(str, x);
return string(max((int)(x - ulen(str, escape)), 0), ' ') + str;
}
else {
if (lim && str.size() > x) str.resize(x);
return string(max((int)(x - str.size()), 0), ' ') + str;
}
}
string rjust(string str, const size_t x, bool utf=false, bool escape=false, bool lim=true);
//* Replace whitespaces " " with escape code for move right
string trans(string str){
size_t pos;
string newstr;
while ((pos = str.find(' ')) != string::npos){
newstr.append(str.substr(0, pos));
str.erase(0, pos);
pos = 1;
while (pos < str.size() && str.at(pos) == ' ') pos++;
newstr.append(Mv::r(pos));
str.erase(0, pos);
}
return (newstr.empty()) ? str : newstr + str;
}
string trans(const string& str);
//* Convert seconds to format "Xd HH:MM:SS" and return string
string sec_to_dhms(uint sec){
string out;
uint d, h, m;
d = sec / (3600 * 24);
sec %= 3600 * 24;
h = sec / 3600;
sec %= 3600;
m = sec / 60;
sec %= 60;
if (d>0) out = to_string(d) + "d ";
out += ((h<10) ? "0" : "") + to_string(h) + ":";
out += ((m<10) ? "0" : "") + to_string(m) + ":";
out += ((sec<10) ? "0" : "") + to_string(sec);
return out;
}
//? Units for floating_humanizer function
const array<string, 11> Units_bit = {"bit", "Kib", "Mib", "Gib", "Tib", "Pib", "Eib", "Zib", "Yib", "Bib", "GEb"};
const array<string, 11> Units_byte = {"Byte", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "BiB", "GEB"};
string sec_to_dhms(uint sec);
//* Scales up in steps of 1024 to highest possible unit and returns string with unit suffixed
//* bit=True or defaults to bytes
//* start=int to set 1024 multiplier starting unit
//* short=True always returns 0 decimals and shortens unit to 1 character
string floating_humanizer(uint64_t value, bool shorten=false, uint start=0, bool bit=false, bool per_second=false){
string out;
uint mult = (bit) ? 8 : 1;
auto& units = (bit) ? Units_bit : Units_byte;
value *= 100 * mult;
while (value >= 102400){
value >>= 10;
if (value < 100){
out = to_string(value);
break;
}
start++;
}
if (out.empty()) {
out = to_string(value);
if (out.size() == 4 && start > 0) { out.pop_back(); out.insert(2, ".");}
else if (out.size() == 3 && start > 0) out.insert(1, ".");
else if (out.size() >= 2) out.resize(out.size() - 2);
}
if (shorten){
if (out.find('.') != string::npos) out = to_string((int)round(stof(out)));
if (out.size() > 3) { out = to_string((int)(out[0] - '0') + 1); start++;}
out.push_back(units[start][0]);
}
else out += " " + units[start];
if (per_second) out += (bit) ? "ps" : "/s";
return out;
}
string floating_humanizer(uint64_t value, bool shorten=false, uint start=0, bool bit=false, bool per_second=false);
//* Add std::string operator "*" : Repeat string <str> <n> number of times
std::string operator*(string str, size_t n){
string out;
out.reserve(str.size() * n);
while (n-- > 0) out += str;
return out;
}
std::string operator*(string str, size_t n);
//* Return current time in <strf> format
string strf_time(string strf){
auto now = std::chrono::system_clock::now();
auto in_time_t = std::chrono::system_clock::to_time_t(now);
std::tm bt {};
std::stringstream ss;
ss << std::put_time(localtime_r(&in_time_t, &bt), strf.c_str());
return ss.str();
}
string strf_time(string strf);
#if (__GNUC__ > 10)
//* Redirects to atomic wait
void atomic_wait(atomic<bool>& atom, bool val=true){
atom.wait(val);
}
#else
//* Crude implementation of atomic wait for GCC 10
void atomic_wait(atomic<bool>& atom, bool val=true){
while (atom.load() == val) sleep_ms(1);
}
#endif
//* Waits for <atom> to not be <val>
void atomic_wait(std::atomic<bool>& atom, bool val=true);
//* Waits for <atom> to not be <val> and then sets it to <val> again
void atomic_wait_set(atomic<bool>& atom, bool val=true){
atomic_wait(atom, val);
atom.store(val);
}
void atomic_wait_set(std::atomic<bool>& atom, bool val=true);
}
//* Simple logging implementation
namespace Logger {
using namespace Tools;
namespace {
std::atomic<bool> busy (false);
bool first = true;
string tdf = "%Y/%m/%d (%T) | ";
vector<string> log_levels = {
"DISABLED",
"ERROR",
"WARNING",
"INFO",
"DEBUG"
};
}
extern vector<string> log_levels;
extern std::filesystem::path logfile;
fs::path logfile;
uint loglevel = 2;
void log_write(uint level, string& msg){
if (loglevel < level || logfile.empty()) return;
atomic_wait_set(busy, true);
std::error_code ec;
if (fs::file_size(logfile, ec) > 1024 << 10 && !ec) {
auto old_log = logfile;
old_log += ".1";
if (fs::exists(old_log)) fs::remove(old_log, ec);
if (!ec) fs::rename(logfile, old_log, ec);
}
if (!ec) {
std::ofstream lwrite(logfile, std::ios::app);
if (first) { first = false; lwrite << "\n" << strf_time(tdf) << "===> btop++ v." << Global::Version << "\n";}
lwrite << strf_time(tdf) << log_levels.at(level) << ": " << msg << "\n";
lwrite.close();
}
else logfile.clear();
busy.store(false);
}
void error(string msg){
log_write(1, msg);
}
void warning(string msg){
log_write(2, msg);
}
void info(string msg){
log_write(3, msg);
}
void debug(string msg){
log_write(4, msg);
}
void set(string level); //* Set log level, valid arguments: "DISABLED", "ERROR", "WARNING", "INFO" and "DEBUG"
void log_write(uint level, string& msg);
void error(string msg);
void warning(string msg);
void info(string msg);
void debug(string msg);
}
#endif