mirror of
https://github.com/aristocratos/btop.git
synced 2026-06-11 01:04:23 -04:00
This fixes #443 and a downstream issue in nixpkgs: https://github.com/NixOS/nixpkgs/issues/460344 Short description of the latter issue: I nixpkgs, different versions of btop are installed under different paths. If a user selects a theme and it is saved to the config by its absolute path, that path will be broken after an update. To solve this, we allow writing the theme name only into the config, and we save the theme by name if possible. When selecting a theme, we check if it is the first (with respect to the load oder priority: custom > user > system) match for this filename in the list of available themes. If it is, we save it by filename instead of using the full path. If there is another theme with the same name and higher priority, we save using the full path. This should also be compatible with the previous behavior of being able to load themes by name from $XDG_CONFIG_PATH/btop/themes. Signed-off-by: Paul Meyer <katexochen0@gmail.com>
474 lines
15 KiB
C++
474 lines
15 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 <algorithm>
|
|
|
|
#include "btop_config.hpp"
|
|
#include "btop_log.hpp"
|
|
#include "btop_theme.hpp"
|
|
#include "btop_tools.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;
|
|
fs::path custom_theme_dir;
|
|
vector<string> themes;
|
|
std::unordered_map<string, string> colors;
|
|
std::unordered_map<string, array<int, 3>> rgbs;
|
|
std::unordered_map<string, array<string, 101>> gradients;
|
|
|
|
const std::unordered_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" },
|
|
{ "proc_pause_bg", "#b54040" },
|
|
{ "proc_follow_bg", "#4040b5"},
|
|
{ "proc_banner_bg", "#7b407b"},
|
|
{ "proc_banner_fg", "#ee"},
|
|
{ "followed_bg", "#4040b5"},
|
|
{ "followed_fg", "#ee"},
|
|
};
|
|
|
|
const std::unordered_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" },
|
|
{ "proc_pause_bg", "\x1b[41m" },
|
|
{ "proc_follow_bg", "\x1b[44m" },
|
|
{ "proc_banner_bg", "\x1b[45m" },
|
|
{ "proc_banner_fg", "\x1b[97m" },
|
|
{ "followed_bg", "\x1b[44m" },
|
|
{ "followed_fg", "\x1b[97m" },
|
|
};
|
|
|
|
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 std::unordered_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 \"{}\". Using value from default.", name);
|
|
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) {
|
|
const fs::path filepath = filename;
|
|
if (not fs::exists(filepath))
|
|
return Default_theme;
|
|
|
|
std::ifstream themefile(filepath);
|
|
if (themefile.good()) {
|
|
std::unordered_map<string, string> theme_out;
|
|
Logger::debug("Loading theme file: {}", filename);
|
|
while (not themefile.bad()) {
|
|
if (themefile.peek() == '#') {
|
|
themefile.ignore(SSmax, '\n');
|
|
continue;
|
|
}
|
|
themefile.ignore(SSmax, '[');
|
|
if (themefile.eof()) break;
|
|
string name, value;
|
|
getline(themefile, name, ']');
|
|
if (not Default_theme.contains(name)) {
|
|
themefile.ignore(SSmax, '\n');
|
|
continue;
|
|
}
|
|
themefile.ignore(SSmax, '=');
|
|
themefile >> std::ws;
|
|
if (themefile.eof()) break;
|
|
if (themefile.peek() == '"') {
|
|
themefile.ignore(1);
|
|
getline(themefile, value, '"');
|
|
themefile.ignore(SSmax, '\n');
|
|
}
|
|
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");
|
|
|
|
//? Priority: custom_theme_dir -> user_theme_dir -> theme_dir
|
|
for (const auto& path : { custom_theme_dir, 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());
|
|
}
|
|
}
|
|
}
|
|
|
|
//? Sort themes alphabetically
|
|
std::stable_sort(themes.begin() + 2, themes.end(), [](const string& a, const string& b) {
|
|
return fs::path(a).filename().string() < fs::path(b).filename().string();
|
|
});
|
|
|
|
}
|
|
|
|
void setTheme() {
|
|
const auto& theme = Config::getS("color_theme");
|
|
const auto theme_cfg_path = fs::path(theme);
|
|
fs::path theme_path;
|
|
for (const fs::path p : themes) {
|
|
//? Check for match by full path, filename, stem or legacy absolute path
|
|
if (p == theme or p.filename() == theme or p.stem() == theme or (theme_cfg_path.is_absolute() and theme_cfg_path.filename() == p.filename())) {
|
|
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;
|
|
}
|
|
|
|
}
|