mirror of
https://github.com/aristocratos/btop.git
synced 2024-09-28 06:11:28 +02:00
Added Proc::_collect_details for process info box collection
This commit is contained in:
parent
3634633e21
commit
ad5864266a
14
Makefile
14
Makefile
@ -55,19 +55,15 @@ distclean: clean
|
|||||||
|
|
||||||
install:
|
install:
|
||||||
@mkdir -p $(DESTDIR)$(PREFIX)/bin
|
@mkdir -p $(DESTDIR)$(PREFIX)/bin
|
||||||
@cp -p bin/btop $(DESTDIR)$(PREFIX)/bin/btop
|
@cp -p $(TARGETDIR)/btop $(DESTDIR)$(PREFIX)/bin/btop
|
||||||
@mkdir -p $(DESTDIR)$(DOCDIR)
|
@mkdir -p $(DESTDIR)$(DOCDIR)
|
||||||
@cp -p README.md $(DESTDIR)$(DOCDIR)
|
@cp -p README.md $(DESTDIR)$(DOCDIR)
|
||||||
@cp -pr themes $(DESTDIR)$(PREFIX)/share/btop
|
@cp -pr themes $(DESTDIR)$(PREFIX)/share/btop
|
||||||
@chmod 755 $(DESTDIR)$(PREFIX)/bin/btop
|
@chmod 755 $(DESTDIR)$(PREFIX)/bin/btop
|
||||||
|
|
||||||
#Set suid bit for btop to root, will make btop run as root regardless of actual user
|
#Set suid bit for btop to root, will make btop run with admin privileges regardless of actual user
|
||||||
su-setuid:
|
su-setuid:
|
||||||
@su --session-command "make as-root-setuid" root
|
@su --session-command "chown root:root $(DESTDIR)$(PREFIX)/bin/btop && chmod 4755 $(DESTDIR)$(PREFIX)/bin/btop" root
|
||||||
|
|
||||||
as-root-setuid:
|
|
||||||
@chown root:root $(DESTDIR)$(PREFIX)/bin/btop
|
|
||||||
@chmod 4755 $(DESTDIR)$(PREFIX)/bin/btop
|
|
||||||
|
|
||||||
uninstall:
|
uninstall:
|
||||||
@rm -rf $(DESTDIR)$(PREFIX)/bin/btop
|
@rm -rf $(DESTDIR)$(PREFIX)/bin/btop
|
||||||
@ -78,8 +74,8 @@ uninstall:
|
|||||||
-include $(OBJECTS:.$(OBJEXT)=.$(DEPEXT))
|
-include $(OBJECTS:.$(OBJEXT)=.$(DEPEXT))
|
||||||
|
|
||||||
#Link
|
#Link
|
||||||
$(TARGET): $(OBJECTS)
|
btop: $(OBJECTS)
|
||||||
$(CXX) -o $(TARGETDIR)/$(TARGET) $^
|
$(CXX) -o $(TARGETDIR)/btop $^
|
||||||
|
|
||||||
#Compile
|
#Compile
|
||||||
$(BUILDDIR)/%.$(OBJEXT): $(SRCDIR)/%.$(SRCEXT)
|
$(BUILDDIR)/%.$(OBJEXT): $(SRCDIR)/%.$(SRCEXT)
|
||||||
|
39
src/btop.cpp
39
src/btop.cpp
@ -16,7 +16,6 @@ indent = tab
|
|||||||
tab-size = 4
|
tab-size = 4
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <list>
|
#include <list>
|
||||||
@ -32,6 +31,7 @@ tab-size = 4
|
|||||||
#include <robin_hood.h>
|
#include <robin_hood.h>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <exception>
|
||||||
|
|
||||||
#include <btop_shared.hpp>
|
#include <btop_shared.hpp>
|
||||||
#include <btop_tools.hpp>
|
#include <btop_tools.hpp>
|
||||||
@ -77,7 +77,6 @@ namespace fs = std::filesystem;
|
|||||||
namespace rng = std::ranges;
|
namespace rng = std::ranges;
|
||||||
using namespace Tools;
|
using namespace Tools;
|
||||||
|
|
||||||
|
|
||||||
namespace Global {
|
namespace Global {
|
||||||
string banner;
|
string banner;
|
||||||
size_t banner_width = 0;
|
size_t banner_width = 0;
|
||||||
@ -89,7 +88,7 @@ namespace Global {
|
|||||||
|
|
||||||
uint64_t start_time;
|
uint64_t start_time;
|
||||||
|
|
||||||
bool quitting = false;
|
atomic<bool> quitting (false);
|
||||||
|
|
||||||
bool arg_tty = false;
|
bool arg_tty = false;
|
||||||
bool arg_low_color = false;
|
bool arg_low_color = false;
|
||||||
@ -277,12 +276,12 @@ int main(int argc, char **argv){
|
|||||||
}
|
}
|
||||||
if (std::error_code ec; not Global::self_path.empty()) {
|
if (std::error_code ec; not Global::self_path.empty()) {
|
||||||
Theme::theme_dir = fs::canonical(Global::self_path / "../share/btop/themes", ec);
|
Theme::theme_dir = fs::canonical(Global::self_path / "../share/btop/themes", ec);
|
||||||
if (ec or access(Theme::theme_dir.c_str(), R_OK) == -1) Theme::theme_dir.clear();
|
if (ec or not fs::is_directory(Theme::theme_dir) or access(Theme::theme_dir.c_str(), R_OK) == -1) Theme::theme_dir.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Theme::theme_dir.empty()) {
|
if (Theme::theme_dir.empty()) {
|
||||||
for (auto theme_path : {"/usr/local/share/btop/themes", "/usr/share/btop/themes"}) {
|
for (auto theme_path : {"/usr/local/share/btop/themes", "/usr/share/btop/themes"}) {
|
||||||
if (fs::exists(fs::path(theme_path)) and access(theme_path, R_OK) != -1) {
|
if (fs::is_directory(fs::path(theme_path)) and access(theme_path, R_OK) != -1) {
|
||||||
Theme::theme_dir = fs::path(theme_path);
|
Theme::theme_dir = fs::path(theme_path);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -465,7 +464,7 @@ int main(int argc, char **argv){
|
|||||||
|
|
||||||
if (false) {
|
if (false) {
|
||||||
|
|
||||||
vector<long long> mydata;
|
deque<long long> mydata;
|
||||||
for (long long i = 0; i <= 100; i++) mydata.push_back(i);
|
for (long long i = 0; i <= 100; i++) mydata.push_back(i);
|
||||||
for (long long i = 100; i >= 0; i--) mydata.push_back(i);
|
for (long long i = 100; i >= 0; i--) mydata.push_back(i);
|
||||||
// mydata.push_back(0);
|
// mydata.push_back(0);
|
||||||
@ -606,6 +605,7 @@ int main(int argc, char **argv){
|
|||||||
string filter;
|
string filter;
|
||||||
string filter_cur;
|
string filter_cur;
|
||||||
string key;
|
string key;
|
||||||
|
vector<Proc::proc_info> plist;
|
||||||
|
|
||||||
int xc;
|
int xc;
|
||||||
for (size_t i : iota(0, (int)Term::height - 19)){
|
for (size_t i : iota(0, (int)Term::height - 19)){
|
||||||
@ -613,14 +613,20 @@ int main(int argc, char **argv){
|
|||||||
greyscale.push_back(Theme::dec_to_color(xc, xc, xc));
|
greyscale.push_back(Theme::dec_to_color(xc, xc, xc));
|
||||||
}
|
}
|
||||||
|
|
||||||
string pbox = Draw::createBox({.x = 1, .y = 10, .width = Term::width, .height = Term::height - 16, .line_color = Theme::c("proc_box"), .title = "testbox", .title2 = "below", .fill = false, .num = 7});
|
string pbox = Draw::createBox({.x = 1, .y = 10, .width = Term::width, .height = Term::height - 18, .line_color = Theme::c("proc_box"), .title = "testbox", .title2 = "below", .fill = false, .num = 7});
|
||||||
pbox += Mv::r(1) + Theme::c("title") + Fx::b + rjust("Pid:", 8) + " " + ljust("Program:", 16) + " " + ljust("Command:", Term::width - 70) + " Threads: " +
|
pbox += Mv::r(1) + Theme::c("title") + Fx::b + rjust("Pid:", 8) + " " + ljust("Program:", 16) + " " + ljust("Command:", Term::width - 70) + " Threads: " +
|
||||||
ljust("User:", 10) + " " + rjust("MemB", 5) + " " + rjust("Cpu%", 14) + "\n" + Fx::reset + Mv::save;
|
ljust("User:", 10) + " " + rjust("MemB", 5) + " " + rjust("Cpu%", 14) + "\n" + Fx::reset + Mv::save;
|
||||||
|
|
||||||
while (key != "q") {
|
while (key != "q") {
|
||||||
timestamp = time_micros();
|
timestamp = time_micros();
|
||||||
tsl = time_ms() + timer;
|
tsl = time_ms() + timer;
|
||||||
auto plist = Proc::collect();
|
try {
|
||||||
|
plist = Proc::collect();
|
||||||
|
}
|
||||||
|
catch (std::exception const& e) {
|
||||||
|
Logger::error("Caught exception in Proc::collect() : "s + e.what());
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
timestamp2 = time_micros();
|
timestamp2 = time_micros();
|
||||||
timestamp = timestamp2 - timestamp;
|
timestamp = timestamp2 - timestamp;
|
||||||
ostring.clear();
|
ostring.clear();
|
||||||
@ -645,21 +651,25 @@ int main(int argc, char **argv){
|
|||||||
cmd_cond = p.cmd.substr(0, std::min(p.cmd.find(' '), p.cmd.size()));
|
cmd_cond = p.cmd.substr(0, std::min(p.cmd.find(' '), p.cmd.size()));
|
||||||
cmd_cond = cmd_cond.substr(std::min(cmd_cond.find_last_of('/') + 1, cmd_cond.size()));
|
cmd_cond = cmd_cond.substr(std::min(cmd_cond.find_last_of('/') + 1, cmd_cond.size()));
|
||||||
}
|
}
|
||||||
ostring += Mv::r(1) + (Config::getB("tty_mode") ? "" : greyscale[lc]) + ljust(p.prefix + to_string(p.pid) + " " + p.name + " " + (not cmd_cond.empty() and cmd_cond != p.name ? "(" + cmd_cond + ")" : ""), Term::width - 40, true) + " "
|
ostring += Mv::r(1) + (Config::getB("tty_mode") ? "" : greyscale[lc]) + ljust(p.prefix + to_string(p.pid) + " " + p.name + " "
|
||||||
|
+ (not cmd_cond.empty() and cmd_cond != p.name ? "(" + cmd_cond + ")" : ""), Term::width - 40, true) + " "
|
||||||
+ rjust(to_string(p.threads), 5) + " " + ljust(p.user, 10) + " " + rjust(floating_humanizer(p.mem, true), 5) + string(11, ' ')
|
+ rjust(to_string(p.threads), 5) + " " + ljust(p.user, 10) + " " + rjust(floating_humanizer(p.mem, true), 5) + string(11, ' ')
|
||||||
+ (p.cpu_p < 10 or p.cpu_p >= 100 ? rjust(to_string(p.cpu_p), 3) + " " : rjust(to_string(p.cpu_p), 4))
|
+ (p.cpu_p < 10 or p.cpu_p >= 100 ? rjust(to_string(p.cpu_p), 3) + " " : rjust(to_string(p.cpu_p), 4))
|
||||||
+ "\n";
|
+ "\n";
|
||||||
}
|
}
|
||||||
if (lc++ > Term::height - 21) break;
|
if (lc++ > Term::height - 23) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (lc++ < Term::height - 19) ostring += Mv::r(1) + string(Term::width - 2, ' ') + "\n";
|
while (lc++ < Term::height - 21) ostring += Mv::r(1) + string(Term::width - 2, ' ') + "\n";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
avgtimes.push_front(timestamp);
|
avgtimes.push_front(timestamp);
|
||||||
if (avgtimes.size() > 30) avgtimes.pop_back();
|
if (avgtimes.size() > 30) avgtimes.pop_back();
|
||||||
cout << pbox << ostring << Fx::reset << "\n" << endl;
|
cout << pbox << ostring << Fx::reset << "\n" << endl;
|
||||||
|
cout << " Details for " << Proc::detailed.entry.name << " (" << Proc::detailed.entry.pid << ") Status: " << Proc::detailed.status << " Elapsed: " << Proc::detailed.elapsed
|
||||||
|
<< " Mem: " << floating_humanizer(Proc::detailed.entry.mem) << " "
|
||||||
|
<< "\n Parent: " << Proc::detailed.parent << " IO in/out: " << Proc::detailed.io_read << "/" << Proc::detailed.io_write << " " << endl;
|
||||||
cout << Mv::to(Term::height - 4, 1) << "Processes call took: " << rjust(to_string(timestamp), 5) << " μs. Average: " <<
|
cout << Mv::to(Term::height - 4, 1) << "Processes call took: " << rjust(to_string(timestamp), 5) << " μs. Average: " <<
|
||||||
rjust(to_string(accumulate(avgtimes.begin(), avgtimes.end(), 0) / avgtimes.size()), 5) << " μs of " << avgtimes.size() <<
|
rjust(to_string(accumulate(avgtimes.begin(), avgtimes.end(), 0) / avgtimes.size()), 5) << " μs of " << avgtimes.size() <<
|
||||||
" samples. Drawing took: " << time_micros() - timestamp2 << " μs.\nNumber of processes: " << Proc::numpids << ". Number in vector: " << plist.size() << ". Run count: " <<
|
" samples. Drawing took: " << time_micros() - timestamp2 << " μs.\nNumber of processes: " << Proc::numpids << ". Number in vector: " << plist.size() << ". Run count: " <<
|
||||||
@ -675,6 +685,7 @@ int main(int argc, char **argv){
|
|||||||
else if (ulen(key) == 1 ) filter.append(key);
|
else if (ulen(key) == 1 ) filter.append(key);
|
||||||
else { key.clear(); continue; }
|
else { key.clear(); continue; }
|
||||||
if (filter != Config::getS("proc_filter")) Config::set("proc_filter", filter);
|
if (filter != Config::getS("proc_filter")) Config::set("proc_filter", filter);
|
||||||
|
key.clear();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
else if (key == "q") break;
|
else if (key == "q") break;
|
||||||
@ -693,6 +704,12 @@ int main(int argc, char **argv){
|
|||||||
else if (key == "r") Config::flip("proc_reversed");
|
else if (key == "r") Config::flip("proc_reversed");
|
||||||
else if (key == "c") Config::flip("proc_per_core");
|
else if (key == "c") Config::flip("proc_per_core");
|
||||||
else if (key == "delete") { filter.clear(); Config::set("proc_filter", filter); }
|
else if (key == "delete") { filter.clear(); Config::set("proc_filter", filter); }
|
||||||
|
else if (key == "d" ) {
|
||||||
|
Config::flip("show_detailed");
|
||||||
|
if (Config::getB("show_detailed")) {
|
||||||
|
Config::set("detailed_pid", (int)plist.at(0).pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
else continue;
|
else continue;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -222,6 +222,8 @@ namespace Config {
|
|||||||
};
|
};
|
||||||
unordered_flat_map<string, int> intsTmp;
|
unordered_flat_map<string, int> intsTmp;
|
||||||
|
|
||||||
|
vector<string> valid_boxes = { "cpu", "mem", "net", "proc" };
|
||||||
|
|
||||||
bool _locked(const string& name){
|
bool _locked(const string& name){
|
||||||
atomic_wait(writelock);
|
atomic_wait(writelock);
|
||||||
if (not write_new and rng::find_if(descriptions, [&name](const auto& a){ return a.at(0) == name; }) != descriptions.end())
|
if (not write_new and rng::find_if(descriptions, [&name](const auto& a){ return a.at(0) == name; }) != descriptions.end())
|
||||||
@ -297,6 +299,13 @@ namespace Config {
|
|||||||
writelock = false;
|
writelock = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool check_boxes(string boxes){
|
||||||
|
for (auto& box : ssplit(boxes)) {
|
||||||
|
if (not v_contains(valid_boxes, box)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void load(fs::path conf_file, vector<string>& load_errors){
|
void load(fs::path conf_file, vector<string>& load_errors){
|
||||||
if (conf_file.empty())
|
if (conf_file.empty())
|
||||||
return;
|
return;
|
||||||
@ -352,6 +361,8 @@ namespace Config {
|
|||||||
load_errors.push_back("Invalid log_level: " + value);
|
load_errors.push_back("Invalid log_level: " + value);
|
||||||
else if (name == "graph_symbol" and not v_contains(valid_graph_symbols, value))
|
else if (name == "graph_symbol" and not v_contains(valid_graph_symbols, value))
|
||||||
load_errors.push_back("Invalid graph symbol identifier: " + value);
|
load_errors.push_back("Invalid graph symbol identifier: " + value);
|
||||||
|
else if (name == "shown_boxes" and not value.empty() and not check_boxes(value))
|
||||||
|
load_errors.push_back("Invalid box name in shown_boxes. Using default.");
|
||||||
else
|
else
|
||||||
strings.at(name) = value;
|
strings.at(name) = value;
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,9 @@ namespace Config {
|
|||||||
|
|
||||||
extern const vector<string> valid_graph_symbols;
|
extern const vector<string> valid_graph_symbols;
|
||||||
|
|
||||||
|
//* Check if string only contains space seperated valid names for boxes
|
||||||
|
bool check_boxes(string boxes);
|
||||||
|
|
||||||
//* Return bool for config key <name>
|
//* Return bool for config key <name>
|
||||||
const bool& getB(string name);
|
const bool& getB(string name);
|
||||||
|
|
||||||
|
@ -25,8 +25,10 @@ tab-size = 4
|
|||||||
#include <btop_draw.hpp>
|
#include <btop_draw.hpp>
|
||||||
#include <btop_config.hpp>
|
#include <btop_config.hpp>
|
||||||
#include <btop_theme.hpp>
|
#include <btop_theme.hpp>
|
||||||
|
#include <btop_shared.hpp>
|
||||||
#include <btop_tools.hpp>
|
#include <btop_tools.hpp>
|
||||||
|
|
||||||
|
|
||||||
using std::round, std::views::iota, std::string_literals::operator""s, std::clamp, std::array, std::floor;
|
using std::round, std::views::iota, std::string_literals::operator""s, std::clamp, std::array, std::floor;
|
||||||
|
|
||||||
namespace rng = std::ranges;
|
namespace rng = std::ranges;
|
||||||
@ -98,7 +100,6 @@ namespace Draw {
|
|||||||
|
|
||||||
using namespace Tools;
|
using namespace Tools;
|
||||||
|
|
||||||
//* Create a box using values from a BoxConf struct and return as a string
|
|
||||||
string createBox(BoxConf c){
|
string createBox(BoxConf c){
|
||||||
string out;
|
string out;
|
||||||
string lcolor = (c.line_color.empty()) ? Theme::c("div_line") : c.line_color;
|
string lcolor = (c.line_color.empty()) ? Theme::c("div_line") : c.line_color;
|
||||||
@ -137,7 +138,7 @@ namespace Draw {
|
|||||||
return out + Fx::reset + Mv::to(c.y + 1, c.x + 1);
|
return out + Fx::reset + Mv::to(c.y + 1, c.x + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//* Meter class ------------------------------------------------------------------------------------------------------------>
|
||||||
void Meter::operator()(int width, string color_gradient, bool invert) {
|
void Meter::operator()(int width, string color_gradient, bool invert) {
|
||||||
this->width = width;
|
this->width = width;
|
||||||
this->color_gradient = color_gradient;
|
this->color_gradient = color_gradient;
|
||||||
@ -164,7 +165,8 @@ namespace Draw {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Graph::_create(const vector<long long>& data, int data_offset) {
|
//* Graph class ------------------------------------------------------------------------------------------------------------>
|
||||||
|
void Graph::_create(const deque<long long>& data, int data_offset) {
|
||||||
const bool mult = (data.size() - data_offset > 1);
|
const bool mult = (data.size() - data_offset > 1);
|
||||||
if (mult and (data.size() - data_offset) % 2 != 0) data_offset--;
|
if (mult and (data.size() - data_offset) % 2 != 0) data_offset--;
|
||||||
auto& graph_symbol = Symbols::graph_symbols.at(symbol + '_' + (invert ? "down" : "up"));
|
auto& graph_symbol = Symbols::graph_symbols.at(symbol + '_' + (invert ? "down" : "up"));
|
||||||
@ -203,7 +205,6 @@ namespace Draw {
|
|||||||
graphs[current][horizon] += (height == 1 and result[0] + result[1] == 0) ? Mv::r(1) : graph_symbol[(result[0] * 5 + result[1])];
|
graphs[current][horizon] += (height == 1 and result[0] + result[1] == 0) ? Mv::r(1) : graph_symbol[(result[0] * 5 + result[1])];
|
||||||
}
|
}
|
||||||
if (mult and i > data_offset) last = data_value;
|
if (mult and i > data_offset) last = data_value;
|
||||||
|
|
||||||
}
|
}
|
||||||
last = data_value;
|
last = data_value;
|
||||||
if (height == 1)
|
if (height == 1)
|
||||||
@ -219,8 +220,7 @@ namespace Draw {
|
|||||||
out += Fx::reset;
|
out += Fx::reset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Graph::operator()(int width, int height, string color_gradient, const deque<long long>& data, string symbol, bool invert, bool no_zero, long long max_value, long long offset) {
|
||||||
void Graph::operator()(int width, int height, string color_gradient, const vector<long long>& data, string symbol, bool invert, bool no_zero, long long max_value, long long offset) {
|
|
||||||
graphs[true].clear(); graphs[false].clear();
|
graphs[true].clear(); graphs[false].clear();
|
||||||
this->width = width; this->height = height;
|
this->width = width; this->height = height;
|
||||||
this->invert = invert; this->offset = offset;
|
this->invert = invert; this->offset = offset;
|
||||||
@ -246,8 +246,7 @@ namespace Draw {
|
|||||||
this->_create(data, data_offset);
|
this->_create(data, data_offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string& Graph::operator()(const deque<long long>& data, bool data_same) {
|
||||||
string& Graph::operator()(const vector<long long>& data, bool data_same) {
|
|
||||||
if (data_same) return out;
|
if (data_same) return out;
|
||||||
|
|
||||||
//? Make room for new characters on graph
|
//? Make room for new characters on graph
|
||||||
@ -263,18 +262,38 @@ namespace Draw {
|
|||||||
string& Graph::operator()() {
|
string& Graph::operator()() {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
//*------------------------------------------------------------------------------------------------------------------------->
|
||||||
|
|
||||||
|
void calcSizes(){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Box {
|
namespace Cpu {
|
||||||
|
|
||||||
|
Draw::BoxConf box;
|
||||||
|
string background;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Mem {
|
||||||
|
|
||||||
|
Draw::BoxConf box;
|
||||||
|
string background;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Net {
|
||||||
|
|
||||||
|
Draw::BoxConf box;
|
||||||
|
string background;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Proc {
|
namespace Proc {
|
||||||
|
|
||||||
// Draw::BoxConf box;
|
Draw::BoxConf box;
|
||||||
|
string background;
|
||||||
|
|
||||||
}
|
}
|
@ -21,8 +21,9 @@ tab-size = 4
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <robin_hood.h>
|
#include <robin_hood.h>
|
||||||
|
#include <deque>
|
||||||
|
|
||||||
using std::string, std::vector, robin_hood::unordered_flat_map;
|
using std::string, std::vector, robin_hood::unordered_flat_map, std::deque;
|
||||||
|
|
||||||
namespace Symbols {
|
namespace Symbols {
|
||||||
extern const string h_line;
|
extern const string h_line;
|
||||||
@ -74,30 +75,44 @@ namespace Draw {
|
|||||||
unordered_flat_map<bool, vector<string>> graphs = { {true, {}}, {false, {}}};
|
unordered_flat_map<bool, vector<string>> graphs = { {true, {}}, {false, {}}};
|
||||||
|
|
||||||
//* Create two representations of the graph to switch between to represent two values for each braille character
|
//* Create two representations of the graph to switch between to represent two values for each braille character
|
||||||
void _create(const vector<long long>& data, int data_offset);
|
void _create(const deque<long long>& data, int data_offset);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
//* Set graph options and initialize with data
|
//* Set graph options and initialize with data
|
||||||
void operator()(int width, int height, string color_gradient, const vector<long long>& data, string symbol="default", bool invert=false, bool no_zero=false, long long max_value=0, long long offset=0);
|
void operator()(int width, int height, string color_gradient, const deque<long long>& data, string symbol="default", bool invert=false, bool no_zero=false, long long max_value=0, long long offset=0);
|
||||||
|
|
||||||
//* Add last value from back of <data> and return string representation of graph
|
//* Add last value from back of <data> and return string representation of graph
|
||||||
string& operator()(const vector<long long>& data, bool data_same=false);
|
string& operator()(const deque<long long>& data, bool data_same=false);
|
||||||
|
|
||||||
//* Return string representation of graph
|
//* Return string representation of graph
|
||||||
string& operator()();
|
string& operator()();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//* Calculate sizes of boxes, draw outlines and save to enabled boxes namespaces
|
||||||
|
void calcSizes();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Box {
|
namespace Cpu {
|
||||||
|
|
||||||
|
extern Draw::BoxConf box;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Mem {
|
||||||
|
|
||||||
|
extern Draw::BoxConf box;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Net {
|
||||||
|
|
||||||
|
extern Draw::BoxConf box;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Proc {
|
namespace Proc {
|
||||||
|
|
||||||
// Draw::BoxConf box;
|
extern Draw::BoxConf box;
|
||||||
|
|
||||||
}
|
}
|
@ -66,12 +66,15 @@ namespace Input {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::atomic<bool> interrupt (false);
|
||||||
|
|
||||||
string last = "";
|
string last = "";
|
||||||
|
|
||||||
bool poll(int timeout){
|
bool poll(int timeout){
|
||||||
if (timeout < 1) return cin.rdbuf()->in_avail() > 0;
|
if (timeout < 1) return cin.rdbuf()->in_avail() > 0;
|
||||||
while (timeout > 0) {
|
while (timeout > 0) {
|
||||||
if (cin.rdbuf()->in_avail() > 0) return true;
|
if (cin.rdbuf()->in_avail() > 0) return true;
|
||||||
|
if (interrupt) { interrupt = false; return false; }
|
||||||
sleep_ms(timeout < 10 ? timeout : 10);
|
sleep_ms(timeout < 10 ? timeout : 10);
|
||||||
timeout -= 10;
|
timeout -= 10;
|
||||||
}
|
}
|
||||||
@ -91,7 +94,10 @@ namespace Input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
string wait(){
|
string wait(){
|
||||||
while (cin.rdbuf()->in_avail() < 1) sleep_ms(10);
|
while (cin.rdbuf()->in_avail() < 1) {
|
||||||
|
if (interrupt) { interrupt = false; return string(); }
|
||||||
|
sleep_ms(10);
|
||||||
|
}
|
||||||
return get();
|
return get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ tab-size = 4
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
/* The input functions relies on the following std::cin options being set:
|
/* The input functions relies on the following std::cin options being set:
|
||||||
cin.sync_with_stdio(false);
|
cin.sync_with_stdio(false);
|
||||||
@ -29,6 +30,8 @@ tab-size = 4
|
|||||||
//* Functions and variables for handling keyboard and mouse input
|
//* Functions and variables for handling keyboard and mouse input
|
||||||
namespace Input {
|
namespace Input {
|
||||||
|
|
||||||
|
extern std::atomic<bool> interrupt;
|
||||||
|
|
||||||
//* Last entered key
|
//* Last entered key
|
||||||
extern std::string last;
|
extern std::string last;
|
||||||
|
|
||||||
|
@ -25,7 +25,6 @@ tab-size = 4
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <ranges>
|
#include <ranges>
|
||||||
#include <list>
|
#include <list>
|
||||||
#include <robin_hood.h>
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@ -39,7 +38,7 @@ tab-size = 4
|
|||||||
|
|
||||||
|
|
||||||
using std::string, std::vector, std::ifstream, std::atomic, std::numeric_limits, std::streamsize,
|
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;
|
std::round, std::string_literals::operator""s;
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
namespace rng = std::ranges;
|
namespace rng = std::ranges;
|
||||||
using namespace Tools;
|
using namespace Tools;
|
||||||
@ -101,6 +100,8 @@ namespace Proc {
|
|||||||
size_t depth = 0;
|
size_t depth = 0;
|
||||||
bool collapsed = false;
|
bool collapsed = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
vector<proc_info> current_procs;
|
||||||
unordered_flat_map<size_t, p_cache> cache;
|
unordered_flat_map<size_t, p_cache> cache;
|
||||||
unordered_flat_map<string, string> uid_user;
|
unordered_flat_map<string, string> uid_user;
|
||||||
|
|
||||||
@ -120,6 +121,19 @@ namespace Proc {
|
|||||||
"cpu direct",
|
"cpu direct",
|
||||||
"cpu lazy",
|
"cpu lazy",
|
||||||
};
|
};
|
||||||
|
unordered_flat_map<char, string> proc_states = {
|
||||||
|
{'R', "Running"},
|
||||||
|
{'S', "Sleeping"},
|
||||||
|
{'D', "Waiting"},
|
||||||
|
{'Z', "Zombie"},
|
||||||
|
{'T', "Stopped"},
|
||||||
|
{'t', "Tracing"},
|
||||||
|
{'X', "Dead"},
|
||||||
|
{'x', "Dead"},
|
||||||
|
{'K', "Wakekill"},
|
||||||
|
{'W', "Unknown"},
|
||||||
|
{'P', "Parked"}
|
||||||
|
};
|
||||||
|
|
||||||
detail_container detailed;
|
detail_container detailed;
|
||||||
|
|
||||||
@ -163,42 +177,100 @@ namespace Proc {
|
|||||||
out_procs.at(cur_pos).prefix = " │ "s * cur_depth + (children > 0 ? (cache.at(cur_proc.pid).collapsed ? "[+] " : "[-] ") : " ├─ ");
|
out_procs.at(cur_pos).prefix = " │ "s * cur_depth + (children > 0 ? (cache.at(cur_proc.pid).collapsed ? "[+] " : "[-] ") : " ├─ ");
|
||||||
}
|
}
|
||||||
|
|
||||||
//* Get datiled info for selected process
|
//* Get detailed info for selected process
|
||||||
void _collect_details(proc_info p){
|
void _collect_details(size_t pid, uint64_t uptime, vector<proc_info>& procs, bool is_filtered){
|
||||||
fs::path pid_path = Shared::proc_path / std::to_string(p.pid);
|
fs::path pid_path = Shared::proc_path / std::to_string(pid);
|
||||||
|
|
||||||
|
if (pid != detailed.last_pid) {
|
||||||
|
detailed.last_pid = pid;
|
||||||
|
detailed.cpu_percent.clear();
|
||||||
|
detailed.parent.clear();
|
||||||
|
detailed.io_read.clear();
|
||||||
|
detailed.io_write.clear();
|
||||||
|
detailed.skip_smaps = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//? Copy proc_info for process from proc vector
|
||||||
|
auto p = rng::find(procs, pid, &proc_info::pid);
|
||||||
|
detailed.entry = *p;
|
||||||
|
|
||||||
|
//? Update cpu percent deque for process cpu graph
|
||||||
|
detailed.cpu_percent.push_back(round(detailed.entry.cpu_c));
|
||||||
|
while (detailed.cpu_percent.size() > Term::width) detailed.cpu_percent.pop_front();
|
||||||
|
|
||||||
|
//? Process runtime
|
||||||
|
detailed.elapsed = sec_to_dhms(uptime - (cache.at(pid).cpu_s / Shared::clk_tck));
|
||||||
|
|
||||||
|
//? Get parent process name
|
||||||
|
if (detailed.parent.empty() and cache.contains(detailed.entry.ppid)) detailed.parent = cache.at(detailed.entry.ppid).name;
|
||||||
|
|
||||||
|
//? Expand process status from single char to explanative string
|
||||||
|
detailed.status = (proc_states.contains(detailed.entry.state)) ? proc_states.at(detailed.entry.state) : "Unknown";
|
||||||
|
|
||||||
detailed.entry = p;
|
|
||||||
ifstream d_read;
|
ifstream d_read;
|
||||||
|
string short_str;
|
||||||
|
|
||||||
//* Get RSS mem from smaps
|
//? Try to get RSS mem from proc/[pid]/smaps
|
||||||
if (fs::exists(pid_path / "smaps")) {
|
detailed.memory.clear();
|
||||||
pid_path /= "smaps";
|
if (not detailed.skip_smaps and fs::exists(pid_path / "smaps")) {
|
||||||
d_read.open(pid_path);
|
d_read.open(pid_path / "smaps");
|
||||||
if (d_read.good()) {
|
if (d_read.good()) {
|
||||||
uint64_t rss = 0;
|
uint64_t rss = 0;
|
||||||
string val;
|
|
||||||
try {
|
try {
|
||||||
while (not d_read.eof()) {
|
while (not d_read.eof()) {
|
||||||
d_read.ignore(SSmax, 'R');
|
d_read.ignore(SSmax, 'R');
|
||||||
if (d_read.peek() == 's') {
|
if (d_read.peek() == 's') {
|
||||||
d_read.ignore(SSmax, ':');
|
d_read.ignore(SSmax, ':');
|
||||||
getline(d_read, val, 'k');
|
getline(d_read, short_str, 'k');
|
||||||
rss += stoull(val) << 10;
|
rss += stoull(short_str);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
detailed.entry.mem = rss;
|
if (rss == detailed.entry.mem >> 10)
|
||||||
|
detailed.skip_smaps = true;
|
||||||
|
else
|
||||||
|
detailed.memory = floating_humanizer(rss, false, 1);
|
||||||
}
|
}
|
||||||
catch (std::invalid_argument const&) {}
|
catch (std::invalid_argument const&) {}
|
||||||
catch (std::out_of_range const&) {}
|
catch (std::out_of_range const&) {}
|
||||||
}
|
}
|
||||||
d_read.close();
|
d_read.close();
|
||||||
}
|
}
|
||||||
|
if (detailed.memory.empty()) detailed.memory = floating_humanizer(detailed.entry.mem, false);
|
||||||
|
|
||||||
|
//? Get bytes read and written from proc/[pid]/io
|
||||||
|
if (fs::exists(pid_path / "io")) {
|
||||||
|
d_read.open(pid_path / "io");
|
||||||
|
if (d_read.good()) {
|
||||||
|
try {
|
||||||
|
string name;
|
||||||
|
while (not d_read.eof()) {
|
||||||
|
getline(d_read, name, ':');
|
||||||
|
if (name.ends_with("read_bytes")) {
|
||||||
|
getline(d_read, short_str);
|
||||||
|
detailed.io_read = floating_humanizer(stoull(short_str));
|
||||||
|
}
|
||||||
|
else if (name.ends_with("write_bytes")) {
|
||||||
|
getline(d_read, short_str);
|
||||||
|
detailed.io_write = floating_humanizer(stoull(short_str));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
d_read.ignore(SSmax, '\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (std::invalid_argument const&) {}
|
||||||
|
catch (std::out_of_range const&) {}
|
||||||
|
}
|
||||||
|
d_read.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_filtered) procs.erase(p);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
vector<proc_info> current_procs;
|
//* Collects and sorts process information from /proc
|
||||||
|
vector<proc_info>& collect(bool return_last){
|
||||||
//* Collects and sorts process information from /proc, saves to and returns reference to Proc::current_procs;
|
if (return_last) return current_procs;
|
||||||
vector<proc_info>& collect(){
|
|
||||||
atomic_wait_set(collecting);
|
atomic_wait_set(collecting);
|
||||||
auto& sorting = Config::getS("proc_sorting");
|
auto& sorting = Config::getS("proc_sorting");
|
||||||
auto reverse = Config::getB("proc_reversed");
|
auto reverse = Config::getB("proc_reversed");
|
||||||
@ -210,14 +282,15 @@ namespace Proc {
|
|||||||
ifstream pread;
|
ifstream pread;
|
||||||
string long_string;
|
string long_string;
|
||||||
string short_str;
|
string short_str;
|
||||||
auto uptime = system_uptime();
|
double uptime = system_uptime();
|
||||||
vector<proc_info> procs;
|
vector<proc_info> procs;
|
||||||
procs.reserve((numpids + 10));
|
procs.reserve((numpids + 10));
|
||||||
int npids = 0;
|
int npids = 0;
|
||||||
int cmult = (per_core) ? Global::coreCount : 1;
|
int cmult = (per_core) ? Global::coreCount : 1;
|
||||||
bool got_detailed = false;
|
bool got_detailed = false;
|
||||||
|
bool detailed_filtered = false;
|
||||||
|
|
||||||
//* Update uid_user map if /etc/passwd changed since last run
|
//? Update uid_user map if /etc/passwd changed since last run
|
||||||
if (not Shared::passwd_path.empty() and fs::last_write_time(Shared::passwd_path) != Shared::passwd_time) {
|
if (not Shared::passwd_path.empty() and fs::last_write_time(Shared::passwd_path) != Shared::passwd_time) {
|
||||||
string r_uid, r_user;
|
string r_uid, r_user;
|
||||||
Shared::passwd_time = fs::last_write_time(Shared::passwd_path);
|
Shared::passwd_time = fs::last_write_time(Shared::passwd_path);
|
||||||
@ -254,7 +327,6 @@ namespace Proc {
|
|||||||
return current_procs;
|
return current_procs;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool new_cache = false;
|
|
||||||
string pid_str = d.path().filename();
|
string pid_str = d.path().filename();
|
||||||
if (not isdigit(pid_str[0])) continue;
|
if (not isdigit(pid_str[0])) continue;
|
||||||
|
|
||||||
@ -264,7 +336,6 @@ namespace Proc {
|
|||||||
//* Cache program name, command and username
|
//* Cache program name, command and username
|
||||||
if (not cache.contains(new_proc.pid)) {
|
if (not cache.contains(new_proc.pid)) {
|
||||||
string name, cmd, user;
|
string name, cmd, user;
|
||||||
new_cache = true;
|
|
||||||
pread.open(d.path() / "comm");
|
pread.open(d.path() / "comm");
|
||||||
if (not pread.good()) continue;
|
if (not pread.good()) continue;
|
||||||
getline(pread, name);
|
getline(pread, name);
|
||||||
@ -302,13 +373,14 @@ namespace Proc {
|
|||||||
|
|
||||||
//* Match filter if defined
|
//* Match filter if defined
|
||||||
if (not tree and not filter.empty()
|
if (not tree and not filter.empty()
|
||||||
and not (show_detailed and new_proc.pid == detailed_pid)
|
|
||||||
and pid_str.find(filter) == string::npos
|
and pid_str.find(filter) == string::npos
|
||||||
and cache[new_proc.pid].name.find(filter) == string::npos
|
and cache[new_proc.pid].name.find(filter) == string::npos
|
||||||
and cache[new_proc.pid].cmd.find(filter) == string::npos
|
and cache[new_proc.pid].cmd.find(filter) == string::npos
|
||||||
and cache[new_proc.pid].user.find(filter) == string::npos) {
|
and cache[new_proc.pid].user.find(filter) == string::npos) {
|
||||||
if (new_cache) cache.erase(new_proc.pid);
|
if (show_detailed and new_proc.pid == detailed_pid)
|
||||||
continue;
|
detailed_filtered = true;
|
||||||
|
else
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
new_proc.name = cache[new_proc.pid].name;
|
new_proc.name = cache[new_proc.pid].name;
|
||||||
new_proc.cmd = cache[new_proc.pid].cmd;
|
new_proc.cmd = cache[new_proc.pid].cmd;
|
||||||
@ -356,7 +428,7 @@ namespace Proc {
|
|||||||
}
|
}
|
||||||
case 20: { //? Number of threads
|
case 20: { //? Number of threads
|
||||||
new_proc.threads = stoull(short_str);
|
new_proc.threads = stoull(short_str);
|
||||||
next_x = (new_cache) ? 22 : 24;
|
next_x = (cache[new_proc.pid].cpu_s == 0) ? 22 : 24;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
case 22: { //? Save cpu seconds to cache if missing
|
case 22: { //? Save cpu seconds to cache if missing
|
||||||
@ -386,8 +458,6 @@ namespace Proc {
|
|||||||
|
|
||||||
if (x-offset < 24) continue;
|
if (x-offset < 24) continue;
|
||||||
|
|
||||||
// _parse_smaps(new_proc);
|
|
||||||
|
|
||||||
//? Process cpu usage since last update
|
//? 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;
|
new_proc.cpu_p = round(cmult * 1000 * (cpu_t - cache[new_proc.pid].cpu_t) / (cputimes - old_cputimes)) / 10.0;
|
||||||
|
|
||||||
@ -397,9 +467,7 @@ namespace Proc {
|
|||||||
//? Update cache with latest cpu times
|
//? Update cache with latest cpu times
|
||||||
cache[new_proc.pid].cpu_t = cpu_t;
|
cache[new_proc.pid].cpu_t = cpu_t;
|
||||||
|
|
||||||
//? Update the details info box for process if active
|
if (show_detailed and not got_detailed and new_proc.pid == detailed_pid) {
|
||||||
if (show_detailed and new_proc.pid == detailed_pid) {
|
|
||||||
_collect_details(new_proc);
|
|
||||||
got_detailed = true;
|
got_detailed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -408,8 +476,12 @@ namespace Proc {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (show_detailed and not got_detailed) {
|
//* Update the details info box for process if active
|
||||||
detailed.entry.state = 'X';
|
if (show_detailed and got_detailed) {
|
||||||
|
_collect_details(detailed_pid, round(uptime), procs, detailed_filtered);
|
||||||
|
}
|
||||||
|
else if (show_detailed and not got_detailed) {
|
||||||
|
detailed.status = "Dead";
|
||||||
}
|
}
|
||||||
|
|
||||||
//* Sort processes
|
//* Sort processes
|
||||||
|
@ -22,8 +22,10 @@ tab-size = 4
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <deque>
|
||||||
|
#include <robin_hood.h>
|
||||||
|
|
||||||
using std::string, std::vector;
|
using std::string, std::vector, std::deque, robin_hood::unordered_flat_map;
|
||||||
|
|
||||||
namespace Global {
|
namespace Global {
|
||||||
extern const string Version;
|
extern const string Version;
|
||||||
@ -49,6 +51,9 @@ namespace Proc {
|
|||||||
//? Contains the valid sorting options for processes
|
//? Contains the valid sorting options for processes
|
||||||
extern vector<string> sort_vector;
|
extern vector<string> sort_vector;
|
||||||
|
|
||||||
|
//? Translation from process state char to explanative string
|
||||||
|
extern unordered_flat_map<char, string> proc_states;
|
||||||
|
|
||||||
//* Container for process information
|
//* Container for process information
|
||||||
struct proc_info {
|
struct proc_info {
|
||||||
size_t pid;
|
size_t pid;
|
||||||
@ -65,13 +70,14 @@ namespace Proc {
|
|||||||
//* Container for process info box
|
//* Container for process info box
|
||||||
struct detail_container {
|
struct detail_container {
|
||||||
proc_info entry;
|
proc_info entry;
|
||||||
string elapsed, parent, status, io_read, io_write;
|
string elapsed, parent, status, io_read, io_write, memory;
|
||||||
|
size_t last_pid = 0;
|
||||||
|
bool skip_smaps = false;
|
||||||
|
deque<long long> cpu_percent;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern detail_container detailed;
|
extern detail_container detailed;
|
||||||
|
|
||||||
extern vector<proc_info> current_procs;
|
|
||||||
|
|
||||||
//* Collects and sorts process information from /proc, saves and returns reference to Proc::current_procs;
|
//* Collects and sorts process information from /proc, saves and returns reference to Proc::current_procs;
|
||||||
vector<proc_info>& collect();
|
vector<proc_info>& collect(bool return_last=false);
|
||||||
}
|
}
|
@ -363,6 +363,7 @@ namespace Theme {
|
|||||||
themes.push_back("TTY");
|
themes.push_back("TTY");
|
||||||
|
|
||||||
for (const auto& path : { theme_dir, user_theme_dir } ) {
|
for (const auto& path : { theme_dir, user_theme_dir } ) {
|
||||||
|
if (path.empty()) continue;
|
||||||
for (auto& file : fs::directory_iterator(path)){
|
for (auto& file : fs::directory_iterator(path)){
|
||||||
if (file.path().extension() == ".theme" and access(file.path().c_str(), R_OK) != -1) {
|
if (file.path().extension() == ".theme" and access(file.path().c_str(), R_OK) != -1) {
|
||||||
themes.push_back(file.path().c_str());
|
themes.push_back(file.path().c_str());
|
||||||
|
Loading…
Reference in New Issue
Block a user