This commit is contained in:
2026-04-23 17:00:41 +03:00
commit ac28065d2a
17 changed files with 5401 additions and 0 deletions

366
src/util.cpp Normal file
View File

@@ -0,0 +1,366 @@
#include "util.h"
#include <algorithm>
#include <cctype>
#include <cstdint>
#include <cstdlib>
#include <ctime>
#include <fstream>
#include <sstream>
#include <wchar.h>
namespace telegram_tui {
namespace {
std::size_t utf8_sequence_size(unsigned char lead) {
if ((lead & 0x80U) == 0) {
return 1;
}
if ((lead & 0xE0U) == 0xC0U) {
return 2;
}
if ((lead & 0xF0U) == 0xE0U) {
return 3;
}
if ((lead & 0xF8U) == 0xF0U) {
return 4;
}
return 1;
}
bool is_utf8_continuation(unsigned char ch) {
return (ch & 0xC0U) == 0x80U;
}
std::uint32_t decode_utf8_codepoint(const std::string& text, std::size_t offset, std::size_t* size_out = nullptr) {
if (offset >= text.size()) {
if (size_out != nullptr) {
*size_out = 0;
}
return 0;
}
const auto lead = static_cast<unsigned char>(text[offset]);
std::size_t size = utf8_sequence_size(lead);
if (offset + size > text.size()) {
size = 1;
}
for (std::size_t i = 1; i < size; ++i) {
if (!is_utf8_continuation(static_cast<unsigned char>(text[offset + i]))) {
size = 1;
break;
}
}
if (size_out != nullptr) {
*size_out = size;
}
if (size == 1) {
return lead;
}
if (size == 2) {
return ((lead & 0x1FU) << 6) |
(static_cast<unsigned char>(text[offset + 1]) & 0x3FU);
}
if (size == 3) {
return ((lead & 0x0FU) << 12) |
((static_cast<unsigned char>(text[offset + 1]) & 0x3FU) << 6) |
(static_cast<unsigned char>(text[offset + 2]) & 0x3FU);
}
return ((lead & 0x07U) << 18) |
((static_cast<unsigned char>(text[offset + 1]) & 0x3FU) << 12) |
((static_cast<unsigned char>(text[offset + 2]) & 0x3FU) << 6) |
(static_cast<unsigned char>(text[offset + 3]) & 0x3FU);
}
} // namespace
std::string get_env(const char* name) {
const char* value = std::getenv(name);
return value == nullptr ? std::string() : std::string(value);
}
std::string trim_copy(std::string value) {
auto not_space = [](unsigned char c) { return !std::isspace(c); };
while (!value.empty() && !not_space(static_cast<unsigned char>(value.front()))) {
value.erase(value.begin());
}
while (!value.empty() && !not_space(static_cast<unsigned char>(value.back()))) {
value.pop_back();
}
return value;
}
std::string single_line(std::string text) {
for (char& c : text) {
if (c == '\n' || c == '\r' || c == '\t') {
c = ' ';
}
}
return trim_copy(std::move(text));
}
bool is_decimal_number(const std::string& value) {
return !value.empty() &&
std::all_of(value.begin(), value.end(), [](unsigned char ch) { return std::isdigit(ch); });
}
std::filesystem::path data_root() {
if (const char* xdg = std::getenv("XDG_DATA_HOME"); xdg != nullptr && *xdg != '\0') {
return std::filesystem::path(xdg) / "telegram-tui";
}
if (const char* home = std::getenv("HOME"); home != nullptr && *home != '\0') {
return std::filesystem::path(home) / ".local" / "share" / "telegram-tui";
}
return std::filesystem::current_path() / ".telegram-tui-data";
}
StoredConfig load_app_config() {
const std::filesystem::path path = data_root() / "config.json";
std::ifstream input(path);
if (!input.is_open()) {
return {};
}
try {
const json config = json::parse(input, nullptr, true, true);
if (!config.is_object()) {
return {};
}
return StoredConfig{
safe_string(config, "api_id"),
safe_string(config, "api_hash"),
config.value("auto_reload_chat_history", false),
};
} catch (const json::exception&) {
return {};
}
}
bool save_app_config(const StoredConfig& config) {
const std::filesystem::path path = data_root() / "config.json";
try {
if (path.has_parent_path()) {
std::filesystem::create_directories(path.parent_path());
}
json document = json::object();
if (!config.api_id.empty()) {
document["api_id"] = config.api_id;
}
if (!config.api_hash.empty()) {
document["api_hash"] = config.api_hash;
}
document["auto_reload_chat_history"] = config.auto_reload_chat_history;
std::ofstream output(path, std::ios::trunc);
if (!output.is_open()) {
return false;
}
output << document.dump(2) << '\n';
return static_cast<bool>(output);
} catch (const std::exception&) {
return false;
}
}
std::string safe_string(const json& object, const char* key) {
if (!object.contains(key) || !object.at(key).is_string()) {
return {};
}
return object.at(key).get<std::string>();
}
std::int64_t safe_i64(const json& object, const char* key) {
if (!object.contains(key) || !object.at(key).is_number_integer()) {
return 0;
}
return object.at(key).get<std::int64_t>();
}
std::int32_t safe_i32(const json& object, const char* key) {
if (!object.contains(key) || !object.at(key).is_number_integer()) {
return 0;
}
return object.at(key).get<std::int32_t>();
}
std::string format_time(std::int32_t unix_time) {
if (unix_time <= 0) {
return "--:--";
}
std::time_t raw = unix_time;
std::tm tm = *std::localtime(&raw);
char buffer[16] = {};
std::strftime(buffer, sizeof(buffer), "%H:%M", &tm);
return buffer;
}
std::string format_date(std::int32_t unix_time) {
if (unix_time <= 0) {
return "Unknown day";
}
std::time_t raw = unix_time;
std::tm tm = *std::localtime(&raw);
char buffer[32] = {};
std::strftime(buffer, sizeof(buffer), "%Y-%m-%d", &tm);
return buffer;
}
std::string format_datetime(std::int32_t unix_time) {
if (unix_time <= 0) {
return "Unknown time";
}
std::time_t raw = unix_time;
std::tm tm = *std::localtime(&raw);
char buffer[32] = {};
std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M", &tm);
return buffer;
}
std::string format_file_size(std::int64_t size_bytes) {
if (size_bytes <= 0) {
return "?";
}
static constexpr const char* units[] = {"B", "KB", "MB", "GB", "TB"};
double size = static_cast<double>(size_bytes);
std::size_t unit_index = 0;
while (size >= 1024.0 && unit_index + 1 < std::size(units)) {
size /= 1024.0;
++unit_index;
}
std::ostringstream stream;
stream.setf(std::ios::fixed);
stream.precision(unit_index == 0 ? 0 : 1);
stream << size << ' ' << units[unit_index];
return stream.str();
}
std::vector<std::string> wrap_text(const std::string& text, int width) {
if (width <= 1) {
return {text};
}
std::vector<std::string> lines;
std::stringstream stream(text);
std::string paragraph;
while (std::getline(stream, paragraph, '\n')) {
paragraph = single_line(std::move(paragraph));
if (paragraph.empty()) {
lines.emplace_back();
continue;
}
std::stringstream words(paragraph);
std::string word;
std::string current;
while (words >> word) {
if (current.empty()) {
current = word;
continue;
}
if (static_cast<int>(current.size() + 1 + word.size()) > width) {
lines.push_back(current);
current = word;
} else {
current += " ";
current += word;
}
}
if (!current.empty()) {
lines.push_back(current);
}
}
if (lines.empty()) {
lines.emplace_back();
}
return lines;
}
std::size_t utf8_byte_index_from_utf16_offset(const std::string& text, std::size_t utf16_offset) {
std::size_t byte_index = 0;
std::size_t utf16_units = 0;
while (byte_index < text.size() && utf16_units < utf16_offset) {
std::size_t size = 0;
const std::uint32_t codepoint = decode_utf8_codepoint(text, byte_index, &size);
if (size == 0) {
break;
}
const std::size_t units = codepoint > 0xFFFFU ? 2 : 1;
if (utf16_units + units > utf16_offset) {
break;
}
utf16_units += units;
byte_index += size;
}
return byte_index;
}
std::size_t utf8_prev_index(const std::string& text, std::size_t byte_index) {
if (byte_index == 0 || text.empty()) {
return 0;
}
std::size_t index = std::min(byte_index, text.size()) - 1;
while (index > 0 && is_utf8_continuation(static_cast<unsigned char>(text[index]))) {
--index;
}
return index;
}
std::size_t utf8_next_index(const std::string& text, std::size_t byte_index) {
if (byte_index >= text.size()) {
return text.size();
}
std::size_t size = 0;
decode_utf8_codepoint(text, byte_index, &size);
return std::min(text.size(), byte_index + std::max<std::size_t>(1, size));
}
int utf8_display_width(const std::string& text, std::size_t byte_limit) {
const std::size_t limit = std::min(byte_limit, text.size());
int width = 0;
std::size_t index = 0;
while (index < limit) {
std::size_t size = 0;
const std::uint32_t codepoint = decode_utf8_codepoint(text, index, &size);
if (size == 0) {
break;
}
int char_width = 1;
if (codepoint <= static_cast<std::uint32_t>(WCHAR_MAX)) {
const int measured = ::wcwidth(static_cast<wchar_t>(codepoint));
if (measured > 0) {
char_width = measured;
}
}
width += char_width;
index += size;
}
return width;
}
void pop_utf8_back(std::string& text) {
if (text.empty()) {
return;
}
std::size_t start = text.size() - 1;
while (start > 0 && is_utf8_continuation(static_cast<unsigned char>(text[start]))) {
--start;
}
text.erase(start);
}
} // namespace telegram_tui