/* 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 <algorithm>
#include <array>
#include <cmath>
#include <iterator>
#include <ranges>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>

#include "btop_draw.hpp"
#include "btop_config.hpp"
#include "btop_theme.hpp"
#include "btop_shared.hpp"
#include "btop_tools.hpp"
#include "btop_input.hpp"
#include "btop_menu.hpp"
#include <fmt/format.h>


using std::array;
using std::clamp;
using std::cmp_equal;
using std::cmp_greater;
using std::cmp_less;
using std::cmp_less_equal;
using std::floor;
using std::max;
using std::min;
using std::round;
using std::to_string;
using std::views::iota;

using namespace Tools;
using namespace std::literals; // for operator""s
namespace rng = std::ranges;

namespace Symbols {
	const string meter = "■";

	const array<string, 10> superscript = { "⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹" };

	const std::unordered_map<string, vector<string>> graph_symbols = {
		{ "braille_up", {
			" ", "⢀", "⢠", "⢰", "⢸",
			"⡀", "⣀", "⣠", "⣰", "⣸",
			"⡄", "⣄", "⣤", "⣴", "⣼",
			"⡆", "⣆", "⣦", "⣶", "⣾",
			"⡇", "⣇", "⣧", "⣷", "⣿"
		}},
		{"braille_down", {
			" ", "⠈", "⠘", "⠸", "⢸",
			"⠁", "⠉", "⠙", "⠹", "⢹",
			"⠃", "⠋", "⠛", "⠻", "⢻",
			"⠇", "⠏", "⠟", "⠿", "⢿",
			"⡇", "⡏", "⡟", "⡿", "⣿"
		}},
		{"block_up", {
			" ", "▗", "▗", "▐", "▐",
			"▖", "▄", "▄", "▟", "▟",
			"▖", "▄", "▄", "▟", "▟",
			"▌", "▙", "▙", "█", "█",
			"▌", "▙", "▙", "█", "█"
		}},
		{"block_down", {
			" ", "▝", "▝", "▐", "▐",
			"▘", "▀", "▀", "▜", "▜",
			"▘", "▀", "▀", "▜", "▜",
			"▌", "▛", "▛", "█", "█",
			"▌", "▛", "▛", "█", "█"
		}},
		{"tty_up", {
			" ", "░", "░", "▒", "▒",
			"░", "░", "▒", "▒", "█",
			"░", "▒", "▒", "▒", "█",
			"▒", "▒", "▒", "█", "█",
			"▒", "█", "█", "█", "█"
		}},
		{"tty_down", {
			" ", "░", "░", "▒", "▒",
			"░", "░", "▒", "▒", "█",
			"░", "▒", "▒", "▒", "█",
			"▒", "▒", "▒", "█", "█",
			"▒", "█", "█", "█", "█"
		}}
	};

}

namespace Draw {

	string banner_gen(int y, int x, bool centered, bool redraw) {
		static string banner;
		static size_t width = 0;
		if (redraw) banner.clear();
		if (banner.empty()) {
			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 > width) 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 = Mv::r(1);
						i -= 2;
					}
					else
						letter = line[1].substr(i, 3);

					b_color = (letter == "█") ? fg : bg;
					if (b_color != oc) banner += b_color;
					banner += letter;
					oc = b_color;
				}
				if (++z < Global::Banner_src.size()) banner += Mv::l(ulen(line[1])) + Mv::d(1);
			}
			banner += Mv::r(18 - Global::Version.size())
					+ Theme::c("main_fg") + Fx::b + Fx::i + "v" + Global::Version + Fx::reset;
		}
		if (redraw) return "";
		return (centered ? Mv::to(y, Term::width / 2 - width / 2) : Mv::to(y, x)) + banner;
	}

	TextEdit::TextEdit() {}
	TextEdit::TextEdit(string text, bool numeric) : numeric(numeric), text(std::move(text)) {
		pos = this->text.size();
		upos = ulen(this->text);
	}

	bool TextEdit::command(const std::string_view key) {
		if (key == "left" and upos > 0) {
			upos--;
			pos = uresize(text, upos).size();
		}
		else if (key == "right" and pos < text.size()) {
			upos++;
			pos = uresize(text, upos).size();
		}
		else if (key == "home" and not text.empty() and pos > 0) {
			pos = upos = 0;
		}
		else if (key == "end" and not text.empty() and pos < text.size()) {
			pos = text.size();
			upos = ulen(text);
		}
		else if (key == "backspace" and pos > 0) {
			if (pos == text.size()) {
				text = uresize(text, --upos);
				pos = text.size();
			}
			else {
				const string first = uresize(text, --upos);
				pos = first.size();
				text = first + luresize(text.substr(pos), ulen(text) - upos - 1);
			}
		}
		else if (key == "delete" and pos < text.size()) {
			const string first = uresize(text, upos + 1);
			text = uresize(first, ulen(first) - 1) + text.substr(first.size());
		}
		else if (key == "space" and not numeric) {
			text.insert(pos++, 1, ' ');
			upos++;
		}
		else if (ulen(key) == 1 and text.size() < text.max_size() - 20) {
			if (numeric and not isint(key)) return false;
			if (key.size() == 1) {
				text.insert(pos++, 1, key.at(0));
				upos++;
			}
			else {
				const auto first = fmt::format("{}{}", uresize(text, upos), key);
				text = first + text.substr(pos);
				upos++;
				pos = first.size();
			}
		}
		else
			return false;

		return true;
	}

	string TextEdit::operator()(const size_t limit) {
		string out;
		size_t c_upos = upos;
		if (text.empty())
			return Fx::ul + " " + Fx::uul;
		if (limit > 0 and ulen(text) + 1 > limit) {
			try {
				const size_t half = (size_t)round((double)limit / 2);
				string first;

				if (upos + half > ulen(text))
					first = luresize(text.substr(0, pos), limit - (ulen(text) - upos));
				else if (upos - half < 1)
					first = text.substr(0, pos);
				else
					first = luresize(text.substr(0, pos), half);

				out = first + uresize(text.substr(pos), limit - ulen(first));
				c_upos = ulen(first);
			}
			catch (const std::exception& e) {
				Logger::error("In TextEdit::operator() : " + string{e.what()});
				return "";
			}
		}
		else
			out = text;

		if (c_upos == 0)
			return Fx::ul + uresize(out, 1) + Fx::uul + luresize(out, ulen(out) - 1);
		else if (c_upos == ulen(out))
			return out + Fx::ul + " " + Fx::uul;
		else
			return uresize(out, c_upos) + Fx::ul + luresize(uresize(out, c_upos + 1), 1) + Fx::uul + luresize(out, ulen(out) - c_upos - 1);
	}

	void TextEdit::clear() {
		this->text.clear();
	}

	string createBox(
			const int x, const int y, const int width, const int height, string line_color, bool fill, const std::string_view title,
			const std::string_view title2, const int num
	) {
		string out;

		if (line_color.empty())
			line_color = Theme::c("div_line");

		auto tty_mode = Config::getB("tty_mode");
		auto rounded = Config::getB("rounded_corners");
		const string numbering = (num == 0) ? "" : Theme::c("hi_fg") + (tty_mode ? std::to_string(num) : Symbols::superscript.at(clamp(num, 0, 9)));
		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);

		out = Fx::reset + line_color;

		//? Draw horizontal lines
		for (const int& hpos : {y, y + height - 1}) {
			out += Mv::to(hpos, x) + Symbols::h_line * (width - 1);
		}

		//? Draw vertical lines and fill if enabled
		for (const int& hpos : iota(y + 1, y + height - 1)) {
			out += Mv::to(hpos, x) + Symbols::v_line
				+  ((fill) ? string(width - 2, ' ') : Mv::r(width - 2))
				+  Symbols::v_line;
		}

		//? Draw corners
		out += 	Mv::to(y, x) + left_up
			+	Mv::to(y, x + width - 1) + right_up
			+	Mv::to(y + height - 1, x) +left_down
			+	Mv::to(y + height - 1, x + width - 1) + right_down;

		//? Draw titles if defined
		if (not title.empty()) {
			out += fmt::format(
				"{}{}{}{}{}{}{}{}{}", Mv::to(y, x + 2), Symbols::title_left, Fx::b, numbering, Theme::c("title"), title, Fx::ub,
				line_color, Symbols::title_right
			);
		}
		if (not title2.empty()) {
			out += fmt::format(
				"{}{}{}{}{}{}{}{}{}", Mv::to(y + height - 1, x + 2), Symbols::title_left_down, Fx::b, numbering, Theme::c("title"), title2, Fx::ub,
				line_color, Symbols::title_right_down
			);
		}

		return out + Fx::reset + Mv::to(y + 1, x + 1);
	}

	bool update_clock(bool force) {
		const auto& clock_format = Config::getS("clock_format");
		if (not Cpu::shown or clock_format.empty()) {
			if (clock_format.empty() and not Global::clock.empty()) Global::clock.clear();
			return false;
		}

		static const std::unordered_map<string, string> clock_custom_format = {
			{"/user", Tools::username()},
			{"/host", Tools::hostname()},
			{"/uptime", ""}
		};

		static time_t c_time{};
		static size_t clock_len{};
		static string clock_str;

		if (auto n_time = time(nullptr); not force and n_time == c_time)
			return false;
		else {
			c_time = n_time;
			const auto new_clock = Tools::strf_time(clock_format);
			if (not force and new_clock == clock_str) return false;
			clock_str = new_clock;
		}

		auto& out = Global::clock;
		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);


		for (const auto& [c_format, replacement] : clock_custom_format) {
			if (clock_str.contains(c_format)) {
				if (c_format == "/uptime") {
					string upstr = sec_to_dhms(system_uptime());
					if (upstr.size() > 8) upstr.resize(upstr.size() - 3);
					clock_str = s_replace(clock_str, c_format, upstr);
				}
				else {
					clock_str = s_replace(clock_str, c_format, replacement);
				}
			}

		}

		clock_str = uresize(clock_str, std::max(10, width - 66 - (Term::width >= 100 and Config::getB("show_battery") and Cpu::has_battery ? 22 : 0)));
		out.clear();

		if (clock_str.size() != 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 = clock_str.size();
		}

		out += Mv::to(y, x+(width / 2)-(clock_len / 2)) + Fx::ub + Theme::c("cpu_box") + title_left
			+ Theme::c("title") + Fx::b + clock_str + Theme::c("cpu_box") + Fx::ub + title_right;

		return true;
	}

	//* Meter class ------------------------------------------------------------------------------------------------------------>
	Meter::Meter() {}

	Meter::Meter(const int width, string color_gradient, bool invert)
		: width(width), color_gradient(std::move(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);
		auto& out = cache.at(value);
		for (const int& i : iota(1, width + 1)) {
			int y = round((double)i * 100.0 / width);
			if (value >= y)
				out += Theme::g(color_gradient).at(invert ? 100 - y : y) + Symbols::meter;
			else {
				out += Theme::c("meter_bg") + Symbols::meter * (width + 1 - i);
				break;
			}
		}
		out += Fx::reset;
		return out;
	}

	//* Graph class ------------------------------------------------------------------------------------------------------------>
	void Graph::_create(const deque<long long>& data, int data_offset) {
		bool mult = (data.size() - data_offset > 1);
		const auto& graph_symbol = Symbols::graph_symbols.at(symbol + '_' + (invert ? "down" : "up"));
		array<int, 2> result;
		const float mod = (height == 1) ? 0.3 : 0.1;
		long long data_value = 0;
		if (mult and data_offset > 0) {
			last = data.at(data_offset - 1);
			if (max_value > 0) last = clamp((last + offset) * 100 / max_value, 0ll, 100ll);
		}

		//? Horizontal iteration over values in <data>
		for (const int& i : iota(data_offset, (int)data.size())) {
			// if (tty_mode and mult and i % 2 != 0) continue;
			if (not tty_mode and mult) current = not current;
			if (i < 0) {
				data_value = 0;
				last = 0;
			}
			else {
				data_value = data.at(i);
				if (max_value > 0) data_value = clamp((data_value + offset) * 100 / max_value, 0ll, 100ll);
			}

			//? Vertical iteration over height of graph
			for (const int& horizon : iota(0, height)) {
				const int cur_high = (height > 1) ? round(100.0 * (height - horizon) / height) : 100;
				const int cur_low = (height > 1) ? round(100.0 * (height - (horizon + 1)) / height) : 0;
				//? Calculate previous + current value to fit two values in 1 braille character
				for (int ai = 0; const auto& value : {last, data_value}) {
					const int clamp_min = (no_zero and horizon == height - 1 and not (mult and i == data_offset and ai == 0)) ? 1 : 0;
					if (value >= cur_high)
						result[ai++] = 4;
					else if (value <= cur_low)
						result[ai++] = clamp_min;
					else {
						result[ai++] = clamp((int)round((float)(value - cur_low) * 4 / (cur_high - cur_low) + mod), clamp_min, 4);
					}
				}
				//? Generate graph symbol from 5x5 2D vector
				if (height == 1) {
					if (result.at(0) + result.at(1) == 0) graphs.at(current).at(horizon) += Mv::r(1);
					else {
						if (not color_gradient.empty()) graphs.at(current).at(horizon) += Theme::g(color_gradient).at(clamp(max(last, data_value), 0ll, 100ll));
						graphs.at(current).at(horizon) += graph_symbol.at((result.at(0) * 5 + result.at(1)));
					}
				}
				else graphs.at(current).at(horizon) += graph_symbol.at((result.at(0) * 5 + result.at(1)));
			}
			if (mult and i >= 0) last = data_value;
		}
		last = data_value;
		out.clear();
		if (height == 1) {
			//if (not color_gradient.empty())
			//	out += (last < 1 ? Theme::c("inactive_fg") : Theme::g(color_gradient).at(clamp(last, 0ll, 100ll)));
			out += graphs.at(current).at(0);
		}
		else {
			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) : Theme::g(color_gradient).at(100 - ((i - 1) * 100 / height));
				out += (invert) ? graphs.at(current).at(height - i) : graphs.at(current).at(i-1);
			}
		}
		if (not color_gradient.empty()) out += Fx::reset;
	}

	Graph::Graph() {}

	Graph::Graph(int width, int height, const string& color_gradient,
				 const deque<long long>& data, const string& symbol,
				 bool invert, bool no_zero, long long max_value, long long offset)
	: width(width), height(height), color_gradient(color_gradient),
	  invert(invert), no_zero(no_zero), offset(offset) {
		if (Config::getB("tty_mode") or symbol == "tty") this->symbol = "tty";
		else if (symbol != "default") this->symbol = symbol;
		else this->symbol = Config::getS("graph_symbol");
		if (this->symbol == "tty") tty_mode = true;

		if (max_value == 0 and offset > 0) max_value = 100;
		this->max_value = max_value;
		const int value_width = (tty_mode ? data.size() : ceil((double)data.size() / 2));
		int data_offset = (value_width > width) ? data.size() - width * (tty_mode ? 1 : 2) : 0;

		if (not tty_mode and (data.size() - data_offset) % 2 != 0) {
			data_offset--;
		}

		//? Populate the two switching graph vectors and fill empty space if data size < width
		for (const int& i : iota(0, height * 2)) {
			if (tty_mode and i % 2 != current) continue;
			graphs[(i % 2 != 0)].push_back((value_width < width) ? ((height == 1) ? Mv::r(1) : " "s) * (width - value_width) : "");
		}
		if (data.size() == 0) return;
		this->_create(data, data_offset);
	}

	string& Graph::operator()(const deque<long long>& data, bool data_same) {
		if (data_same) return out;

		//? Make room for new characters on graph
		if (not tty_mode) current = not current;
		for (const int& i : iota(0, height)) {
			if (height == 1 and graphs.at(current).at(i).at(1) == '[') {
				if (graphs.at(current).at(i).at(3) == 'C') graphs.at(current).at(i).erase(0, 4);
				else graphs.at(current).at(i).erase(0, graphs.at(current).at(i).find_first_of('m') + 4);
			}
			else if (graphs.at(current).at(i).at(0) == ' ') graphs.at(current).at(i).erase(0, 1);
			else graphs.at(current).at(i).erase(0, 3);
		}
		this->_create(data, (int)data.size() - 1);
		return out;
	}

	string& Graph::operator()() {
		return out;
	}
	//*------------------------------------------------------------------------------------------------------------------------->

}

namespace Cpu {
	int width_p = 100, height_p = 32;
	int min_width = 60, min_height = 8;
	int x = 1, y = 1, width = 20, height;
	int b_columns, b_column_size;
	int b_x, b_y, b_width, b_height;
	float max_observed_pwr = 1.0f;

	int graph_up_height, graph_low_height;
	int graph_up_width, graph_low_width;
	int gpu_meter_width;
	bool shown = true, redraw = true, mid_line = false;
	string box;
	vector<Draw::Graph> graphs_upper;
	vector<Draw::Graph> graphs_lower;
	Draw::Meter cpu_meter;
	vector<Draw::Meter> gpu_meters;
	vector<Draw::Graph> core_graphs;
	vector<Draw::Graph> temp_graphs;
	vector<Draw::Graph> gpu_temp_graphs;
	vector<Draw::Graph> gpu_mem_graphs;

    string draw(const cpu_info& cpu, const vector<Gpu::gpu_info>& gpus, bool force_redraw, bool data_same) {
		if (Runner::stopping) return "";
		if (force_redraw) redraw = true;
		bool show_temps = (Config::getB("check_temp") and got_sensors);
		bool show_watts = (Config::getB("show_cpu_watts") and supports_watts);
		auto single_graph = Config::getB("cpu_single_graph");
		bool hide_cores = show_temps and (cpu_temp_only or not Config::getB("show_coretemp"));
		const int extra_width = (hide_cores ? max(6, 6 * b_column_size) : (b_columns == 1 && !show_temps) ? 8 : 0);
	#ifdef GPU_SUPPORT
		const auto& show_gpu_info = Config::getS("show_gpu_info");
		const bool gpu_always = show_gpu_info == "On";
		const bool gpu_auto = show_gpu_info == "Auto";
		const bool show_gpu = (gpus.size() > 0 and (gpu_always or (gpu_auto and Gpu::shown < Gpu::count)));
	#else
		(void)gpus;
	#endif
		auto graph_up_field = Config::getS("cpu_graph_upper");
		if (graph_up_field == "Auto" or not v_contains(Cpu::available_fields, graph_up_field))
			graph_up_field = "total";
		auto graph_lo_field = Config::getS("cpu_graph_lower");
		if (graph_lo_field == "Auto" or not v_contains(Cpu::available_fields, graph_lo_field)) {
		#ifdef GPU_SUPPORT
			graph_lo_field = show_gpu ? "gpu-totals" : graph_up_field;
		#else
			graph_lo_field = graph_up_field;
		#endif
		}
		auto tty_mode = Config::getB("tty_mode");
		auto& graph_symbol = (tty_mode ? "tty" : Config::getS("graph_symbol_cpu"));
		auto& graph_bg = Symbols::graph_symbols.at((graph_symbol == "default" ? Config::getS("graph_symbol") + "_up" : graph_symbol + "_up")).at(6);
		auto& temp_scale = Config::getS("temp_scale");
		auto cpu_bottom = Config::getB("cpu_bottom");

		const string& title_left = Theme::c("cpu_box") + (cpu_bottom ? Symbols::title_left_down : Symbols::title_left);
		const string& title_right = Theme::c("cpu_box") + (cpu_bottom ? Symbols::title_right_down : Symbols::title_right);
		static int bat_pos = 0, bat_len = 0;
		if (safeVal(cpu.cpu_percent, "total"s).empty()
			or safeVal(cpu.core_percent, 0).empty()
			or (show_temps and safeVal(cpu.temp, 0).empty())) return "";
		if (safeVal(cpu.cpu_percent, "total"s).empty()
			or safeVal(cpu.core_percent, 0).empty()
			or (show_temps and safeVal(cpu.temp, 0).empty())) return "";
		string out;
		out.reserve(width * height);

		//* Redraw elements not needed to be updated every cycle
		if (redraw) {
			mid_line = (not single_graph and graph_up_field != graph_lo_field);
			graph_up_height = (single_graph ? height - 2 : ceil((double)(height - 2) / 2) - (mid_line and height % 2 != 0));
			graph_low_height = height - 2 - graph_up_height - mid_line;
			const int button_y = cpu_bottom ? y + height - 1 : y;
			out += box;

			//? Buttons on title
			out += Mv::to(button_y, x + 10) + title_left + Theme::c("hi_fg") + Fx::b + 'm' + Theme::c("title") + "enu" + Fx::ub + title_right;
			Input::mouse_mappings["m"] = {button_y, x + 11, 1, 4};
			out += Mv::to(button_y, x + 16) + title_left + Theme::c("hi_fg") + Fx::b + 'p' + Theme::c("title") + "reset "
				+ (Config::current_preset < 0 ? "*" : to_string(Config::current_preset)) + Fx::ub + title_right;
			Input::mouse_mappings["p"] = {button_y, x + 17, 1, 8};
			const string update = to_string(Config::getI("update_ms")) + "ms";
			out += Mv::to(button_y, x + width - update.size() - 8) + title_left + Fx::b + Theme::c("hi_fg") + "- " + Theme::c("title") + update
				+ Theme::c("hi_fg") + " +" + Fx::ub + title_right;
			Input::mouse_mappings["-"] = {button_y, x + width - (int)update.size() - 7, 1, 2};
			Input::mouse_mappings["+"] = {button_y, x + width - 5, 1, 2};

			// Draw container engine name
			if (Cpu::container_engine.has_value()) {
				fmt::format_to(std::back_inserter(out), "{}{}{}{}{}", Mv::to(button_y, x + 28), title_left, Theme::c("title"), Cpu::container_engine.value(), title_right);
			}

			//? Graphs & meters
			const int graph_default_width = x + width - b_width - 3;

			auto init_graphs = [&](vector<Draw::Graph>& graphs, const int graph_height, int& graph_width, const string& graph_field, bool invert) {
			#ifdef GPU_SUPPORT
				if (graph_field.starts_with("gpu")) {
					if (graph_field.find("totals") != string::npos) {
						graphs.resize(gpus.size());
						gpu_temp_graphs.resize(gpus.size());
						gpu_mem_graphs.resize(gpus.size());
						gpu_meters.resize(gpus.size());
						const int gpu_draw_count = gpu_always ? Gpu::count : Gpu::count - Gpu::shown;
						graph_width = gpu_draw_count <= 0 ? graph_default_width : graph_default_width/gpu_draw_count - gpu_draw_count + 1 + graph_default_width%gpu_draw_count;
						for (size_t i = 0; i < gpus.size(); i++) {
							if (gpu_auto and v_contains(Gpu::shown_panels, i))
								continue;
							auto& gpu = gpus[i]; auto& graph = graphs[i];

							//? GPU graphs
							if (gpu.supported_functions.gpu_utilization) {
								if (i + 1 < gpus.size()) {
									graph = Draw::Graph{graph_width, graph_height, "cpu", safeVal(gpu.gpu_percent, graph_field), graph_symbol, invert, true};
								}
								else {
									graph = Draw::Graph{
										graph_width + graph_default_width%graph_width - (int)gpus.size() + 1,
										graph_height, "cpu", safeVal(gpu.gpu_percent, graph_field), graph_symbol, invert, true
									};
								}
							}
						}
					} else {
						graphs.resize(1);
						graph_width = graph_default_width;
						graphs[0] = Draw::Graph{ graph_width, graph_height, "cpu", safeVal(Gpu::shared_gpu_percent, graph_field), graph_symbol, invert, true };
					}
				}
				else {
			#endif
					graphs.resize(1);
					graph_width = graph_default_width;
					graphs[0] = Draw::Graph{ graph_width, graph_height, "cpu", safeVal(cpu.cpu_percent, graph_field), graph_symbol, invert, true };
			#ifdef GPU_SUPPORT
				}
			#endif
			};

            init_graphs(graphs_upper, graph_up_height, graph_up_width, graph_up_field, false);
            if (not single_graph)
            	init_graphs(graphs_lower, graph_low_height, graph_low_width, graph_lo_field, Config::getB("cpu_invert_lower"));

			#ifdef GPU_SUPPORT
			if (show_gpu and b_columns > 1) {
				gpu_temp_graphs.resize(gpus.size());
				gpu_mem_graphs.resize(gpus.size());
				gpu_meters.resize(gpus.size());
	
				// Shrink gpu graph width in small boxes to prevent line width extending past box border
				auto gpu_graph_width = b_width < 42 ? 4 : 5;

				for (size_t i = 0; i < gpus.size(); i++) {
					if (gpu_auto and v_contains(Gpu::shown_panels, i))
						continue;
					auto& gpu = gpus[i];

					//? GPU graphs/meters
					auto width_left = b_width - 10 - (gpus.size() > 9 ? 2 : gpus.size() > 1 ? 1 : 0);
					if (gpu.supported_functions.temp_info and show_temps) {
						gpu_temp_graphs[i] = Draw::Graph{ gpu_graph_width, 1, "temp", gpu.temp, graph_symbol, false, false, gpu.temp_max, -23 };
						width_left -= 11;
					}
					if (gpu.supported_functions.mem_used and gpu.supported_functions.mem_total and b_columns > 1) {
						gpu_mem_graphs[i] = Draw::Graph{ gpu_graph_width, 1, "used", safeVal(gpu.gpu_percent, "gpu-vram-totals"s), graph_symbol };
						width_left -= 5;
					}
					width_left -= (gpu.supported_functions.mem_used ? 5 : 0);
					width_left -= (gpu.supported_functions.mem_total ? 6 : 0);
					width_left -= (gpu.supported_functions.pwr_usage ? 6 : 0);
					if (gpu.supported_functions.gpu_utilization) {
						gpu_meters[i] = Draw::Meter{width_left, "cpu" };
					}
				}
			}
			#endif

			int cpu_meter_width = b_width - (show_temps ? 23 - (b_column_size <= 1 and b_columns == 1 ? 6 : 0) : 11);
			if (show_watts) {
				cpu_meter_width -= 6;
			}

			cpu_meter = Draw::Meter{cpu_meter_width, "cpu"};

			if (mid_line) {
				out += Mv::to(y + graph_up_height + 1, x) + Fx::ub + Theme::c("cpu_box") + Symbols::div_left + Theme::c("div_line")
					+ Symbols::h_line * (width - b_width - 2) + Symbols::div_right
					+ Mv::to(y + graph_up_height + 1, x + ((width - b_width) / 2) - ((graph_up_field.size() + graph_lo_field.size()) / 2) - 4)
					+ Theme::c("main_fg") + graph_up_field + Mv::r(1) + "▲▼" + Mv::r(1) + graph_lo_field;
			}

			if (b_column_size > 0 or extra_width > 0) {
				core_graphs.clear();
				for (const auto& core_data : cpu.core_percent) {
					core_graphs.emplace_back(5 * b_column_size + extra_width, 1, "cpu", core_data, graph_symbol);
				}
			}

			if (show_temps) {
				temp_graphs.clear();
				temp_graphs.emplace_back(5, 1, "temp", safeVal(cpu.temp, 0), graph_symbol, false, false, cpu.temp_max, -23);
				if (not hide_cores and b_column_size > 1) {
					for (const auto& i : iota((size_t)1, cpu.temp.size())) {
						temp_graphs.emplace_back(5, 1, "temp", safeVal(cpu.temp, i), graph_symbol, false, false, cpu.temp_max, -23);
					}
				}
			}
		}

		//? Draw battery if enabled and present
		if (Config::getB("show_battery") and has_battery) {
			static int old_percent{};   // defaults to = 0
			static long old_seconds{};  // defaults to = 0
			static float old_watts{};	// defaults to = 0
			static string old_status;
			static Draw::Meter bat_meter {10, "cpu", true};
			static const std::unordered_map<string, string> bat_symbols = {
				{"charging", "▲"},
				{"discharging", "▼"},
				{"full", "■"},
				{"unknown", "○"}
			};

			const auto& [percent, watts, seconds, status] = current_bat;

			if (redraw or percent != old_percent or (watts != old_watts and Config::getB("show_battery_watts")) or seconds != old_seconds or status != old_status) {
				old_percent = percent;
				old_watts = watts;
				old_seconds = seconds;
				old_status = status;
				const string str_time = (seconds > 0 ? sec_to_dhms(seconds, false, true) : "");
				const string str_percent = to_string(percent) + '%';
				const string str_watts = (watts != -1 and Config::getB("show_battery_watts") ? fmt::format("{:.2f}", watts) + 'W' : "");
				const auto& bat_symbol = bat_symbols.at((bat_symbols.contains(status) ? status : "unknown"));
				const int current_len = (Term::width >= 100 ? 11 : 0) + str_time.size() + str_percent.size() + str_watts.size() + to_string(Config::getI("update_ms")).size();
				const int current_pos = Term::width - current_len - 17;

				if ((bat_pos != current_pos or bat_len != current_len) and bat_pos > 0 and not redraw)
					out += Mv::to(y, bat_pos) + Fx::ub + Theme::c("cpu_box") + Symbols::h_line * (bat_len + 4);
				bat_pos = current_pos;
				bat_len = current_len;

				out += Mv::to(y, bat_pos) + title_left + Theme::c("title") + Fx::b + "BAT" + bat_symbol + ' ' + str_percent
					+ (Term::width >= 100 ? Fx::ub + ' ' + bat_meter(percent) + Fx::b : "")
					+ (not str_time.empty() ? ' ' + Theme::c("title") + str_time : "") + (not str_watts.empty() ? " " + Theme::c("title") + Fx::b + str_watts : "") + Fx::ub + title_right;
			}
		}
		else if (bat_pos > 0) {
			out += Mv::to(y, bat_pos) + Fx::ub + Theme::c("cpu_box") + Symbols::h_line * (bat_len + 4);
			bat_pos = bat_len = 0;
		}

		try {
			//? Cpu/Gpu graphs
			out += Fx::ub + Mv::to(y + 1, x + 1);
			auto draw_graphs = [&](vector<Draw::Graph>& graphs, const int graph_height, const int graph_width, const string& graph_field) {
			#ifdef GPU_SUPPORT
				if (graph_field.starts_with("gpu"))
					if (graph_field.ends_with("totals")) {
						int gpu_drawn = 0;
						for (size_t i = 0; i < gpus.size(); i++) {
							if (gpu_auto and v_contains(Gpu::shown_panels, i)) {
								continue;
							}
							try {
								const auto& gpu_percent = gpus[i].gpu_percent;
								out += graphs[i](safeVal(gpu_percent, graph_field), (data_same or redraw));
							} catch (std::out_of_range& /* unused */) {
								continue;
							}
							if (Gpu::count - (gpu_auto ? Gpu::shown : 0) > 1) {
								auto i_str = to_string(i);
								out += Mv::l(graph_width-1) + Mv::u(graph_height/2) + (graph_width > 5 ? "GPU" : "") + i_str
									+ Mv::d(graph_height/2) + Mv::r(graph_width - 1 - (graph_width > 5)*3 - i_str.size());
							}

							if (++gpu_drawn < Gpu::count - (gpu_auto ? Gpu::shown : 0))
								out += Theme::c("div_line") + (Symbols::v_line + Mv::l(1) + Mv::u(1))*graph_height + Mv::r(1) + Mv::d(1);
						}
					}
					else
						out += graphs[0](safeVal(Gpu::shared_gpu_percent, graph_field), (data_same or redraw));
				else
			#else
				(void)graph_height;
				(void)graph_width;
			#endif
					out += graphs[0](safeVal(cpu.cpu_percent, graph_field), (data_same or redraw));
			};

			draw_graphs(graphs_upper, graph_up_height, graph_up_width, graph_up_field);
			if (not single_graph) {
				out += Mv::to(y + graph_up_height + 1 + mid_line, x + 1);
				draw_graphs(graphs_lower, graph_low_height, graph_low_width, graph_lo_field);
			}

			//? Uptime
			if (Config::getB("show_uptime")) {
				string upstr = sec_to_dhms(system_uptime());
				if (upstr.size() > 8) {
					upstr.resize(upstr.size() - 3);
					upstr = trans(upstr);
				}
				out += Mv::to(y + (single_graph or not Config::getB("cpu_invert_lower") ? 1 : height - 2), x + 2)
					+ Theme::c("graph_text") + "up" + Mv::r(1) + upstr;
			}

		#ifdef __linux__
			const bool freq_range = Config::getS("freq_mode") == "range";
		#else
			const bool freq_range = false;
		#endif

			//? Cpu clock and cpu meter
			if (Config::getB("show_cpu_freq") and not cpuHz.empty())
				out += Mv::to(b_y, b_x + b_width - (freq_range ? 20 : 10)) + Fx::ub + Theme::c("div_line")
					+ Symbols::h_line * ((freq_range ? 17 : 7) - cpuHz.size())
					+ Symbols::title_left + Fx::b + Theme::c("title") + cpuHz + Fx::ub + Theme::c("div_line") + Symbols::title_right;

		out += Mv::to(b_y + 1, b_x + 1) + Theme::c("main_fg") + Fx::b + "CPU " + cpu_meter(safeVal(cpu.cpu_percent, "total"s).back())
			+ Theme::g("cpu").at(clamp(safeVal(cpu.cpu_percent, "total"s).back(), 0ll, 100ll)) + rjust(to_string(safeVal(cpu.cpu_percent, "total"s).back()), 4) + Theme::c("main_fg") + '%';
		if (show_temps) {
			const auto [temp, unit] = celsius_to(safeVal(cpu.temp, 0).back(), temp_scale);
			const auto temp_color = Theme::g("temp").at(clamp(safeVal(cpu.temp, 0).back() * 100 / cpu.temp_max, 0ll, 100ll));
			if ((b_column_size > 1 or b_columns > 1) and temp_graphs.size() >= 1ll)
				out += ' ' + Theme::c("inactive_fg") + graph_bg * 5 + Mv::l(5) + temp_color
					+ temp_graphs.at(0)(safeVal(cpu.temp, 0), data_same or redraw);
			out += rjust(to_string(temp), 4) + Theme::c("main_fg") + unit;
		}

		if (show_watts) {
			string cwatts = fmt::format(" {:>4.{}f}", cpu.usage_watts, cpu.usage_watts < 10.0f ? 2 : cpu.usage_watts < 100.0f ? 1 : 0);
			string cwatts_post = "W";

			max_observed_pwr = max(max_observed_pwr, cpu.usage_watts);
			out += Theme::g("cached").at(clamp(cpu.usage_watts / max_observed_pwr * 100.0f, 0.0f, 100.0f)) + cwatts + Theme::c("main_fg") + cwatts_post; 
		}

			out += Theme::c("div_line") + Symbols::v_line;
		} catch (const std::exception& e) {
			throw std::runtime_error("graphs, clock, meter : " + string{e.what()});
		}

		int max_row = b_height - 3; // Subtracting one extra row for the load average (and power if enabled)
		int n_gpus_to_show = 0;
	#ifdef GPU_SUPPORT
		n_gpus_to_show = show_gpu ? (gpus.size() - (gpu_always ? 0 : Gpu::shown)) : 0;
	#endif
		max_row -= n_gpus_to_show;

		auto is_cpu_enabled = [&cpu](const std::int32_t num) -> bool {
			return !cpu.active_cpus.has_value() || std::ranges::find(cpu.active_cpus.value(), num) != cpu.active_cpus.value().end();
		};

		//? Core text and graphs
		int cx = 0, cy = 1, cc = 0, core_width = (b_column_size == 0 ? 2 : 3);
		if (Shared::coreCount >= 100) core_width++;
		for (const auto& n : iota(0, Shared::coreCount)) {
			auto enabled = is_cpu_enabled(n);
			out += Mv::to(b_y + cy + 1, b_x + cx + 1) + Theme::c(enabled ? "main_fg" : "inactive_fg") + (Shared::coreCount < 100 ? Fx::b + 'C' + Fx::ub : "")
				+ ljust(to_string(n), core_width);
			if ((b_column_size > 0 or extra_width > 0) and cmp_less(n, core_graphs.size()))
				out += Theme::c("inactive_fg") + graph_bg * (5 * b_column_size + extra_width) + Mv::l(5 * b_column_size + extra_width)
					+ core_graphs.at(n)(safeVal(cpu.core_percent, n), data_same or redraw);

			out += enabled ? Theme::g("cpu").at(clamp(safeVal(cpu.core_percent, n).back(), 0ll, 100ll)) : Theme::c("inactive_fg");
			out += rjust(to_string(safeVal(cpu.core_percent, n).back()), (b_column_size < 2 ? 3 : 4)) + Theme::c(enabled ? "main_fg" : "inactive_fg") + '%';

			if (show_temps and not hide_cores) {
				const auto [temp, unit] = celsius_to(safeVal(cpu.temp, n+1).back(), temp_scale);
				const auto temp_color = enabled ? Theme::g("temp").at(clamp(safeVal(cpu.temp, n+1).back() * 100 / cpu.temp_max, 0ll, 100ll)) : Theme::c("inactive_fg");
				if (b_column_size > 1 and std::cmp_greater_equal(temp_graphs.size(), n))
					out += ' ' + Theme::c("inactive_fg") + graph_bg * 5 + Mv::l(5)
						+ temp_graphs.at(n+1)(safeVal(cpu.temp, n+1), data_same or redraw);
				out += temp_color + rjust(to_string(temp), 4) + Theme::c(enabled ? "main_fg" : "inactive_fg") + unit;
			}

			out += Theme::c("div_line") + Symbols::v_line;

			if ((++cy > ceil((double)Shared::coreCount / b_columns) or cy == max_row) and n != Shared::coreCount - 1) {
				if (++cc >= b_columns) break;
				cy = 1; cx = (b_width / b_columns) * cc;
			}
		}

		//? Load average
		if (cy < b_height - 1 and cc <= b_columns) {
			cy = b_height - 2 - n_gpus_to_show;

			string load_avg_pre = "Load avg:";
			string load_avg;

			for (const auto& val : cpu.load_avg) {
				load_avg += fmt::format(" {:.2f}", val);
			}

			int len = load_avg_pre.size() + load_avg.size();
			out += Mv::to(b_y + cy, b_x + 1) + string(max(b_width - len - 2, 0), ' ') + Theme::c("main_fg") + Fx::b + load_avg_pre + Fx::ub + load_avg;
		}

	#ifdef GPU_SUPPORT
		//? Gpu brief info
		if (show_gpu) {
			for (unsigned long i = 0; i < gpus.size(); ++i) {
				if (gpu_auto and v_contains(Gpu::shown_panels, i))
					continue;
				out += Mv::to(b_y + ++cy, b_x + 1) + Theme::c("main_fg") + Fx::b + "GPU";
				if (gpus.size() > 1) out += rjust(to_string(i), 1 + (gpus.size() > 9));
				if (gpus[i].supported_functions.gpu_utilization) {
					out += ' ';
					if (b_columns > 1) {
					out += gpu_meters[i](safeVal(gpus[i].gpu_percent, "gpu-totals"s).back())
						+ Theme::g("cpu").at(clamp(safeVal(gpus[i].gpu_percent, "gpu-totals"s).back(), 0ll, 100ll));
					}
					out += rjust(to_string(safeVal(gpus[i].gpu_percent, "gpu-totals"s).back()), 3) + Theme::c("main_fg") + '%';
					if (b_columns == 1)
						out += ' ';
				}
				if (gpus[i].supported_functions.mem_used and gpus[i].supported_functions.mem_total and b_columns > 1) {
					out += ' ' + Theme::c("inactive_fg") + graph_bg * 5 + Mv::l(5) + Theme::g("used").at(safeVal(gpus[i].gpu_percent, "gpu-vram-totals"s).back())
						+ gpu_mem_graphs[i](safeVal(gpus[i].gpu_percent, "gpu-vram-totals"s), data_same or redraw);
				}
				if (gpus[i].supported_functions.mem_used) {
						out += Theme::c("main_fg")
						+ rjust(floating_humanizer(gpus[i].mem_used, true), 5);
				}
				if (gpus[i].supported_functions.mem_total) {
						out += Theme::c("inactive_fg") + '/' + Theme::c("main_fg") + ljust(floating_humanizer(gpus[i].mem_total, true), 4);
				}
				if (show_temps and gpus[i].supported_functions.temp_info) {
					const auto [temp, unit] = celsius_to(gpus[i].temp.back(), temp_scale);
					out += ' ';
					if (b_columns > 1)
						out += Theme::c("inactive_fg") + graph_bg * 5 + Mv::l(5) + Theme::g("temp").at(clamp(gpus[i].temp.back() * 100 / gpus[i].temp_max, 0ll, 100ll))
							+ gpu_temp_graphs[i](gpus[i].temp, data_same or redraw);
					else out += Theme::g("temp").at(clamp(gpus[i].temp.back() * 100 / gpus[i].temp_max, 0ll, 100ll));
					out += rjust(to_string(temp), 3) + Theme::c("main_fg") + unit;
				}
				if (gpus[i].supported_functions.pwr_usage) {
					out += ' ' + Theme::g("cached").at(clamp(safeVal(gpus[i].gpu_percent, "gpu-pwr-totals"s).back(), 0ll, 100ll))
						+ fmt::format("{:>4.{}f}", gpus[i].pwr_usage / 1000.0, gpus[i].pwr_usage < 10'000 ? 2 : gpus[i].pwr_usage < 100'000 ? 1 : 0) + Theme::c("main_fg") + 'W';
				}

				if (cy > b_height - 1) break;
			}
		}
	#endif

		redraw = false;
		return out + Fx::reset;
	}

}

#ifdef GPU_SUPPORT
namespace Gpu {
	int width_p = 100, height_p = 32;
	int min_width = 41, min_height = 8;
	int width = 41, total_height;
	vector<int> x_vec = {}, y_vec = {}, b_height_vec = {};
	int b_width;
	vector<int> b_x_vec = {}, b_y_vec = {};
	vector<bool> redraw = {};
	int shown = 0;
	int count = 0;
	vector<int> shown_panels = {};
	int graph_up_height;
	vector<Draw::Graph> graph_upper_vec = {}, graph_lower_vec = {};
	vector<Draw::Graph> temp_graph_vec = {};
	vector<Draw::Graph> mem_used_graph_vec = {}, mem_util_graph_vec = {};
	vector<Draw::Meter> gpu_meter_vec = {};
	vector<Draw::Meter> pwr_meter_vec = {};
	vector<Draw::Meter> enc_meter_vec = {};
	vector<string> box = {};

    string draw(const gpu_info& gpu, unsigned long index, bool force_redraw, bool data_same) {
		if (Runner::stopping) return "";

		auto& b_x = b_x_vec[index];
		auto& b_y = b_y_vec[index];
		auto& x = x_vec[index];
		auto& y = y_vec[index];

		auto& graph_upper = graph_upper_vec[index];
		auto& graph_lower = graph_lower_vec[index];
		auto& temp_graph = temp_graph_vec[index];
		auto& mem_used_graph = mem_used_graph_vec[index];
		auto& mem_util_graph = mem_util_graph_vec[index];
		auto& gpu_meter = gpu_meter_vec[index];
		auto& pwr_meter = pwr_meter_vec[index];
		auto& enc_meter = enc_meter_vec[index];

		if (force_redraw) redraw[index] = true;
        bool show_temps = gpu.supported_functions.temp_info and (Config::getB("check_temp"));
        auto tty_mode = Config::getB("tty_mode");
		auto& temp_scale = Config::getS("temp_scale");
		auto& graph_symbol = (tty_mode ? "tty" : Config::getS("graph_symbol_gpu"));
		auto& graph_bg = Symbols::graph_symbols.at((graph_symbol == "default" ? Config::getS("graph_symbol") + "_up" : graph_symbol + "_up")).at(6);
        auto single_graph = !Config::getB("gpu_mirror_graph");
		string out;
		int height = gpu_b_height_offsets[index] + 4;
		out.reserve(width * height);

		//* Redraw elements not needed to be updated every cycle
		if (redraw[index]) {
			out += box[index];

			graph_up_height = single_graph ? b_height_vec[index] : (b_height_vec[index] + 1) / 2;
			int graph_low_height = single_graph ? 0 : b_height_vec[index] - graph_up_height;

			if (gpu.supported_functions.gpu_utilization) {
				graph_upper = Draw::Graph{x + width - b_width - 3, graph_up_height, "cpu", safeVal(gpu.gpu_percent, "gpu-totals"s), graph_symbol, false, true}; // TODO cpu -> gpu
            	if (not single_graph) {
                	graph_lower = Draw::Graph{
                    	x + width - b_width - 3,
                    	graph_low_height, "cpu",
                    	safeVal(gpu.gpu_percent, "gpu-totals"s),
                    	graph_symbol,
                    	Config::getB("cpu_invert_lower"), true
                	};
            	}
				gpu_meter = Draw::Meter{b_width - (show_temps ? 25 : 12), "cpu"};
			}
			if (gpu.supported_functions.temp_info)
				temp_graph = Draw::Graph{6, 1, "temp", gpu.temp, graph_symbol, false, false, gpu.temp_max, -23};
			if (gpu.supported_functions.pwr_usage)
				pwr_meter = Draw::Meter{b_width - (gpu.supported_functions.pwr_state and gpu.pwr_state != 32 ? 25 : 12), "cached"};
			if (gpu.supported_functions.mem_utilization)
				mem_util_graph = Draw::Graph{b_width/2 - 1, 2, "free", gpu.mem_utilization_percent, graph_symbol, 0, 0, 100, 4}; // offset so the graph isn't empty at 0-5% utilization
			if (gpu.supported_functions.mem_used and gpu.supported_functions.mem_total)
				mem_used_graph = Draw::Graph{b_width/2 - 2, 2 + 2*(gpu.supported_functions.mem_utilization), "used", safeVal(gpu.gpu_percent, "gpu-vram-totals"s), graph_symbol};
			if (gpu.supported_functions.encoder_utilization)
				enc_meter = Draw::Meter{b_width/2 - 10, "cpu"};
		}


		//* General GPU info
		int rows_used = 1;
		//? Gpu graph, meter & clock speed
		if (gpu.supported_functions.gpu_utilization) {
			out += Fx::ub + Mv::to(y + rows_used, x + 1) + graph_upper(safeVal(gpu.gpu_percent, "gpu-totals"s), (data_same or redraw[index]));
			if (not single_graph)
				out += Mv::to(y + rows_used + graph_up_height, x + 1) + graph_lower(safeVal(gpu.gpu_percent, "gpu-totals"s), (data_same or redraw[index]));

			out += Mv::to(b_y + rows_used, b_x + 1) + Theme::c("main_fg") + Fx::b + "GPU " + gpu_meter(safeVal(gpu.gpu_percent, "gpu-totals"s).back())
				+ Theme::g("cpu").at(clamp(safeVal(gpu.gpu_percent, "gpu-totals"s).back(), 0ll, 100ll)) + rjust(to_string(safeVal(gpu.gpu_percent, "gpu-totals"s).back()), 5) + Theme::c("main_fg") + '%';

			//? Temperature graph, I assume the device supports utilization if it supports temperature
			if (show_temps) {
				const auto [temp, unit] = celsius_to(gpu.temp.back(), temp_scale);
				out += ' ' + Theme::c("inactive_fg") + graph_bg * 6 + Mv::l(6) + Theme::g("temp").at(clamp(gpu.temp.back() * 100 / gpu.temp_max, 0ll, 100ll))
					+ temp_graph(gpu.temp, data_same or redraw[index]);
				out += rjust(to_string(temp), 4) + Theme::c("main_fg") + unit;
			}
			out += Theme::c("div_line") + Symbols::v_line;
			rows_used++;
		}

		if (gpu.supported_functions.gpu_clock) {
			string clock_speed_string = to_string(gpu.gpu_clock_speed);
			out += Mv::to(b_y, b_x + b_width - 12) + Theme::c("div_line") + Symbols::h_line*(5-clock_speed_string.size())
				+ Symbols::title_left + Fx::b + Theme::c("title") + clock_speed_string + " MHz" + Fx::ub + Theme::c("div_line") + Symbols::title_right;
		}

		//? Power usage meter, power state
		if (gpu.supported_functions.pwr_usage) {
			out += Mv::to(b_y + rows_used, b_x + 1) + Theme::c("main_fg") + Fx::b + "PWR " + pwr_meter(safeVal(gpu.gpu_percent, "gpu-pwr-totals"s).back())
				+ Theme::g("cached").at(clamp(safeVal(gpu.gpu_percent, "gpu-pwr-totals"s).back(), 0ll, 100ll))
				+ fmt::format("{:>5.{}f}", gpu.pwr_usage / 1000.0, gpu.pwr_usage < 10'000 ? 2 : gpu.pwr_usage < 100'000 ? 1 : 0) + Theme::c("main_fg") + 'W';
			if (gpu.supported_functions.pwr_state and gpu.pwr_state != 32) // NVML_PSTATE_UNKNOWN; unsupported or non-nvidia card
				out += std::string(" P-state: ") + (gpu.pwr_state > 9 ? "" : " ") + 'P' + Theme::g("cached").at(clamp(gpu.pwr_state, 0ll, 100ll)) + to_string(gpu.pwr_state);
			rows_used++;
		}

		//? Encode and Decode meters
		bool drawnEncDec = gpu.supported_functions.encoder_utilization and gpu.supported_functions.decoder_utilization;
		if (drawnEncDec) {
			out += Mv::to(b_y + rows_used, b_x +1) + Theme::c("main_fg") + Fx::b + "ENC " + enc_meter(gpu.encoder_utilization)
				+ Theme::g("cpu").at(clamp(gpu.encoder_utilization, 0ll, 100ll)) + rjust(to_string(gpu.encoder_utilization), 4) + Theme::c("main_fg") + '%'
				+ Theme::c("div_line") + Symbols::v_line + Theme::c("main_fg") + Fx::b + "DEC " + enc_meter(gpu.decoder_utilization)
				+ Theme::g("cpu").at(clamp(gpu.decoder_utilization, 0ll, 100ll)) + rjust(to_string(gpu.decoder_utilization), 4) + Theme::c("main_fg") + '%';
			rows_used++;
		}

		if (gpu.supported_functions.mem_total or gpu.supported_functions.mem_used) {
			out += Mv::to(b_y + rows_used, b_x);
			if (gpu.supported_functions.mem_total and gpu.supported_functions.mem_used) {
				string used_memory_string = floating_humanizer(gpu.mem_used);

				auto offset = (gpu.supported_functions.mem_total or gpu.supported_functions.mem_used)
					* (1 + 2*(gpu.supported_functions.mem_total and gpu.supported_functions.mem_used) + 2*gpu.supported_functions.mem_utilization);

				//? Used graph, memory section header, total vram
				out += Theme::c("div_line") + Symbols::div_left + Symbols::h_line + Symbols::title_left + Fx::b + Theme::c("title") + "vram" + Theme::c("div_line") + Fx::ub + Symbols::title_right
					+  Symbols::h_line*(b_width/2-8) + Symbols::div_up + Mv::d(offset)+Mv::l(1) + Symbols::div_down + Mv::l(1)+Mv::u(1) + (Symbols::v_line + Mv::l(1)+Mv::u(1))*(offset-1) + Symbols::div_up
					+  Symbols::h_line + Theme::c("title") + "Used:" + Theme::c("div_line")
					+  Symbols::h_line*(b_width/2+b_width%2-9-used_memory_string.size()) + Theme::c("title") + used_memory_string + Theme::c("div_line") + Symbols::h_line + Symbols::div_right
					+  Mv::d(1) + Mv::l(b_width/2-1) + mem_used_graph(safeVal(gpu.gpu_percent, "gpu-vram-totals"s), (data_same or redraw[index]))
					+  Mv::l(b_width-3) + Mv::u(1+2*gpu.supported_functions.mem_utilization) + Theme::c("main_fg") + Fx::b + "Total:" + rjust(floating_humanizer(gpu.mem_total), b_width/2-9) + Fx::ub
					+  Mv::r(3) + rjust(to_string(safeVal(gpu.gpu_percent, "gpu-vram-totals"s).back()), 3) + '%';

				//? Memory utilization
				if (gpu.supported_functions.mem_utilization)
					out += Mv::l(b_width/2+6) + Mv::d(1) + Theme::c("div_line") + Symbols::div_left+Symbols::h_line + Theme::c("title") + "Utilization:" + Theme::c("div_line") + Symbols::h_line*(b_width/2-14) + Symbols::div_right
						+  Mv::l(b_width/2)   + Mv::d(1) + mem_util_graph(gpu.mem_utilization_percent, (data_same or redraw[index]))
						+  Mv::l(b_width/2-1) + Mv::u(1) + rjust(to_string(gpu.mem_utilization_percent.back()), 3) + '%';

				//? Memory clock speed
				if (gpu.supported_functions.mem_clock) {
					string clock_speed_string = to_string(gpu.mem_clock_speed);
					out += Mv::to(b_y + rows_used, b_x + b_width/2 - 11) + Theme::c("div_line") + Symbols::h_line*(5-clock_speed_string.size())
						+ Symbols::title_left + Fx::b + Theme::c("title") + clock_speed_string + " MHz" + Fx::ub + Theme::c("div_line") + Symbols::title_right;
				}
			} else {
				out += Theme::c("main_fg") + Mv::r(1);
				if (gpu.supported_functions.mem_total)
					out += "VRAM total:" + rjust(floating_humanizer(gpu.mem_total), b_width/(1 + gpu.supported_functions.mem_clock)-14);
				else out += "VRAM usage:" + rjust(floating_humanizer(gpu.mem_used), b_width/(1 + gpu.supported_functions.mem_clock)-14);

				if (gpu.supported_functions.mem_clock)
					out += "   VRAM clock:" + rjust(to_string(gpu.mem_clock_speed) + " MHz", b_width/2-13);
			}
		}

		//? Processes section header
		//out += Mv::to(b_y+8, b_x) + Theme::c("div_line") + Symbols::div_left + Symbols::h_line + Symbols::title_left + Theme::c("main_fg") + Fx::b + "gpu-proc" + Fx::ub + Theme::c("div_line")
		//	+ Symbols::title_right + Symbols::h_line*(b_width/2-12) + Symbols::div_down + Symbols::h_line*(b_width/2-2) + Symbols::div_right;

		//? PCIe link throughput
		if (gpu.supported_functions.pcie_txrx and Config::getB("nvml_measure_pcie_speeds")) {
			string tx_string = floating_humanizer(gpu.pcie_tx, 0, 1, 0, 1);
			string rx_string = floating_humanizer(gpu.pcie_rx, 0, 1, 0, 1);
			out += Mv::to(b_y + b_height_vec[index] - 1, b_x+2) + Theme::c("div_line")
				+ Symbols::title_left_down + Theme::c("title") + Fx::b + "TX:" + Fx::ub + Theme::c("div_line") + Symbols::title_right_down + Symbols::h_line*(b_width/2-9-tx_string.size())
				+ Symbols::title_left_down + Theme::c("title") + Fx::b + tx_string + Fx::ub + Theme::c("div_line") + Symbols::title_right_down + (gpu.supported_functions.mem_total and gpu.supported_functions.mem_used ? Symbols::div_down : Symbols::h_line)
				+ Symbols::title_left_down + Theme::c("title") + Fx::b + "RX:" + Fx::ub + Theme::c("div_line") + Symbols::title_right_down + Symbols::h_line*(b_width/2+b_width%2-9-rx_string.size())
				+ Symbols::title_left_down + Theme::c("title") + Fx::b + rx_string + Fx::ub + Theme::c("div_line") + Symbols::title_right_down + Symbols::round_right_down;
		}

		redraw[index] = false;
		return out + Fx::reset;
	}

}
#endif

namespace Mem {
	int width_p = 45, height_p = 36;
	int min_width = 36, min_height = 10;
	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;
	std::unordered_map<string, Draw::Meter> mem_meters;
	std::unordered_map<string, Draw::Graph> mem_graphs;
	std::unordered_map<string, Draw::Meter> disk_meters_used;
	std::unordered_map<string, Draw::Meter> disk_meters_free;
	std::unordered_map<string, Draw::Graph> io_graphs;

	string draw(const mem_info& mem, bool force_redraw, bool data_same) {
		if (Runner::stopping) return "";
		if (force_redraw) redraw = true;
		auto show_swap = Config::getB("show_swap");
		auto swap_disk = Config::getB("swap_disk");
		auto show_disks = Config::getB("show_disks");
		auto show_io_stat = Config::getB("show_io_stat");
		auto io_mode = Config::getB("io_mode");
		auto io_graph_combined = Config::getB("io_graph_combined");
		auto use_graphs = Config::getB("mem_graphs");
		auto tty_mode = Config::getB("tty_mode");
		auto& graph_symbol = (tty_mode ? "tty" : Config::getS("graph_symbol_mem"));
		auto& graph_bg = Symbols::graph_symbols.at((graph_symbol == "default" ? Config::getS("graph_symbol") + "_up" : graph_symbol + "_up")).at(6);
		auto totalMem = Mem::get_totalMem();
		string out;
		out.reserve(height * width);

		//* Redraw elements not needed to be updated every cycle
		if (redraw) {
			out += box;
			mem_meters.clear();
			mem_graphs.clear();
			disk_meters_free.clear();
			disk_meters_used.clear();
			io_graphs.clear();

			//? Mem graphs and meters
			for (const auto& name : mem_names) {

				if (use_graphs)
					mem_graphs[name] = Draw::Graph{mem_meter, graph_height, name, safeVal(mem.percent, name), graph_symbol};
				else
					mem_meters[name] = Draw::Meter{mem_meter, name};
			}
			if (show_swap and has_swap) {
				for (const auto& name : swap_names) {
					if (use_graphs)
						mem_graphs[name] = Draw::Graph{mem_meter, graph_height, name.substr(5), safeVal(mem.percent, name), graph_symbol};
					else
						mem_meters[name] = Draw::Meter{mem_meter, name.substr(5)};
				}
			}

			//? Disk meters and io graphs
			if (show_disks) {
				if (show_io_stat or io_mode) {
					std::unordered_map<string, int> custom_speeds;
					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)))
									try {
										custom_speeds[vals.at(0)] = std::stoi(vals.at(1));
									}
									catch (const std::out_of_range&) { continue; }
							}
						}
					}

					for (const auto& [name, disk] : mem.disks) {
						if (disk.io_read.empty()) continue;

						io_graphs[name + "_activity"] = Draw::Graph{disks_width - 6, 1, "available", disk.io_activity, graph_symbol};

						if (io_mode) {
							//? Create one combined graph for IO read/write if enabled
							long long speed = (custom_speeds.contains(name) ? custom_speeds.at(name) : 100) << 20;
							if (io_graph_combined) {
								deque<long long> combined(disk.io_read.size(), 0);
								rng::transform(disk.io_read, disk.io_write, combined.begin(), std::plus<long long>());
								io_graphs[name] = Draw::Graph{
									disks_width, 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};
							}
						}
					}
				}

				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};
			}

		}

		//? Mem and swap
		int cx = 1, cy = 1;
		string divider = (graph_height > 0 ? Mv::l(2) + Theme::c("mem_box") + Symbols::div_left + Theme::c("div_line") + Symbols::h_line * (mem_width - 1)
						+ (show_disks ? "" : Theme::c("mem_box")) + Symbols::div_right + Mv::l(mem_width - 1) + Theme::c("main_fg") : "");
		string up = (graph_height >= 2 ? Mv::l(mem_width - 2) + Mv::u(graph_height - 1) : "");
		bool big_mem = mem_width > 21;

		out += Mv::to(y + 1, x + 2) + Theme::c("title") + Fx::b + "Total:" + rjust(floating_humanizer(totalMem), mem_width - 9) + Fx::ub + Theme::c("main_fg");
		vector<string> comb_names (mem_names.begin(), mem_names.end());
		if (show_swap and has_swap and not swap_disk) comb_names.insert(comb_names.end(), swap_names.begin(), swap_names.end());
		for (const auto& name : comb_names) {
			if (cy > height - 4) break;
			string title;
			if (name == "swap_used") {
				if (cy > height - 5) break;
				if (height - cy > 6) {
					if (graph_height > 0) out += Mv::to(y+1+cy, x+1+cx) + divider;
					cy += 1;
				}
				out += Mv::to(y+1+cy, x+1+cx) + Theme::c("title") + Fx::b + "Swap:" + rjust(floating_humanizer(safeVal(mem.stats, "swap_total"s)), mem_width - 8)
					+ Theme::c("main_fg") + Fx::ub;
				cy += 1;
				title = "Used";
			}
			else if (name == "swap_free")
				title = "Free";

			if (title.empty()) title = capitalize(name);
			const string humanized = floating_humanizer(safeVal(mem.stats, name));
			const int offset = max(0, divider.empty() ? 9 - (int)humanized.size() : 0);
			const string graphics = (
				use_graphs and mem_graphs.contains(name) ? mem_graphs.at(name)(safeVal(mem.percent, name), redraw or data_same)
				: mem_meters.contains(name) ? mem_meters.at(name)(safeVal(mem.percent, name).back())
				: "");
			if (mem_size > 2) {
				out += Mv::to(y+1+cy, x+1+cx) + divider + title.substr(0, big_mem ? 10 : 5) + ":"
					+ Mv::to(y+1+cy, x+cx + mem_width - 2 - humanized.size()) + (divider.empty() ? Mv::l(offset) + string(" ") * offset + humanized : trans(humanized))
					+ Mv::to(y+2+cy, x+cx + (graph_height >= 2 ? 0 : 1)) + graphics + up + rjust(to_string(safeVal(mem.percent, name).back()) + "%", 4);
				cy += (graph_height == 0 ? 2 : graph_height + 1);
			}
			else {
				out += Mv::to(y+1+cy, x+1+cx) + ljust(title, (mem_size > 1 ? 5 : 1)) + (graph_height >= 2 ? "" : " ")
					+ graphics + Theme::c("title") + rjust(humanized, (mem_size > 1 ? 9 : 7));
				cy += (graph_height == 0 ? 1 : graph_height);
			}
		}
		if (graph_height > 0 and cy < height - 2)
			out += Mv::to(y+1+cy, x+1+cx) + divider;

		//? Disks
		if (show_disks) {
			const auto& disks = mem.disks;
			cx = mem_width; cy = 0;
			bool big_disk = disks_width >= 25;
			divider = Mv::l(1) + Theme::c("div_line") + Symbols::div_left + Symbols::h_line * disks_width + Theme::c("mem_box") + Fx::ub + Symbols::div_right + Mv::l(disks_width);
			const string hu_div = Theme::c("div_line") + Symbols::h_line + Theme::c("main_fg");
			if (io_mode) {
				for (const auto& mount : mem.disks_order) {
					if (not disks.contains(mount)) continue;
					if (cy > height - 3) break;
					const auto disk = safeVal(disks, mount);
					if (disk.io_read.empty()) continue;
					const string total = floating_humanizer(disk.total, not big_disk);
					out += Mv::to(y+1+cy, x+1+cx) + divider + Theme::c("title") + Fx::b + uresize(disk.name, disks_width - 8) + Mv::to(y+1+cy, x+cx + disks_width - total.size())
						+ trans(total) + Fx::ub;
					if (big_disk) {
						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) - 1) + hu_div + used_percent + '%' + hu_div;
					}
					if (io_graphs.contains(mount + "_activity")) {
					out += Mv::to(y+2+cy++, x+1+cx) + (big_disk ? " IO% " : " IO   " + Mv::l(2)) + Theme::c("inactive_fg") + graph_bg * (disks_width - 6)
						+ Mv::l(disks_width - 6) + io_graphs.at(mount + "_activity")(disk.io_activity, redraw or data_same) + Theme::c("main_fg");
					}
					if (++cy > height - 3) break;
					if (io_graph_combined) {
						if (not io_graphs.contains(mount)) continue;
						auto comb_val = disk.io_read.back() + disk.io_write.back();
						const string humanized = (disk.io_write.back() > 0 ? "▼"s : ""s) + (disk.io_read.back() > 0 ? "▲"s : ""s)
												+ (comb_val > 0 ? Mv::r(1) + floating_humanizer(comb_val, true) : "RW");
						if (disks_io_h == 1) out += Mv::to(y+1+cy, x+1+cx) + string(5, ' ');
						out += Mv::to(y+1+cy, x+1+cx) + io_graphs.at(mount)({comb_val}, redraw or data_same)
							+ Mv::to(y+1+cy, x+1+cx) + Theme::c("main_fg") + humanized;
						cy += disks_io_h;
					}
					else {
						if (not io_graphs.contains(mount + "_read") or not io_graphs.contains(mount + "_write")) continue;
						const string human_read = (disk.io_read.back() > 0 ? "▲" + floating_humanizer(disk.io_read.back(), true) : "R");
						const string human_write = (disk.io_write.back() > 0 ? "▼" + floating_humanizer(disk.io_write.back(), true) : "W");
						if (disks_io_h <= 3) out += Mv::to(y+1+cy, x+1+cx) + string(5, ' ') + Mv::to(y+cy + disks_io_h, x+1+cx) + string(5, ' ');
						out += Mv::to(y+1+cy, x+1+cx) + io_graphs.at(mount + "_read")(disk.io_read, redraw or data_same) + Mv::l(disks_width)
							+ Mv::d(1) + io_graphs.at(mount + "_write")(disk.io_write, redraw or data_same)
							+ Mv::to(y+1+cy, x+1+cx) + human_read + Mv::to(y+cy + disks_io_h, x+1+cx) + human_write;
						cy += disks_io_h;
					}
				}
			}
			else {
				for (const auto& mount : mem.disks_order) {
					if (not disks.contains(mount)) continue;
					if (cy > height - 3) break;
					const auto disk = safeVal(disks, mount);
					if (disk.name.empty() or not disk_meters_used.contains(mount)) continue;
					auto comb_val = (not disk.io_read.empty() ? disk.io_read.back() + disk.io_write.back() : 0ll);
					const string human_io = (comb_val > 0 ? (disk.io_write.back() > 0 and big_disk ? "▼"s : ""s) + (disk.io_read.back() > 0 and big_disk ? "▲"s : ""s)
											+ floating_humanizer(comb_val, true) : "");
					const string human_total = floating_humanizer(disk.total, not big_disk);
					const string human_used = floating_humanizer(disk.used, not big_disk);
					const string human_free = floating_humanizer(disk.free, not big_disk);

					out += Mv::to(y+1+cy, x+1+cx) + divider + Theme::c("title") + Fx::b + uresize(disk.name, disks_width - 8) + Mv::to(y+1+cy, x+cx + disks_width - human_total.size())
						+ trans(human_total) + Fx::ub + Theme::c("main_fg");
					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) - 1) + hu_div + human_io + hu_div;
					if (++cy > height - 3) break;
					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)) + Theme::c("inactive_fg") + graph_bg * (disks_width - 6) + Theme::g("available").at(clamp(disk.io_activity.back(), 50ll, 100ll))
							+ Mv::l(disks_width - 6) + io_graphs.at(mount + "_activity")(disk.io_activity, redraw or data_same) + Theme::c("main_fg");
						if (not big_disk) out += Mv::to(y+1+cy, x+cx+1) + Theme::c("main_fg") + human_io;
						if (++cy > height - 3) break;
					}

					out += Mv::to(y+1+cy, x+1+cx) + (big_disk ? " Used:" + rjust(to_string(disk.used_percent) + '%', 4) : "U") + ' '
						+ disk_meters_used.at(mount)(disk.used_percent) + rjust(human_used, (big_disk ? 9 : 5));
					if (++cy > height - 3) break;

					if (disk_meters_free.contains(mount) and cmp_less_equal(disks.size() * 3 + (show_io_stat ? disk_ios : 0), height - 1)) {
						out += Mv::to(y+1+cy, x+1+cx) + (big_disk ? " Free:" + rjust(to_string(disk.free_percent) + '%', 4) : "F") + ' '
						+ disk_meters_free.at(mount)(disk.free_percent) + rjust(human_free, (big_disk ? 9 : 5));
						cy++;
						if (cmp_less_equal(disks.size() * 4 + (show_io_stat ? disk_ios : 0), height - 1)) cy++;
					}

				}
			}
			if (cy < height - 2) out += Mv::to(y+1+cy, x+1+cx) + divider;
		}

		redraw = false;
		return out + Fx::reset;
	}

}

namespace Net {
	int width_p = 45, height_p = 32;
	int min_width = 36, min_height = 6;
	int x = 1, y, width = 20, height;
	int b_x, b_y, b_width, b_height, d_graph_height, u_graph_height;
	bool shown = true, redraw = true;
	const int MAX_IFNAMSIZ = 15;
	string old_ip;
	std::unordered_map<string, Draw::Graph> graphs;
	string box;

	string draw(const net_info& net, bool force_redraw, bool data_same) {
		if (Runner::stopping) return "";
		if (force_redraw) redraw = true;
		auto net_sync = Config::getB("net_sync");
		auto net_auto = Config::getB("net_auto");
		auto tty_mode = Config::getB("tty_mode");
		auto& graph_symbol = (tty_mode ? "tty" : Config::getS("graph_symbol_net"));
		string ip_addr = (net.ipv4.empty() ? net.ipv6 : net.ipv4);
		if (old_ip != ip_addr) {
			old_ip = ip_addr;
			redraw = true;
		}
		string out;
		out.reserve(width * height);
		const string title_left = Theme::c("net_box") + Fx::ub + Symbols::title_left;
		const string title_right = Theme::c("net_box") + Fx::ub + Symbols::title_right;
		const int i_size = min((int)selected_iface.size(), MAX_IFNAMSIZ);
		const long long down_max = (net_auto ? safeVal(graph_max, "download"s) : ((long long)(Config::getI("net_download")) << 20) / 8);
		const long long up_max = (net_auto ? safeVal(graph_max, "upload"s) : ((long long)(Config::getI("net_upload")) << 20) / 8);

		//* Redraw elements not needed to be updated every cycle
		if (redraw) {
			out = box;
			//? Graphs
			graphs.clear();
			if (safeVal(net.bandwidth, "download"s).empty() or safeVal(net.bandwidth, "upload"s).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};

			//? Interface selector and buttons

			out += Mv::to(y, x+width - i_size - 9) + title_left + Fx::b + Theme::c("hi_fg") + Symbols::left + "b " + Theme::c("title")
				+ uresize(selected_iface, MAX_IFNAMSIZ) + Theme::c("hi_fg") + " n" + Symbols::right + title_right
				+ Mv::to(y, x+width - i_size - 15) + title_left + Theme::c("hi_fg") + (safeVal(net.stat, "download"s).offset + safeVal(net.stat, "upload"s).offset > 0 ? Fx::b : "") + 'z'
				+ Theme::c("title") + "ero" + title_right;
			Input::mouse_mappings["b"] = {y, x+width - i_size - 8, 1, 3};
			Input::mouse_mappings["n"] = {y, x+width - 6, 1, 3};
			Input::mouse_mappings["z"] = {y, x+width - i_size - 14, 1, 4};
			if (width - i_size - 20 > 6) {
				out += Mv::to(y, x+width - i_size - 21) + title_left + Theme::c("hi_fg") + (net_auto ? Fx::b : "") + 'a' + Theme::c("title") + "uto" + title_right;
				Input::mouse_mappings["a"] = {y, x+width - i_size - 20, 1, 4};
			}
			if (width - i_size - 20 > 13) {
				out += Mv::to(y, x+width - i_size - 27) + title_left + Theme::c("title") + (net_sync ? Fx::b : "") + 's' + Theme::c("hi_fg")
					+ 'y' + Theme::c("title") + "nc" + title_right;
				Input::mouse_mappings["y"] = {y, x+width - i_size - 26, 1, 4};
			}
		}

		//? IP or device address
		if (not ip_addr.empty() and cmp_greater(width - i_size - 36, ip_addr.size())) {
			out += Mv::to(y, x + 8) + title_left + Theme::c("title") + Fx::b + ip_addr + title_right;
		}

		//? Graphs and stats
		int cy = 0;
		for (const string dir : {"download", "upload"}) {
			out += Mv::to(y+1 + (dir == "upload" ? u_graph_height : 0), x + 1) + graphs.at(dir)(safeVal(net.bandwidth, dir), redraw or data_same or not net.connected)
				+ Mv::to(y+1 + (dir == "upload" ? height - 3: 0), x + 1) + Fx::ub + Theme::c("graph_text")
				+ floating_humanizer((dir == "upload" ? up_max : down_max), true);
			const string speed = floating_humanizer(safeVal(net.stat, dir).speed, false, 0, false, true);
			const string speed_bits = (b_width >= 20 ? floating_humanizer(safeVal(net.stat, dir).speed, false, 0, true, true) : "");
			const string top = floating_humanizer(safeVal(net.stat, dir).top, false, 0, true, true);
			const string total = floating_humanizer(safeVal(net.stat, dir).total);
			const string symbol = (dir == "upload" ? "▲" : "▼");
			out += Mv::to(b_y+1+cy, b_x+1) + Fx::ub + Theme::c("main_fg") + symbol + ' ' + ljust(speed, 10) + (b_width >= 20 ? rjust('(' + speed_bits + ')', 13) : "");
			cy += (b_height == 5 ? 2 : 1);
			if (b_height >= 8) {
				out += Mv::to(b_y+1+cy, b_x+1) + symbol + ' ' + "Top: " + rjust('(' + top, (b_width >= 20 ? 17 : 9)) + ')';
				cy++;
			}
			if (b_height >= 6) {
				out += Mv::to(b_y+1+cy, b_x+1) + symbol + ' ' + "Total: " + rjust(total, (b_width >= 20 ? 16 : 8));
				cy += (b_height > 6 and b_height % 2 ? 2 : 1);
			}
		}

		redraw = false;
		return out + Fx::reset;
	}

}

namespace Proc {
	int width_p = 55, height_p = 68;
	int min_width = 44, min_height = 16;
	int x, y, width = 20, height;
	int start, selected, select_max;
	bool shown = true, redraw = true;
	int selected_pid = 0, selected_depth = 0;
	string selected_name;
	std::unordered_map<size_t, Draw::Graph> p_graphs;
	std::unordered_map<size_t, bool> p_wide_cmd;
	std::unordered_map<size_t, int> p_counters;
	int counter = 0;
	Draw::TextEdit filter;
	Draw::Graph detailed_cpu_graph;
	Draw::Graph detailed_mem_graph;
	int user_size, thread_size, prog_size, cmd_size, tree_size;
	int dgraph_x, dgraph_width, d_width, d_x, d_y;

	string box;

	int selection(const std::string_view cmd_key) {
		auto start = Config::getI("proc_start");
		auto selected = Config::getI("proc_selected");
		auto last_selected = Config::getI("proc_last_selected");
		const int select_max = (Config::getB("show_detailed") ? (Config::getB("pause_proc_list") ? Proc::select_max - 9 : Proc::select_max - 8) :
																(Config::getB("pause_proc_list") ? Proc::select_max - 1 : Proc::select_max));
		auto vim_keys = Config::getB("vim_keys");

		int numpids = Proc::numpids;
		if ((cmd_key == "up" or (vim_keys and cmd_key == "k")) and selected > 0) {
			if (start > 0 and selected == 1) start--;
			else selected--;
			if (Config::getI("proc_last_selected") > 0) Config::set("proc_last_selected", 0);
		}
		else if (cmd_key == "mouse_scroll_up" and start > 0) {
			start = max(0, start - 3);
		}
		else if (cmd_key == "mouse_scroll_down" and start < numpids - select_max) {
			start = min(numpids - select_max, start + 3);
		}
		else if (cmd_key == "down" or (vim_keys and cmd_key == "j")) {
			if (start < numpids - select_max and selected == select_max) start++;
			else if (selected == 0 and last_selected > 0) {
				selected = last_selected;
				Config::set("proc_last_selected", 0);
			}
			else selected++;
		}
		else if (cmd_key == "page_up") {
			if (selected > 0 and start == 0) selected = 0;
			else start = max(0, start - select_max);
		}
		else if (cmd_key == "page_down") {
			if (selected > 0 and start >= numpids - select_max) selected = select_max;
			else start = clamp(start + select_max, 0, max(0, numpids - select_max));
		}
		else if (cmd_key == "home" or (vim_keys and cmd_key == "g")) {
			start = 0;
			if (selected > 0) selected = 1;
		}
		else if (cmd_key == "end" or (vim_keys and cmd_key == "G")) {
			start = max(0, numpids - select_max);
			if (selected > 0) selected = select_max;
		}
		else if (cmd_key.starts_with("mousey")) {
			int mouse_y = std::atoi(cmd_key.substr(6).data());
			start = clamp((int)round((double)mouse_y * (numpids - select_max - 2) / (select_max - 2)), 0, max(0, numpids - select_max));
		}

		bool changed = false;
		if (start != Config::getI("proc_start")) {
			Config::set("proc_start", start);
			changed = true;
		}
		if (selected != Config::getI("proc_selected")) {
			Config::set("proc_selected", selected);
			changed = true;
		}
		return (not changed ? -1 : selected);
	}

	string draw(const vector<proc_info>& plist, bool force_redraw, bool data_same) {
		if (Runner::stopping) return "";
		auto proc_tree = Config::getB("proc_tree");
		// AltData: 关闭进程详情弹窗/展开逻辑
		bool show_detailed = false;
		bool proc_gradient = (Config::getB("proc_gradient") and not Config::getB("lowcolor") and Theme::gradients.contains("proc"));
		auto proc_colors = Config::getB("proc_colors");
		auto tty_mode = Config::getB("tty_mode");
		auto& graph_symbol = (tty_mode ? "tty" : Config::getS("graph_symbol_proc"));
		auto& graph_bg = Symbols::graph_symbols.at((graph_symbol == "default" ? Config::getS("graph_symbol") + "_up" : graph_symbol + "_up")).at(6);
		auto mem_bytes = Config::getB("proc_mem_bytes");
		auto vim_keys = Config::getB("vim_keys");
		auto show_graphs = Config::getB("proc_cpu_graphs");
		const auto pause_proc_list = Config::getB("pause_proc_list");
		start = Config::getI("proc_start");
		selected = Config::getI("proc_selected");
		const int y = show_detailed ? Proc::y + 8 : Proc::y;
		const int height = show_detailed ? Proc::height - 8 : Proc::height;
		const int select_max = show_detailed ? (pause_proc_list ? Proc::select_max - 9 : Proc::select_max - 8) : 
												(pause_proc_list ? Proc::select_max - 1 : Proc::select_max);
		auto totalMem = Mem::get_totalMem();
		int numpids = Proc::numpids;
		if (force_redraw) redraw = true;
		string out;
		out.reserve(width * height);

		//* Redraw elements not needed to be updated every cycle
		if (redraw) {
			out = box;
			const string title_left = Theme::c("proc_box") + Symbols::title_left;
			const string title_right = Theme::c("proc_box") + Symbols::title_right;
			const string title_left_down = Theme::c("proc_box") + Symbols::title_left_down;
			const string title_right_down = Theme::c("proc_box") + Symbols::title_right_down;
			// AltData: remove enter mapping completely
		for (const auto& key : {"T", "K", "S"})
				if (Input::mouse_mappings.contains(key)) Input::mouse_mappings.erase(key);

			//? AltData: 固定列宽
			user_size   = 8;   // (未使用)
			thread_size = -1;  // 不显示 threads
			prog_size   = 4;   // type: 2中文字符=4列宽
			// time(8) + 空格(1) + type(4) + 空格(1) + 边框(2) + 滑块(1) = 17
			cmd_size    = max(10, width - 17);
			tree_size   = width - 17;

			//? Detailed box
			if (show_detailed) {
				bool alive = detailed.status != "Dead";
				dgraph_x = x;
				dgraph_width = max(width / 3, width - 121);
				d_width = width - dgraph_width - 1;
				d_x = x + dgraph_width + 1;
				d_y = Proc::y;

				//? Create cpu and mem graphs if process is alive
				if (alive or pause_proc_list) {
					detailed_cpu_graph = Draw::Graph{dgraph_width - 1, 7, "cpu", detailed.cpu_percent, graph_symbol, false, true};
					detailed_mem_graph = Draw::Graph{d_width / 3, 1, "", detailed.mem_bytes, graph_symbol, false, false, detailed.first_mem};
				}

				//? Draw structure of details box
				const string pid_str = to_string(detailed.entry.pid);
				out += Mv::to(y, x) + Theme::c("proc_box") + Symbols::div_left + Symbols::h_line + title_left + Theme::c("hi_fg") + Fx::b
				+ (tty_mode ? "4" : Symbols::superscript.at(4)) + Theme::c("title") + "proc"
					+ Fx::ub + title_right + Symbols::h_line * (width - 10) + Symbols::div_right
					+ Mv::to(d_y, dgraph_x + 2) + title_left + Fx::b + Theme::c("title") + pid_str + Fx::ub + title_right
					+ title_left + Fx::b + Theme::c("title") + uresize(detailed.entry.name, dgraph_width - pid_str.size() - 7, true) + Fx::ub + title_right;

				out += Mv::to(d_y, d_x - 1) + Theme::c("proc_box") + Symbols::div_up + Mv::to(y, d_x - 1) + Symbols::div_down + Theme::c("div_line");
				for (const int& i : iota(1, 8)) out += Mv::to(d_y + i, d_x - 1) + Symbols::v_line;

				const string t_color = (not alive or selected > 0 ? Theme::c("inactive_fg") : Theme::c("title"));
				const string hi_color = (not alive or selected > 0 ? t_color : Theme::c("hi_fg"));
				int mouse_x = d_x + 2;
				out += Mv::to(d_y, d_x + 1);
				if (width > 55) {
					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;
				}
				out += title_left + hi_color + Fx::b + (vim_keys ? 'K' : 'k') + t_color + "ill" + Fx::ub + title_right
					+ title_left + hi_color + Fx::b + 's' + t_color + "ignals" + Fx::ub + title_right
					+ title_left + hi_color + Fx::b + 'N' + t_color + "ice" + Fx::ub + title_right;
				if (alive and selected == 0) {
					Input::mouse_mappings["k"] = {d_y, mouse_x, 1, 4};
					mouse_x += 6;
					Input::mouse_mappings["s"] = {d_y, mouse_x, 1, 7};
				    mouse_x += 9;
					Input::mouse_mappings["N"] = {d_y, mouse_x, 1, 5};
				}

				//? Labels
				const int item_fit = floor((double)(d_width - 2) / 10);
				const int item_width = floor((double)(d_width - 2) / min(item_fit, 8));
				out += Mv::to(d_y + 1, d_x + 1) + Fx::b + Theme::c("title")
										+ cjust("Status:", item_width)
										+ cjust("Elapsed:", item_width);
				if (item_fit >= 3) out += cjust("IO/R:", item_width);
				if (item_fit >= 4) out += cjust("IO/W:", item_width);
				if (item_fit >= 5) out += cjust("Parent:", item_width);
				if (item_fit >= 6) out += cjust("User:", item_width);
				if (item_fit >= 7) out += cjust("Threads:", item_width);
				if (item_fit >= 8) out += cjust("Nice:", item_width);


				//? Command line
				for (int i = 0; const auto& l : {'C', 'M', 'D'})
				out += Mv::to(d_y + 5 + i++, d_x + 1) + l;

				out += Theme::c("main_fg") + Fx::ub;
				const auto san_cmd = replace_ascii_control(detailed.entry.cmd);
				const int cmd_size = ulen(san_cmd, true);
				for (int num_lines = min(3, (int)ceil((double)cmd_size / (d_width - 5))), i = 0; i < num_lines; i++) {
					out += Mv::to(d_y + 5 + (num_lines == 1 ? 1 : i), d_x + 3)
						+ cjust(luresize(san_cmd, cmd_size - (d_width - 5) * i, true), d_width - 5, true, true);
				}

			}

			//? Filter
			auto filtering = Config::getB("proc_filtering"); // ? filter(20) : Config::getS("proc_filter"))
			const auto filter_text = (filtering) ? filter(max(6, width - 58)) : uresize(Config::getS("proc_filter"), max(6, width - 58));
			out += Mv::to(y, x+9) + title_left + (not filter_text.empty() ? Fx::b : "") + Theme::c("hi_fg") + 'f'
				+ Theme::c("title") + (not filter_text.empty() ? ' ' + filter_text : "ilter")
				+ (not filtering and not filter_text.empty() ? Theme::c("hi_fg") + " del" : "")
				+ (filtering ? Theme::c("hi_fg") + ' ' + Symbols::enter : "") + Fx::ub + title_right;
			if (not filtering) {
				int f_len = (filter_text.empty() ? 6 : ulen(filter_text) + 2);
				Input::mouse_mappings["f"] = {y, x + 10, 1, f_len};
				if (filter_text.empty() and Input::mouse_mappings.contains("delete"))
					Input::mouse_mappings.erase("delete");
				else if (not filter_text.empty())
					Input::mouse_mappings["delete"] = {y, x + 11 + f_len, 1, 3};
			}

			//? AltData: 移除 per-core, reverse, tree, sorting 按钮

			//? AltData: 移除底部 select/info/terminate/kill/signals/Nice 按钮

			//? Labels for fields in list
			if (not proc_tree)
				out += Mv::to(y+1, x+1) + Theme::c("title") + Fx::b
					+ rjust("time:", 8) + ' '
					+ ljust("tag:", prog_size, true) + ' '
					+ (cmd_size > 0 ? "message:" : "");
			else
				out += Mv::to(y+1, x+1) + Theme::c("title") + Fx::b
					+ ljust("Tree:", tree_size) + ' ';

			out += Fx::ub;  // AltData: 移除 User/Mem%/Cpu% 列
		}
		//* End of redraw block

		//? Draw details box if shown
		if (show_detailed) {
			bool alive = detailed.status != "Dead";
			const int item_fit = floor((double)(d_width - 2) / 10);
			const int item_width = floor((double)(d_width - 2) / min(item_fit, 8));

			//? Graph part of box
			string cpu_str = (alive or pause_proc_list ? fmt::format("{:.2f}", detailed.entry.cpu_p) : "");
			if (alive or pause_proc_list) {
				cpu_str.resize(4);
				if (cpu_str.ends_with('.')) { cpu_str.pop_back(); cpu_str.pop_back(); }
			}
			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 + rjust(cpu_str, 4) + "%";
			for (int i = 0; const auto& l : {'C', 'P', 'U'})
					out += Mv::to(d_y + 3 + i++, dgraph_x + 1) + l;

			//? Info part of box
			const string stat_color = (not alive ? Theme::c("inactive_fg") : (detailed.status == "Running" ? Theme::c("proc_misc") : Theme::c("main_fg")));
			out += Mv::to(d_y + 2, d_x + 1) + stat_color + Fx::ub
									+ cjust(detailed.status, item_width) + Theme::c("main_fg")
									+ cjust(detailed.elapsed, item_width);
			if (item_fit >= 3) out += cjust(detailed.io_read, item_width);
			if (item_fit >= 4) out += cjust(detailed.io_write, item_width);
			if (item_fit >= 5) out += cjust(detailed.parent, item_width, true);
			if (item_fit >= 6) out += cjust(detailed.entry.user, item_width, true);
			if (item_fit >= 7) out += cjust(to_string(detailed.entry.threads), item_width);
			if (item_fit >= 8) out += cjust(to_string(detailed.entry.p_nice), item_width);


			const double mem_p = detailed.mem_bytes.back() * 100.0 / totalMem;
			string mem_str = fmt::format("{:.2f}", mem_p);
			mem_str.resize(4);
			if (mem_str.ends_with('.')) mem_str.pop_back();
			out += Mv::to(d_y + 4, d_x + 1) + Theme::c("title") + Fx::b + rjust((item_fit > 4 ? "Memory: " : "M:") + rjust(mem_str, 4) + "% ", (d_width / 3) - 2)
				+ Theme::c("inactive_fg") + Fx::ub + graph_bg * (d_width / 3) + Mv::l(d_width / 3)
				+ Theme::c("proc_misc") + detailed_mem_graph(detailed.mem_bytes, (redraw or data_same or not alive)) + ' '
				+ Theme::c("title") + Fx::b + detailed.memory;
		}

		//? Check bounds of current selection and view
		if (start > 0 and numpids <= select_max)
			start = 0;
		if (start > numpids - select_max)
			start = max(0, numpids - select_max);
		if (selected > select_max)
			selected = select_max;
		if (selected > numpids)
			selected = numpids;

		//* Iteration over processes
		int lc = 0;
		for (int n=0; auto& p : plist) {
			if (p.filtered or (proc_tree and p.tree_index == plist.size()) or n++ < start) continue;
			bool is_selected = (lc + 1 == selected);
			if (is_selected) {
				selected_pid = (int)p.pid;
				selected_name = p.name;
				selected_depth = p.depth;
			}

			//? Update graphs for processes with above 0.0% cpu usage, delete if below 0.1% 10x times
			bool has_graph = show_graphs ? p_counters.contains(p.pid) : false;
			if (show_graphs and ((p.cpu_p > 0 and not has_graph) or (not data_same and has_graph))) {
				if (not has_graph) {
					p_graphs[p.pid] = Draw::Graph{5, 1, "", {}, graph_symbol};
					p_counters[p.pid] = 0;
				}
				else if (p.cpu_p < 0.1 and ++p_counters[p.pid] >= 10) {
					if (p_graphs.contains(p.pid)) p_graphs.erase(p.pid);
					p_counters.erase(p.pid);
				}
				else
					p_counters[p.pid] = 0;
			}

			out += Fx::reset;

			//? Set correct gradient colors if enabled
			string c_color, m_color, t_color, g_color, end;
			if (is_selected) {
				c_color = m_color = t_color = g_color = Fx::b;
				end = Fx::ub;
				out += Theme::c("selected_bg") + Theme::c("selected_fg") + Fx::b;
			}
			else {
				int calc = (selected > lc) ? selected - lc : lc - selected;
				if (proc_colors) {
					end = Theme::c("main_fg") + Fx::ub;
					array<string, 3> colors;
					for (int i = 0; int v : {(int)round(p.cpu_p), (int)round(p.mem * 100 / totalMem), (int)p.threads / 3}) {
						if (proc_gradient) {
							int val = (min(v, 100) + 100) - calc * 100 / select_max;
							if (val < 100) colors[i++] = Theme::g("proc_color").at(max(0, val));
							else colors[i++] = Theme::g("process").at(clamp(val - 100, 0, 100));
						}
						else
							colors[i++] = Theme::g("process").at(clamp(v, 0, 100));
					}
					c_color = colors.at(0); m_color = colors.at(1); t_color = colors.at(2);
				}
				else {
					c_color = m_color = t_color = Fx::b;
					end = Fx::ub;
				}
				if (proc_gradient) {
					g_color = Theme::g("proc").at(clamp(calc * 100 / select_max, 0, 100));
				}
			}

			const auto san_cmd = replace_ascii_control(p.cmd);

			// AltData: 每次重新计算是否包含宽字符，不缓存
			const bool is_wide = ulen(san_cmd) != ulen(san_cmd, true);

			//? Normal view line
			if (not proc_tree) {
				const bool name_wide = ulen(p.name) != ulen(p.name, true);
				// AltData: 按重要程度分配颜色
				string tc = c_color;
				if (p.name == "链上异动" || p.name == "清算数据") tc = Theme::c("hi_fg");  // 红-最重要
				else if (p.name == "新闻") tc = Theme::c("title");                         // 白亮-重要
				else if (p.name == "信号" || p.name == "公告") tc = Theme::c("proc_misc"); // 绿-中等
				// 舆情/推特/日历/交易 用默认 c_color

				out += Mv::to(y+2+lc, x+1)
					+ g_color + rjust(p.short_cmd.substr(0,5), 8) + ' '
					+ tc + ljust(p.name, prog_size, true, name_wide, true) + ' ' + end
					+ (cmd_size > 0 ? g_color + ljust(san_cmd, cmd_size, true, is_wide, true) : "");
			}
			//? Tree view line
			else {
				const string prefix_pid = p.prefix + to_string(p.pid);
				int width_left = tree_size;
				out += Mv::to(y+2+lc, x+1) + g_color + uresize(prefix_pid, width_left) + ' ';
				width_left -= ulen(prefix_pid);
				if (width_left > 0) {
					out += c_color + uresize(p.name, width_left - 1) + end + ' ';
					width_left -= (ulen(p.name) + 1);
				}
				if (width_left > 7) {
					const string_view cmd = width_left > 40 ? rtrim(san_cmd) : p.short_cmd;
					if (not cmd.empty() and cmd != p.name) {
						out += g_color + '(' + uresize(string{cmd}, width_left - 3, p_wide_cmd[p.pid]) + ") ";
						width_left -= (ulen(string{cmd}, true) + 3);
					}
				}
				out += string(max(0, width_left), ' ') + Mv::to(y+2+lc, x+2+tree_size);
			}
			//? Common end of line
			string cpu_str = fmt::format("{:.2f}", p.cpu_p);
			if (p.cpu_p < 10 or (p.cpu_p >= 100 and p.cpu_p < 1000)) cpu_str.resize(3);
			else if (p.cpu_p >= 10'000) {
				cpu_str = fmt::format("{:.2f}", p.cpu_p / 1000);
				cpu_str.resize(3);
				if (cpu_str.ends_with('.')) cpu_str.pop_back();
				cpu_str += "k";
			}
			string mem_str = (mem_bytes ? floating_humanizer(p.mem, true) : "");
			if (not mem_bytes) {
				double mem_p = clamp((double)p.mem * 100 / totalMem, 0.0, 100.0);
				mem_str = mem_p < 0.01 ? "0" : fmt::format("{:.1f}", mem_p);
				if (mem_str.size() > 3) mem_str.resize(3);
				if (mem_str.ends_with('.')) mem_str.pop_back();
				mem_str += '%';
			}

			// Shorten process thread representation when larger than 5 digits: 10000 -> 10K ...
			const std::string proc_threads_string = [&] {
				if (p.threads > 9999) {
					return std::to_string(p.threads / 1000) + 'K';
				} else {
					return std::to_string(p.threads);
				}
			}();

			out += end;  // AltData: 移除 User/Mem%/Cpu% 数据列
			if (lc++ > height - 5) break;
			else if (lc > height - 5 and pause_proc_list) break;
		}

		out += Fx::reset;
		while (lc++ < height - 3) out += Mv::to(y+lc+1, x+1) + string(width - 2, ' ');
		if (pause_proc_list) {
			fmt::format_to(std::back_inserter(out), "{}{}{}{}{:^{}}{}",
				Mv::to(y + height - 2, x + 1),
				Theme::c("proc_pause_bg"), Theme::c("title"), 
				Fx::b, "Process list paused", width - 2,
				Fx::reset);
		}

		//? Draw scrollbar if needed
		if (numpids > select_max) {
			const int scroll_pos = clamp((int)round((double)start * select_max / (numpids - select_max)), 0, height - 5);
			out += Mv::to(y + 1, x + width - 2) + Fx::b + Theme::c("main_fg") + Symbols::up
				+ Mv::to(y + height - 2, x + width - 2) + Symbols::down;

			for (int i = y + 2; i < y + height - 2; i++) {
				out += Mv::to(i, x + width - 2) + ((i == y + 2 + scroll_pos) ? "█" : " ");
			}
		}

		//? Current selection and number of processes
		string location = to_string(start + selected) + '/' + to_string(numpids);
		string loc_clear = Symbols::h_line * max((size_t)0, 9 - location.size());
		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
		if (not data_same and ++counter >= 100) {
			counter = 0;

			std::erase_if(p_graphs, [&](const auto& pair) {
				return rng::find(plist, pair.first, &proc_info::pid) == plist.end();
			});

			std::erase_if(p_counters, [&](const auto& pair) {
				return rng::find(plist, pair.first, &proc_info::pid) == plist.end();
			});

			std::erase_if(p_wide_cmd, [&](const auto& pair) {
				return rng::find(plist, pair.first, &proc_info::pid) == plist.end();
			});
		}

		//? Draw hide button if detailed view is shown
		// AltData: 关闭详细视图入口，取消 enter/hide 按钮

		if (selected == 0 and selected_pid != 0) {
			selected_pid = 0;
			selected_name.clear();
		}
		redraw = false;
		return out + Fx::reset;
	}

}

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");
		auto proc_left = Config::getB("proc_left");

		Cpu::box.clear();

		Mem::box.clear();
		Net::box.clear();
		Proc::box.clear();
		Global::clock.clear();
		Global::overlay.clear();
		Runner::pause_output = false;
		Runner::redraw = true;
		Proc::p_counters.clear();
		Proc::p_graphs.clear();
		if (Menu::active) Menu::redraw = true;

		Input::mouse_mappings.clear();

		Cpu::x = Mem::x = Net::x = Proc::x = 1;
		Cpu::y = Mem::y = Net::y = Proc::y = 1;
		Cpu::width = Mem::width = Net::width = Proc::width = 0;
		Cpu::height = Mem::height = Net::height = Proc::height = 0;
		Cpu::redraw = Mem::redraw = Net::redraw = Proc::redraw = true;

		Cpu::shown = boxes.contains("cpu");
	#ifdef GPU_SUPPORT
		Gpu::box.clear();
		Gpu::width = 0;
		Gpu::shown_panels.clear();
		if (Gpu::count > 0) {
			std::istringstream iss(boxes, std::istringstream::in);
			string current;
			while (iss >> current) {
				if (current.starts_with("gpu"))
					Gpu::shown_panels.push_back(current.back()-'0');
			}
		}
		Gpu::shown = Gpu::shown_panels.size();

		// Calculate the minimum possible GPU height, store in total_height
		// The actual total_height value will of course be overwritten later
		Gpu::total_height = 0;
		for (int i = 0; i < Gpu::shown; i++) {
			using namespace Gpu;
			total_height += 4 + gpu_b_height_offsets[shown_panels[i]];
		}
	#endif
		Mem::shown = boxes.contains("mem");
		Net::shown = boxes.contains("net");
		Proc::shown = boxes.contains("proc");

		//* Calculate and draw cpu box outlines
		if (Cpu::shown) {
			using namespace Cpu;
		#ifdef GPU_SUPPORT
			// inline GPU information
			int gpus_extra_height =
				Config::getS("show_gpu_info") == "On" ? Gpu::count
				: Config::getS("show_gpu_info") == "Auto" ? Gpu::count - Gpu::shown
				: 0;
		#endif
            const bool show_temp = (Config::getB("check_temp") and got_sensors);
			width = round((double)Term::width * width_p / 100);
		#ifdef GPU_SUPPORT
			if (Gpu::shown != 0 and not (Mem::shown or Net::shown or Proc::shown)) {
				height = Term::height - Gpu::total_height - gpus_extra_height;
			} else {
				height = max(8, (int)ceil((double)Term::height * (trim(boxes) == "cpu" ? 100 : height_p/(Gpu::shown+1) + (Gpu::shown != 0)*5) / 100));
			}
			if (height <= Term::height-gpus_extra_height) height += gpus_extra_height;
		#else
			height = max(8, (int)ceil((double)Term::height * (trim(boxes) == "cpu" ? 100 : height_p) / 100));
		#endif
			x = 1;
			y = cpu_bottom ? Term::height - height + 1 : 1;

		#ifdef GPU_SUPPORT
			b_columns = max(2, (int)ceil((double)(Shared::coreCount + 1) / (height - gpus_extra_height - 5)));
		#else
			b_columns = max(1, (int)ceil((double)(Shared::coreCount + 1) / (height - 5)));
		#endif
			if (b_columns * (21 + 12 * show_temp) < width - (width / 3)) {
				b_column_size = 2;
				b_width =  max(29, (21 + 12 * show_temp) * b_columns - (b_columns - 1));
			}
			else if (b_columns * (15 + 6 * show_temp) < width - (width / 3)) {
				b_column_size = 1;
				b_width = (15 + 6 * show_temp) * b_columns - (b_columns - 1);
			}
			else if (b_columns * (8 + 6 * show_temp) < width - (width / 3)) {
				b_column_size = 0;
			}
			else {
				b_columns = (width - width / 3) / (8 + 6 * show_temp);
				b_column_size = 0;
			}

			if (b_column_size == 0) b_width = (8 + 6 * show_temp) * b_columns + 1;
		#ifdef GPU_SUPPORT
			//gpus_extra_height = max(0, gpus_extra_height - 1);
			b_height = min(height - 2, (int)ceil((double)Shared::coreCount / b_columns) + 4 + gpus_extra_height);
		#else
			b_height = min(height - 2, (int)ceil((double)Shared::coreCount / b_columns) + 4);
		#endif

			b_x = x + width - b_width - 1;
			b_y = y + ceil((double)(height - 2) / 2) - ceil((double)b_height / 2) + 1;

			box = createBox(x, y, width, height, Theme::c("cpu_box"), true, (cpu_bottom ? "" : "cpu"), (cpu_bottom ? "cpu" : ""), 1);

			auto& custom = Config::getS("custom_cpu_name");
			static const bool hasCpuHz = not Cpu::get_cpuHz().empty();
		#ifdef __linux__
			static const bool freq_range = Config::getS("freq_mode") == "range";
		#else
			static const bool freq_range = false;
		#endif
			const string cpu_title = uresize(
					(custom.empty() ? Cpu::cpuName : custom),
					b_width - (Config::getB("show_cpu_freq") and hasCpuHz ? (freq_range ? 24 : 14) : 5)
			);
			box += createBox(b_x, b_y, b_width, b_height, "", false, cpu_title);
		}

	#ifdef GPU_SUPPORT
		//* Calculate and draw gpu box outlines
		if (Gpu::shown != 0) {
			using namespace Gpu;
			x_vec.resize(shown); y_vec.resize(shown);
			b_x_vec.resize(shown); b_y_vec.resize(shown);
			b_height_vec.resize(shown);
			box.resize(shown);
			graph_upper_vec.resize(shown); graph_lower_vec.resize(shown);
			temp_graph_vec.resize(shown);
			mem_used_graph_vec.resize(shown); mem_util_graph_vec.resize(shown);
			gpu_meter_vec.resize(shown);
			pwr_meter_vec.resize(shown);
			enc_meter_vec.resize(shown);
			redraw.resize(shown);
			total_height = 0;
			for (auto i = 0; i < shown; ++i) {
				redraw[i] = true;
				int height = 0;
				width = Term::width;
				if (Cpu::shown)
					if (not (Mem::shown or Net::shown or Proc::shown))
						height = min_height;
					else height = Cpu::height;
				else
					if (not (Mem::shown or Net::shown or Proc::shown))
						height = (Term::height - total_height) / (Gpu::shown - i) + (i == 0) * ((Term::height - total_height) % (Gpu::shown - i));
					else
						height = max(min_height, (int)ceil((double)Term::height * height_p/Gpu::shown / 100));

				b_height_vec[i] = gpu_b_height_offsets[shown_panels[i]] + 2;
				height += (height+Cpu::height == Term::height-1);
				height = max(height, b_height_vec[i] + 2);
				x_vec[i] = 1; y_vec[i] = 1 + total_height + (not Config::getB("cpu_bottom"))*Cpu::shown*Cpu::height;
				box[i] = createBox(x_vec[i], y_vec[i], width, height, Theme::c("cpu_box"), true, std::string("gpu") + (char)(shown_panels[i]+'0'), "", (shown_panels[i]+5)%10); // TODO gpu_box
				b_width = clamp(width/2, min_width, 65);
				total_height += height;

				//? Main statistics box
				b_x_vec[i] = x_vec[i] + width - b_width - 1;
				b_y_vec[i] = y_vec[i] + ceil((double)(height - 2 - b_height_vec[i]) / 2) + 1;

				string name = Config::getS(std::string("custom_gpu_name") + (char)(shown_panels[i]+'0'));
				if (name.empty()) name = gpu_names[shown_panels[i]];

				box[i] += createBox(b_x_vec[i], b_y_vec[i], b_width, b_height_vec[i], "", false, name.substr(0, b_width-5));
				b_height_vec[i] = height - 2;
			}
		}
	#endif

		//* Calculate and draw mem box outlines
		if (Mem::shown) {
			using namespace Mem;
			auto show_disks = Config::getB("show_disks");
			auto swap_disk = Config::getB("swap_disk");
			auto mem_graphs = Config::getB("mem_graphs");

			width = round((double)Term::width * (Proc::shown ? width_p : 100) / 100);
		#ifdef GPU_SUPPORT
			height = ceil((double)Term::height * (100 - Net::height_p * Net::shown*4 / ((Gpu::shown != 0 and Cpu::shown) + 4)) / 100) - Cpu::height - Gpu::total_height;
		#else
			height = ceil((double)Term::height * (100 - Cpu::height_p * Cpu::shown - Net::height_p * Net::shown) / 100) + 1;
		#endif
			x = (proc_left and Proc::shown) ? Term::width - width + 1: 1;
			if (mem_below_net and Net::shown)
		#ifdef GPU_SUPPORT
				y = Term::height - height + 1 - (cpu_bottom ? Cpu::height : 0);
			else
				y = (cpu_bottom ? 1 : Cpu::height + 1) + Gpu::total_height;
		#else
				y = Term::height - height + 1 - (cpu_bottom ? Cpu::height : 0);
			else
				y = cpu_bottom ? 1 : Cpu::height + 1;
		#endif

			if (show_disks) {
				mem_width = ceil((double)(width - 3) / 2);
				mem_width += mem_width % 2;
				disks_width = width - mem_width - 2;
				divider = x + mem_width;
			}
			else
				mem_width = width - 1;

			item_height = has_swap and not swap_disk ? 6 : 4;
			if (height - (has_swap and not swap_disk ? 3 : 2) > 2 * item_height)
				mem_size = 3;
			else if (mem_width > 25)
				mem_size = 2;
			else
				mem_size = 1;

			mem_meter = max(0, mem_width - (mem_size > 2 ? 7 : 17));
			if (mem_size == 1) mem_meter += 6;

			if (mem_graphs) {
				graph_height = max(1, (int)round((double)((height - (has_swap and not swap_disk ? 2 : 1)) - (mem_size == 3 ? 2 : 1) * item_height) / item_height));
				if (graph_height > 1) mem_meter += 6;
			}
			else
				graph_height = 0;

			if (show_disks) {
				disk_meter = max(-14, width - mem_width - 23);
				if (disks_width < 25) disk_meter += 14;
			}

			box = createBox(x, y, width, height, Theme::c("mem_box"), true, "mem", "", 2);
			box += Mv::to(y, (show_disks ? divider + 2 : x + width - 9)) + Theme::c("mem_box") + Symbols::title_left + (show_disks ? Fx::b : "")
				+ Theme::c("hi_fg") + 'd' + Theme::c("title") + "isks" + Fx::ub + Theme::c("mem_box") + Symbols::title_right;
			Input::mouse_mappings["d"] = {y, (show_disks ? divider + 3 : x + width - 8), 1, 5};
			if (show_disks) {
				box += Mv::to(y, divider) + Symbols::div_up + Mv::to(y + height - 1, divider) + Symbols::div_down + Theme::c("div_line");
				for (auto i : iota(1, height - 1))
					box += Mv::to(y + i, divider) + Symbols::v_line;
			}
		}

		//* Calculate and draw net box outlines
		if (Net::shown) {
			using namespace Net;
			width = round((double)Term::width * (Proc::shown ? width_p : 100) / 100);
		#ifdef GPU_SUPPORT
			height = Term::height - Cpu::height - Gpu::total_height - Mem::height;
		#else
			height = Term::height - Cpu::height - Mem::height;
		#endif
			x = (proc_left and Proc::shown) ? Term::width - width + 1 : 1;
			if (mem_below_net and Mem::shown)
			#ifdef GPU_SUPPORT
				y = (cpu_bottom ? 1 : Cpu::height + 1) + Gpu::total_height;
			#else
				y = cpu_bottom ? 1 : Cpu::height + 1;
			#endif
			else
				y = Term::height - height + 1 - (cpu_bottom ? Cpu::height : 0);

			b_width = (width > 45) ? 27 : 19;
			b_height = (height > 10) ? 9 : height - 2;
			b_x = x + width - b_width - 1;
			b_y = y + ((height - 2) / 2) - b_height / 2 + 1;
			d_graph_height = round((double)(height - 2) / 2);
			u_graph_height = height - 2 - d_graph_height;

			box = createBox(x, y, width, height, Theme::c("net_box"), true, "net", "", 3);
			box += createBox(b_x, b_y, b_width, b_height, "", false, "download", "upload");
		}

		//* Calculate and draw proc box outlines
		if (Proc::shown) {
			using namespace Proc;
			width = Term::width - (Mem::shown ? Mem::width : (Net::shown ? Net::width : 0));
		#ifdef GPU_SUPPORT
			height = Term::height - Cpu::height - Gpu::total_height;
		#else
			height = Term::height - Cpu::height;
		#endif
			x = proc_left ? 1 : Term::width - width + 1;
		#ifdef GPU_SUPPORT
			y = ((cpu_bottom and Cpu::shown) ? 1 : Cpu::height + 1) + Gpu::total_height;
		#else
			y = (cpu_bottom and Cpu::shown) ? 1 : Cpu::height + 1;
		#endif
			select_max = height - 3;
			box = createBox(x, y, width, height, Theme::c("proc_box"), true, "proc", "", 4);
		}
	}
}
