diff --git a/Makefile b/Makefile index 19265ad..3cf8d31 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/btop.cpp b/src/btop.cpp similarity index 86% rename from btop.cpp rename to src/btop.cpp index feb15ab..8ed0a60 100644 --- a/btop.cpp +++ b/src/btop.cpp @@ -30,19 +30,7 @@ tab-size = 4 #include #include #include - -namespace Global { - const std::vector> Banner_src = { - {"#E62525", "██████╗ ████████╗ ██████╗ ██████╗"}, - {"#CD2121", "██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗ ██╗ ██╗"}, - {"#B31D1D", "██████╔╝ ██║ ██║ ██║██████╔╝ ██████╗██████╗"}, - {"#9A1919", "██╔══██╗ ██║ ██║ ██║██╔═══╝ ╚═██╔═╝╚═██╔═╝"}, - {"#801414", "██████╔╝ ██║ ╚██████╔╝██║ ╚═╝ ╚═╝"}, - {"#000000", "╚═════╝ ╚═╝ ╚═════╝ ╚═╝"}, - }; - const std::string Version = "0.0.20"; - int coreCount; -} +#include #include #include @@ -71,9 +59,23 @@ namespace Global { #error Platform not supported! #endif +namespace Global { + const std::vector> 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 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(); diff --git a/src/btop_config.cpp b/src/btop_config.cpp new file mode 100644 index 0000000..c250a78 --- /dev/null +++ b/src/btop_config.cpp @@ -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 +#include +#include +#include +#include + +#include +#include + +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 locked (false); + atomic writelock (false); + bool write_new; + + vector> 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 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 stringsTmp; + + unordered_flat_map 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 boolsTmp; + + unordered_flat_map ints = { + {"update_ms", 2000}, + {"proc_update_mult", 2}, + }; + unordered_flat_map 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 valid_graph_symbols = { "braille", "block", "tty" }; + + //* Return bool config value + const bool& getB(string name){ + return bools.at(name); + } + + //* Return integer config value + const int& getI(string name){ + return ints.at(name); + } + + //* Return string config value + const string& getS(string name){ + return strings.at(name); + } + + //* Set config value to bool + void set(string name, bool value){ + if (_locked(name)) boolsTmp.insert_or_assign(name, value); + else bools.at(name) = value; + } + + //* Set config value to int + void set(string name, int value){ + if (_locked(name)) intsTmp.insert_or_assign(name, value); + ints.at(name) = value; + } + + //* Set config value to string + void set(string name, string value){ + if (_locked(name)) stringsTmp.insert_or_assign(name, value); + else strings.at(name) = value; + } + + //* Flip config bool + 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& 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 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(); + } + } +} \ No newline at end of file diff --git a/src/btop_config.h b/src/btop_config.h index 3ab70e6..98e239c 100644 --- a/src/btop_config.h +++ b/src/btop_config.h @@ -16,303 +16,52 @@ indent = tab tab-size = 4 */ -#ifndef _btop_config_included_ -#define _btop_config_included_ +#pragma once #include #include -#include -#include #include -#include - -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 locked (false); - atomic writelock (false); - bool write_new; - - vector> 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 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 stringsTmp; - - unordered_flat_map 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 boolsTmp; - - unordered_flat_map ints = { - {"update_ms", 2000}, - {"proc_update_mult", 2}, - }; - unordered_flat_map intsTmp; - - bool _locked(){ - atomic_wait(writelock); - if (!write_new) write_new = true; - return locked.load(); - } - } + extern vector valid_graph_symbols; //* Return bool config value - const bool& getB(string name){ - return bools.at(name); - } + const bool& getB(string name); //* Return integer config value - const int& getI(string name){ - return ints.at(name); - } + const int& getI(string name); //* Return string config value - const string& getS(string name){ - return strings.at(name); - } + const string& getS(string name); //* Set config value to bool - 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 to int - 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 to string - 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 - 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& 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 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& 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 \ No newline at end of file + void write(); +} \ No newline at end of file diff --git a/src/btop_draw.cpp b/src/btop_draw.cpp new file mode 100644 index 0000000..f059e0f --- /dev/null +++ b/src/btop_draw.cpp @@ -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 +#include +#include +#include +#include + +#include +#include +#include +#include + +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 superscript = { "⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹" }; + + const unordered_flat_map> 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& 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 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 + 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& 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& 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; + +} \ No newline at end of file diff --git a/src/btop_draw.h b/src/btop_draw.h index 8e0d38e..3e21a6d 100644 --- a/src/btop_draw.h +++ b/src/btop_draw.h @@ -16,63 +16,32 @@ indent = tab tab-size = 4 */ - +#pragma once #include #include -#include -#include #include -#include -#include -#include #include #include -#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 superscript = { "⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹" }; - - const vector graph_up = { - " ", "⢀", "⢠", "⢰", "⢸", - "⡀", "⣀", "⣠", "⣰", "⣸", - "⡄", "⣄", "⣤", "⣴", "⣼", - "⡆", "⣆", "⣦", "⣶", "⣾", - "⡇", "⣇", "⣧", "⣷", "⣿" - }; - - const vector 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 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> graphs = { {true, {}}, {false, {}}}; //* Create two representations of the graph to switch between to represent two values for each braille character - void _create(const vector& 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 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 - 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& data, int data_offset); public: //* Set graph options and initialize with data - void operator()(int width, int height, string color_gradient, const vector& 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& 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 and return string representation of graph - string& operator()(const vector& 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& 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 \ No newline at end of file +} \ No newline at end of file diff --git a/src/btop_input.h b/src/btop_input.h index 7ba087b..5d694e8 100644 --- a/src/btop_input.h +++ b/src/btop_input.h @@ -16,8 +16,7 @@ indent = tab tab-size = 4 */ -#ifndef _btop_input_included_ -#define _btop_input_included_ +#pragma once #include #include @@ -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 \ No newline at end of file +} \ No newline at end of file diff --git a/src/btop_linux.cpp b/src/btop_linux.cpp new file mode 100644 index 0000000..0541752 --- /dev/null +++ b/src/btop_linux.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + + + +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 cache; + unordered_flat_map 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 stop (false); + atomic collecting (false); + vector sort_vector = { + "pid", + "name", + "command", + "threads", + "user", + "memory", + "cpu direct", + "cpu lazy", + }; + + //* Generate process tree list + void _tree_gen(proc_info& cur_proc, vector& in_procs, vector& 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 current_procs; + + //* Collects and sorts process information from /proc, saves to and returns reference to Proc::current_procs; + vector& 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 procs; + vector 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 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 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 \ No newline at end of file diff --git a/src/btop_linux.h b/src/btop_linux.h index 626d1cc..64ea044 100644 --- a/src/btop_linux.h +++ b/src/btop_linux.h @@ -16,82 +16,30 @@ indent = tab tab-size = 4 */ -#ifndef _btop_linux_included_ -#define _btop_linux_included_ +#pragma once #include #include -#include -#include #include -#include -#include -#include -#include #include -#include -#include -#include -#include +using std::string, std::vector, std::atomic; -#include -#include - - - -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 cache; - unordered_flat_map 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 stop (false); - atomic collecting (false); - atomic drawing (false); - vector sort_vector = { - "pid", - "name", - "command", - "threads", - "user", - "memory", - "cpu direct", - "cpu lazy", - }; + extern std::filesystem::path proc_path; + extern size_t numpids; + extern atomic stop; + extern atomic collecting; + extern vector 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& in_procs, vector& 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 current_procs; - + extern vector 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 procs; - vector 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 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 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& 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(); +} \ No newline at end of file diff --git a/src/btop_menu.h b/src/btop_menu.h index d11177a..ce5479f 100644 --- a/src/btop_menu.h +++ b/src/btop_menu.h @@ -16,8 +16,7 @@ indent = tab tab-size = 4 */ -#ifndef _btop_menu_included_ -#define _btop_menu_included_ 1 +#pragma once #include #include @@ -73,7 +72,3 @@ namespace Menu { } - - - -#endif diff --git a/src/btop_theme.cpp b/src/btop_theme.cpp new file mode 100644 index 0000000..9792c87 --- /dev/null +++ b/src/btop_theme.cpp @@ -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 +#include +#include +#include +#include + +#include +#include +#include + +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 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 esc_to_rgb(string c_string){ + array 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 colors; + unordered_flat_map> rgbs; + unordered_flat_map> gradients; + + //* Convert hex color to a array of decimals + array hex_to_dec(string hexa){ + if (hexa.size() > 1){ + hexa.erase(0, 1); + for (auto& c : hexa) if (!isxdigit(c)) return array{-1, -1, -1}; + + if (hexa.size() == 2){ + int h_int = stoi(hexa, 0, 16); + return array{h_int, h_int, h_int}; + } + else if (hexa.size() == 6){ + return array{ + 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& source){ + vector 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{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{-1, -1, -1}; + } + } + } + + //* Generate color gradients from two or three colors, 101 values indexed 0-100 + void generateGradients(){ + gradients.clear(); + array c_gradient; + bool t_to_256 = !Config::getB("truecolor"); + for (auto& [name, source_arr] : rgbs) { + if (!name.ends_with("_start")) continue; + array, 101> dec_arr; + dec_arr[0][0] = -1; + string wname = rtrim(name, "_start"); + array, 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 map + void set(unordered_flat_map 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 + const string& c(string name){ + return colors.at(name); + } + + //* Return array of escape codes for color gradient + const array& g(string name){ + return gradients.at(name); + } + + //* Return array of red, green and blue in decimal for color + const std::array& dec(string name){ + return rgbs.at(name); + } + + robin_hood::unordered_flat_map& test_colors(){ + return colors; + } + robin_hood::unordered_flat_map>& test_gradients(){ + return gradients; + } + +} \ No newline at end of file diff --git a/src/btop_theme.h b/src/btop_theme.h index 637326d..699d416 100644 --- a/src/btop_theme.h +++ b/src/btop_theme.h @@ -16,277 +16,49 @@ indent = tab tab-size = 4 */ -#ifndef _btop_theme_included_ -#define _btop_theme_included_ +#pragma once #include -#include -#include -#include #include -#include -#include +#include -#include -#include - -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 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 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 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 colors; - unordered_flat_map> rgbs; - unordered_flat_map> gradients; - - //* Convert hex color to a array of decimals - array hex_to_dec(string hexa){ - if (hexa.size() > 1){ - hexa.erase(0, 1); - for (auto& c : hexa) if (!isxdigit(c)) return array{-1, -1, -1}; - - if (hexa.size() == 2){ - int h_int = stoi(hexa, 0, 16); - return array{h_int, h_int, h_int}; - } - else if (hexa.size() == 6){ - return array{ - 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& source){ - vector 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{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{-1, -1, -1}; - } - } - } - - //* Generate color gradients from two or three colors, 101 values indexed 0-100 - void generateGradients(){ - gradients.clear(); - array c_gradient; - bool t_to_256 = !Config::getB("truecolor"); - for (auto& [name, source_arr] : rgbs) { - if (!name.ends_with("_start")) continue; - array, 101> dec_arr; - dec_arr[0][0] = -1; - string wname = rtrim(name, "_start"); - array, 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 esc_to_rgb(string c_string); //* Set current theme using map - void set(unordered_flat_map 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 source); //* Return escape code for color - auto& c(string name){ - return colors.at(name); - } + const string& c(string name); //* Return array of escape codes for color gradient - auto& g(string name){ - return gradients.at(name); - } + const std::array& g(string name); //* Return array of red, green and blue in decimal for color - auto& dec(string name){ - return rgbs.at(name); - } + const std::array& dec(string name); -} + //? Testing + robin_hood::unordered_flat_map& test_colors(); + robin_hood::unordered_flat_map>& test_gradients(); -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/src/btop_tools.cpp b/src/btop_tools.cpp new file mode 100644 index 0000000..85e6cbc --- /dev/null +++ b/src/btop_tools.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +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 Units_bit = {"bit", "Kib", "Mib", "Gib", "Tib", "Pib", "Eib", "Zib", "Yib", "Bib", "GEb"}; + const array 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(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(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::system_clock::now().time_since_epoch()).count(); + } + + uint64_t time_ms(){ + return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + } + + uint64_t time_micros(){ + return std::chrono::duration_cast(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 ssplit(const string& str, const char delim){ + vector 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& atom, bool val){ + atom.wait(val); + } + #else + //* Crude implementation of atomic wait for GCC 10 + void atomic_wait(atomic& atom, bool val){ + while (atom.load() == val) sleep_ms(1); + } + #endif + + void atomic_wait_set(atomic& atom, bool val){ + atomic_wait(atom, val); + atom.store(val); + } + +} + +namespace Logger { + using namespace Tools; + namespace { + std::atomic busy (false); + bool first = true; + string tdf = "%Y/%m/%d (%T) | "; + } + + vector 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); + } +} diff --git a/src/btop_tools.h b/src/btop_tools.h index 2c4f178..c9fc091 100644 --- a/src/btop_tools.h +++ b/src/btop_tools.h @@ -16,238 +16,142 @@ indent = tab tab-size = 4 */ -#ifndef _btop_tools_included_ -#define _btop_tools_included_ +#pragma once #include -#include #include -#include -#include -#include -#include -#include -#include #include -#include #include #include -#include +#include -#include -#include -#include -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 , - 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 columns - string r(int x){ return Fx::e + to_string(x) + "C";} + const string r(int x); //* Move cursor left 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::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(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(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 contains value template @@ -262,269 +166,80 @@ namespace Tools { } //* Return current time since epoch in seconds - uint64_t time_s(){ - return std::chrono::duration_cast(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::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::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 from 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 from 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 from 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 at