/* Copyright 2021 Aristocratos (jakob@qvantnet.com) Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. indent = tab tab-size = 4 */ #include #include #include #include #include #include #include #include #include using std::round, std::vector, std::stoi, std::views::iota, std::clamp, std::max, std::min, std::ceil, std::to_string; using namespace Tools; namespace rng = std::ranges; namespace fs = std::filesystem; string Term::fg, Term::bg; string Fx::reset = reset_base; namespace Theme { fs::path theme_dir; fs::path user_theme_dir; vector themes; unordered_flat_map colors; unordered_flat_map> rgbs; unordered_flat_map> gradients; const unordered_flat_map Default_theme = { { "main_bg", "#00" }, { "main_fg", "#cc" }, { "title", "#ee" }, { "hi_fg", "#b54040" }, { "selected_bg", "#6a2f2f" }, { "selected_fg", "#ee" }, { "inactive_fg", "#40" }, { "graph_text", "#60" }, { "meter_bg", "#40" }, { "proc_misc", "#0de756" }, { "cpu_box", "#556d59" }, { "mem_box", "#6c6c4b" }, { "net_box", "#5c588d" }, { "proc_box", "#805252" }, { "div_line", "#30" }, { "temp_start", "#4897d4" }, { "temp_mid", "#5474e8" }, { "temp_end", "#ff40b6" }, { "cpu_start", "#77ca9b" }, { "cpu_mid", "#cbc06c" }, { "cpu_end", "#dc4c4c" }, { "free_start", "#384f21" }, { "free_mid", "#b5e685" }, { "free_end", "#dcff85" }, { "cached_start", "#163350" }, { "cached_mid", "#74e6fc" }, { "cached_end", "#26c5ff" }, { "available_start", "#4e3f0e" }, { "available_mid", "#ffd77a" }, { "available_end", "#ffb814" }, { "used_start", "#592b26" }, { "used_mid", "#d9626d" }, { "used_end", "#ff4769" }, { "download_start", "#291f75" }, { "download_mid", "#4f43a3" }, { "download_end", "#b0a9de" }, { "upload_start", "#620665" }, { "upload_mid", "#7d4180" }, { "upload_end", "#dcafde" }, { "process_start", "#80d0a3" }, { "process_mid", "#dcd179" }, { "process_end", "#d45454" } }; const unordered_flat_map TTY_theme = { { "main_bg", "\x1b[0;40m" }, { "main_fg", "\x1b[37m" }, { "title", "\x1b[97m" }, { "hi_fg", "\x1b[91m" }, { "selected_bg", "\x1b[41m" }, { "selected_fg", "\x1b[97m" }, { "inactive_fg", "\x1b[90m" }, { "graph_text", "\x1b[37m" }, { "meter_bg", "\x1b[90m" }, { "proc_misc", "\x1b[92m" }, { "cpu_box", "\x1b[32m" }, { "mem_box", "\x1b[33m" }, { "net_box", "\x1b[35m" }, { "proc_box", "\x1b[31m" }, { "div_line", "\x1b[90m" }, { "temp_start", "\x1b[94m" }, { "temp_mid", "\x1b[96m" }, { "temp_end", "\x1b[95m" }, { "cpu_start", "\x1b[92m" }, { "cpu_mid", "\x1b[93m" }, { "cpu_end", "\x1b[91m" }, { "free_start", "\x1b[32m" }, { "free_mid", "" }, { "free_end", "\x1b[92m" }, { "cached_start", "\x1b[36m" }, { "cached_mid", "" }, { "cached_end", "\x1b[96m" }, { "available_start", "\x1b[33m" }, { "available_mid", "" }, { "available_end", "\x1b[93m" }, { "used_start", "\x1b[31m" }, { "used_mid", "" }, { "used_end", "\x1b[91m" }, { "download_start", "\x1b[34m" }, { "download_mid", "" }, { "download_end", "\x1b[94m" }, { "upload_start", "\x1b[35m" }, { "upload_mid", "" }, { "upload_end", "\x1b[95m" }, { "process_start", "\x1b[32m" }, { "process_mid", "\x1b[33m" }, { "process_end", "\x1b[31m" } }; namespace { //* Convert 24-bit colors to 256 colors int truecolor_to_256(const int& r, const int& g, const int& b) { //? Use upper 232-255 greyscale values if the downscaled red, green and blue are the same value if (const int red = round((double)r / 11); red == round((double)g / 11) and red == round((double)b / 11)) { return 232 + red; } //? Else use 6x6x6 color cube to calculate approximate colors else { return round((double)r / 51) * 36 + round((double)g / 51) * 6 + round((double)b / 51) + 16; } } } string hex_to_color(string hexa, const bool& t_to_256, const string& depth) { if (hexa.size() > 1) { hexa.erase(0, 1); for (auto& c : hexa) if (not isxdigit(c)) { Logger::error("Invalid hex value: " + hexa); return ""; } string pre = Fx::e + (depth == "fg" ? "38" : "48") + ";" + (t_to_256 ? "5;" : "2;"); if (hexa.size() == 2) { int h_int = stoi(hexa, 0, 16); if (t_to_256) { return pre + to_string(truecolor_to_256(h_int, h_int, h_int)) + "m"; } else { string h_str = to_string(h_int); return pre + h_str + ";" + h_str + ";" + h_str + "m"; } } else if (hexa.size() == 6) { if (t_to_256) { return pre + to_string(truecolor_to_256( stoi(hexa.substr(0, 2), 0, 16), stoi(hexa.substr(2, 2), 0, 16), stoi(hexa.substr(4, 2), 0, 16))) + "m"; } else { return pre + to_string(stoi(hexa.substr(0, 2), 0, 16)) + ";" + to_string(stoi(hexa.substr(2, 2), 0, 16)) + ";" + to_string(stoi(hexa.substr(4, 2), 0, 16)) + "m"; } } else Logger::error("Invalid size of hex value: " + hexa); } else Logger::error("Hex value missing: " + hexa); return ""; } string dec_to_color(int r, int g, int b, const bool& t_to_256, const string& depth) { string pre = Fx::e + (depth == "fg" ? "38" : "48") + ";" + (t_to_256 ? "5;" : "2;"); r = std::clamp(r, 0, 255); g = std::clamp(g, 0, 255); b = std::clamp(b, 0, 255); if (t_to_256) return pre + to_string(truecolor_to_256(r, g, b)) + "m"; else return pre + to_string(r) + ";" + to_string(g) + ";" + to_string(b) + "m"; } namespace { //* Convert hex color to a array of decimals array hex_to_dec(string hexa) { if (hexa.size() > 1) { hexa.erase(0, 1); for (auto& c : hexa) if (not isxdigit(c)) return array{-1, -1, -1}; if (hexa.size() == 2) { int h_int = stoi(hexa, 0, 16); return array{h_int, h_int, h_int}; } else if (hexa.size() == 6) { return array{ stoi(hexa.substr(0, 2), 0, 16), stoi(hexa.substr(2, 2), 0, 16), stoi(hexa.substr(4, 2), 0, 16) }; } } return {-1 ,-1 ,-1}; } //* Generate colors and rgb decimal vectors for the theme void generateColors(const unordered_flat_map& source) { vector t_rgb; string depth; const bool& t_to_256 = Config::getB("lowcolor"); colors.clear(); rgbs.clear(); for (const auto& [name, color] : Default_theme) { if (name == "main_bg" and not Config::getB("theme_background")) { colors[name] = "\x1b[49m"; rgbs[name] = {-1, -1, -1}; continue; } depth = (name.ends_with("bg") and name != "meter_bg") ? "bg" : "fg"; if (source.contains(name)) { if (name == "main_bg" and source.at(name).empty()) { colors[name] = "\x1b[49m"; rgbs[name] = {-1, -1, -1}; continue; } else if (source.at(name).empty() and (name.ends_with("_mid") or name.ends_with("_end"))) { colors[name] = ""; rgbs[name] = {-1, -1, -1}; continue; } else if (source.at(name).starts_with('#')) { colors[name] = hex_to_color(source.at(name), t_to_256, depth); rgbs[name] = hex_to_dec(source.at(name)); } else if (not source.at(name).empty()) { t_rgb = ssplit(source.at(name)); if (t_rgb.size() != 3) Logger::error("Invalid RGB decimal value: \"" + source.at(name) + "\""); else { colors[name] = dec_to_color(stoi(t_rgb[0]), stoi(t_rgb[1]), stoi(t_rgb[2]), t_to_256, depth); rgbs[name] = array{stoi(t_rgb[0]), stoi(t_rgb[1]), stoi(t_rgb[2])}; } } } if (not colors.contains(name) and not is_in(name, "meter_bg", "process_start", "process_mid", "process_end", "graph_text")) { Logger::debug("Missing color value for \"" + name + "\". Using value from default."); colors[name] = hex_to_color(color, t_to_256, depth); rgbs[name] = hex_to_dec(color); } } //? Set fallback values for optional colors not defined in theme file if (not colors.contains("meter_bg")) { colors["meter_bg"] = colors.at("inactive_fg"); rgbs["meter_bg"] = rgbs.at("inactive_fg"); } if (not colors.contains("process_start")) { colors["process_start"] = colors.at("cpu_start"); colors["process_mid"] = colors.at("cpu_mid"); colors["process_end"] = colors.at("cpu_end"); rgbs["process_start"] = rgbs.at("cpu_start"); rgbs["process_mid"] = rgbs.at("cpu_mid"); rgbs["process_end"] = rgbs.at("cpu_end"); } if (not colors.contains("graph_text")) { colors["graph_text"] = colors.at("inactive_fg"); rgbs["graph_text"] = rgbs.at("inactive_fg"); } } //* Generate color gradients from two or three colors, 101 values indexed 0-100 void generateGradients() { gradients.clear(); const bool& t_to_256 = Config::getB("lowcolor"); //? Insert values for processes greyscale gradient and processes color gradient rgbs.insert({ { "proc_start", rgbs["main_fg"] }, { "proc_mid", {-1, -1, -1} }, { "proc_end", rgbs["inactive_fg"] }, { "proc_color_start", rgbs["inactive_fg"] }, { "proc_color_mid", {-1, -1, -1} }, { "proc_color_end", rgbs["process_start"] }, }); for (const auto& [name, source_arr] : rgbs) { if (not name.ends_with("_start")) continue; const string color_name = rtrim(name, "_start"); //? input_colors[start,mid,end][red,green,blue] const array, 3> input_colors = { source_arr, rgbs[color_name + "_mid"], rgbs[color_name + "_end"] }; //? output_colors[red,green,blue][0-100] array, 101> output_colors; output_colors[0][0] = -1; //? Only start iteration if gradient has an end color defined if (input_colors[2][0] >= 0) { //? Split iteration in two passes of 50 + 51 instead of one pass of 101 if gradient has start, mid and end values defined int current_range = (input_colors[1][0] >= 0) ? 50 : 100; for (const int& rgb : iota(0, 3)) { int start = 0, offset = 0; int end = (current_range == 50) ? 1 : 2; for (const int& i : iota(0, 101)) { output_colors[i][rgb] = input_colors[start][rgb] + (i - offset) * (input_colors[end][rgb] - input_colors[start][rgb]) / current_range; //? Switch source arrays from start->mid to mid->end at 50 passes if mid is defined if (i == current_range) { ++start; ++end; offset = 50; } } } } //? Generate color escape codes for the generated rgb decimals array color_gradient; if (output_colors[0][0] != -1) { for (int y = 0; const auto& [red, green, blue] : output_colors) color_gradient[y++] = dec_to_color(red, green, blue, t_to_256); } else { //? If only start was defined fill array with start color color_gradient.fill(colors[name]); } gradients[color_name] = std::move(color_gradient); } } //* Set colors and generate gradients for the TTY theme void generateTTYColors() { rgbs.clear(); gradients.clear(); colors = TTY_theme; for (const auto& c : colors) { if (not c.first.ends_with("_start")) continue; const string base_name = rtrim(c.first, "_start"); string section = "_start"; int split = colors.at(base_name + "_mid").empty() ? 50 : 33; for (const int& i : iota(0, 101)) { gradients[base_name][i] = colors.at(base_name + section); if (i == split) { section = (split == 33) ? "_mid" : "_end"; split *= 2; } } } } //* Load a .theme file from disk auto loadFile(const string& filename) { unordered_flat_map theme_out; const fs::path filepath = filename; if (not fs::exists(filepath)) return Default_theme; std::ifstream themefile(filepath); if (themefile.good()) { Logger::debug("Loading theme file: " + filename); while (not themefile.bad()) { themefile.ignore(SSmax, '['); if (themefile.eof()) break; string name, value; getline(themefile, name, ']'); if (not Default_theme.contains(name)) { continue; } themefile.ignore(SSmax, '='); themefile >> std::ws; if (themefile.eof()) break; if (themefile.peek() == '"') { themefile.ignore(1); getline(themefile, value, '"'); } else getline(themefile, value, '\n'); theme_out[name] = value; } return theme_out; } return Default_theme; } } void updateThemes() { themes.clear(); themes.push_back("Default"); themes.push_back("TTY"); for (const auto& path : { theme_dir, user_theme_dir } ) { if (path.empty()) continue; for (auto& file : fs::directory_iterator(path)) { if (file.path().extension() == ".theme" and access(file.path().c_str(), R_OK) != -1) { themes.push_back(file.path().c_str()); } } } } void setTheme() { string theme = Config::getS("color_theme"); if (theme == "TTY" or Config::getB("tty_mode")) generateTTYColors(); else { generateColors((theme == "Default" ? Default_theme : loadFile(theme))); generateGradients(); } Term::fg = colors.at("main_fg"); Term::bg = colors.at("main_bg"); Fx::reset = Fx::reset_base + Term::fg + Term::bg; } }