init
This commit is contained in:
366
src/util.cpp
Normal file
366
src/util.cpp
Normal 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
|
||||
Reference in New Issue
Block a user