/* Copyright 2021 Aristocratos (jakob@qvantnet.com) Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. indent = tab tab-size = 4 */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(__linux__) #define LINUX #elif defined(__unix__) or not defined(__APPLE__) and defined(__MACH__) #include #if defined(BSD) #error BSD support not yet implemented! #endif #elif defined(__APPLE__) and defined(__MACH__) #include #if TARGET_OS_MAC == 1 #define OSX #error OSX support not yet implemented! #endif #else #error Platform not supported! #endif using std::string, std::string_view, std::vector, std::atomic, std::endl, std::cout, std::min, std::flush, std::endl; using std::string_literals::operator""s, std::to_string, std::future, std::async, std::bitset, std::future_status; namespace fs = std::filesystem; namespace rng = std::ranges; using namespace Tools; namespace Global { const vector> Banner_src = { {"#E62525", "██████╗ ████████╗ ██████╗ ██████╗"}, {"#CD2121", "██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗ ██╗ ██╗"}, {"#B31D1D", "██████╔╝ ██║ ██║ ██║██████╔╝ ██████╗██████╗"}, {"#9A1919", "██╔══██╗ ██║ ██║ ██║██╔═══╝ ╚═██╔═╝╚═██╔═╝"}, {"#801414", "██████╔╝ ██║ ╚██████╔╝██║ ╚═╝ ╚═╝"}, {"#000000", "╚═════╝ ╚═╝ ╚═════╝ ╚═╝"}, }; const string Version = "0.5.0"; int coreCount; string banner; size_t banner_width = 0; string overlay; string clock; fs::path self_path; string exit_error_msg; atomic thread_exception (false); bool debuginit = false; bool debug = false; bool utf_force = false; uint64_t start_time; atomic resized (false); atomic resizing (0); atomic quitting (false); bool arg_tty = false; bool arg_low_color = false; } //* A simple argument parser void argumentParser(const int& argc, char **argv) { for(int i = 1; i < argc; i++) { const string argument = argv[i]; if (is_in(argument, "-h", "--help")) { cout << "usage: btop [-h] [-v] [-/+t] [--debug]\n\n" << "optional arguments:\n" << " -h, --help show this help message and exit\n" << " -v, --version show version info and exit\n" << " -lc, --low-color disable truecolor, converts 24-bit colors to 256-color\n" << " -t, --tty_on force (ON) tty mode, max 16 colors and tty friendly graph symbols\n" << " +t, --tty_off force (OFF) tty mode\n" << " --utf-foce force start even if no UTF-8 locale was detected\n" << " --debug start in DEBUG mode: shows microsecond timer for information collect\n" << " and screen draw functions and sets loglevel to DEBUG\n" << endl; exit(0); } if (is_in(argument, "-v", "--version")) { cout << "btop version: " << Global::Version << endl; exit(0); } else if (is_in(argument, "-lc", "--low-color")) { Global::arg_low_color = true; } else if (is_in(argument, "-t", "--tty_on")) { Config::set("tty_mode", true); Global::arg_tty = true; } else if (is_in(argument, "+t", "--tty_off")) { Config::set("tty_mode", false); Global::arg_tty = true; } else if (argument == "--utf-force") Global::utf_force = true; else if (argument == "--debug") Global::debug = true; else { cout << " Unknown argument: " << argument << "\n" << " Use -h or --help for help." << endl; exit(1); } } } //* Handler for SIGWINCH and general resizing events, does nothing if terminal hasn't been resized unless force=true void term_resize(bool force) { if (auto refreshed = Term::refresh(); refreshed or force) { if (force and refreshed) force = false; } else return; auto rez_state = ++Global::resizing; if (rez_state > 1) return; Global::resized = true; Runner::stop(); auto min_size = Term::get_min_size(Config::getS("shown_boxes")); while (not force) { sleep_ms(100); if (rez_state != Global::resizing) rez_state = --Global::resizing; else if (not Term::refresh() and Term::width >= min_size[0] and Term::height >= min_size[1]) break; } Input::interrupt = true; Global::resizing = 0; } //* Exit handler; stops threads, restores terminal and saves config changes void clean_quit(int sig) { if (Global::quitting) return; Global::quitting = true; Runner::stop(); if (pthread_join(Runner::runner_id, NULL) != 0) Logger::error("Failed to join _runner thread!"); if (not Global::exit_error_msg.empty()) { sig = 1; Logger::error(Global::exit_error_msg); std::cerr << "ERROR: " << Global::exit_error_msg << endl; } Config::write(); Input::clear(); Logger::info("Quitting! Runtime: " + sec_to_dhms(time_s() - Global::start_time)); //? Wait for any remaining Tools::atomic_lock destructors to finish for max 1000ms for (int i = 0; Tools::active_locks > 0 and i < 100; i++) { sleep_ms(10); } if (Term::initialized) { Term::restore(); } //? Assume error if still not cleaned up and call quick_exit to avoid a segfault from Tools::atomic_lock destructor if (Tools::active_locks > 0) { quick_exit((sig != -1 ? sig : 0)); } if (sig != -1) exit(sig); } //* Handler for SIGTSTP; stops threads, restores terminal and sends SIGSTOP void _sleep() { Runner::stop(); Term::restore(); std::raise(SIGSTOP); } //* Handler for SIGCONT; re-initialize terminal and force a resize event void _resume() { Term::init(); term_resize(true); } void _exit_handler() { clean_quit(-1); } void _signal_handler(const int sig) { switch (sig) { case SIGINT: clean_quit(0); break; case SIGTSTP: _sleep(); break; case SIGCONT: _resume(); break; case SIGWINCH: term_resize(); break; } } //* Generate the btop++ banner void banner_gen() { Global::banner.clear(); Global::banner_width = 0; string b_color, bg, fg, oc, letter; auto& lowcolor = Config::getB("lowcolor"); auto& tty_mode = Config::getB("tty_mode"); for (size_t z = 0; const auto& line : Global::Banner_src) { if (const auto w = ulen(line[1]); w > Global::banner_width) Global::banner_width = w; if (tty_mode) { fg = (z > 2) ? "\x1b[31m" : "\x1b[91m"; bg = (z > 2) ? "\x1b[90m" : "\x1b[37m"; } else { fg = Theme::hex_to_color(line[0], lowcolor); int bg_i = 120 - z * 12; bg = Theme::dec_to_color(bg_i, bg_i, bg_i, lowcolor); } for (size_t i = 0; i < line[1].size(); i += 3) { if (line[1][i] == ' ') { letter = ' '; i -= 2; } 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; oc = b_color; } 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; } bool update_clock() { const auto& clock_format = Config::getS("clock_format"); if (not Cpu::shown or clock_format.empty()) return false; static const unordered_flat_map clock_custom_format = { {"/user", Tools::username()}, {"/host", Tools::hostname()}, {"/uptime", ""} }; static time_t c_time = 0; static size_t clock_len = 0; if (auto n_time = time(NULL); n_time == c_time) return false; else c_time = n_time; auto& out = Global::clock; const auto& cpu_bottom = Config::getB("cpu_bottom"); const auto& x = Cpu::x; const auto y = (cpu_bottom ? Cpu::y + Cpu::height - 1 : Cpu::y); 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)) { if (c_format == "/uptime") { string upstr = sec_to_dhms(system_uptime()); if (upstr.size() > 8) upstr.resize(upstr.size() - 3); new_clock = s_replace(new_clock, c_format, upstr); } else { new_clock = s_replace(new_clock, c_format, replacement); } } } new_clock = uresize(Tools::strf_time(new_clock), std::max(0, width - 56)); out.clear(); if (new_clock.size() != clock_len) { 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(); } out += Mv::to(y, x+(width / 2)-(clock_len / 2)) + Fx::ub + Theme::c("cpu_box") + title_left + Theme::c("title") + Fx::b + new_clock + Theme::c("cpu_box") + Fx::ub + title_right; return true; } //* Manages secondary thread for collection and drawing of boxes namespace Runner { atomic active (false); atomic stopping (false); atomic waiting (false); //* Setup semaphore for triggering thread to do work #if __GNUC__ < 11 #include sem_t do_work; inline void thread_sem_init() { sem_init(&do_work, 0, 0); } inline void thread_wait() { sem_wait(&do_work); } inline void thread_trigger() { sem_post(&do_work); } #else #include std::binary_semaphore do_work(0); inline void thread_sem_init() { ; } inline void thread_wait() { do_work.acquire(); } inline void thread_trigger() { do_work.release(); } #endif //* RAII wrapper for pthread_mutex locking class thread_lock { pthread_mutex_t& pt_mutex; public: int status; thread_lock(pthread_mutex_t& mtx) : pt_mutex(mtx) { status = pthread_mutex_lock(&pt_mutex); } ~thread_lock() { if (status == 0) pthread_mutex_unlock(&pt_mutex); } }; string output; sigset_t mask; pthread_t runner_id; pthread_mutex_t mtx; const unordered_flat_map box_bits = { {"proc", 0b0000'0001}, {"net", 0b0000'0100}, {"mem", 0b0001'0000}, {"cpu", 0b0100'0000}, }; enum bit_pos { proc_present, proc_running, net_present, net_running, mem_present, mem_running, cpu_present, cpu_running }; enum debug_actions { collect_begin, draw_begin, draw_done }; enum debug_array { collect, draw }; const uint_fast8_t proc_done = 0b0000'0011; const uint_fast8_t net_done = 0b0000'1100; const uint_fast8_t mem_done = 0b0011'0000; const uint_fast8_t cpu_done = 0b1100'0000; string debug_bg; unordered_flat_map> debug_times; struct runner_conf { bitset<8> box_mask; bool no_update; bool force_redraw; string overlay; string clock; }; struct runner_conf current_conf; void debug_timer(const char* name, const int action) { switch (action) { case collect_begin: debug_times[name].at(collect) = time_micros(); return; case draw_begin: debug_times[name].at(draw) = time_micros(); debug_times[name].at(collect) = debug_times[name].at(draw) - debug_times[name].at(collect); debug_times["total"].at(collect) += debug_times[name].at(collect); return; case draw_done: debug_times[name].at(draw) = time_micros() - debug_times[name].at(draw); debug_times["total"].at(draw) += debug_times[name].at(draw); return; } } //? ------------------------------- Secondary thread: async launcher and drawing ---------------------------------- void * _runner(void * _) { (void)_; //? Block all signals in this thread to avoid deadlock from any signal handlers trying to stop this thread sigemptyset(&mask); sigaddset(&mask, SIGINT); sigaddset(&mask, SIGTSTP); sigaddset(&mask, SIGWINCH); sigaddset(&mask, SIGTERM); pthread_sigmask(SIG_BLOCK, &mask, NULL); //? pthread_mutex_lock to lock thread and monitor health from main thread thread_lock pt_lck(mtx); if (pt_lck.status != 0) { Global::exit_error_msg = "Exception in runner thread -> pthread_mutex_lock error id: " + to_string(pt_lck.status); Global::thread_exception = true; Input::interrupt = true; stopping = true; } //* ----------------------------------------------- THREAD LOOP ----------------------------------------------- while (not Global::quitting) { thread_wait(); if (stopping or Global::resized) { continue; } //? Atomic lock used for blocking non-thread safe actions in main thread atomic_lock lck(active); auto& conf = current_conf; //! DEBUG stats if (Global::debug) { debug_times.clear(); debug_times["total"] = {0, 0}; } output.clear(); //* Start collection functions for all boxes in async threads and draw in this thread when finished //? Starting order below based on mean time to finish try { future cpu; future mem; future net; future&> proc; //? Loop until all box flags present in bitmask have been zeroed while (conf.box_mask.count() > 0) { if (stopping) break; //? PROC if (conf.box_mask.test(proc_present)) { if (not conf.box_mask.test(proc_running)) { if (Global::debug) debug_timer("proc", collect_begin); //? Start async collect proc = async(Proc::collect, conf.no_update); conf.box_mask.set(proc_running); } 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) { try { if (Global::debug) debug_timer("proc", draw_begin); //? Draw box output += Proc::draw(proc.get(), conf.force_redraw, conf.no_update); if (Global::debug) debug_timer("proc", draw_done); } catch (const std::exception& e) { throw std::runtime_error("Proc:: -> " + (string)e.what()); } conf.box_mask ^= proc_done; } } //? NET if (conf.box_mask.test(net_present)) { if (not conf.box_mask.test(net_running)) { if (Global::debug) debug_timer("net", collect_begin); //? Start async collect net = async(Net::collect, conf.no_update); conf.box_mask.set(net_running); } else if (not net.valid()) throw std::runtime_error("Net::collect() future not valid."); else if (net.wait_for(ZeroSec) == future_status::ready) { try { if (Global::debug) debug_timer("net", draw_begin); //? Draw box output += Net::draw(net.get(), conf.force_redraw, conf.no_update); if (Global::debug) debug_timer("net", draw_done); } catch (const std::exception& e) { throw std::runtime_error("Net:: -> " + (string)e.what()); } conf.box_mask ^= net_done; } } //? MEM if (conf.box_mask.test(mem_present)) { if (not conf.box_mask.test(mem_running)) { if (Global::debug) debug_timer("mem", collect_begin); //? Start async collect mem = async(Mem::collect, conf.no_update); conf.box_mask.set(mem_running); } else if (not mem.valid()) throw std::runtime_error("Mem::collect() future not valid."); else if (mem.wait_for(ZeroSec) == future_status::ready) { try { if (Global::debug) debug_timer("mem", draw_begin); //? Draw box output += Mem::draw(mem.get(), conf.force_redraw, conf.no_update); if (Global::debug) debug_timer("mem", draw_done); } catch (const std::exception& e) { throw std::runtime_error("Mem:: -> " + (string)e.what()); } conf.box_mask ^= mem_done; } } //? CPU if (conf.box_mask.test(cpu_present)) { if (not conf.box_mask.test(cpu_running)) { if (Global::debug) debug_timer("cpu", collect_begin); //? Start async collect cpu = async(Cpu::collect, conf.no_update); conf.box_mask.set(cpu_running); } else if (not cpu.valid()) throw std::runtime_error("Cpu::collect() future not valid."); else if (cpu.wait_for(ZeroSec) == future_status::ready) { try { if (Global::debug) debug_timer("cpu", draw_begin); //? Draw box output += Cpu::draw(cpu.get(), conf.force_redraw, conf.no_update); if (Global::debug) debug_timer("cpu", draw_done); } catch (const std::exception& e) { throw std::runtime_error("Cpu:: -> " + (string)e.what()); } conf.box_mask ^= cpu_done; } } } } catch (const std::exception& e) { Global::exit_error_msg = "Exception in runner thread -> " + (string)e.what(); Global::thread_exception = true; Input::interrupt = true; stopping = true; } if (stopping) { continue; } //! DEBUG stats --> if (Global::debug) { output += debug_bg + Theme::c("title") + Fx::b + ljust(" Box", 9) + ljust("Collect μs", 12, true) + ljust("Draw μs", 9, true) + Theme::c("main_fg") + Fx::ub; for (const string name : {"cpu", "mem", "net", "proc", "total"}) { if (not debug_times.contains(name)) debug_times[name] = {0,0}; const auto& [time_collect, time_draw] = debug_times.at(name); if (name == "total") output += Fx::b; output += Mv::l(29) + Mv::d(1) + ljust(name, 8) + ljust(to_string(time_collect), 12) + ljust(to_string(time_draw), 9); } } //? If overlay isn't empty, print output without color and then print overlay on top cout << Term::sync_start << (conf.overlay.empty() ? output + conf.clock : Fx::ub + Theme::c("inactive_fg") + Fx::uncolor(output + conf.clock) + conf.overlay) << Term::sync_end << flush; } //* ----------------------------------------------- THREAD LOOP ----------------------------------------------- pthread_exit(NULL); } //? ------------------------------------------ Secondary thread end ----------------------------------------------- //* Runs collect and draw in a secondary thread, unlocks and locks config to update cached values void run(const string& box, const bool no_update, const bool force_redraw) { atomic_lock lck(waiting); atomic_wait(active); if (stopping or Global::resized) return; if (box == "overlay") { cout << Term::sync_start << Global::overlay << Term::sync_end << flush; } else if (box == "clock") { cout << Term::sync_start << Global::clock << Term::sync_end << flush; } else if (box.empty() and Config::current_boxes.empty()) { cout << Term::sync_start << Term::clear + Mv::to(10, 10) << "No boxes shown!" << Term::sync_end << flush; } else { Config::unlock(); Config::lock(); //? Setup bitmask for selected boxes instead of parsing strings in _runner thread loop bitset<8> box_mask; for (const auto& box : (box == "all" ? Config::current_boxes : vector{box})) { box_mask |= box_bits.at(box); } current_conf = {box_mask, no_update, force_redraw, Global::overlay, Global::clock}; thread_trigger(); //? Wait for _runner thread to be active before returning for (int i = 0; not active and i < 10; i++) sleep_ms(1); } } //* Stops any work being done in runner thread and checks for thread errors void stop() { stopping = true; int ret = pthread_mutex_trylock(&mtx); if (ret != EBUSY and not Global::quitting) { if (active) active = false; Global::exit_error_msg = "Runner thread died unexpectedly!"; exit(1); } else if (ret == EBUSY) { atomic_wait(active); thread_trigger(); sleep_ms(1); } stopping = false; } } //* --------------------------------------------- Main starts here! --------------------------------------------------- int main(int argc, char **argv) { //? ------------------------------------------------ INIT --------------------------------------------------------- Global::start_time = time_s(); //? Call argument parser if launched with arguments if (argc > 1) argumentParser(argc, argv); //? Setup signal handlers for CTRL-C, CTRL-Z, resume and terminal resize std::atexit(_exit_handler); std::signal(SIGINT, _signal_handler); std::signal(SIGTSTP, _signal_handler); std::signal(SIGCONT, _signal_handler); std::signal(SIGWINCH, _signal_handler); //? Setup paths for config, log and user themes for (const auto& env : {"XDG_CONFIG_HOME", "HOME"}) { if (getenv(env) != NULL and access(getenv(env), W_OK) != -1) { Config::conf_dir = fs::path(getenv(env)) / (((string)env == "HOME") ? ".config/btop" : "btop"); break; } } if (not Config::conf_dir.empty()) { 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; } else { Config::conf_file = Config::conf_dir / "btop.conf"; Logger::logfile = Config::conf_dir / "btop.log"; Theme::user_theme_dir = Config::conf_dir / "themes"; if (not fs::exists(Theme::user_theme_dir) and not fs::create_directory(Theme::user_theme_dir, ec)) Theme::user_theme_dir.clear(); } } //? Try to find global btop theme path relative to binary path #if defined(LINUX) { std::error_code ec; Global::self_path = fs::read_symlink("/proc/self/exe", ec).remove_filename(); } #endif if (std::error_code ec; not Global::self_path.empty()) { Theme::theme_dir = fs::canonical(Global::self_path / "../share/btop/themes", ec); 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 relative path failed, check two most common absolute paths if (Theme::theme_dir.empty()) { for (auto theme_path : {"/usr/local/share/btop/themes", "/usr/share/btop/themes"}) { if (fs::is_directory(fs::path(theme_path)) and access(theme_path, R_OK) != -1) { Theme::theme_dir = fs::path(theme_path); break; } } } //? Config init { vector load_warnings; Config::load(Config::conf_file, load_warnings); if (Config::current_boxes.empty()) Config::check_boxes(Config::getS("shown_boxes")); Config::set("lowcolor", (Global::arg_low_color ? true : not Config::getB("truecolor"))); if (Global::debug) { Logger::set("DEBUG"); Logger::debug("Starting in DEBUG mode!"); } else Logger::set(Config::getS("log_level")); Logger::info("Logger set to " + Config::getS("log_level")); for (const auto& err_str : load_warnings) Logger::warning(err_str); } //? Try to find and set a UTF-8 locale if (bool found = false; not str_to_upper((string)std::setlocale(LC_ALL, NULL)).ends_with("UTF-8")) { if (const string lang = (string)getenv("LANG"); str_to_upper(lang).ends_with("UTF-8")) { found = true; std::setlocale(LC_ALL, lang.c_str()); } else if (const string loc = std::locale("").name(); not loc.empty()) { try { for (auto& l : ssplit(loc, ';')) { if (str_to_upper(l).ends_with("UTF-8")) { found = true; std::setlocale(LC_ALL, l.substr(l.find('=') + 1).c_str()); break; } } } catch (const std::out_of_range&) { found = false; } } if (not found and Global::utf_force) Logger::warning("No UTF-8 locale detected! Forcing start with --utf-force argument."); else if (not found) { Global::exit_error_msg = "No UTF-8 locale detected! Use --utf-force argument to start anyway."; exit(1); } else Logger::debug("Setting LC_ALL=" + (string)std::setlocale(LC_ALL, NULL)); } //? Initialize terminal and set options if (not Term::init()) { Global::exit_error_msg = "No tty detected!\nbtop++ needs an interactive shell to run."; exit(1); } Logger::info("Running on " + Term::current_tty); if (not Global::arg_tty and Config::getB("force_tty")) { Config::set("tty_mode", true); Logger::info("Forcing tty mode: setting 16 color mode and using tty friendly graph symbols"); } else if (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"); } //? Platform dependent init and error check try { Shared::init(); } catch (const std::exception& e) { Global::exit_error_msg = "Exception in Shared::init() -> " + (string)e.what(); exit(1); } //? Update list of available themes and generate the selected theme Theme::updateThemes(); Theme::setTheme(); //? Start runner thread Runner::thread_sem_init(); if (pthread_create(&Runner::runner_id, NULL, &Runner::_runner, NULL) != 0) { Global::exit_error_msg = "Failed to create _runner thread!"; exit(1); } if (Global::debug) { Runner::debug_bg = Draw::createBox(2, 2, 32, 8, "", true, "debug"); } //? Create the btop++ banner banner_gen(); //? Calculate sizes of all boxes Draw::calcSizes(); //? Print out box outlines cout << Term::sync_start << Cpu::box << Mem::box << Net::box << Proc::box << Term::sync_end << flush; //? ------------------------------------------------ MAIN LOOP ---------------------------------------------------- uint64_t update_ms = Config::getI("update_ms"); auto future_time = time_ms(); try { while (not true not_eq not false) { //? Check for exceptions in secondary thread and exit with fail signal if true if (Global::thread_exception) exit(1); //? Make sure terminal size hasn't changed (in case of SIGWINCH not working properly) term_resize(); //? Trigger secondary thread to redraw if terminal has been resized if (Global::resized) { Global::resized = false; Draw::calcSizes(); update_clock(); Runner::run("all", true); atomic_wait(Runner::active); } //? Update clock if needed if (update_clock()) { Runner::run("clock"); } //? Start secondary collect & draw thread at the interval set by config value if (time_ms() >= future_time) { Runner::run("all"); update_ms = Config::getI("update_ms"); future_time = time_ms() + update_ms; } //? Loop over input polling and input action processing for (auto current_time = time_ms(); current_time < future_time; current_time = time_ms()) { //? Check for external clock changes and for changes to the update timer if (update_ms != (uint64_t)Config::getI("update_ms")) { update_ms = Config::getI("update_ms"); future_time = time_ms() + update_ms; } else if (future_time - current_time > update_ms) future_time = current_time; //? 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()); } //? Break the loop at 1000ms intervals or if input polling was interrupted else break; } } } catch (std::exception& e) { Global::exit_error_msg = "Exception in main loop -> " + (string)e.what(); exit(1); } }