From db96a20e165b7565a6dad074c949a69f4286e113 Mon Sep 17 00:00:00 2001 From: aristocratos Date: Wed, 1 Sep 2021 21:40:13 +0200 Subject: [PATCH] Added menus and boxes for signal sending --- Makefile | 125 ++++++++------ src/btop.cpp | 61 ++++--- src/btop_config.cpp | 10 +- src/btop_draw.cpp | 106 +++++++----- src/btop_draw.hpp | 5 +- src/btop_input.cpp | 46 ++--- src/btop_menu.cpp | 343 ++++++++++++++++++++++++++++++++++++- src/btop_menu.hpp | 52 +++++- src/btop_shared.hpp | 9 +- src/btop_tools.hpp | 2 +- src/linux/btop_collect.cpp | 57 +++--- 11 files changed, 638 insertions(+), 178 deletions(-) diff --git a/Makefile b/Makefile index b0ee353..c144a0b 100644 --- a/Makefile +++ b/Makefile @@ -1,28 +1,17 @@ #* Btop++ makefile v1.0 -BANNER = \n \033[38;5;196m██████\033[38;5;240m╗ \033[38;5;196m████████\033[38;5;240m╗ \033[38;5;196m██████\033[38;5;240m╗ \033[38;5;196m██████\033[38;5;240m╗\n \033[38;5;160m██\033[38;5;239m╔══\033[38;5;160m██\033[38;5;239m╗╚══\033[38;5;160m██\033[38;5;239m╔══╝\033[38;5;160m██\033[38;5;239m╔═══\033[38;5;160m██\033[38;5;239m╗\033[38;5;160m██\033[38;5;239m╔══\033[38;5;160m██\033[38;5;239m╗ \033[38;5;160m██\033[38;5;239m╗ \033[38;5;160m██\033[38;5;239m╗\n \033[38;5;124m██████\033[38;5;238m╔╝ \033[38;5;124m██\033[38;5;238m║ \033[38;5;124m██\033[38;5;238m║ \033[38;5;124m██\033[38;5;238m║\033[38;5;124m██████\033[38;5;238m╔╝ \033[38;5;124m██████\033[38;5;238m╗\033[38;5;124m██████\033[38;5;238m╗\n \033[38;5;88m██\033[38;5;237m╔══\033[38;5;88m██\033[38;5;237m╗ \033[38;5;88m██\033[38;5;237m║ \033[38;5;88m██\033[38;5;237m║ \033[38;5;88m██\033[38;5;237m║\033[38;5;88m██\033[38;5;237m╔═══╝ ╚═\033[38;5;88m██\033[38;5;237m╔═╝╚═\033[38;5;88m██\033[38;5;237m╔═╝\n \033[38;5;52m██████\033[38;5;236m╔╝ \033[38;5;52m██\033[38;5;236m║ ╚\033[38;5;52m██████\033[38;5;236m╔╝\033[38;5;52m██\033[38;5;236m║ ╚═╝ ╚═╝\n \033[38;5;235m╚═════╝ ╚═╝ ╚═════╝ ╚═╝ \033[1;3;38;5;240mMakefile v1.0\033[0m +BANNER = \n \033[38;5;196m██████\033[38;5;240m╗ \033[38;5;196m████████\033[38;5;240m╗ \033[38;5;196m██████\033[38;5;240m╗ \033[38;5;196m██████\033[38;5;240m╗\n \033[38;5;160m██\033[38;5;239m╔══\033[38;5;160m██\033[38;5;239m╗╚══\033[38;5;160m██\033[38;5;239m╔══╝\033[38;5;160m██\033[38;5;239m╔═══\033[38;5;160m██\033[38;5;239m╗\033[38;5;160m██\033[38;5;239m╔══\033[38;5;160m██\033[38;5;239m╗ \033[38;5;160m██\033[38;5;239m╗ \033[38;5;160m██\033[38;5;239m╗\n \033[38;5;124m██████\033[38;5;238m╔╝ \033[38;5;124m██\033[38;5;238m║ \033[38;5;124m██\033[38;5;238m║ \033[38;5;124m██\033[38;5;238m║\033[38;5;124m██████\033[38;5;238m╔╝ \033[38;5;124m██████\033[38;5;238m╗\033[38;5;124m██████\033[38;5;238m╗\n \033[38;5;88m██\033[38;5;237m╔══\033[38;5;88m██\033[38;5;237m╗ \033[38;5;88m██\033[38;5;237m║ \033[38;5;88m██\033[38;5;237m║ \033[38;5;88m██\033[38;5;237m║\033[38;5;88m██\033[38;5;237m╔═══╝ ╚═\033[38;5;88m██\033[38;5;237m╔═╝╚═\033[38;5;88m██\033[38;5;237m╔═╝\n \033[38;5;52m██████\033[38;5;236m╔╝ \033[38;5;52m██\033[38;5;236m║ ╚\033[38;5;52m██████\033[38;5;236m╔╝\033[38;5;52m██\033[38;5;236m║ ╚═╝ ╚═╝\n \033[38;5;235m╚═════╝ ╚═╝ ╚═════╝ ╚═╝ \033[1;3;38;5;240mMakefile v1.2\033[0m -BTOP_VERSION := $(shell head -n100 src/btop.cpp 2>/dev/null | grep "Version =" | cut -f2 -d"\"" || echo " unknown") -TIMESTAMP := $(shell date +%s 2>/dev/null || echo "0") +override BTOP_VERSION := $(shell head -n100 src/btop.cpp 2>/dev/null | grep "Version =" | cut -f2 -d"\"" || echo " unknown") +override TIMESTAMP := $(shell date +%s 2>/dev/null || echo "0") PREFIX ?= /usr/local -#? Compiler and Linker -CXX ?= g++ -CXX_VERSION := $(shell $(CXX) -dumpfullversion -dumpversion || echo 0) - -#? Try to make sure we are using GCC/G++ version 11 or later if not instructed to use g++-10 -ifneq ($(CXX),g++-10) - V_MAJOR = $(shell echo $(CXX_VERSION) | cut -f1 -d".") - ifneq ($(shell test $(V_MAJOR) -ge 11; echo $$?),0) - ifeq ($(shell command -v g++-11 >/dev/null; echo $$?),0) - override CXX = g++-11 - endif - endif -endif +#? NOTICE! Manually set PLATFORM and ARCH if not compiling for host system +PLATFORM ?= $(shell uname -s || echo unknown) +ARCH ?= $(shell uname -p || echo unknown) #? Only enable fcf-protection if on x86_64 -ARCH := $(shell uname -p || echo unknown) ifeq ($(ARCH),unknown) ARCH := $(shell uname -m || echo unknown) endif @@ -30,11 +19,34 @@ ifeq ($(ARCH),x86_64) ADDFLAGS = -fcf-protection endif -#? Manually set this to (Linux|FreeBSD|Darwin) if not building for host platform -PLATFORM ?= $(shell uname -s || echo unknown) +#? Make sure PLATFORM Darwin is OSX and not Darwin +ifeq ($(PLATFORM),Darwin) + ifeq ($(shell sw_vers >/dev/null 2>&1; echo $$?),0) + PLATFORM = OSX + endif +endif + +#? Compiler and Linker +CXX ?= g++ +override CXX_VERSION := $(shell $(CXX) -dumpfullversion -dumpversion || echo 0) + +#? Try to make sure we are using GCC/G++ version 11 or later if not instructed to use g++-10 +ifneq ($(CXX),g++-10) + V_MAJOR = $(shell echo $(CXX_VERSION) | cut -f1 -d".") + ifneq ($(shell test $(V_MAJOR) -ge 11; echo $$?),0) + ifeq ($(shell command -v g++-11 >/dev/null; echo $$?),0) + override CXX = g++-11 + override CXX_VERSION := $(shell $(CXX) -dumpfullversion -dumpversion || echo 0) + endif + endif +endif #? Use all CPU cores (will only be set if using Make 4.3+) -MAKEFLAGS := --jobs=$(shell getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1) +THREADS := $(shell getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1) +MAKEFLAGS := --jobs=$(THREADS) +ifeq ($(THREADS),1) + override THREADS := auto +endif #? The Directories, Source, Includes, Objects and Binary SRCDIR := src @@ -46,10 +58,10 @@ DEPEXT := d OBJEXT := o #? Flags, Libraries and Includes -REQFLAGS := -std=c++20 -WARNFLAGS := -Wall -Wextra -Wno-stringop-overread -pedantic -pedantic-errors -Wfatal-errors -OPTFLAGS := -O2 -ftree-loop-vectorize -override LDCXXFLAGS += -pthread -D_FORTIFY_SOURCE=2 -D_GLIBCXX_ASSERTIONS -fexceptions -fstack-protector -fstack-clash-protection -flto $(ADDFLAGS) +override REQFLAGS := -std=c++20 +WARNFLAGS := -Wall -Wextra -pedantic -pedantic-errors -Wfatal-errors +OPTFLAGS ?= -O2 -ftree-loop-vectorize -flto=$(THREADS) +LDCXXFLAGS := -pthread -D_FORTIFY_SOURCE=2 -D_GLIBCXX_ASSERTIONS -fexceptions -fstack-protector -fstack-clash-protection $(ADDFLAGS) override CXXFLAGS += $(REQFLAGS) $(LDCXXFLAGS) $(OPTFLAGS) $(WARNFLAGS) override LDFLAGS += $(LDCXXFLAGS) $(OPTFLAGS) $(WARNFLAGS) INC := -I$(INCDIR) -I$(SRCDIR) @@ -58,29 +70,38 @@ SU_GROUP := root SOURCES := $(shell find $(SRCDIR) -maxdepth 1 -type f -name *.$(SRCEXT)) +#? Pull in platform specific source files ifeq ($(PLATFORM),Linux) - SOURCES += $(shell find $(SRCDIR)/linux -type f -name *.$(SRCEXT)) -endif -ifeq ($(PLATFORM),FreeBSD) - SOURCES += $(shell find $(SRCDIR)/freebsd -type f -name *.$(SRCEXT)) -endif -ifeq ($(PLATFORM),Darwin) - SOURCES += $(shell find $(SRCDIR)/osx -type f -name *.$(SRCEXT)) + PLATFORM_DIR = linux +else ifeq ($(PLATFORM),FreeBSD) + PLATFORM_DIR = freebsd +else ifeq ($(PLATFORM),OSX) + PLATFORM_DIR = osx +else +$(error $(shell printf "\033[1;91mERROR: \033[97mUnsupported platform ($(PLATFORM))\033[0m")) endif +SOURCES += $(shell find $(SRCDIR)/$(PLATFORM_DIR) -type f -name *.$(SRCEXT)) + OBJECTS := $(patsubst $(SRCDIR)/%,$(BUILDDIR)/%,$(SOURCES:.$(SRCEXT)=.$(OBJEXT))) #? Default Make -all: msg directories btop +all: pre directories btop -msg: +pre: @printf " $(BANNER)\n" - @printf "\033[1;97mCXX : \033[0m$(CXX) ($(CXX_VERSION))\n" - @printf "\033[1;97mREQFLAGS : \033[0m$(REQFLAGS)\n" - @printf "\033[1;97mWARNFLAGS : \033[0m$(WARNFLAGS)\n" - @printf "\033[1;97mOPTFLAGS : \033[0m$(OPTFLAGS)\n" - @printf "\033[1;97mLDCXXFLAGS : \033[0m$(LDCXXFLAGS)\n" - @printf "\n\033[1;92mBuilding \033[1;91mbtop++ \033[1;93mv$(BTOP_VERSION) \033[0;37mfor \033[1;97m$(PLATFORM) \033[1;96m($(ARCH))\033[0m\n" + @printf "\033[1;92mPLATFORM \033[1;93m?| \033[0m$(PLATFORM)\n" + @printf "\033[1;96mARCH \033[1;93m?| \033[0m$(ARCH)\n" + @printf "\033[1;93mCXX \033[1;93m?| \033[0m$(CXX) \033[1;93m(\033[97m$(CXX_VERSION)\033[93m)\n" + @printf "\033[1;94mTHREADS \033[1;94m:| \033[0m$(THREADS)\n" + @printf "\033[1;92mREQFLAGS \033[1;91m!| \033[0m$(REQFLAGS)\n" + @printf "\033[1;91mWARNFLAGS \033[1;94m:| \033[0m$(WARNFLAGS)\n" + @printf "\033[1;94mOPTFLAGS \033[1;94m:| \033[0m$(OPTFLAGS)\n" + @printf "\033[1;93mLDCXXFLAGS \033[1;94m:| \033[0m$(LDCXXFLAGS)\n" + @printf "\033[1;95mCXXFLAGS \033[1;92m+| \033[0;37m\$$(\033[92mREQFLAGS\033[37m) \$$(\033[93mLDCXXFLAGS\033[37m) \$$(\033[94mOPTFLAGS\033[37m) \$$(\033[91mWARNFLAGS\033[37m)\n" + @printf "\033[1;95mLDFLAGS \033[1;92m+| \033[0;37m\$$(\033[93mLDCXXFLAGS\033[37m) \$$(\033[94mOPTFLAGS\033[37m) \$$(\033[91mWARNFLAGS\033[37m)\n" + + @printf "\n\033[1;92mBuilding btop++ \033[93m(\033[97mv$(BTOP_VERSION)\033[93m)\033[0m\n" help: @printf "\033[1;97mbtop++ makefile\033[0m\n" @@ -96,10 +117,7 @@ help: #? Make the Directories directories: @mkdir -p $(TARGETDIR) - @mkdir -p $(BUILDDIR) - @mkdir -p $(BUILDDIR)/linux - @mkdir -p $(BUILDDIR)/freebsd - @mkdir -p $(BUILDDIR)/osx + @mkdir -p $(BUILDDIR)/$(PLATFORM_DIR) #? Clean only Objects clean: @@ -122,7 +140,7 @@ install: @printf "\033[1;92mInstalling themes to: \033[1;97m$(DESTDIR)$(PREFIX)/share/btop/themes\n" @cp -pr themes $(DESTDIR)$(PREFIX)/share/btop -#? Set suid bit for btop for $SU_USER in SU_GROUP, will make btop run with (root by default) privileges regardless of actual user +#? Set SUID bit for btop as $SU_USER in $SU_GROUP setuid: @printf "\033[1;97mFile: $(DESTDIR)$(PREFIX)/bin/btop\n" @printf "\033[1;92mSetting owner \033[1;97m$(SU_USER):$(SU_GROUP)\033[0m\n" @@ -140,23 +158,28 @@ uninstall: -include $(OBJECTS:.$(OBJEXT)=.$(DEPEXT)) #? Link +.ONESHELL: btop: $(OBJECTS) @sleep 0.1 2>/dev/null || true - @printf "\n\033[1;92mLinking and optimizing binary\033[0m\n" - @$(CXX) -o $(TARGETDIR)/btop $^ $(LDFLAGS) - @printf "\033[1;97m./$(TARGETDIR)/btop ($$(du -ah $(TARGETDIR)/btop | cut -f1)iB)\033[0m\n" - @printf "\n\033[1;92mBuild complete in (\033[1;97m$$(date -d @$$(expr $$(date +%s 2>/dev/null || echo "0") - $(TIMESTAMP)) -u +%Mm:%Ss)\033[1;92m)\033[0m\n" + @TSTAMP=$$(date +%s 2>/dev/null || echo "0") + @printf "\n\033[1;92mLinking and optimizing binary\033[37m...\033[0m\n" + @$(CXX) -o $(TARGETDIR)/btop $^ $(LDFLAGS) || exit 1 + @printf "\033[1;92m-> \033[1;37m$(TARGETDIR)/btop \033[100D\033[35C\033[1;93m(\033[1;97m$$(du -ah $(TARGETDIR)/btop | cut -f1)iB\033[1;93m) \033[92m(\033[97m$$(date -d @$$(expr $$(date +%s 2>/dev/null || echo "0") - $${TSTAMP} 2>/dev/null) -u +%Mm:%Ss 2>/dev/null | sed 's/^00m://' || echo '')\033[92m)\033[0m\n" + @printf "\n\033[1;92mBuild complete in \033[92m(\033[97m$$(date -d @$$(expr $$(date +%s 2>/dev/null || echo "0") - $(TIMESTAMP) 2>/dev/null) -u +%Mm:%Ss 2>/dev/null | sed 's/^00m://' || echo "unknown")\033[92m)\033[0m\n" #? Compile +.ONESHELL: $(BUILDDIR)/%.$(OBJEXT): $(SRCDIR)/%.$(SRCEXT) @sleep 0.1 2>/dev/null || true + @TSTAMP=$$(date +%s 2>/dev/null || echo "0") @printf "\033[1;97mCompiling $<\033[0m\n" - @$(CXX) $(CXXFLAGS) $(INC) -c -o $@ $< - @$(CXX) $(CXXFLAGS) $(INC) -MM $(SRCDIR)/$*.$(SRCEXT) > $(BUILDDIR)/$*.$(DEPEXT) >/dev/null + @$(CXX) $(CXXFLAGS) $(INC) -c -o $@ $< || exit 1 + @$(CXX) $(CXXFLAGS) $(INC) -MM $(SRCDIR)/$*.$(SRCEXT) > $(BUILDDIR)/$*.$(DEPEXT) >/dev/null || exit 1 @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 + @printf "\033[1;92m-> \033[1;37m$@ \033[100D\033[35C\033[1;93m(\033[1;97m$$(du -ah $@ | cut -f1)iB\033[1;93m) \033[92m(\033[97m$$(date -d @$$(expr $$(date +%s 2>/dev/null || echo "0") - $${TSTAMP} 2>/dev/null) -u +%Mm:%Ss 2>/dev/null | sed 's/^00m://' || echo '')\033[92m)\033[0m\n" #? Non-File Targets -.PHONY: all msg help +.PHONY: all msg help pre diff --git a/src/btop.cpp b/src/btop.cpp index 5c84c29..30597e9 100644 --- a/src/btop.cpp +++ b/src/btop.cpp @@ -29,6 +29,7 @@ tab-size = 4 #include #include #include +#include #include #include @@ -43,6 +44,7 @@ using std::string_literals::operator""s, std::to_string, std::future, std::async namespace fs = std::filesystem; namespace rng = std::ranges; using namespace Tools; +using namespace std::chrono_literals; namespace Global { const vector> Banner_src = { @@ -140,12 +142,13 @@ void term_resize(bool force) { Global::resized = true; Runner::stop(); - auto min_size = Term::get_min_size(Config::getS("shown_boxes")); - while (not force) { + auto boxes = Config::getS("shown_boxes"); + auto min_size = Term::get_min_size(boxes); + + while (not force or (Term::width < min_size.at(0) or Term::height < min_size.at(1))) { sleep_ms(100); if (Term::width < min_size.at(0) or Term::height < min_size.at(1)) { - min_size = Term::get_min_size(Config::getS("shown_boxes")); cout << Term::clear << Global::bg_black << Global::fg_white << Mv::to((Term::height / 2) - 2, (Term::width / 2) - 11) << "Terminal size too small:" << Mv::to((Term::height / 2) - 1, (Term::width / 2) - 10) << " Width = " << (Term::width < min_size.at(1) ? Global::fg_red : Global::fg_green) << Term::width @@ -155,6 +158,7 @@ void term_resize(bool force) { << "Width = " << min_size.at(0) << " Height = " << min_size.at(1) << flush; while (not Term::refresh() and not Input::poll()) sleep_ms(10); if (Input::poll() and Input::get() == "q") exit(0); + min_size = Term::get_min_size(boxes); } else if (not Term::refresh()) break; } @@ -167,8 +171,9 @@ void clean_quit(int sig) { if (Global::quitting) return; Global::quitting = true; Runner::stop(); - if (pthread_join(Runner::runner_id, NULL) != 0) + if (pthread_join(Runner::runner_id, NULL) != 0) { Logger::error("Failed to join _runner thread!"); + } Config::write(); Input::clear(); @@ -257,7 +262,6 @@ void banner_gen() { else letter = line[1].substr(i, 3); - // if (tty_mode and letter != "█" and letter != " ") letter = "░"; b_color = (letter == "█") ? fg : bg; if (b_color != oc) Global::banner += b_color; Global::banner += letter; @@ -266,8 +270,9 @@ void banner_gen() { if (++z < Global::Banner_src.size()) Global::banner += Mv::l(ulen(line[1])) + Mv::d(1); } Global::banner += Mv::r(18 - Global::Version.size()) - + (tty_mode ? "\x1b[0;40;37m" : Theme::dec_to_color(0,0,0, lowcolor, "bg") + Theme::dec_to_color(150, 150, 150, lowcolor)) - + Fx::i + "v" + Global::Version + Fx::ui; + + (tty_mode ? "\x1b[0;40;37m" : Theme::dec_to_color(0,0,0, lowcolor, "bg") + + Theme::dec_to_color(150, 150, 150, lowcolor)) + + Fx::i + "v" + Global::Version + Fx::ui; } bool update_clock() { @@ -281,11 +286,17 @@ bool update_clock() { }; static time_t c_time = 0; static size_t clock_len = 0; + static string old_clock; + string new_clock; if (auto n_time = time(NULL); n_time == c_time) return false; - else + else { c_time = n_time; + new_clock = Tools::strf_time(clock_format); + if (new_clock == old_clock) return false; + old_clock = new_clock; + } auto& out = Global::clock; const auto& cpu_bottom = Config::getB("cpu_bottom"); @@ -294,7 +305,7 @@ bool update_clock() { const auto& width = Cpu::width; const auto& title_left = (cpu_bottom ? Symbols::title_left_down : Symbols::title_left); const auto& title_right = (cpu_bottom ? Symbols::title_right_down : Symbols::title_right); - string new_clock = clock_format; + for (const auto& [c_format, replacement] : clock_custom_format) { if (s_contains(new_clock, c_format)) { @@ -310,11 +321,11 @@ bool update_clock() { } - new_clock = uresize(Tools::strf_time(new_clock), std::max(0, width - 56)); + new_clock = uresize(new_clock, std::max(0, width - 56)); out.clear(); if (new_clock.size() != clock_len) { - if (not Global::resized) out = Mv::to(y, x+(width / 2)-(clock_len / 2)) + Fx::ub + Theme::c("cpu_box") + Symbols::h_line * clock_len; + if (not Global::resized and clock_len > 0) out = Mv::to(y, x+(width / 2)-(clock_len / 2)) + Fx::ub + Theme::c("cpu_box") + Symbols::h_line * clock_len; clock_len = new_clock.size(); } @@ -483,7 +494,7 @@ namespace Runner { else if (not proc.valid()) throw std::runtime_error("Proc::collect() future not valid."); - else if (proc.wait_for(std::chrono::microseconds(10)) == future_status::ready) { + else if (proc.wait_for(10us) == future_status::ready) { try { if (Global::debug) debug_timer("proc", draw_begin); @@ -511,7 +522,7 @@ namespace Runner { else if (not net.valid()) throw std::runtime_error("Net::collect() future not valid."); - else if (net.wait_for(ZeroSec) == future_status::ready) { + else if (net.wait_for(10us) == future_status::ready) { try { if (Global::debug) debug_timer("net", draw_begin); @@ -539,7 +550,7 @@ namespace Runner { else if (not mem.valid()) throw std::runtime_error("Mem::collect() future not valid."); - else if (mem.wait_for(ZeroSec) == future_status::ready) { + else if (mem.wait_for(10us) == future_status::ready) { try { if (Global::debug) debug_timer("mem", draw_begin); @@ -567,7 +578,7 @@ namespace Runner { else if (not cpu.valid()) throw std::runtime_error("Cpu::collect() future not valid."); - else if (cpu.wait_for(ZeroSec) == future_status::ready) { + else if (cpu.wait_for(10us) == future_status::ready) { try { if (Global::debug) debug_timer("cpu", draw_begin); @@ -696,7 +707,11 @@ int main(int argc, char **argv) { break; } } - if (not Config::conf_dir.empty()) { + if (Config::conf_dir.empty()) { + cout << "WARNING: Could not get path user HOME folder.\n" + << "Make sure $XDG_CONFIG_HOME or $HOME environment variables is correctly set to fix this." << endl; + } + else { if (std::error_code ec; not fs::is_directory(Config::conf_dir) and not fs::create_directories(Config::conf_dir, ec)) { cout << "WARNING: Could not create or access btop config directory. Logging and config saving disabled.\n" << "Make sure $XDG_CONFIG_HOME or $HOME environment variables is correctly set to fix this." << endl; @@ -741,7 +756,7 @@ int main(int argc, char **argv) { } else Logger::set(Config::getS("log_level")); - Logger::info("Logger set to " + Config::getS("log_level")); + Logger::info("Logger set to " + (Global::debug ? "DEBUG" : Config::getS("log_level"))); for (const auto& err_str : load_warnings) Logger::warning(err_str); } @@ -788,7 +803,7 @@ int main(int argc, char **argv) { } else if (not Global::arg_tty and 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"); + Logger::info("Real tty detected: setting 16 color mode and using tty friendly graph symbols"); } //? Platform dependent init and error check @@ -854,7 +869,8 @@ int main(int argc, char **argv) { Draw::calcSizes(); update_clock(); Global::resized = false; - Runner::run("all", true); + if (Menu::active) Menu::process(); + else Runner::run("all", true, true); atomic_wait(Runner::active); } @@ -864,7 +880,7 @@ int main(int argc, char **argv) { } //? Start secondary collect & draw thread at the interval set by config value - if (time_ms() >= future_time) { + if (time_ms() >= future_time and not Global::resized) { Runner::run("all"); update_ms = Config::getI("update_ms"); future_time = time_ms() + update_ms; @@ -884,11 +900,14 @@ int main(int argc, char **argv) { //? Poll for input and process any input detected else if (Input::poll(min(1000ul, future_time - current_time))) { if (not Runner::active) Config::unlock(); - Input::process(Input::get()); + + if (Menu::active) Menu::process(Input::get()); + else Input::process(Input::get()); } //? Break the loop at 1000ms intervals or if input polling was interrupted else break; + } } diff --git a/src/btop_config.cpp b/src/btop_config.cpp index ff6de59..085a252 100644 --- a/src/btop_config.cpp +++ b/src/btop_config.cpp @@ -20,12 +20,13 @@ tab-size = 4 #include #include #include +#include #include #include #include -using std::array, std::atomic; +using std::array, std::atomic, std::string_view; namespace fs = std::filesystem; namespace rng = std::ranges; using namespace Tools; @@ -114,7 +115,8 @@ namespace Config { {"show_cpu_freq", "#* Show CPU frequency."}, - {"clock_format", "#* Draw a clock at top of screen, formatting according to strftime, empty string to disable."}, + {"clock_format", "#* Draw a clock at top of screen, formatting according to strftime, empty string to disable.\n" + "#* Special formatting: /host = hostname | /user = username | /uptime = system uptime"}, {"background_update", "#* Update main ui in background when menus are showing, set this to false if the menus is flickering too much for comfort."}, @@ -184,6 +186,7 @@ namespace Config { {"log_level", "WARNING"}, {"proc_filter", ""}, {"proc_command", ""}, + {"selected_name", ""}, }; unordered_flat_map stringsTmp; @@ -272,6 +275,7 @@ namespace Config { try { if (Proc::shown) { ints.at("selected_pid") = Proc::selected_pid; + strings.at("selected_name") = Proc::selected_name; ints.at("proc_start") = Proc::start; ints.at("proc_selected") = Proc::selected; } @@ -337,7 +341,7 @@ namespace Config { valid_names.push_back(n[0]); string v_string; getline(cread, v_string, '\n'); - if (not v_string.ends_with(Global::Version)) + if (not s_contains(v_string, Global::Version)) write_new = true; while (not cread.eof()) { cread >> std::ws; diff --git a/src/btop_draw.cpp b/src/btop_draw.cpp index 297b00a..cc37ac8 100644 --- a/src/btop_draw.cpp +++ b/src/btop_draw.cpp @@ -27,6 +27,7 @@ tab-size = 4 #include #include #include +#include using std::round, std::views::iota, std::string_literals::operator""s, std::clamp, std::array, std::floor, std::max, std::min, @@ -217,15 +218,13 @@ namespace Draw { //* Meter class ------------------------------------------------------------------------------------------------------------> Meter::Meter() {} - Meter::Meter(const int width, const string& color_gradient, const bool invert) : width(width), color_gradient(color_gradient), invert(invert) { - cache.insert(cache.begin(), 101, ""); - } + Meter::Meter(const int width, const string& color_gradient, const bool invert) : width(width), color_gradient(color_gradient), invert(invert) {} string Meter::operator()(int value) { if (width < 1) return ""; value = clamp(value, 0, 100); if (not cache.at(value).empty()) return cache.at(value); - string& out = cache.at(value); + auto& out = cache.at(value); for (const int& i : iota(1, width + 1)) { int y = round((double)i * 100.0 / width); if (value >= y) @@ -292,11 +291,11 @@ namespace Draw { out += graphs.at(current).at(0); } else { - for (const int& i : iota(0, height)) { - if (i > 0) out += Mv::d(1) + Mv::l(width); + for (const int& i : iota(1, height + 1)) { + if (i > 1) out += Mv::d(1) + Mv::l(width); if (not color_gradient.empty()) - out += (invert) ? Theme::g(color_gradient).at(i * 100 / (height - 1)) : Theme::g(color_gradient).at(100 - (i * 100 / (height - 1))); - out += (invert) ? graphs.at(current).at((height - 1) - i) : graphs.at(current).at(i); + out += (invert) ? Theme::g(color_gradient).at((i - 1) * 100 / (height - 1)) : Theme::g(color_gradient).at(100 - (i * 100 / height)); + out += (invert) ? graphs.at(current).at(height - i) : graphs.at(current).at(i-1); } } if (not color_gradient.empty()) out += Fx::reset; @@ -520,6 +519,7 @@ namespace Mem { int x = 1, y, width = 20, height; int mem_width, disks_width, divider, item_height, mem_size, mem_meter, graph_height, disk_meter; int disks_io_h = 0; + int disks_io_half = 0; bool shown = true, redraw = true; string box; unordered_flat_map mem_meters; @@ -573,45 +573,50 @@ namespace Mem { //? Disk meters and io graphs if (show_disks) { if (show_io_stat or io_mode) { - if (io_mode) - disks_io_h = max((int)floor((double)(height - 2 - disk_ios) / max(1, disk_ios)), (io_graph_combined ? 1 : 2)); - else - disks_io_h = 1; - int half_height = ceil((double)disks_io_h / 2); - unordered_flat_map custom_speeds; - if (not Config::getS("io_graph_speeds").empty()) { - auto split = ssplit(Config::getS("io_graph_speeds")); - for (const auto& entry : split) { - auto vals = ssplit(entry); - if (vals.size() == 2 and mem.disks.contains(vals.at(0)) and isint(vals.at(1))) - custom_speeds[vals.at(0)] = std::stoi(vals.at(1)); + int half_height = 0; + if (io_mode) { + disks_io_h = max((int)floor((double)(height - 2 - (disk_ios * 2)) / max(1, disk_ios)), (io_graph_combined ? 1 : 2)); + half_height = ceil((double)disks_io_h / 2); + + if (not Config::getS("io_graph_speeds").empty()) { + auto split = ssplit(Config::getS("io_graph_speeds")); + for (const auto& entry : split) { + auto vals = ssplit(entry); + if (vals.size() == 2 and mem.disks.contains(vals.at(0)) and isint(vals.at(1))) + custom_speeds[vals.at(0)] = std::stoi(vals.at(1)); + } } } + for (const auto& [name, disk] : mem.disks) { if (disk.io_read.empty()) continue; - long long speed = (custom_speeds.contains(name) ? custom_speeds.at(name) : 10) << 20; - //? Create one combined graph for IO read/write if enabled - if (not io_mode or (io_mode and io_graph_combined)) { - deque combined(disk.io_read.size(), 0); - rng::transform(disk.io_read, disk.io_write, combined.begin(), std::plus()); - io_graphs[name] = Draw::Graph{disks_width - (io_mode ? 0 : 6), disks_io_h, "available", combined, graph_symbol, false, true, speed}; - } - else { - io_graphs[name + "_read"] = Draw::Graph{disks_width, half_height, "free", disk.io_read, graph_symbol, false, true, speed}; - io_graphs[name + "_write"] = Draw::Graph{disks_width, disks_io_h - half_height, "used", disk.io_write, graph_symbol, true, true, speed}; + io_graphs[name + "_activity"] = Draw::Graph{disks_width - 6, 1, "available", disk.io_activity, graph_symbol, false, true}; + + if (io_mode) { + //? Create one combined graph for IO read/write if enabled + long long speed = (custom_speeds.contains(name) ? custom_speeds.at(name) : 10) << 20; + if (io_graph_combined) { + deque combined(disk.io_read.size(), 0); + rng::transform(disk.io_read, disk.io_write, combined.begin(), std::plus()); + io_graphs[name] = Draw::Graph{disks_width - (io_mode ? 0 : 6), disks_io_h, "available", combined, graph_symbol, false, true, speed}; + } + else { + io_graphs[name + "_read"] = Draw::Graph{disks_width, half_height, "free", disk.io_read, graph_symbol, false, true, speed}; + io_graphs[name + "_write"] = Draw::Graph{disks_width, disks_io_h - half_height, "used", disk.io_write, graph_symbol, true, true, speed}; + } } } } - if (disk_meter > 0) { - for (int i = 0; const auto& name : mem.disks_order) { - if (i * 2 > height - 2) break; - disk_meters_used[name] = Draw::Meter{disk_meter, "used"}; - if (cmp_less_equal(mem.disks_order.size() * 3, height - 1)) - disk_meters_free[name] = Draw::Meter{disk_meter, "free"}; - } + + for (int i = 0; const auto& [name, ignored] : mem.disks) { + if (i * 2 > height - 2) break; + disk_meters_used[name] = Draw::Meter{disk_meter, "used"}; + if (cmp_less_equal(mem.disks.size() * 3, height - 1)) + disk_meters_free[name] = Draw::Meter{disk_meter, "free"}; } + out += Mv::to(y, x + width - 6) + Fx::ub + Theme::c("mem_box") + Symbols::title_left + (io_mode ? Fx::b : "") + Theme::c("hi_fg") + 'i' + Theme::c("title") + 'o' + Fx::ub + Theme::c("mem_box") + Symbols::title_right; Input::mouse_mappings["i"] = {y, x + width - 5, 1, 2}; @@ -683,6 +688,7 @@ namespace Mem { const string used_percent = to_string(disk.used_percent); out += Mv::to(y+1+cy, x+1+cx + round((double)disks_width / 2) - round((double)used_percent.size() / 2)) + Theme::c("main_fg") + used_percent + '%'; } + out += Mv::to(y+2+cy++, x+1+cx) + (big_disk ? " IO% " : " IO " + Mv::l(2)) + io_graphs.at(mount + "_activity")(disk.io_activity, redraw or data_same); if (++cy > height - 3) break; if (io_graph_combined) { auto comb_val = disk.io_read.back() + disk.io_write.back(); @@ -721,8 +727,8 @@ namespace Mem { if (big_disk and not human_io.empty()) out += Mv::to(y+1+cy, x+1+cx + round((double)disks_width / 2) - round((double)human_io.size() / 2)) + Theme::c("main_fg") + human_io; if (++cy > height - 3) break; - if (show_io_stat and io_graphs.contains(mount)) { - out += Mv::to(y+1+cy, x+1+cx) + (big_disk ? " IO: " : " IO " + Mv::l(2)) + io_graphs.at(mount)({comb_val}, redraw or data_same); + if (show_io_stat and io_graphs.contains(mount + "_activity")) { + out += Mv::to(y+1+cy, x+1+cx) + (big_disk ? " IO% " : " IO " + Mv::l(2)) + io_graphs.at(mount + "_activity")(disk.io_activity, redraw or data_same); if (not big_disk) out += Mv::to(y+1+cy, x+cx) + Theme::c("main_fg") + human_io; if (++cy > height - 3) break; } @@ -786,6 +792,8 @@ namespace Net { out = box; //? Graphs graphs.clear(); + if (net.bandwidth.at("download").empty() or net.bandwidth.at("upload").empty()) + return out + Fx::reset; graphs["download"] = Draw::Graph{width - b_width - 2, u_graph_height, "download", net.bandwidth.at("download"), graph_symbol, false, true, down_max}; graphs["upload"] = Draw::Graph{width - b_width - 2, d_graph_height, "upload", net.bandwidth.at("upload"), graph_symbol, true, true, up_max}; @@ -851,6 +859,7 @@ namespace Proc { int start, selected, select_max; bool shown = true, redraw = true; int selected_pid = 0; + string selected_name; unordered_flat_map p_graphs; unordered_flat_map p_counters; int counter = 0; @@ -990,7 +999,7 @@ namespace Proc { int mouse_x = d_x + 2; out += Mv::to(d_y, d_x + 1); if (width > 55) { - out += title_left + hi_color + Fx::b + 't' + t_color + "erminate" + Fx::ub + title_right; + out += Fx::ub + title_left + hi_color + Fx::b + 't' + t_color + "erminate" + Fx::ub + title_right; if (alive and selected == 0) Input::mouse_mappings["t"] = {d_y, mouse_x, 1, 9}; mouse_x += 11; } @@ -1121,7 +1130,7 @@ namespace Proc { string cpu_str = (alive ? to_string(detailed.entry.cpu_p) : ""); if (alive) { cpu_str.resize((detailed.entry.cpu_p < 10 or detailed.entry.cpu_p >= 100 ? 3 : 4)); - cpu_str += '%' + Mv::r(1) + (dgraph_width < 20 ? "C" : "Core") + to_string(detailed.entry.cpu_n); + cpu_str += '%'; } out += Mv::to(d_y + 1, dgraph_x + 1) + Fx::ub + detailed_cpu_graph(detailed.cpu_percent, (redraw or data_same or not alive)) + Mv::to(d_y + 1, dgraph_x + 1) + Theme::c("title") + Fx::b + cpu_str; @@ -1168,7 +1177,10 @@ namespace Proc { for (int n=0; auto& p : plist) { if (n++ < start or p.filtered) continue; bool is_selected = (lc + 1 == selected); - if (is_selected) selected_pid = (int)p.pid; + if (is_selected) { + selected_pid = (int)p.pid; + selected_name = p.name; + } //? Update graphs for processes with above 0.0% cpu usage, delete if below 0.1% 10x times const bool has_graph = p_counters.contains(p.pid); @@ -1275,7 +1287,7 @@ namespace Proc { //? Current selection and number of processes string location = to_string(start + selected) + '/' + to_string(numpids); string loc_clear = Symbols::h_line * max(0ul, 9 - location.size()); - out += Mv::to(y + height - 1, x+width - 3 - max(9, (int)location.size())) + Theme::c("proc_box") + loc_clear + out += Mv::to(y + height - 1, x+width - 3 - max(9, (int)location.size())) + Fx::ub + Theme::c("proc_box") + loc_clear + Symbols::title_left_down + Theme::c("title") + Fx::b + location + Fx::ub + Theme::c("proc_box") + Symbols::title_right_down; //? Clear out left over graphs from dead processes at a regular interval @@ -1293,7 +1305,10 @@ namespace Proc { p_counters.compact(); } - if (selected == 0 and selected_pid != 0) selected_pid = 0; + if (selected == 0 and selected_pid != 0) { + selected_pid = 0; + selected_name.clear(); + } redraw = false; return out + Fx::reset; } @@ -1303,6 +1318,7 @@ namespace Proc { namespace Draw { void calcSizes() { atomic_wait(Runner::active); + Config::unlock(); auto& boxes = Config::getS("shown_boxes"); auto& cpu_bottom = Config::getB("cpu_bottom"); auto& mem_below_net = Config::getB("mem_below_net"); @@ -1314,8 +1330,10 @@ namespace Draw { Proc::box.clear(); Global::clock.clear(); Global::overlay.clear(); + if (Menu::active) Menu::redraw = true; Input::mouse_mappings.clear(); + Menu::mouse_mappings.clear(); Cpu::x = Mem::x = Net::x = Proc::x = 1; Cpu::y = Mem::y = Net::y = Proc::y = 1; diff --git a/src/btop_draw.hpp b/src/btop_draw.hpp index f8adf6b..f6e078d 100644 --- a/src/btop_draw.hpp +++ b/src/btop_draw.hpp @@ -20,10 +20,11 @@ tab-size = 4 #include #include +#include #include #include -using std::string, std::vector, robin_hood::unordered_flat_map, std::deque; +using std::string, std::array, std::vector, robin_hood::unordered_flat_map, std::deque; namespace Symbols { const string h_line = "─"; @@ -76,7 +77,7 @@ namespace Draw { int width; string color_gradient; bool invert; - vector cache; + array cache; public: Meter(); Meter(const int width, const string& color_gradient, const bool invert = false); diff --git a/src/btop_input.cpp b/src/btop_input.cpp index 932f561..7f94aba 100644 --- a/src/btop_input.cpp +++ b/src/btop_input.cpp @@ -18,6 +18,7 @@ tab-size = 4 #include #include +#include #include #include @@ -27,7 +28,7 @@ tab-size = 4 #include #include -using std::cin, std::string_literals::operator""s; +using std::cin, std::vector, std::string_literals::operator""s; using namespace Tools; namespace rng = std::ranges; @@ -80,7 +81,10 @@ namespace Input { bool poll(int timeout) { if (timeout < 1) return cin.rdbuf()->in_avail() > 0; while (timeout > 0) { - if (interrupt) return interrupt = false; + if (interrupt) { + interrupt = false; + return false; + } if (cin.rdbuf()->in_avail() > 0) return true; sleep_ms(timeout < 10 ? timeout : 10); timeout -= 10; @@ -178,12 +182,14 @@ namespace Input { if (key.empty()) return; try { auto& filtering = Config::getB("proc_filtering"); - if (not filtering and key == "q") exit(0); //? Global input actions if (not filtering) { bool keep_going = false; - if (is_in(key, "1", "2", "3", "4")) { + if (str_to_lower(key) == "q") { + exit(0); + } + else if (is_in(key, "1", "2", "3", "4")) { atomic_wait(Runner::active); static const array boxes = {"cpu", "mem", "net", "proc"}; Config::toggle_box(boxes.at(std::stoi(key) - 1)); @@ -250,15 +256,9 @@ namespace Input { Config::set("proc_filter", ""s); else if (key == "ö") { - if (Global::overlay.empty()) { - Global::overlay = Mv::to(Term::height / 2, Term::width / 2) + "\x1b[1;32mTESTING"; - Menu::active = true; - } - else { - Global::overlay.clear(); - Menu::active = false; - } - Runner::run("all", true, true); + Menu::menuMask.set(Menu::Menus::SignalSend); + Menu::process(); + return; } else if (key.starts_with("mouse_")) { redraw = false; @@ -321,16 +321,20 @@ namespace Input { if (key == "-" or key == "space") Proc::collapse = pid; no_update = false; } - else if (key == "t") { - Logger::debug(key); + else if (is_in(key, "t", "k") and (Config::getB("show_detailed") or Config::getI("selected_pid") > 0)) { + atomic_wait(Runner::active); + if (Config::getB("show_detailed") and Config::getI("proc_selected") == 0 and Proc::detailed.status == "Dead") return; + Menu::menuMask.set(Menu::SignalSend); + Menu::signalToSend = (key == "t" ? SIGTERM : SIGKILL); + Menu::process(); return; } - else if (key == "k") { - Logger::debug(key); - return; - } - else if (key == "s") { - Logger::debug(key); + else if (key == "s" and (Config::getB("show_detailed") or Config::getI("selected_pid") > 0)) { + atomic_wait(Runner::active); + if (Config::getB("show_detailed") and Config::getI("proc_selected") == 0 and Proc::detailed.status == "Dead") return; + Menu::menuMask.set(Menu::SignalChoose); + Menu::signalToSend = -1; + Menu::process(); return; } else if (is_in(key, "up", "down", "page_up", "page_down", "home", "end")) { diff --git a/src/btop_menu.cpp b/src/btop_menu.cpp index 444d813..4aaa45c 100644 --- a/src/btop_menu.cpp +++ b/src/btop_menu.cpp @@ -16,25 +16,47 @@ indent = tab tab-size = 4 */ -#include #include #include #include +#include +#include +#include #include #include #include #include #include +#include -using std::vector, std::deque, robin_hood::unordered_flat_map, std::array; +using std::deque, robin_hood::unordered_flat_map, std::array, std::views::iota, std::ref; +using namespace Tools; +namespace rng = std::ranges; namespace Menu { atomic active (false); - string output; + string bg; + bool redraw = true; + int currentMenu = -1; + msgBox messageBox; + int signalToSend = 0; + int signalKillRet = 0; - unordered_flat_map mouse_mappings; + const array P_Signals = { + "0", + "SIGHUP", "SIGINT", "SIGQUIT", "SIGILL", + "SIGTRAP", "SIGABRT", "SIGBUS", "SIGFPE", + "SIGKILL", "SIGUSR1", "SIGSEGV", "SIGUSR2", + "SIGPIPE", "SIGALRM", "SIGTERM", "16", "SIGCHLD", + "SIGCONT", "SIGSTOP", "SIGTSTP", "SIGTTIN", + "SIGTTOU", "SIGURG", "SIGXCPU", "SIGXFSZ", + "SIGVTALRM", "SIGPROF", "SIGWINCH", "SIGIO", + "SIGPWR", "SIGSYS" + }; + + unordered_flat_map mouse_mappings; const unordered_flat_map>> menus = { { "options", { @@ -74,4 +96,317 @@ namespace Menu { } } } } }; + msgBox::msgBox() {}; + msgBox::msgBox(int width, int boxtype, vector& content, string title) + : width(width), boxtype(boxtype) { + const auto& tty_mode = Config::getB("tty_mode"); + const auto& rounded = Config::getB("rounded_corners"); + const auto& right_up = (tty_mode or not rounded ? Symbols::right_up : Symbols::round_right_up); + const auto& left_up = (tty_mode or not rounded ? Symbols::left_up : Symbols::round_left_up); + const auto& right_down = (tty_mode or not rounded ? Symbols::right_down : Symbols::round_right_down); + const auto& left_down = (tty_mode or not rounded ? Symbols::left_down : Symbols::round_left_down); + height = content.size() + 7; + x = Term::width / 2 - width / 2; + y = Term::height/2 - height/2; + if (boxtype == 2) selected = 1; + + + button_left = left_up + Symbols::h_line * 6 + Mv::l(7) + Mv::d(2) + left_down + Symbols::h_line * 6 + Mv::l(7) + Mv::u(1) + Symbols::v_line; + button_right = Symbols::v_line + Mv::l(7) + Mv::u(1) + Symbols::h_line * 6 + right_up + Mv::l(7) + Mv::d(2) + Symbols::h_line * 6 + right_down + Mv::u(2); + + box_contents = Draw::createBox(x, y, width, height, Theme::c("hi_fg"), true, title) + Mv::d(1); + for (const auto& line : content) { + box_contents += Mv::save + Mv::r(width / 2 - Fx::uncolor(line).size() / 2) + line + Mv::restore + Mv::d(1); + } + } + + string msgBox::operator()() { + string out; + int pos = width / 2 - (boxtype == 0 ? 6 : 14); + auto& first_color = (selected == 0 ? Theme::c("hi_fg") : Theme::c("div_line")); + out = Mv::d(1) + Mv::r(pos) + Fx::b + first_color + button_left + (selected == 0 ? Theme::c("title") : Theme::c("main_fg") + Fx::ub) + + (boxtype == 0 ? " Ok " : " Yes ") + first_color + button_right; + mouse_mappings["button1"] = Input::Mouse_loc{y + height - 4, x + pos + 1, 3, 12 + (boxtype > 0 ? 1 : 0)}; + if (boxtype > 0) { + auto& second_color = (selected == 1 ? Theme::c("hi_fg") : Theme::c("div_line")); + out += Mv::r(2) + second_color + button_left + (selected == 1 ? Theme::c("title") : Theme::c("main_fg") + Fx::ub) + + " No " + second_color + button_right; + mouse_mappings["button2"] = Input::Mouse_loc{y + height - 4, x + pos + 15 + (boxtype > 0 ? 1 : 0), 3, 12}; + } + return box_contents + out + Fx::reset; + } + + //? Process input + int msgBox::input(string key) { + if (key.empty()) return Invalid; + + if (is_in(key, "escape", "backspace", "q") or key == "button2") { + return No_Esc; + } + else if (key == "button1" or (boxtype == 0 and str_to_upper(key) == "O")) { + return Ok_Yes; + } + else if (is_in(key, "enter", "space")) { + return selected + 1; + } + else if (boxtype == 0) { + return Invalid; + } + else if (str_to_upper(key) == "Y") { + return Ok_Yes; + } + else if (str_to_upper(key) == "N") { + return No_Esc; + } + else if (is_in(key, "right", "tab")) { + if (++selected > 1) selected = 0; + return Select; + } + else if (is_in(key, "left", "shift_tab")) { + if (--selected < 0) selected = 1; + return Select; + } + + return Invalid; + } + + void msgBox::clear() { + box_contents.clear(); + box_contents.shrink_to_fit(); + button_left.clear(); + button_left.shrink_to_fit(); + button_right.clear(); + button_right.shrink_to_fit(); + if (mouse_mappings.contains("button1")) mouse_mappings.erase("button1"); + if (mouse_mappings.contains("button2")) mouse_mappings.erase("button2"); + } + + enum menuReturnCodes { + NoChange, + Changed, + Closed + }; + + int signalChoose(const string& key) { + auto& s_pid = (Config::getB("show_detailed") and Config::getI("selected_pid") == 0 ? Config::getI("detailed_pid") : Config::getI("selected_pid")); + static int x = 0, y = 0, selected_signal = -1; + if (bg.empty()) selected_signal = -1; + auto& out = Global::overlay; + int retval = Changed; + + if (redraw) { + x = Term::width/2 - 40; + y = Term::height/2 - 9; + bg = Draw::createBox(x, y, 80, 18, Theme::c("hi_fg"), true, "signals"); + bg += Mv::to(y+2, x+1) + Theme::c("title") + Fx::b + cjust("Send signal to PID " + to_string(s_pid) + " (" + + uresize((s_pid == Config::getI("detailed_pid") ? Proc::detailed.entry.name : Config::getS("selected_name")), 30) + ")", 78); + } + + if (is_in(key, "escape", "q")) { + return Closed; + } + else if (is_in(key, "enter", "space") and selected_signal >= 0) { + signalKillRet = 0; + if (s_pid < 1) { + signalKillRet = ESRCH; + menuMask.set(SignalReturn); + } + else if (kill(s_pid, selected_signal) != 0) { + signalKillRet = errno; + menuMask.set(SignalReturn); + } + return Closed; + } + else if (key.size() == 1 and isdigit(key.at(0)) and selected_signal < 10) { + selected_signal = std::min(std::stoi((selected_signal < 1 ? key : to_string(selected_signal) + key)), 64); + } + else if (key == "backspace" and selected_signal != -1) { + selected_signal = (selected_signal < 10 ? -1 : selected_signal / 10); + } + else if (key == "up" and selected_signal != 16) { + if (selected_signal < 6) selected_signal += 25; + else { + bool offset = (selected_signal > 16); + selected_signal -= 5; + if (selected_signal <= 16 and offset) selected_signal--; + } + } + else if (key == "down") { + if (selected_signal < 1 or selected_signal == 16) selected_signal = 1; + else if (selected_signal > 26) selected_signal -= 25; + else { + bool offset = (selected_signal < 16); + selected_signal += 5; + if (selected_signal >= 16 and offset) selected_signal++; + if (selected_signal > 31) selected_signal = 31; + } + } + else if (key == "left" and selected_signal > 1 and selected_signal != 16) { + selected_signal--; + if (selected_signal == 16) selected_signal--; + } + else if (key == "right" and selected_signal < 31 and selected_signal != 16) { + selected_signal++; + if (selected_signal == 16) selected_signal++; + } + else { + retval = NoChange; + } + + int cy = y+3; + out = bg + Mv::to(cy++, x+1) + Theme::c("main_fg") + Fx::ub + + rjust("Enter signal number: ", 48) + (selected_signal >= 0 ? to_string(selected_signal) : "") + Fx::bl + "█" + Fx::ubl; + + out += Mv::to(++cy, x+4); + auto sig_str = to_string(selected_signal); + for (int count = 0, i = 0; const auto& sig : P_Signals) { + if (count == 0 or count == 16) { count++; continue; } + if (i++ % 5 == 0) out += Mv::to(++cy, x+4); + if (count == selected_signal) out += Theme::c("selected_bg") + Theme::c("selected_fg") + Fx::b + ljust(to_string(count), 3) + ljust('(' + sig + ')', 12) + Fx::reset; + else out += Theme::c("hi_fg") + ljust(to_string(count), 3) + Theme::c("main_fg") + ljust('(' + sig + ')', 12); + count++; + } + + cy++; + out += Mv::to(++cy, x+1) + Fx::b + rjust("ENTER | ", 35) + Fx::ub + "To send signal."; + out += Mv::to(++cy, x+1) + Fx::b + rjust( "↑ ↓ ← → | ", 35, true) + Fx::ub + "To choose signal."; + out += Mv::to(++cy, x+1) + Fx::b + rjust("ESC or \"q\" | ", 35) + Fx::ub + "To abort."; + + out += Fx::reset; + + + + + + return (redraw ? Changed : retval); + } + + int signalSend(const string& key) { + auto& s_pid = (Config::getB("show_detailed") and Config::getI("selected_pid") == 0 ? Config::getI("detailed_pid") : Config::getI("selected_pid")); + if (s_pid == 0) return Closed; + if (redraw) { + atomic_wait(Runner::active); + auto& p_name = (s_pid == Config::getI("detailed_pid") ? Proc::detailed.entry.name : Config::getS("selected_name")); + vector cont_vec = { + Fx::b + Theme::c("main_fg") + "Send signal: " + Fx::ub + Theme::c("hi_fg") + to_string(signalToSend) + + (signalToSend > 0 and signalToSend <= 32 ? Theme::c("main_fg") + " (" + P_Signals.at(signalToSend) + ')' : ""), + + Fx::b + Theme::c("main_fg") + "To PID: " + Fx::ub + Theme::c("hi_fg") + to_string(s_pid) + Theme::c("main_fg") + " (" + + uresize(p_name, 16) + ')' + Fx::reset, + }; + messageBox = Menu::msgBox{50, 1, cont_vec, (signalToSend > 1 and signalToSend <= 32 and signalToSend != 17 ? P_Signals.at(signalToSend) : "signal")}; + Global::overlay = messageBox(); + } + auto ret = messageBox.input(key); + if (ret == msgBox::Ok_Yes) { + signalKillRet = 0; + if (kill(s_pid, signalToSend) != 0) { + signalKillRet = errno; + menuMask.set(SignalReturn); + } + messageBox.clear(); + return Closed; + } + else if (ret == msgBox::No_Esc) { + messageBox.clear(); + return Closed; + } + else if (ret == msgBox::Select) { + Global::overlay = messageBox(); + return Changed; + } + else if (redraw) { + return Changed; + } + return NoChange; + } + + int signalReturn(const string& key) { + if (redraw) { + vector cont_vec; + cont_vec.push_back(Fx::b + Theme::g("used")[100] + "Failure:" + Theme::c("main_fg") + Fx::ub); + if (signalKillRet == EINVAL) { + cont_vec.push_back("Unsupported signal!" + Fx::reset); + } + else if (signalKillRet == EPERM) { + cont_vec.push_back("Insufficient permissions to send signal!" + Fx::reset); + } + else if (signalKillRet == ESRCH) { + cont_vec.push_back("Process not found!" + Fx::reset); + } + else { + cont_vec.push_back("Unknown error! (errno: " + to_string(signalKillRet) + ')' + Fx::reset); + } + + messageBox = Menu::msgBox{50, 0, cont_vec, "error"}; + Global::overlay = messageBox(); + } + + auto ret = messageBox.input(key); + if (ret == msgBox::Ok_Yes or ret == msgBox::No_Esc) { + messageBox.clear(); + return Closed; + } + else if (redraw) { + return Changed; + } + return NoChange; + } + + int mainMenu(const string& key) { + (void)key; + return NoChange; + } + int optionsMenu(const string& key) { + (void)key; + return NoChange; + } + int helpMenu(const string& key) { + (void)key; + return NoChange; + } + + //* Add menus here and update enum Menus in header + const auto menuFunc = vector{ + ref(signalChoose), + ref(signalSend), + ref(signalReturn), + ref(optionsMenu), + ref(helpMenu), + ref(mainMenu), + }; + bitset<8> menuMask; + + void process(string key) { + if (menuMask.none()) { + Menu::active = false; + Global::overlay.clear(); + Global::overlay.shrink_to_fit(); + bg.clear(); + bg.shrink_to_fit(); + currentMenu = -1; + Runner::run("all", true, true); + return; + } + + if (currentMenu < 0 or not menuMask.test(currentMenu)) { + Menu::active = true; + redraw = true; + for (const auto& i : iota(0, (int)menuMask.size())) { + if (menuMask.test(i)) currentMenu = i; + } + } + + auto retCode = menuFunc.at(currentMenu)(key); + if (retCode == Closed) { + menuMask.reset(currentMenu); + process(); + } + else if (redraw) { + redraw = false; + Runner::run("all", true, true); + } + else if (retCode == Changed) + Runner::run("overlay"); + } } \ No newline at end of file diff --git a/src/btop_menu.hpp b/src/btop_menu.hpp index fc106b7..53f4e71 100644 --- a/src/btop_menu.hpp +++ b/src/btop_menu.hpp @@ -4,7 +4,7 @@ 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 + 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, @@ -20,17 +20,63 @@ tab-size = 4 #include #include +#include +#include #include -using std::string, std::atomic; +using std::string, std::atomic, std::vector, std::bitset; namespace Menu { extern atomic active; extern string output; + extern int signalToSend; + extern bool redraw; - //? line, col, height, width + //? line, col, height, width extern unordered_flat_map mouse_mappings; + //* Creates a message box centered on screen + //? Height of box is determined by size of content vector + //? Boxtypes: 0 = OK button | 1 = YES and NO with YES selected | 2 = Same as 1 but with NO selected + //? Strings in content vector is not checked for box width overflow + class msgBox { + string box_contents, button_left, button_right; + int height = 0, width = 0, boxtype = 0, selected = 0, x = 0, y = 0; + public: + enum msgReturn { + Invalid, + Ok_Yes, + No_Esc, + Select + }; + msgBox(); + msgBox(int width, int boxtype, vector& content, string title); + + //? Draw and return box as a string + string operator()(); + + //? Process input and returns value from enum Ret + int input(string key); + + //? Clears content vector and private strings + void clear(); + }; + + extern bitset<8> menuMask; + + //* Enum for functions in vector menuFuncs + enum Menus { + SignalChoose, + SignalSend, + SignalReturn, + Options, + Help, + Main + }; + + //* Handles redirection of input for menu functions and handles return codes + void process(string key=""); + } diff --git a/src/btop_shared.hpp b/src/btop_shared.hpp index 5f40d0a..1225515 100644 --- a/src/btop_shared.hpp +++ b/src/btop_shared.hpp @@ -116,9 +116,10 @@ namespace Mem { std::filesystem::path stat = ""; int64_t total = 0, used = 0, free = 0; int used_percent = 0, free_percent = 0; - array old_io = {0, 0}; + array old_io = {0, 0, 0}; deque io_read = {}; deque io_write = {}; + deque io_activity = {}; }; struct mem_info { @@ -175,6 +176,7 @@ namespace Proc { extern int select_max; extern atomic detailed_pid; extern int selected_pid, start, selected, collapse, expand; + extern string selected_name; //? Contains the valid sorting options for processes const vector sort_vector = { @@ -208,12 +210,13 @@ namespace Proc { size_t pid = 0; string name = "", cmd = ""; string short_cmd = ""; - size_t threads = 0, name_offset = 0; + size_t threads = 0; + int name_offset = 0; string user = ""; uint64_t mem = 0; double cpu_p = 0.0, cpu_c = 0.0; char state = '0'; - uint64_t cpu_n = 0, p_nice = 0, ppid = 0, cpu_s = 0, cpu_t = 0; + uint64_t p_nice = 0, ppid = 0, cpu_s = 0, cpu_t = 0; string prefix = ""; size_t depth = 0, tree_index = 0; bool collapsed = false, filtered = false; diff --git a/src/btop_tools.hpp b/src/btop_tools.hpp index 5885726..430f9de 100644 --- a/src/btop_tools.hpp +++ b/src/btop_tools.hpp @@ -65,6 +65,7 @@ namespace Fx { //* Return a string with all colors and text styling removed inline string uncolor(const string& s) { return regex_replace(s, color_regex, ""); } + } //* Collection of escape codes and functions for cursor manipulation @@ -129,7 +130,6 @@ namespace Term { namespace Tools { constexpr auto SSmax = std::numeric_limits::max(); - constexpr auto ZeroSec = std::chrono::seconds(0); extern atomic active_locks; //* Return number of UTF8 characters in a string (wide=true counts UTF-8 characters with a width > 1 as 2 characters) diff --git a/src/linux/btop_collect.cpp b/src/linux/btop_collect.cpp index fc1963c..254ceb9cf 100644 --- a/src/linux/btop_collect.cpp +++ b/src/linux/btop_collect.cpp @@ -31,8 +31,8 @@ tab-size = 4 #include #include -using std::string, std::vector, std::ifstream, std::atomic, std::numeric_limits, std::streamsize, std::round, std::max, std::min, - std::clamp, std::string_literals::operator""s, std::cmp_equal, std::cmp_less, std::cmp_greater; +using std::ifstream, std::numeric_limits, std::streamsize, std::round, std::max, std::min; +using std::clamp, std::string_literals::operator""s, std::cmp_equal, std::cmp_less, std::cmp_greater; namespace fs = std::filesystem; namespace rng = std::ranges; using namespace Tools; @@ -73,8 +73,8 @@ namespace Cpu { unordered_flat_map core_mapping; } -namespace Net { - uint64_t timestamp = 0; +namespace Mem { + double old_uptime; } namespace Shared { @@ -136,12 +136,9 @@ namespace Shared { Cpu::core_mapping = Cpu::get_core_mapping(); //? Init for namespace Mem + Mem::old_uptime = system_uptime(); Mem::collect(); - //? Init for namespace Net - Net::timestamp = time_ms(); - Net::collect(); - } } @@ -545,7 +542,6 @@ namespace Mem { int disk_ios = 0; vector last_found; - mem_info current_mem {}; auto collect(const bool no_update) -> mem_info& { @@ -614,6 +610,7 @@ namespace Mem { //? Get disks stats if (show_disks) { + double uptime = system_uptime(); try { auto& disks_filter = Config::getS("disks_filter"); bool filter_exclude = false; @@ -753,7 +750,7 @@ namespace Mem { if (not is_in(name, "/", "swap")) mem.disks_order.push_back(name); //? Get disks IO - int64_t sectors_read, sectors_write; + int64_t sectors_read, sectors_write, io_ticks; disk_ios = 0; for (auto& [ignored, disk] : disks) { if (disk.stat.empty() or access(disk.stat.c_str(), R_OK) != 0) continue; @@ -777,9 +774,19 @@ namespace Mem { disk.io_write.push_back(max(0l, (sectors_write - disk.old_io.at(1)) * 512)); disk.old_io.at(1) = sectors_write; while (cmp_greater(disk.io_write.size(), width * 2)) disk.io_write.pop_front(); + + for (int i = 0; i < 2; i++) { diskread >> std::ws; diskread.ignore(SSmax, ' '); } + diskread >> io_ticks; + if (disk.io_activity.empty()) + disk.io_activity.push_back(0); + else + disk.io_activity.push_back(clamp((long)round((double)(io_ticks - disk.old_io.at(2)) / (uptime - old_uptime) / 10), 0l, 100l)); + disk.old_io.at(2) = io_ticks; + while (cmp_greater(disk.io_activity.size(), width * 2)) disk.io_activity.pop_front(); } diskread.close(); } + old_uptime = uptime; } catch (const std::exception& e) { Logger::warning("Error in Mem::collect() : " + (string)e.what()); @@ -800,6 +807,7 @@ namespace Net { unordered_flat_map graph_max = { {"download", {}}, {"upload", {}} }; unordered_flat_map> max_count = { {"download", {}}, {"upload", {}} }; bool rescale = true; + uint64_t timestamp = 0; //* RAII wrapper for getifaddrs class getifaddr_wrapper { @@ -824,6 +832,7 @@ namespace Net { if (if_wrap.status != 0) { errors++; Logger::error("Net::collect() -> getifaddrs() failed with id " + to_string(if_wrap.status)); + redraw = true; return empty_net; } int family = 0; @@ -931,7 +940,9 @@ namespace Net { break; } //? If no interface is connected set to first available - if (selected_iface.empty()) selected_iface = sorted_interfaces.at(0); + if (selected_iface.empty() and not sorted_interfaces.empty()) selected_iface = sorted_interfaces.at(0); + else if (sorted_interfaces.empty()) return empty_net; + } } @@ -1152,7 +1163,7 @@ namespace Proc { const auto& tree = Config::getB("proc_tree"); const auto& show_detailed = Config::getB("show_detailed"); const size_t detailed_pid = Config::getI("detailed_pid"); - const bool should_filter = current_filter != filter; + bool should_filter = current_filter != filter; if (should_filter) current_filter = filter; const bool sorted_change = (sorting != current_sort or reverse != current_rev or should_filter); if (sorted_change) { @@ -1174,6 +1185,8 @@ namespace Proc { } //* ---------------------------------------------Collection start---------------------------------------------- else { + should_filter = true; + //? 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) != passwd_time) { string r_uid, r_user; @@ -1269,20 +1282,18 @@ namespace Proc { const auto& offset = new_proc.name_offset; short_str.clear(); - size_t x = 0, next_x = 3; + int x = 0, next_x = 3; uint64_t cpu_t = 0; try { - while (x < 40) { - while (pread.good() and ++x < next_x + offset) { - pread.ignore(SSmax, ' '); - } - if (pread.bad()) break; - + for (;;) { + while (++x < next_x + offset) pread.ignore(SSmax, ' '); getline(pread, short_str, ' '); + if (not pread.good()) break; switch (x-offset) { case 3: //? Process state new_proc.state = short_str.at(0); + if (new_proc.ppid != 0) next_x = 14; continue; case 4: //? Parent pid new_proc.ppid = stoull(short_str); @@ -1313,18 +1324,14 @@ namespace Proc { continue; case 24: //? RSS memory (can be inaccurate, but parsing smaps increases total cpu usage by ~20x) new_proc.mem = stoull(short_str) * Shared::pageSize; - next_x = 39; - continue; - case 39: //? CPU number last executed on - new_proc.cpu_n = stoull(short_str); - x++; - break; } + break; } } catch (const std::invalid_argument&) { continue; } catch (const std::out_of_range&) { continue; } + pread.close(); if (x-offset < 24) continue;