#include "util.h" #include #include #include #include #include #include #include #include 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(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(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(text[offset + 1]) & 0x3FU); } if (size == 3) { return ((lead & 0x0FU) << 12) | ((static_cast(text[offset + 1]) & 0x3FU) << 6) | (static_cast(text[offset + 2]) & 0x3FU); } return ((lead & 0x07U) << 18) | ((static_cast(text[offset + 1]) & 0x3FU) << 12) | ((static_cast(text[offset + 2]) & 0x3FU) << 6) | (static_cast(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(value.front()))) { value.erase(value.begin()); } while (!value.empty() && !not_space(static_cast(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(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::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::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::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(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 wrap_text(const std::string &text, int width) { if (width <= 1) { return {text}; } std::vector 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(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(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(WCHAR_MAX)) { const int measured = ::wcwidth(static_cast(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(text[start]))) { --start; } text.erase(start); } } // namespace telegram_tui