btop/src/btop_theme.cpp
aristocratos bd5d697830 Squashed commit of the following:
commit c296ac13cd
Merge: 9a1e760 091c30a
Author: Jakob P. Liljenberg <admin@qvantnet.com>
Date:   Sat Aug 26 19:29:57 2023 +0200

    Merge pull request #590 from nobounce/dangling-reference-config

    Convert parameters and config keys to std::string_view

commit 9a1e760a66
Merge: 9c8af4d 22e64ca
Author: Jakob P. Liljenberg <admin@qvantnet.com>
Date:   Sat Aug 26 19:20:18 2023 +0200

    Merge pull request #602 from jfouquart/main

    Fix getting zfs pool name with '.' char in freebsd

commit 9c8af4df43
Merge: 8a49d8c 2217cbe
Author: Jakob P. Liljenberg <admin@qvantnet.com>
Date:   Sat Aug 26 19:18:55 2023 +0200

    Merge pull request #601 from joske/cleanup

    [macos] don't check /sys on macos

commit 8a49d8cf45
Merge: 1556388 008fcd8
Author: Jakob P. Liljenberg <admin@qvantnet.com>
Date:   Sat Aug 26 19:18:07 2023 +0200

    Merge pull request #600 from joske/makefile

    [macos/freebsd] support gcc13

commit 1556388c83
Merge: 1b126f5 d17e1a2
Author: Jakob P. Liljenberg <admin@qvantnet.com>
Date:   Sat Aug 26 19:14:00 2023 +0200

    Merge pull request #599 from joske/main

    [macos] fix temp sensor on system with many cores

commit d17e1a2dac
Author: Jos Dehaes <jos.dehaes@gmail.com>
Date:   Fri Aug 25 16:18:39 2023 +0200

    fix some warnings

commit 4d8aa6b118
Author: Jos Dehaes <jos.dehaes@gmail.com>
Date:   Fri Aug 25 15:52:58 2023 +0200

    fix core check

commit 22e64caaff
Author: Jonathan Fouquart <jfouquart@hotmail.fr>
Date:   Fri Aug 25 09:37:49 2023 +0200

    Fix getting zfs pool name with '.' char in freebsd

commit 2217cbe143
Author: Jos Dehaes <jos.dehaes@gmail.com>
Date:   Wed Aug 23 16:01:04 2023 +0200

    [macos] don't check /sys on macos

commit 008fcd889e
Author: Jos Dehaes <jos.dehaes@gmail.com>
Date:   Wed Aug 23 16:05:00 2023 +0200

    also add g++13

commit 0fdca5eb03
Author: Jos Dehaes <jos.dehaes@gmail.com>
Date:   Wed Aug 23 15:54:07 2023 +0200

    support gcc13

commit dcbdb7360d
Author: Jos Dehaes <jos.dehaes@gmail.com>
Date:   Wed Aug 23 15:46:47 2023 +0200

    [macos] fix temp sensor on system with many cores

commit 1b126f55e3
Author: aristocratos <gnmjpl@gmail.com>
Date:   Fri Aug 4 01:08:27 2023 +0200

    Update Makefile for partial static compilation on freebsd

commit c8ec6bbb00
Author: aristocratos <gnmjpl@gmail.com>
Date:   Thu Aug 3 23:08:33 2023 +0200

    Fix freebsd nullptr changes and makefile for gcc12 and newer

commit 8a33aab588
Merge: 94e5c02 e4abcef
Author: Jakob P. Liljenberg <admin@qvantnet.com>
Date:   Sun Jul 30 13:21:48 2023 +0200

    Merge pull request #539 from nobounce/replace-NULL-nullptr

    Modernize using nullptr.

commit 94e5c02d11
Author: aristocratos <gnmjpl@gmail.com>
Date:   Thu Jul 27 20:51:21 2023 +0200

    Better text editing

commit 091c30ab2b
Author: nobounce <steffen.winter@proton.me>
Date:   Thu Jul 27 14:17:54 2023 +0200

    Convert parameters and config keys to std::string_view

    Using std::string_view instead of std::string& silences a new warning
    from GCC 13, -Wdangling-reference

    Also switch return type of `getI` from int& to int, trivial types are
    cheaper to copy by value

commit e4abcefbf9
Author: nobounce <steffen.winter@proton.me>
Date:   Wed Jul 26 16:19:17 2023 +0200

    Use nullptr instead of NULL.

    See https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2431.pdf
    TLDR: NULL is of type int and relies on proper implicit pointer
    conversion which may lead to issues when using overloaded functions

    It is also considered a 'best practise' for modern C++ and
    conveys the programmers intention more precisly.

commit d53307f14c
Author: nobounce <steffen.winter@proton.me>
Date:   Sun Jul 23 19:53:36 2023 +0200

    Fix path to Linux CI file in itself

    The CI file has a list of dependent files including itself. The path was
    not updated when the CI was split into different files

commit 594f42b9eb
Merge: aca2e4b 53d6eba
Author: Jakob P. Liljenberg <admin@qvantnet.com>
Date:   Wed Jul 26 15:38:01 2023 +0200

    Merge pull request #584 from nobounce/nb/fix-ci-path

    Fix path to Linux CI file in itself

commit aca2e4be75
Author: aristocratos <gnmjpl@gmail.com>
Date:   Wed Jul 26 14:38:48 2023 +0200

    Fix whitespace indent -> tab indent

commit 33faa01910
Author: aristocratos <gnmjpl@gmail.com>
Date:   Wed Jul 26 14:34:15 2023 +0200

    Revert fmt submodule to static fmt folder in include

commit 53d6ebabc0
Author: nobounce <steffen.winter@proton.me>
Date:   Sun Jul 23 19:53:36 2023 +0200

    Fix path to Linux CI file in itself

    The CI file has a list of dependent files including itself. The path was
    not updated when the CI was split into different files
2023-08-26 20:29:43 +02:00

445 lines
14 KiB
C++

/* 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 <cmath>
#include <fstream>
#include <unistd.h>
#include "btop_tools.hpp"
#include "btop_config.hpp"
#include "btop_theme.hpp"
using std::round;
using std::stoi;
using std::to_string;
using std::vector;
using std::views::iota;
using namespace Tools;
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<string> themes;
unordered_flat_map<string, string> colors;
unordered_flat_map<string, array<int, 3>> rgbs;
unordered_flat_map<string, array<string, 101>> gradients;
const unordered_flat_map<string, string> 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<string, string> 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[90m" },
{ "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, 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, nullptr, 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), nullptr, 16),
stoi(hexa.substr(2, 2), nullptr, 16),
stoi(hexa.substr(4, 2), nullptr, 16))) + "m";
} else {
return pre +
to_string(stoi(hexa.substr(0, 2), nullptr, 16)) + ";" +
to_string(stoi(hexa.substr(2, 2), nullptr, 16)) + ";" +
to_string(stoi(hexa.substr(4, 2), nullptr, 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, 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<int, 3> 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, nullptr, 16);
return array{h_int, h_int, h_int};
}
else if (hexa.size() == 6) {
return array{
stoi(hexa.substr(0, 2), nullptr, 16),
stoi(hexa.substr(2, 2), nullptr, 16),
stoi(hexa.substr(4, 2), nullptr, 16)
};
}
}
return {-1 ,-1 ,-1};
}
//* Generate colors and rgb decimal vectors for the theme
void generateColors(const unordered_flat_map<string, string>& source) {
vector<string> t_rgb;
string depth;
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();
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<array<int, 3>, 3> input_colors = {
source_arr,
rgbs[color_name + "_mid"],
rgbs[color_name + "_end"]
};
//? output_colors[red,green,blue][0-100]
array<array<int, 3>, 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 (int rgb : iota(0, 3)) {
int start = 0, offset = 0;
int end = (current_range == 50) ? 1 : 2;
for (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<string, 101> 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;
if (not Config::getB("theme_background"))
colors["main_bg"] = "\x1b[49m";
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 (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<string, string> 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 : { user_theme_dir, 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 and not v_contains(themes, file.path().c_str())) {
themes.push_back(file.path().c_str());
}
}
}
}
void setTheme() {
const auto& theme = Config::getS("color_theme");
fs::path theme_path;
for (const fs::path p : themes) {
if (p == theme or p.stem() == theme or p.filename() == theme) {
theme_path = p;
break;
}
}
if (theme == "TTY" or Config::getB("tty_mode"))
generateTTYColors();
else {
generateColors((theme == "Default" or theme_path.empty() ? Default_theme : loadFile(theme_path)));
generateGradients();
}
Term::fg = colors.at("main_fg");
Term::bg = colors.at("main_bg");
Fx::reset = Fx::reset_base + Term::fg + Term::bg;
}
}