368 lines
8.8 KiB
C++
368 lines
8.8 KiB
C++
#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 (utf8_display_width(current) + 1 + utf8_display_width(word) > 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
|