Format remaining C++ sources
This commit is contained in:
@@ -21,12 +21,8 @@ namespace telegram_tui {
|
||||
namespace {
|
||||
|
||||
constexpr std::array<AttachmentType, 7> kAttachmentTypes = {
|
||||
AttachmentType::Photo,
|
||||
AttachmentType::Video,
|
||||
AttachmentType::Document,
|
||||
AttachmentType::Audio,
|
||||
AttachmentType::Voice,
|
||||
AttachmentType::Animation,
|
||||
AttachmentType::Photo, AttachmentType::Video, AttachmentType::Document,
|
||||
AttachmentType::Audio, AttachmentType::Voice, AttachmentType::Animation,
|
||||
AttachmentType::Sticker,
|
||||
};
|
||||
|
||||
@@ -59,9 +55,9 @@ std::filesystem::path downloads_directory() {
|
||||
}
|
||||
|
||||
std::string sanitize_filename(std::string value) {
|
||||
for (char& ch : value) {
|
||||
if (ch == '/' || ch == '\\' || ch == ':' || ch == '*' || ch == '?' || ch == '"' || ch == '<' ||
|
||||
ch == '>' || ch == '|') {
|
||||
for (char &ch : value) {
|
||||
if (ch == '/' || ch == '\\' || ch == ':' || ch == '*' || ch == '?' || ch == '"' ||
|
||||
ch == '<' || ch == '>' || ch == '|') {
|
||||
ch = '_';
|
||||
}
|
||||
}
|
||||
@@ -72,7 +68,7 @@ std::string sanitize_filename(std::string value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
std::filesystem::path preferred_attachment_filename(const AttachmentInfo& attachment) {
|
||||
std::filesystem::path preferred_attachment_filename(const AttachmentInfo &attachment) {
|
||||
std::string filename = sanitize_filename(attachment.name);
|
||||
if (filename.empty() || filename == "." || filename == "..") {
|
||||
filename = "attachment";
|
||||
@@ -80,7 +76,8 @@ std::filesystem::path preferred_attachment_filename(const AttachmentInfo& attach
|
||||
|
||||
std::filesystem::path candidate(filename);
|
||||
if (!candidate.has_extension() && !attachment.local_path.empty()) {
|
||||
const std::string extension = std::filesystem::path(attachment.local_path).extension().string();
|
||||
const std::string extension =
|
||||
std::filesystem::path(attachment.local_path).extension().string();
|
||||
if (!extension.empty()) {
|
||||
candidate += extension;
|
||||
}
|
||||
@@ -88,18 +85,19 @@ std::filesystem::path preferred_attachment_filename(const AttachmentInfo& attach
|
||||
return candidate;
|
||||
}
|
||||
|
||||
std::filesystem::path unique_destination_path(
|
||||
const std::filesystem::path& directory, const std::filesystem::path& filename) {
|
||||
std::filesystem::path unique_destination_path(const std::filesystem::path &directory,
|
||||
const std::filesystem::path &filename) {
|
||||
const std::filesystem::path stem = filename.stem();
|
||||
const std::filesystem::path extension = filename.extension();
|
||||
std::filesystem::path candidate = directory / filename;
|
||||
for (int index = 1; std::filesystem::exists(candidate); ++index) {
|
||||
candidate = directory / (stem.string() + " (" + std::to_string(index) + ")" + extension.string());
|
||||
candidate = directory / (stem.string() + " (" + std::to_string(index) + ")" +
|
||||
extension.string());
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
std::string inline_preview_unavailable_message(const AttachmentInfo& attachment) {
|
||||
std::string inline_preview_unavailable_message(const AttachmentInfo &attachment) {
|
||||
switch (attachment.type) {
|
||||
case AttachmentType::Photo:
|
||||
if (!attachment.is_downloaded || attachment.local_path.empty()) {
|
||||
@@ -108,7 +106,8 @@ std::string inline_preview_unavailable_message(const AttachmentInfo& attachment)
|
||||
if (!std::filesystem::exists(attachment.local_path)) {
|
||||
return "Downloaded photo file is missing on disk.";
|
||||
}
|
||||
return "No supported preview backend found. Install `kitten`, `img2sixel`, or `chafa`.";
|
||||
return "No supported preview backend found. Install `kitten`, `img2sixel`, or "
|
||||
"`chafa`.";
|
||||
case AttachmentType::Video:
|
||||
if (!attachment.is_downloaded || attachment.local_path.empty()) {
|
||||
return "Video is not downloaded yet.";
|
||||
@@ -116,13 +115,14 @@ std::string inline_preview_unavailable_message(const AttachmentInfo& attachment)
|
||||
if (!std::filesystem::exists(attachment.local_path)) {
|
||||
return "Downloaded video file is missing on disk.";
|
||||
}
|
||||
return "Video preview requires `ffmpegthumbnailer` or `ffmpeg`, plus `kitten`, `img2sixel`, or `chafa`.";
|
||||
return "Video preview requires `ffmpegthumbnailer` or `ffmpeg`, plus `kitten`, "
|
||||
"`img2sixel`, or `chafa`.";
|
||||
default:
|
||||
return "Inline preview is only supported for photos and videos.";
|
||||
}
|
||||
}
|
||||
|
||||
int attachment_progress_percent(const AttachmentInfo& attachment) {
|
||||
int attachment_progress_percent(const AttachmentInfo &attachment) {
|
||||
if (attachment.is_downloaded) {
|
||||
return 100;
|
||||
}
|
||||
@@ -133,7 +133,7 @@ int attachment_progress_percent(const AttachmentInfo& attachment) {
|
||||
return static_cast<int>((downloaded * 100) / attachment.size_bytes);
|
||||
}
|
||||
|
||||
std::string attachment_progress_bar(const AttachmentInfo& attachment, int width) {
|
||||
std::string attachment_progress_bar(const AttachmentInfo &attachment, int width) {
|
||||
if (width <= 2) {
|
||||
return {};
|
||||
}
|
||||
@@ -142,12 +142,13 @@ std::string attachment_progress_bar(const AttachmentInfo& attachment, int width)
|
||||
if (attachment.is_downloaded) {
|
||||
filled = inner_width;
|
||||
} else if (attachment.size_bytes > 0 && attachment.downloaded_size > 0) {
|
||||
filled = std::clamp((inner_width * attachment_progress_percent(attachment)) / 100, 0, inner_width);
|
||||
filled = std::clamp((inner_width * attachment_progress_percent(attachment)) / 100,
|
||||
0, inner_width);
|
||||
}
|
||||
return "[" + std::string(filled, '#') + std::string(inner_width - filled, '.') + "]";
|
||||
}
|
||||
|
||||
std::string shell_quote(const std::string& value) {
|
||||
std::string shell_quote(const std::string &value) {
|
||||
std::string quoted = "'";
|
||||
for (char ch : value) {
|
||||
if (ch == '\'') {
|
||||
@@ -160,8 +161,8 @@ std::string shell_quote(const std::string& value) {
|
||||
return quoted;
|
||||
}
|
||||
|
||||
std::string run_command_capture(const std::string& command) {
|
||||
FILE* pipe = popen(command.c_str(), "r");
|
||||
std::string run_command_capture(const std::string &command) {
|
||||
FILE *pipe = popen(command.c_str(), "r");
|
||||
if (pipe == nullptr) {
|
||||
return {};
|
||||
}
|
||||
@@ -178,7 +179,7 @@ std::string run_command_capture(const std::string& command) {
|
||||
return output;
|
||||
}
|
||||
|
||||
std::vector<std::string> split_preserved_lines(const std::string& text, int fallback_wrap_width) {
|
||||
std::vector<std::string> split_preserved_lines(const std::string &text, int fallback_wrap_width) {
|
||||
std::vector<std::string> lines;
|
||||
std::stringstream stream(text);
|
||||
std::string line;
|
||||
@@ -202,20 +203,21 @@ std::vector<std::string> split_preserved_lines(const std::string& text, int fall
|
||||
return lines;
|
||||
}
|
||||
|
||||
bool command_exists(const char* command) {
|
||||
bool command_exists(const char *command) {
|
||||
static std::map<std::string, bool> cache;
|
||||
const auto cached = cache.find(command);
|
||||
if (cached != cache.end()) {
|
||||
return cached->second;
|
||||
}
|
||||
|
||||
const std::string resolved = run_command_capture("command -v " + std::string(command) + " 2>/dev/null");
|
||||
const std::string resolved =
|
||||
run_command_capture("command -v " + std::string(command) + " 2>/dev/null");
|
||||
const bool exists = !trim_copy(resolved).empty();
|
||||
cache[command] = exists;
|
||||
return exists;
|
||||
}
|
||||
|
||||
void write_terminal_output(const std::string& data) {
|
||||
void write_terminal_output(const std::string &data) {
|
||||
if (data.empty()) {
|
||||
return;
|
||||
}
|
||||
@@ -223,11 +225,11 @@ void write_terminal_output(const std::string& data) {
|
||||
std::fflush(stdout);
|
||||
}
|
||||
|
||||
int run_command_to_terminal(const std::string& command) {
|
||||
int run_command_to_terminal(const std::string &command) {
|
||||
return std::system(command.c_str());
|
||||
}
|
||||
|
||||
bool spawn_detached_process(const std::vector<std::string>& args) {
|
||||
bool spawn_detached_process(const std::vector<std::string> &args) {
|
||||
if (args.empty()) {
|
||||
return false;
|
||||
}
|
||||
@@ -256,10 +258,10 @@ bool spawn_detached_process(const std::vector<std::string>& args) {
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<char*> argv;
|
||||
std::vector<char *> argv;
|
||||
argv.reserve(args.size() + 1);
|
||||
for (const std::string& arg : args) {
|
||||
argv.push_back(const_cast<char*>(arg.c_str()));
|
||||
for (const std::string &arg : args) {
|
||||
argv.push_back(const_cast<char *>(arg.c_str()));
|
||||
}
|
||||
argv.push_back(nullptr);
|
||||
execvp(argv.front(), argv.data());
|
||||
@@ -275,7 +277,7 @@ bool spawn_detached_process(const std::vector<std::string>& args) {
|
||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
||||
}
|
||||
|
||||
bool open_with_system_app(const std::string& path) {
|
||||
bool open_with_system_app(const std::string &path) {
|
||||
if (path.empty()) {
|
||||
return false;
|
||||
}
|
||||
@@ -320,8 +322,8 @@ bool terminal_supports_sixels() {
|
||||
const std::string term = lower_copy(get_env("TERM"));
|
||||
const std::string term_program = lower_copy(get_env("TERM_PROGRAM"));
|
||||
return term == "foot" || term.find("xterm") != std::string::npos ||
|
||||
term.find("mlterm") != std::string::npos || term.find("wezterm") != std::string::npos ||
|
||||
term_program == "wezterm";
|
||||
term.find("mlterm") != std::string::npos ||
|
||||
term.find("wezterm") != std::string::npos || term_program == "wezterm";
|
||||
}
|
||||
|
||||
struct TerminalWindowSize {
|
||||
@@ -333,7 +335,7 @@ struct TerminalWindowSize {
|
||||
|
||||
TerminalWindowSize terminal_window_size() {
|
||||
TerminalWindowSize size;
|
||||
struct winsize ws {};
|
||||
struct winsize ws{};
|
||||
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) {
|
||||
size.cols = ws.ws_col;
|
||||
size.rows = ws.ws_row;
|
||||
@@ -396,14 +398,15 @@ void App::sync_message_attachment_selection() {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& message : chat_it->second.messages) {
|
||||
for (const auto &message : chat_it->second.messages) {
|
||||
if (message.id == message_attachment_message_id_ && message.has_attachment) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
message_attachment_message_id_ = 0;
|
||||
for (auto it = chat_it->second.messages.rbegin(); it != chat_it->second.messages.rend(); ++it) {
|
||||
for (auto it = chat_it->second.messages.rbegin(); it != chat_it->second.messages.rend();
|
||||
++it) {
|
||||
if (it->has_attachment) {
|
||||
message_attachment_message_id_ = it->id;
|
||||
return;
|
||||
@@ -428,7 +431,7 @@ bool App::move_message_attachment_selection(int delta) {
|
||||
}
|
||||
|
||||
std::vector<std::int64_t> attachment_message_ids;
|
||||
for (const auto& message : chat_it->second.messages) {
|
||||
for (const auto &message : chat_it->second.messages) {
|
||||
if (message.has_attachment) {
|
||||
attachment_message_ids.push_back(message.id);
|
||||
}
|
||||
@@ -438,9 +441,7 @@ bool App::move_message_attachment_selection(int delta) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto selected = std::find(
|
||||
attachment_message_ids.begin(),
|
||||
attachment_message_ids.end(),
|
||||
auto selected = std::find(attachment_message_ids.begin(), attachment_message_ids.end(),
|
||||
message_attachment_message_id_);
|
||||
if (selected == attachment_message_ids.end()) {
|
||||
message_attachment_message_id_ = attachment_message_ids.back();
|
||||
@@ -449,11 +450,13 @@ bool App::move_message_attachment_selection(int delta) {
|
||||
|
||||
const std::ptrdiff_t index = selected - attachment_message_ids.begin();
|
||||
const std::ptrdiff_t next_index = index + (delta < 0 ? -1 : 1);
|
||||
if (next_index < 0 || next_index >= static_cast<std::ptrdiff_t>(attachment_message_ids.size())) {
|
||||
if (next_index < 0 ||
|
||||
next_index >= static_cast<std::ptrdiff_t>(attachment_message_ids.size())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
message_attachment_message_id_ = attachment_message_ids[static_cast<std::size_t>(next_index)];
|
||||
message_attachment_message_id_ =
|
||||
attachment_message_ids[static_cast<std::size_t>(next_index)];
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -468,7 +471,7 @@ std::optional<AttachmentInfo> App::selected_message_attachment() const {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
for (const auto& message : chat_it->second.messages) {
|
||||
for (const auto &message : chat_it->second.messages) {
|
||||
if (message.id == message_attachment_message_id_ && message.has_attachment) {
|
||||
return message.attachment;
|
||||
}
|
||||
@@ -502,7 +505,8 @@ std::optional<AttachmentInfo> App::selected_attachment() const {
|
||||
|
||||
const AttachmentType selected_type = kAttachmentTypes[category_index];
|
||||
std::vector<AttachmentInfo> attachments;
|
||||
for (auto it = chat_it->second.messages.rbegin(); it != chat_it->second.messages.rend(); ++it) {
|
||||
for (auto it = chat_it->second.messages.rbegin(); it != chat_it->second.messages.rend();
|
||||
++it) {
|
||||
if (it->has_attachment && it->attachment.type == selected_type) {
|
||||
attachments.push_back(it->attachment);
|
||||
}
|
||||
@@ -522,7 +526,8 @@ std::optional<AttachmentInfo> App::selected_attachment() const {
|
||||
return attachments[static_cast<std::size_t>(selection_index)];
|
||||
}
|
||||
|
||||
std::string App::render_attachment_preview(const AttachmentInfo& attachment, int width, int height) const {
|
||||
std::string App::render_attachment_preview(const AttachmentInfo &attachment, int width,
|
||||
int height) const {
|
||||
const std::string preview_path = attachment_preview_path(attachment);
|
||||
if (preview_path.empty()) {
|
||||
return inline_preview_unavailable_message(attachment);
|
||||
@@ -530,9 +535,10 @@ std::string App::render_attachment_preview(const AttachmentInfo& attachment, int
|
||||
|
||||
const int render_width = std::max(10, width);
|
||||
const int render_height = std::max(4, height);
|
||||
const std::string command =
|
||||
"command -v chafa >/dev/null 2>&1 && chafa --format symbols --symbols all --colors none --size " +
|
||||
std::to_string(render_width) + "x" + std::to_string(render_height) + " " +
|
||||
const std::string command = "command -v chafa >/dev/null 2>&1 && chafa --format symbols "
|
||||
"--symbols all --colors none --size " +
|
||||
std::to_string(render_width) + "x" +
|
||||
std::to_string(render_height) + " " +
|
||||
shell_quote(preview_path) + " 2>/dev/null";
|
||||
std::string preview = run_command_capture(command);
|
||||
if (!preview.empty()) {
|
||||
@@ -541,29 +547,32 @@ std::string App::render_attachment_preview(const AttachmentInfo& attachment, int
|
||||
return "No supported preview backend found. Install `kitten`, `img2sixel`, or `chafa`.";
|
||||
}
|
||||
|
||||
std::string App::build_attachment_preview_graphics(const AttachmentInfo& attachment, int width, int height) const {
|
||||
std::string App::build_attachment_preview_graphics(const AttachmentInfo &attachment, int width,
|
||||
int height) const {
|
||||
const std::string preview_path = attachment_preview_path(attachment);
|
||||
if (preview_path.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::string forced_protocol = configured_image_protocol();
|
||||
const bool want_sixels =
|
||||
forced_protocol == "sixels" || (forced_protocol.empty() && terminal_supports_sixels());
|
||||
const bool want_sixels = forced_protocol == "sixels" ||
|
||||
(forced_protocol.empty() && terminal_supports_sixels());
|
||||
if (want_sixels) {
|
||||
if (command_exists("chafa")) {
|
||||
return "chafa --format sixels --size " + std::to_string(std::max(4, width)) + "x" +
|
||||
return "chafa --format sixels --size " +
|
||||
std::to_string(std::max(4, width)) + "x" +
|
||||
std::to_string(std::max(2, height)) + " " +
|
||||
shell_quote(preview_path) + " >/dev/tty 2>/dev/null";
|
||||
}
|
||||
if (command_exists("img2sixel")) {
|
||||
return "img2sixel -w " + std::to_string(approximate_pixel_width(width)) + "px -h " +
|
||||
std::to_string(approximate_pixel_height(height)) + "px " +
|
||||
return "img2sixel -w " + std::to_string(approximate_pixel_width(width)) +
|
||||
"px -h " + std::to_string(approximate_pixel_height(height)) + "px " +
|
||||
shell_quote(preview_path) + " >/dev/tty 2>/dev/null";
|
||||
}
|
||||
}
|
||||
|
||||
if ((forced_protocol == "kitty" || (forced_protocol.empty() && terminal_supports_kitty_graphics())) &&
|
||||
if ((forced_protocol == "kitty" ||
|
||||
(forced_protocol.empty() && terminal_supports_kitty_graphics())) &&
|
||||
command_exists("kitten")) {
|
||||
TerminalWindowSize size = terminal_window_size();
|
||||
if (size.cols <= 0) {
|
||||
@@ -578,29 +587,34 @@ std::string App::build_attachment_preview_graphics(const AttachmentInfo& attachm
|
||||
if (size.height_pixels <= 0) {
|
||||
size.height_pixels = std::max(1, size.rows * 16);
|
||||
}
|
||||
return "kitten icat --stdin=no --passthrough=none --transfer-mode=stream --align=left "
|
||||
"--place " + std::to_string(std::max(1, width)) + "x" + std::to_string(std::max(1, height)) +
|
||||
"@0x0 --use-window-size " + std::to_string(size.cols) + "," + std::to_string(size.rows) + "," +
|
||||
std::to_string(size.width_pixels) + "," + std::to_string(size.height_pixels) + " " +
|
||||
shell_quote(preview_path) + " >/dev/tty 2>/dev/null";
|
||||
return "kitten icat --stdin=no --passthrough=none --transfer-mode=stream "
|
||||
"--align=left "
|
||||
"--place " +
|
||||
std::to_string(std::max(1, width)) + "x" +
|
||||
std::to_string(std::max(1, height)) + "@0x0 --use-window-size " +
|
||||
std::to_string(size.cols) + "," + std::to_string(size.rows) + "," +
|
||||
std::to_string(size.width_pixels) + "," +
|
||||
std::to_string(size.height_pixels) + " " + shell_quote(preview_path) +
|
||||
" >/dev/tty 2>/dev/null";
|
||||
}
|
||||
|
||||
if (command_exists("chafa")) {
|
||||
return "chafa --size " + std::to_string(std::max(4, width)) + "x" +
|
||||
std::to_string(std::max(2, height)) + " " +
|
||||
shell_quote(preview_path) + " >/dev/tty 2>/dev/null";
|
||||
std::to_string(std::max(2, height)) + " " + shell_quote(preview_path) +
|
||||
" >/dev/tty 2>/dev/null";
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void App::request_attachment_download(const AttachmentInfo& attachment, bool open_after_download) {
|
||||
void App::request_attachment_download(const AttachmentInfo &attachment, bool open_after_download) {
|
||||
if (attachment.file_id == 0) {
|
||||
status_line_ = "Attachment file is unavailable.";
|
||||
return;
|
||||
}
|
||||
|
||||
pending_attachment_open_ = open_after_download ? std::optional<AttachmentInfo>(attachment) : std::nullopt;
|
||||
pending_attachment_open_ =
|
||||
open_after_download ? std::optional<AttachmentInfo>(attachment) : std::nullopt;
|
||||
if (open_after_download) {
|
||||
pending_attachment_download_.reset();
|
||||
}
|
||||
@@ -622,7 +636,7 @@ void App::request_attachment_download(const AttachmentInfo& attachment, bool ope
|
||||
}
|
||||
}
|
||||
|
||||
void App::open_attachment(const AttachmentInfo& attachment) {
|
||||
void App::open_attachment(const AttachmentInfo &attachment) {
|
||||
reset_attachment_viewer_send_preview();
|
||||
if (!attachment.is_downloaded || attachment.local_path.empty()) {
|
||||
request_attachment_download(attachment, true);
|
||||
@@ -639,7 +653,8 @@ void App::open_attachment(const AttachmentInfo& attachment) {
|
||||
if (open_with_system_app(attachment.local_path)) {
|
||||
status_line_ = "Video playback failed. Opened in the system app.";
|
||||
} else {
|
||||
status_line_ = "No supported video player found. Install `mpv` or `ffplay`.";
|
||||
status_line_ =
|
||||
"No supported video player found. Install `mpv` or `ffplay`.";
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -652,7 +667,8 @@ void App::open_attachment(const AttachmentInfo& attachment) {
|
||||
attachment_viewer_frame_index_ = 0;
|
||||
if (attachment.type == AttachmentType::Video) {
|
||||
attachment_viewer_animation_frames_ = attachment_animation_frames(attachment);
|
||||
attachment_viewer_next_frame_at_ = std::chrono::steady_clock::now() + std::chrono::milliseconds(125);
|
||||
attachment_viewer_next_frame_at_ =
|
||||
std::chrono::steady_clock::now() + std::chrono::milliseconds(125);
|
||||
}
|
||||
if (!has_inline_attachment_preview(attachment, preview_width, preview_height)) {
|
||||
attachment_viewer_attachment_.reset();
|
||||
@@ -660,7 +676,8 @@ void App::open_attachment(const AttachmentInfo& attachment) {
|
||||
attachment_viewer_is_animated_ = false;
|
||||
attachment_viewer_frame_index_ = 0;
|
||||
if (open_with_system_app(attachment.local_path)) {
|
||||
status_line_ = "Inline preview unavailable. Opened attachment in the system app.";
|
||||
status_line_ =
|
||||
"Inline preview unavailable. Opened attachment in the system app.";
|
||||
} else {
|
||||
status_line_ = inline_preview_unavailable_message(attachment);
|
||||
}
|
||||
@@ -674,7 +691,7 @@ void App::open_attachment(const AttachmentInfo& attachment) {
|
||||
status_line_ = "Attachment preview.";
|
||||
}
|
||||
|
||||
void App::download_attachment(const AttachmentInfo& attachment) {
|
||||
void App::download_attachment(const AttachmentInfo &attachment) {
|
||||
pending_attachment_open_.reset();
|
||||
if (!attachment.is_downloaded || attachment.local_path.empty()) {
|
||||
pending_attachment_download_ = attachment;
|
||||
@@ -685,7 +702,7 @@ void App::download_attachment(const AttachmentInfo& attachment) {
|
||||
export_attachment_to_downloads(attachment);
|
||||
}
|
||||
|
||||
void App::delete_attachment(const AttachmentInfo& attachment) {
|
||||
void App::delete_attachment(const AttachmentInfo &attachment) {
|
||||
if (!attachment.is_downloaded || attachment.local_path.empty()) {
|
||||
status_line_ = "Attachment is not downloaded.";
|
||||
return;
|
||||
@@ -695,7 +712,8 @@ void App::delete_attachment(const AttachmentInfo& attachment) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (attachment_viewer_attachment_.has_value() && attachment_viewer_attachment_->file_id == attachment.file_id) {
|
||||
if (attachment_viewer_attachment_.has_value() &&
|
||||
attachment_viewer_attachment_->file_id == attachment.file_id) {
|
||||
attachment_viewer_open_ = false;
|
||||
attachment_viewer_attachment_.reset();
|
||||
attachment_viewer_animation_frames_.clear();
|
||||
@@ -712,7 +730,7 @@ void App::delete_attachment(const AttachmentInfo& attachment) {
|
||||
status_line_ = "Deleting cached attachment...";
|
||||
}
|
||||
|
||||
bool App::export_attachment_to_downloads(const AttachmentInfo& attachment) {
|
||||
bool App::export_attachment_to_downloads(const AttachmentInfo &attachment) {
|
||||
if (!attachment.is_downloaded || attachment.local_path.empty()) {
|
||||
status_line_ = "Attachment is not downloaded yet.";
|
||||
return false;
|
||||
@@ -726,20 +744,19 @@ bool App::export_attachment_to_downloads(const AttachmentInfo& attachment) {
|
||||
const std::filesystem::path target_name = preferred_attachment_filename(attachment);
|
||||
try {
|
||||
std::filesystem::create_directories(target_dir);
|
||||
const std::filesystem::path target_path = unique_destination_path(target_dir, target_name);
|
||||
std::filesystem::copy_file(
|
||||
attachment.local_path,
|
||||
target_path,
|
||||
const std::filesystem::path target_path =
|
||||
unique_destination_path(target_dir, target_name);
|
||||
std::filesystem::copy_file(attachment.local_path, target_path,
|
||||
std::filesystem::copy_options::none);
|
||||
status_line_ = "Saved to " + target_path.string();
|
||||
return true;
|
||||
} catch (const std::exception& error) {
|
||||
} catch (const std::exception &error) {
|
||||
status_line_ = std::string("Failed to save attachment: ") + error.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool App::play_video_attachment(const AttachmentInfo& attachment) {
|
||||
bool App::play_video_attachment(const AttachmentInfo &attachment) {
|
||||
if (attachment.local_path.empty() || !std::filesystem::exists(attachment.local_path)) {
|
||||
status_line_ = "Downloaded video file is missing on disk.";
|
||||
return false;
|
||||
@@ -775,7 +792,7 @@ bool App::play_video_attachment(const AttachmentInfo& attachment) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void App::refresh_attachment_viewer_content(const AttachmentInfo& attachment) {
|
||||
void App::refresh_attachment_viewer_content(const AttachmentInfo &attachment) {
|
||||
const int preview_width = COLS > 0 ? COLS - 8 : 72;
|
||||
const int preview_height = LINES > 0 ? LINES - 8 : 24;
|
||||
if (!attachment_viewer_is_animated_ &&
|
||||
@@ -783,20 +800,18 @@ void App::refresh_attachment_viewer_content(const AttachmentInfo& attachment) {
|
||||
attachment_viewer_lines_.clear();
|
||||
return;
|
||||
}
|
||||
const std::string preview = render_attachment_preview(
|
||||
attachment,
|
||||
preview_width,
|
||||
preview_height);
|
||||
attachment_viewer_lines_ = split_preserved_lines(
|
||||
preview,
|
||||
std::max(10, (COLS > 0 ? COLS : 80) - 8));
|
||||
const std::string preview =
|
||||
render_attachment_preview(attachment, preview_width, preview_height);
|
||||
attachment_viewer_lines_ =
|
||||
split_preserved_lines(preview, std::max(10, (COLS > 0 ? COLS : 80) - 8));
|
||||
if (attachment_viewer_lines_.empty()) {
|
||||
attachment_viewer_lines_.push_back("Preview is empty.");
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> App::attachment_animation_frames(const AttachmentInfo& attachment) const {
|
||||
if (attachment.type != AttachmentType::Video || !attachment.is_downloaded || attachment.local_path.empty()) {
|
||||
std::vector<std::string> App::attachment_animation_frames(const AttachmentInfo &attachment) const {
|
||||
if (attachment.type != AttachmentType::Video || !attachment.is_downloaded ||
|
||||
attachment.local_path.empty()) {
|
||||
return {};
|
||||
}
|
||||
if (!std::filesystem::exists(attachment.local_path) || !command_exists("ffmpeg")) {
|
||||
@@ -804,13 +819,14 @@ std::vector<std::string> App::attachment_animation_frames(const AttachmentInfo&
|
||||
}
|
||||
|
||||
const std::filesystem::path preview_dir =
|
||||
files_dir_ / "previews" / (std::string("video-") + std::to_string(attachment.file_id));
|
||||
files_dir_ / "previews" /
|
||||
(std::string("video-") + std::to_string(attachment.file_id));
|
||||
std::vector<std::string> frames;
|
||||
try {
|
||||
std::filesystem::create_directories(preview_dir);
|
||||
const auto source_time = std::filesystem::last_write_time(attachment.local_path);
|
||||
bool needs_regeneration = true;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(preview_dir)) {
|
||||
for (const auto &entry : std::filesystem::directory_iterator(preview_dir)) {
|
||||
if (!entry.is_regular_file() || entry.path().extension() != ".jpg") {
|
||||
continue;
|
||||
}
|
||||
@@ -824,13 +840,13 @@ std::vector<std::string> App::attachment_animation_frames(const AttachmentInfo&
|
||||
return frames;
|
||||
}
|
||||
|
||||
for (const auto& entry : std::filesystem::directory_iterator(preview_dir)) {
|
||||
for (const auto &entry : std::filesystem::directory_iterator(preview_dir)) {
|
||||
if (entry.is_regular_file() && entry.path().extension() == ".jpg") {
|
||||
std::filesystem::remove(entry.path());
|
||||
}
|
||||
}
|
||||
frames.clear();
|
||||
} catch (const std::exception&) {
|
||||
} catch (const std::exception &) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -844,12 +860,12 @@ std::vector<std::string> App::attachment_animation_frames(const AttachmentInfo&
|
||||
}
|
||||
|
||||
try {
|
||||
for (const auto& entry : std::filesystem::directory_iterator(preview_dir)) {
|
||||
for (const auto &entry : std::filesystem::directory_iterator(preview_dir)) {
|
||||
if (entry.is_regular_file() && entry.path().extension() == ".jpg") {
|
||||
frames.push_back(entry.path().string());
|
||||
}
|
||||
}
|
||||
} catch (const std::exception&) {
|
||||
} catch (const std::exception &) {
|
||||
return {};
|
||||
}
|
||||
std::sort(frames.begin(), frames.end());
|
||||
@@ -857,7 +873,8 @@ std::vector<std::string> App::attachment_animation_frames(const AttachmentInfo&
|
||||
}
|
||||
|
||||
bool App::advance_attachment_animation() {
|
||||
if (!attachment_viewer_open_ || !attachment_viewer_is_animated_ || attachment_viewer_animation_frames_.empty() ||
|
||||
if (!attachment_viewer_open_ || !attachment_viewer_is_animated_ ||
|
||||
attachment_viewer_animation_frames_.empty() ||
|
||||
!attachment_viewer_attachment_.has_value()) {
|
||||
return false;
|
||||
}
|
||||
@@ -874,14 +891,15 @@ bool App::advance_attachment_animation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string App::attachment_preview_path(const AttachmentInfo& attachment) const {
|
||||
std::string App::attachment_preview_path(const AttachmentInfo &attachment) const {
|
||||
if (!attachment.is_downloaded || attachment.local_path.empty()) {
|
||||
return {};
|
||||
}
|
||||
if (!std::filesystem::exists(attachment.local_path)) {
|
||||
return {};
|
||||
}
|
||||
if (attachment_viewer_attachment_.has_value() && attachment_viewer_attachment_->file_id == attachment.file_id &&
|
||||
if (attachment_viewer_attachment_.has_value() &&
|
||||
attachment_viewer_attachment_->file_id == attachment.file_id &&
|
||||
!attachment_viewer_animation_frames_.empty() &&
|
||||
attachment_viewer_frame_index_ < attachment_viewer_animation_frames_.size()) {
|
||||
return attachment_viewer_animation_frames_[attachment_viewer_frame_index_];
|
||||
@@ -894,17 +912,19 @@ std::string App::attachment_preview_path(const AttachmentInfo& attachment) const
|
||||
}
|
||||
|
||||
const std::filesystem::path preview_dir = files_dir_ / "previews";
|
||||
const std::filesystem::path preview_path = preview_dir / (std::to_string(attachment.file_id) + ".jpg");
|
||||
const std::filesystem::path preview_path =
|
||||
preview_dir / (std::to_string(attachment.file_id) + ".jpg");
|
||||
try {
|
||||
std::filesystem::create_directories(preview_dir);
|
||||
if (std::filesystem::exists(preview_path)) {
|
||||
const auto source_time = std::filesystem::last_write_time(attachment.local_path);
|
||||
const auto source_time =
|
||||
std::filesystem::last_write_time(attachment.local_path);
|
||||
const auto preview_time = std::filesystem::last_write_time(preview_path);
|
||||
if (preview_time >= source_time) {
|
||||
return preview_path.string();
|
||||
}
|
||||
}
|
||||
} catch (const std::exception&) {
|
||||
} catch (const std::exception &) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -914,7 +934,8 @@ std::string App::attachment_preview_path(const AttachmentInfo& attachment) const
|
||||
shell_quote(preview_path.string()) + " -s 0 -q 8 >/dev/null 2>&1";
|
||||
} else if (command_exists("ffmpeg")) {
|
||||
command = "ffmpeg -y -ss 1 -i " + shell_quote(attachment.local_path) +
|
||||
" -frames:v 1 -vf thumbnail " + shell_quote(preview_path.string()) + " >/dev/null 2>&1";
|
||||
" -frames:v 1 -vf thumbnail " + shell_quote(preview_path.string()) +
|
||||
" >/dev/null 2>&1";
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
@@ -925,11 +946,13 @@ std::string App::attachment_preview_path(const AttachmentInfo& attachment) const
|
||||
return preview_path.string();
|
||||
}
|
||||
|
||||
bool App::has_inline_attachment_preview(const AttachmentInfo& attachment, int width, int height) const {
|
||||
bool App::has_inline_attachment_preview(const AttachmentInfo &attachment, int width,
|
||||
int height) const {
|
||||
if (attachment_preview_path(attachment).empty()) {
|
||||
return false;
|
||||
}
|
||||
return command_exists("chafa") || !build_attachment_preview_graphics(attachment, width, height).empty();
|
||||
return command_exists("chafa") ||
|
||||
!build_attachment_preview_graphics(attachment, width, height).empty();
|
||||
}
|
||||
|
||||
void App::clear_attachment_preview_graphics() {
|
||||
@@ -956,27 +979,28 @@ void App::render_attachment_preview_graphics(int top, int left, int width, int h
|
||||
return;
|
||||
}
|
||||
|
||||
const AttachmentInfo& attachment = *attachment_viewer_attachment_;
|
||||
const AttachmentInfo &attachment = *attachment_viewer_attachment_;
|
||||
const std::string preview_path = attachment_preview_path(attachment);
|
||||
if (preview_path.empty() || attachment_viewer_is_animated_) {
|
||||
clear_attachment_preview_graphics();
|
||||
return;
|
||||
}
|
||||
const std::string forced_protocol = configured_image_protocol();
|
||||
const bool want_kitty =
|
||||
forced_protocol == "kitty" || (forced_protocol.empty() && terminal_supports_kitty_graphics());
|
||||
const std::string signature =
|
||||
preview_path + "|" + std::to_string(width) + "x" + std::to_string(height) + "|" +
|
||||
std::to_string(top) + ":" + std::to_string(left) + "|" + forced_protocol;
|
||||
const bool want_sixels =
|
||||
forced_protocol == "sixels" || (forced_protocol.empty() && terminal_supports_sixels());
|
||||
const bool want_kitty = forced_protocol == "kitty" ||
|
||||
(forced_protocol.empty() && terminal_supports_kitty_graphics());
|
||||
const std::string signature = preview_path + "|" + std::to_string(width) + "x" +
|
||||
std::to_string(height) + "|" + std::to_string(top) + ":" +
|
||||
std::to_string(left) + "|" + forced_protocol;
|
||||
const bool want_sixels = forced_protocol == "sixels" ||
|
||||
(forced_protocol.empty() && terminal_supports_sixels());
|
||||
const std::string command = build_attachment_preview_graphics(attachment, width, height);
|
||||
if (command.empty()) {
|
||||
clear_attachment_preview_graphics();
|
||||
return;
|
||||
}
|
||||
|
||||
const bool same_signature = attachment_preview_signature_ == signature && attachment_preview_graphics_visible_;
|
||||
const bool same_signature =
|
||||
attachment_preview_signature_ == signature && attachment_preview_graphics_visible_;
|
||||
if (!same_signature) {
|
||||
clear_attachment_preview_graphics();
|
||||
}
|
||||
@@ -997,18 +1021,23 @@ void App::render_attachment_preview_graphics(int top, int left, int width, int h
|
||||
}
|
||||
|
||||
const std::string kitty_command =
|
||||
"kitten icat --stdin=no --passthrough=none --transfer-mode=stream --align=left "
|
||||
"--place " + std::to_string(std::max(1, width)) + "x" + std::to_string(std::max(1, height)) +
|
||||
"@" + std::to_string(std::max(0, left)) + "x" + std::to_string(std::max(0, top)) +
|
||||
" --use-window-size " + std::to_string(size.cols) + "," + std::to_string(size.rows) + "," +
|
||||
std::to_string(size.width_pixels) + "," + std::to_string(size.height_pixels) +
|
||||
" " + shell_quote(preview_path) + " >/dev/tty 2>/dev/null";
|
||||
"kitten icat --stdin=no --passthrough=none --transfer-mode=stream "
|
||||
"--align=left "
|
||||
"--place " +
|
||||
std::to_string(std::max(1, width)) + "x" +
|
||||
std::to_string(std::max(1, height)) + "@" +
|
||||
std::to_string(std::max(0, left)) + "x" + std::to_string(std::max(0, top)) +
|
||||
" --use-window-size " + std::to_string(size.cols) + "," +
|
||||
std::to_string(size.rows) + "," + std::to_string(size.width_pixels) + "," +
|
||||
std::to_string(size.height_pixels) + " " + shell_quote(preview_path) +
|
||||
" >/dev/tty 2>/dev/null";
|
||||
if (run_command_to_terminal(kitty_command) != 0) {
|
||||
attachment_preview_graphics_is_sixel_ = false;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
write_terminal_output("\0337\033[" + std::to_string(top + 1) + ";" + std::to_string(left + 1) + "H");
|
||||
write_terminal_output("\0337\033[" + std::to_string(top + 1) + ";" +
|
||||
std::to_string(left + 1) + "H");
|
||||
if (run_command_to_terminal(command) != 0) {
|
||||
write_terminal_output("\0338");
|
||||
attachment_preview_graphics_is_sixel_ = false;
|
||||
@@ -1046,14 +1075,14 @@ void App::handle_attachments_menu_key(int ch) {
|
||||
status_line_ = "Attachment actions.";
|
||||
return;
|
||||
case KEY_LEFT:
|
||||
attachment_category_index_ =
|
||||
(attachment_category_index_ + static_cast<int>(kAttachmentTypes.size()) - 1) %
|
||||
attachment_category_index_ = (attachment_category_index_ +
|
||||
static_cast<int>(kAttachmentTypes.size()) - 1) %
|
||||
static_cast<int>(kAttachmentTypes.size());
|
||||
attachment_selection_index_ = 0;
|
||||
return;
|
||||
case KEY_RIGHT:
|
||||
attachment_category_index_ =
|
||||
(attachment_category_index_ + 1) % static_cast<int>(kAttachmentTypes.size());
|
||||
attachment_category_index_ = (attachment_category_index_ + 1) %
|
||||
static_cast<int>(kAttachmentTypes.size());
|
||||
attachment_selection_index_ = 0;
|
||||
return;
|
||||
case KEY_UP:
|
||||
@@ -1141,9 +1170,11 @@ void App::handle_attachment_viewer_key(int ch) {
|
||||
return;
|
||||
}
|
||||
{
|
||||
const std::string local_path = attachment_viewer_attachment_->local_path;
|
||||
const std::string local_path =
|
||||
attachment_viewer_attachment_->local_path;
|
||||
const std::string caption = attachment_viewer_send_caption_;
|
||||
const auto reply_to_message_id = attachment_viewer_send_reply_to_message_id_;
|
||||
const auto reply_to_message_id =
|
||||
attachment_viewer_send_reply_to_message_id_;
|
||||
const std::int64_t chat_id = attachment_viewer_send_chat_id_;
|
||||
attachment_viewer_open_ = false;
|
||||
attachment_viewer_attachment_.reset();
|
||||
@@ -1152,7 +1183,8 @@ void App::handle_attachment_viewer_key(int ch) {
|
||||
attachment_viewer_frame_index_ = 0;
|
||||
reset_attachment_viewer_send_preview();
|
||||
clear_attachment_preview_graphics();
|
||||
send_photo_message(chat_id, local_path, caption, reply_to_message_id);
|
||||
send_photo_message(chat_id, local_path, caption,
|
||||
reply_to_message_id);
|
||||
}
|
||||
return;
|
||||
default:
|
||||
@@ -1163,8 +1195,7 @@ void App::handle_attachment_viewer_key(int ch) {
|
||||
switch (ch) {
|
||||
case 27:
|
||||
case 'q':
|
||||
case ' ':
|
||||
{
|
||||
case ' ': {
|
||||
const bool was_send_preview = attachment_viewer_send_on_enter_;
|
||||
attachment_viewer_open_ = false;
|
||||
attachment_viewer_attachment_.reset();
|
||||
@@ -1173,8 +1204,7 @@ void App::handle_attachment_viewer_key(int ch) {
|
||||
attachment_viewer_frame_index_ = 0;
|
||||
reset_attachment_viewer_send_preview();
|
||||
clear_attachment_preview_graphics();
|
||||
status_line_ = was_send_preview
|
||||
? "Cancelled clipboard image send."
|
||||
status_line_ = was_send_preview ? "Cancelled clipboard image send."
|
||||
: "Closed attachment preview.";
|
||||
return;
|
||||
}
|
||||
@@ -1217,7 +1247,7 @@ void App::draw_attachments_menu(int height, int width) {
|
||||
const int top = std::max(1, (height - menu_height) / 2);
|
||||
const int left = std::max(1, (width - menu_width) / 2);
|
||||
|
||||
WINDOW* window = newwin(menu_height, menu_width, top, left);
|
||||
WINDOW *window = newwin(menu_height, menu_width, top, left);
|
||||
if (window == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -1231,8 +1261,9 @@ void App::draw_attachments_menu(int height, int width) {
|
||||
}
|
||||
|
||||
const AttachmentType selected_type = kAttachmentTypes[attachment_category_index_];
|
||||
std::vector<const MessageInfo*> attachments;
|
||||
for (auto it = chat_it->second.messages.rbegin(); it != chat_it->second.messages.rend(); ++it) {
|
||||
std::vector<const MessageInfo *> attachments;
|
||||
for (auto it = chat_it->second.messages.rbegin(); it != chat_it->second.messages.rend();
|
||||
++it) {
|
||||
if (it->has_attachment && it->attachment.type == selected_type) {
|
||||
attachments.push_back(&*it);
|
||||
}
|
||||
@@ -1269,7 +1300,8 @@ void App::draw_attachments_menu(int height, int width) {
|
||||
int date_width = std::min(16, std::max(8, content_width / 3));
|
||||
int sender_width = std::max(8, content_width - status_width - size_width - date_width - 3);
|
||||
if (sender_width + status_width + size_width + date_width + 3 > content_width) {
|
||||
date_width = std::max(8, content_width - sender_width - status_width - size_width - 3);
|
||||
date_width =
|
||||
std::max(8, content_width - sender_width - status_width - size_width - 3);
|
||||
}
|
||||
|
||||
mvwaddnstr(window, 3, 2, "Sender", sender_width);
|
||||
@@ -1286,7 +1318,8 @@ void App::draw_attachments_menu(int height, int width) {
|
||||
}
|
||||
|
||||
if (attachments.empty()) {
|
||||
const std::string message = "No " + attachment_type_label(selected_type) + " in this chat.";
|
||||
const std::string message =
|
||||
"No " + attachment_type_label(selected_type) + " in this chat.";
|
||||
mvwaddnstr(window, list_top, 2, message.c_str(), menu_width - 4);
|
||||
} else {
|
||||
for (int row = 0; row < list_height; ++row) {
|
||||
@@ -1297,24 +1330,29 @@ void App::draw_attachments_menu(int height, int width) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const MessageInfo& message = *attachments[static_cast<std::size_t>(item_index)];
|
||||
const MessageInfo &message =
|
||||
*attachments[static_cast<std::size_t>(item_index)];
|
||||
if (item_index == attachment_selection_index_) {
|
||||
wattron(window, A_REVERSE);
|
||||
}
|
||||
const std::string sender = truncate_text(message.sender, sender_width);
|
||||
const std::string dl_status = truncate_text(
|
||||
message.attachment.is_downloading_active && !message.attachment.is_downloaded
|
||||
? attachment_progress_bar(message.attachment, std::max(10, status_width - 2))
|
||||
message.attachment.is_downloading_active &&
|
||||
!message.attachment.is_downloaded
|
||||
? attachment_progress_bar(message.attachment,
|
||||
std::max(10, status_width - 2))
|
||||
: (message.attachment.is_downloaded ? "Ready" : ""),
|
||||
status_width);
|
||||
const std::string file_size = truncate_text(
|
||||
format_file_size(message.attachment.size_bytes),
|
||||
size_width);
|
||||
const std::string sent = truncate_text(format_datetime(message.date), date_width);
|
||||
format_file_size(message.attachment.size_bytes), size_width);
|
||||
const std::string sent =
|
||||
truncate_text(format_datetime(message.date), date_width);
|
||||
mvwaddnstr(window, y, 2, sender.c_str(), sender_width);
|
||||
mvwaddnstr(window, y, 3 + sender_width, dl_status.c_str(), status_width);
|
||||
mvwaddnstr(window, y, 4 + sender_width + status_width, file_size.c_str(), size_width);
|
||||
mvwaddnstr(window, y, 5 + sender_width + status_width + size_width, sent.c_str(), date_width);
|
||||
mvwaddnstr(window, y, 4 + sender_width + status_width, file_size.c_str(),
|
||||
size_width);
|
||||
mvwaddnstr(window, y, 5 + sender_width + status_width + size_width,
|
||||
sent.c_str(), date_width);
|
||||
if (item_index == attachment_selection_index_) {
|
||||
wattroff(window, A_REVERSE);
|
||||
}
|
||||
@@ -1325,7 +1363,8 @@ void App::draw_attachments_menu(int height, int width) {
|
||||
std::string title_line;
|
||||
std::string actions_line = "Left/Right type Up/Down move Space actions Esc close";
|
||||
if (!attachments.empty()) {
|
||||
const MessageInfo& selected = *attachments[static_cast<std::size_t>(attachment_selection_index_)];
|
||||
const MessageInfo &selected =
|
||||
*attachments[static_cast<std::size_t>(attachment_selection_index_)];
|
||||
title_line = truncate_text(selected.attachment.name, menu_width - 4);
|
||||
} else {
|
||||
title_line = truncate_text("No attachment selected.", menu_width - 4);
|
||||
@@ -1350,48 +1389,53 @@ void App::draw_attachment_action_menu(int height, int width) {
|
||||
const int top = std::max(1, (height - menu_height) / 2);
|
||||
const int left = std::max(1, (width - menu_width) / 2);
|
||||
|
||||
WINDOW* window = newwin(menu_height, menu_width, top, left);
|
||||
WINDOW *window = newwin(menu_height, menu_width, top, left);
|
||||
if (window == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
box(window, 0, 0);
|
||||
mvwprintw(window, 0, 2, " Attachment Action ");
|
||||
mvwaddnstr(window, 1, 2, truncate_text(attachment->name, menu_width - 4).c_str(), menu_width - 4);
|
||||
mvwaddnstr(window, 1, 2, truncate_text(attachment->name, menu_width - 4).c_str(),
|
||||
menu_width - 4);
|
||||
mvwhline(window, 3, 1, ACS_HLINE, menu_width - 2);
|
||||
|
||||
const std::vector<std::string> prompt_lines = wrap_text(
|
||||
"What do you want to do with this file?",
|
||||
std::max(10, menu_width - 8));
|
||||
for (std::size_t i = 0; i < prompt_lines.size() && static_cast<int>(i) < menu_height - 9; ++i) {
|
||||
mvwaddnstr(window, 5 + static_cast<int>(i), 4, prompt_lines[i].c_str(), menu_width - 8);
|
||||
const std::vector<std::string> prompt_lines =
|
||||
wrap_text("What do you want to do with this file?", std::max(10, menu_width - 8));
|
||||
for (std::size_t i = 0; i < prompt_lines.size() && static_cast<int>(i) < menu_height - 9;
|
||||
++i) {
|
||||
mvwaddnstr(window, 5 + static_cast<int>(i), 4, prompt_lines[i].c_str(),
|
||||
menu_width - 8);
|
||||
}
|
||||
|
||||
mvwhline(window, menu_height - 5, 1, ACS_HLINE, menu_width - 2);
|
||||
const std::array<const char*, 4> actions = {"Open", "Download", "Delete", "Cancel"};
|
||||
const std::array<const char *, 4> actions = {"Open", "Download", "Delete", "Cancel"};
|
||||
std::vector<std::string> labels;
|
||||
int total_label_width = 0;
|
||||
for (const char* action : actions) {
|
||||
for (const char *action : actions) {
|
||||
labels.push_back(std::string("[ ") + action + " ]");
|
||||
total_label_width += static_cast<int>(labels.back().size());
|
||||
}
|
||||
|
||||
const int button_y = menu_height - 3;
|
||||
const int inner_width = menu_width - 2;
|
||||
const int gap = std::max(2, (inner_width - total_label_width) / (static_cast<int>(labels.size()) + 1));
|
||||
const int gap = std::max(2, (inner_width - total_label_width) /
|
||||
(static_cast<int>(labels.size()) + 1));
|
||||
int button_x = 1 + gap;
|
||||
for (std::size_t i = 0; i < actions.size(); ++i) {
|
||||
if (static_cast<int>(i) == attachment_action_index_) {
|
||||
wattron(window, A_REVERSE | A_BOLD);
|
||||
}
|
||||
mvwaddnstr(window, button_y, button_x, labels[i].c_str(), menu_width - button_x - 2);
|
||||
mvwaddnstr(window, button_y, button_x, labels[i].c_str(),
|
||||
menu_width - button_x - 2);
|
||||
if (static_cast<int>(i) == attachment_action_index_) {
|
||||
wattroff(window, A_REVERSE | A_BOLD);
|
||||
}
|
||||
button_x += static_cast<int>(labels[i].size()) + gap;
|
||||
}
|
||||
|
||||
mvwaddnstr(window, menu_height - 2, 2, "Left/Right move Enter select Esc close", menu_width - 4);
|
||||
mvwaddnstr(window, menu_height - 2, 2, "Left/Right move Enter select Esc close",
|
||||
menu_width - 4);
|
||||
wrefresh(window);
|
||||
delwin(window);
|
||||
}
|
||||
@@ -1402,7 +1446,7 @@ void App::draw_attachment_viewer(int height, int width) {
|
||||
const int top = std::max(1, (height - menu_height) / 2);
|
||||
const int left = std::max(1, (width - menu_width) / 2);
|
||||
|
||||
WINDOW* window = newwin(menu_height, menu_width, top, left);
|
||||
WINDOW *window = newwin(menu_height, menu_width, top, left);
|
||||
if (window == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -1414,7 +1458,8 @@ void App::draw_attachment_viewer(int height, int width) {
|
||||
|
||||
const int content_top = 3;
|
||||
const int content_height = std::max(1, menu_height - 5);
|
||||
const int max_scroll = std::max(0, static_cast<int>(attachment_viewer_lines_.size()) - content_height);
|
||||
const int max_scroll =
|
||||
std::max(0, static_cast<int>(attachment_viewer_lines_.size()) - content_height);
|
||||
if (attachment_viewer_scroll_ > max_scroll) {
|
||||
attachment_viewer_scroll_ = max_scroll;
|
||||
}
|
||||
@@ -1425,10 +1470,7 @@ void App::draw_attachment_viewer(int height, int width) {
|
||||
if (line_index >= static_cast<int>(attachment_viewer_lines_.size())) {
|
||||
continue;
|
||||
}
|
||||
mvwaddnstr(
|
||||
window,
|
||||
content_top + row,
|
||||
2,
|
||||
mvwaddnstr(window, content_top + row, 2,
|
||||
attachment_viewer_lines_[static_cast<std::size_t>(line_index)].c_str(),
|
||||
menu_width - 4);
|
||||
}
|
||||
@@ -1439,7 +1481,8 @@ void App::draw_attachment_viewer(int height, int width) {
|
||||
mvwaddnstr(window, menu_height - 2, 2, controls.c_str(), menu_width - 4);
|
||||
wrefresh(window);
|
||||
if (attachment_viewer_attachment_.has_value()) {
|
||||
render_attachment_preview_graphics(top + content_top, left + 2, menu_width - 4, content_height);
|
||||
render_attachment_preview_graphics(top + content_top, left + 2, menu_width - 4,
|
||||
content_height);
|
||||
} else {
|
||||
clear_attachment_preview_graphics();
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ namespace telegram_tui {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string format_download_progress(std::int64_t downloaded_size, std::int64_t size_bytes, bool is_downloaded) {
|
||||
std::string format_download_progress(std::int64_t downloaded_size, std::int64_t size_bytes,
|
||||
bool is_downloaded) {
|
||||
if (is_downloaded) {
|
||||
return "100%";
|
||||
}
|
||||
@@ -36,7 +37,7 @@ bool App::process_updates() {
|
||||
return changed;
|
||||
}
|
||||
|
||||
void App::handle_td_object(const json& object) {
|
||||
void App::handle_td_object(const json &object) {
|
||||
const std::string type = safe_string(object, "@type");
|
||||
if (type == "updateAuthorizationState") {
|
||||
authorization_state_ = object.value("authorization_state", json::object());
|
||||
@@ -48,7 +49,7 @@ void App::handle_td_object(const json& object) {
|
||||
return;
|
||||
}
|
||||
if (type == "updateChatOnlineMemberCount") {
|
||||
ChatInfo& chat = chats_[safe_i64(object, "chat_id")];
|
||||
ChatInfo &chat = chats_[safe_i64(object, "chat_id")];
|
||||
chat.id = safe_i64(object, "chat_id");
|
||||
chat.online_member_count = safe_i32(object, "online_member_count");
|
||||
chat.has_online_member_count = true;
|
||||
@@ -59,15 +60,19 @@ void App::handle_td_object(const json& object) {
|
||||
return;
|
||||
}
|
||||
if (type == "updateUserStatus") {
|
||||
update_user_status(safe_i64(object, "user_id"), object.value("status", json::object()));
|
||||
update_user_status(safe_i64(object, "user_id"),
|
||||
object.value("status", json::object()));
|
||||
return;
|
||||
}
|
||||
if (type == "updateBasicGroup" || type == "basicGroup") {
|
||||
upsert_basic_group(type == "basicGroup" ? object : object.value("basic_group", json::object()));
|
||||
upsert_basic_group(type == "basicGroup"
|
||||
? object
|
||||
: object.value("basic_group", json::object()));
|
||||
return;
|
||||
}
|
||||
if (type == "updateSupergroup" || type == "supergroup") {
|
||||
upsert_supergroup(type == "supergroup" ? object : object.value("supergroup", json::object()));
|
||||
upsert_supergroup(
|
||||
type == "supergroup" ? object : object.value("supergroup", json::object()));
|
||||
return;
|
||||
}
|
||||
if (type == "updateNewChat") {
|
||||
@@ -75,27 +80,28 @@ void App::handle_td_object(const json& object) {
|
||||
return;
|
||||
}
|
||||
if (type == "updateChatTitle") {
|
||||
ChatInfo& chat = chats_[safe_i64(object, "chat_id")];
|
||||
ChatInfo &chat = chats_[safe_i64(object, "chat_id")];
|
||||
chat.id = safe_i64(object, "chat_id");
|
||||
chat.title = safe_string(object, "title");
|
||||
resort_chats();
|
||||
return;
|
||||
}
|
||||
if (type == "updateChatPosition") {
|
||||
ChatInfo& chat = chats_[safe_i64(object, "chat_id")];
|
||||
ChatInfo &chat = chats_[safe_i64(object, "chat_id")];
|
||||
chat.id = safe_i64(object, "chat_id");
|
||||
apply_chat_position(chat, object.value("position", json::object()));
|
||||
resort_chats();
|
||||
return;
|
||||
}
|
||||
if (type == "updateChatLastMessage") {
|
||||
ChatInfo& chat = chats_[safe_i64(object, "chat_id")];
|
||||
ChatInfo &chat = chats_[safe_i64(object, "chat_id")];
|
||||
chat.id = safe_i64(object, "chat_id");
|
||||
chat.last_message_preview = preview_message(object.value("last_message", json::object()));
|
||||
chat.last_message_preview =
|
||||
preview_message(object.value("last_message", json::object()));
|
||||
if (object.contains("positions") && object.at("positions").is_array()) {
|
||||
chat.in_main_list = false;
|
||||
chat.main_order = 0;
|
||||
for (const auto& position : object.at("positions")) {
|
||||
for (const auto &position : object.at("positions")) {
|
||||
apply_chat_position(chat, position);
|
||||
}
|
||||
}
|
||||
@@ -103,14 +109,14 @@ void App::handle_td_object(const json& object) {
|
||||
return;
|
||||
}
|
||||
if (type == "updateChatReadInbox") {
|
||||
ChatInfo& chat = chats_[safe_i64(object, "chat_id")];
|
||||
ChatInfo &chat = chats_[safe_i64(object, "chat_id")];
|
||||
chat.id = safe_i64(object, "chat_id");
|
||||
chat.unread_count = safe_i32(object, "unread_count");
|
||||
chat.last_read_inbox_message_id = safe_i64(object, "last_read_inbox_message_id");
|
||||
return;
|
||||
}
|
||||
if (type == "updateChatReadOutbox") {
|
||||
ChatInfo& chat = chats_[safe_i64(object, "chat_id")];
|
||||
ChatInfo &chat = chats_[safe_i64(object, "chat_id")];
|
||||
chat.id = safe_i64(object, "chat_id");
|
||||
chat.last_read_outbox_message_id = safe_i64(object, "last_read_outbox_message_id");
|
||||
return;
|
||||
@@ -136,11 +142,12 @@ void App::handle_td_object(const json& object) {
|
||||
if (chat_it == chats_.end()) {
|
||||
return;
|
||||
}
|
||||
for (auto& message : chat_it->second.messages) {
|
||||
for (auto &message : chat_it->second.messages) {
|
||||
if (message.id == message_id) {
|
||||
const json content = object.value("new_content", json::object());
|
||||
message.text = content_to_text(content, true);
|
||||
if (const auto attachment = parse_attachment(content); attachment.has_value()) {
|
||||
if (const auto attachment = parse_attachment(content);
|
||||
attachment.has_value()) {
|
||||
message.has_attachment = true;
|
||||
message.attachment = *attachment;
|
||||
} else {
|
||||
@@ -158,14 +165,14 @@ void App::handle_td_object(const json& object) {
|
||||
return;
|
||||
}
|
||||
std::set<std::int64_t> deleted;
|
||||
for (const auto& id : object.value("message_ids", json::array())) {
|
||||
for (const auto &id : object.value("message_ids", json::array())) {
|
||||
if (id.is_number_integer()) {
|
||||
deleted.insert(id.get<std::int64_t>());
|
||||
}
|
||||
}
|
||||
std::vector<MessageInfo> kept;
|
||||
kept.reserve(chat_it->second.messages.size());
|
||||
for (const auto& message : chat_it->second.messages) {
|
||||
for (const auto &message : chat_it->second.messages) {
|
||||
if (deleted.find(message.id) == deleted.end()) {
|
||||
kept.push_back(message);
|
||||
}
|
||||
@@ -244,7 +251,7 @@ void App::request_more_chats() {
|
||||
}
|
||||
|
||||
bool App::request_chat_history(std::int64_t chat_id, bool force) {
|
||||
ChatInfo& chat = chats_[chat_id];
|
||||
ChatInfo &chat = chats_[chat_id];
|
||||
chat.id = chat_id;
|
||||
if (chat.history_loading && !force) {
|
||||
return false;
|
||||
@@ -283,7 +290,7 @@ void App::mark_chat_messages_as_read(std::int64_t chat_id) {
|
||||
|
||||
std::vector<std::int64_t> unread_message_ids;
|
||||
unread_message_ids.reserve(chat_it->second.messages.size());
|
||||
for (const auto& message : chat_it->second.messages) {
|
||||
for (const auto &message : chat_it->second.messages) {
|
||||
if (message.is_outgoing || message.id == 0) {
|
||||
continue;
|
||||
}
|
||||
@@ -330,7 +337,7 @@ void App::set_open_chat(std::int64_t chat_id) {
|
||||
}
|
||||
|
||||
const bool changed_chat = open_chat_id_ != chat_id;
|
||||
ChatInfo& chat = chats_[chat_id];
|
||||
ChatInfo &chat = chats_[chat_id];
|
||||
chat.id = chat_id;
|
||||
if (tdlib_open_chat_id_ != 0 && tdlib_open_chat_id_ != chat_id) {
|
||||
td_.send({
|
||||
@@ -369,14 +376,14 @@ void App::set_open_chat(std::int64_t chat_id) {
|
||||
}
|
||||
}
|
||||
|
||||
void App::sync_chat_ids_from_response(const json& response) {
|
||||
void App::sync_chat_ids_from_response(const json &response) {
|
||||
if (!response.contains("chat_ids") || !response.at("chat_ids").is_array()) {
|
||||
status_line_ = "Chat list response missing chat ids.";
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<std::int64_t> chat_ids;
|
||||
for (const auto& entry : response.at("chat_ids")) {
|
||||
for (const auto &entry : response.at("chat_ids")) {
|
||||
if (!entry.is_number_integer()) {
|
||||
continue;
|
||||
}
|
||||
@@ -393,7 +400,8 @@ void App::sync_chat_ids_from_response(const json& response) {
|
||||
if (!chat_ids.empty()) {
|
||||
sorted_chat_ids_ = std::move(chat_ids);
|
||||
if (selected_chat_index_ >= static_cast<int>(sorted_chat_ids_.size())) {
|
||||
selected_chat_index_ = std::max(0, static_cast<int>(sorted_chat_ids_.size()) - 1);
|
||||
selected_chat_index_ =
|
||||
std::max(0, static_cast<int>(sorted_chat_ids_.size()) - 1);
|
||||
}
|
||||
if (!open_chat_id().has_value()) {
|
||||
const auto chat_id = highlighted_chat_id();
|
||||
@@ -410,10 +418,11 @@ void App::sync_chat_ids_from_response(const json& response) {
|
||||
}
|
||||
|
||||
void App::append_message(std::int64_t chat_id, MessageInfo message) {
|
||||
ChatInfo& chat = chats_[chat_id];
|
||||
ChatInfo &chat = chats_[chat_id];
|
||||
chat.id = chat_id;
|
||||
auto existing = std::find_if(chat.messages.begin(), chat.messages.end(),
|
||||
[&](const MessageInfo& item) { return item.id == message.id; });
|
||||
auto existing =
|
||||
std::find_if(chat.messages.begin(), chat.messages.end(),
|
||||
[&](const MessageInfo &item) { return item.id == message.id; });
|
||||
if (existing != chat.messages.end()) {
|
||||
*existing = std::move(message);
|
||||
} else {
|
||||
@@ -421,7 +430,7 @@ void App::append_message(std::int64_t chat_id, MessageInfo message) {
|
||||
}
|
||||
|
||||
std::sort(chat.messages.begin(), chat.messages.end(),
|
||||
[](const MessageInfo& lhs, const MessageInfo& rhs) {
|
||||
[](const MessageInfo &lhs, const MessageInfo &rhs) {
|
||||
if (lhs.date != rhs.date) {
|
||||
return lhs.date < rhs.date;
|
||||
}
|
||||
@@ -429,10 +438,10 @@ void App::append_message(std::int64_t chat_id, MessageInfo message) {
|
||||
});
|
||||
|
||||
if (kMaxMessagesPerChat > 0 && chat.messages.size() > kMaxMessagesPerChat) {
|
||||
chat.messages.erase(
|
||||
chat.messages.begin(),
|
||||
chat.messages.erase(chat.messages.begin(),
|
||||
chat.messages.begin() +
|
||||
static_cast<std::ptrdiff_t>(chat.messages.size() - kMaxMessagesPerChat));
|
||||
static_cast<std::ptrdiff_t>(chat.messages.size() -
|
||||
kMaxMessagesPerChat));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,20 +455,18 @@ void App::remove_message(std::int64_t chat_id, std::int64_t message_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& messages = chat_it->second.messages;
|
||||
auto &messages = chat_it->second.messages;
|
||||
messages.erase(
|
||||
std::remove_if(
|
||||
messages.begin(),
|
||||
messages.end(),
|
||||
[&](const MessageInfo& item) { return item.id == message_id; }),
|
||||
std::remove_if(messages.begin(), messages.end(),
|
||||
[&](const MessageInfo &item) { return item.id == message_id; }),
|
||||
messages.end());
|
||||
}
|
||||
|
||||
void App::merge_history(std::int64_t chat_id, const json& messages) {
|
||||
ChatInfo& chat = chats_[chat_id];
|
||||
void App::merge_history(std::int64_t chat_id, const json &messages) {
|
||||
ChatInfo &chat = chats_[chat_id];
|
||||
chat.history_loading = false;
|
||||
chat.history_loaded = true;
|
||||
for (const auto& message : messages) {
|
||||
for (const auto &message : messages) {
|
||||
append_message(chat_id, parse_message(message));
|
||||
}
|
||||
if (chat_id == open_chat_id_) {
|
||||
@@ -468,13 +475,14 @@ void App::merge_history(std::int64_t chat_id, const json& messages) {
|
||||
status_line_ = "History loaded.";
|
||||
}
|
||||
|
||||
void App::update_attachment_file(const json& file) {
|
||||
void App::update_attachment_file(const json &file) {
|
||||
const std::int32_t file_id = safe_i32(file, "id");
|
||||
if (file_id == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::int64_t size_bytes = std::max(safe_i64(file, "size"), safe_i64(file, "expected_size"));
|
||||
const std::int64_t size_bytes =
|
||||
std::max(safe_i64(file, "size"), safe_i64(file, "expected_size"));
|
||||
const json local = file.value("local", json::object());
|
||||
const std::string local_path = safe_string(local, "path");
|
||||
const std::int64_t downloaded_size = safe_i64(local, "downloaded_size");
|
||||
@@ -483,9 +491,9 @@ void App::update_attachment_file(const json& file) {
|
||||
const bool can_be_downloaded = local.value("can_be_downloaded", false);
|
||||
const bool can_be_deleted = local.value("can_be_deleted", false);
|
||||
|
||||
for (auto& [chat_id, chat] : chats_) {
|
||||
(void) chat_id;
|
||||
for (auto& message : chat.messages) {
|
||||
for (auto &[chat_id, chat] : chats_) {
|
||||
(void)chat_id;
|
||||
for (auto &message : chat.messages) {
|
||||
if (!message.has_attachment || message.attachment.file_id != file_id) {
|
||||
continue;
|
||||
}
|
||||
@@ -502,7 +510,8 @@ void App::update_attachment_file(const json& file) {
|
||||
}
|
||||
|
||||
if (pending_attachment_open_.has_value() && pending_attachment_open_->file_id == file_id) {
|
||||
pending_attachment_open_->size_bytes = size_bytes > 0 ? size_bytes : pending_attachment_open_->size_bytes;
|
||||
pending_attachment_open_->size_bytes =
|
||||
size_bytes > 0 ? size_bytes : pending_attachment_open_->size_bytes;
|
||||
pending_attachment_open_->downloaded_size = downloaded_size;
|
||||
pending_attachment_open_->local_path = local_path;
|
||||
pending_attachment_open_->is_downloading_active = is_downloading_active;
|
||||
@@ -510,9 +519,9 @@ void App::update_attachment_file(const json& file) {
|
||||
pending_attachment_open_->can_be_downloaded = can_be_downloaded;
|
||||
pending_attachment_open_->can_be_deleted = can_be_deleted;
|
||||
if (!is_downloaded && is_downloading_active) {
|
||||
status_line_ = "Downloading attachment to open " +
|
||||
format_download_progress(
|
||||
pending_attachment_open_->downloaded_size,
|
||||
status_line_ =
|
||||
"Downloading attachment to open " +
|
||||
format_download_progress(pending_attachment_open_->downloaded_size,
|
||||
pending_attachment_open_->size_bytes,
|
||||
pending_attachment_open_->is_downloaded);
|
||||
}
|
||||
@@ -522,7 +531,8 @@ void App::update_attachment_file(const json& file) {
|
||||
}
|
||||
}
|
||||
|
||||
if (pending_attachment_download_.has_value() && pending_attachment_download_->file_id == file_id) {
|
||||
if (pending_attachment_download_.has_value() &&
|
||||
pending_attachment_download_->file_id == file_id) {
|
||||
pending_attachment_download_->size_bytes =
|
||||
size_bytes > 0 ? size_bytes : pending_attachment_download_->size_bytes;
|
||||
pending_attachment_download_->downloaded_size = downloaded_size;
|
||||
|
||||
@@ -9,7 +9,7 @@ int main() {
|
||||
try {
|
||||
telegram_tui::App app;
|
||||
return app.run();
|
||||
} catch (const std::exception& error) {
|
||||
} catch (const std::exception &error) {
|
||||
endwin();
|
||||
std::fprintf(stderr, "fatal: %s\n", error.what());
|
||||
return 1;
|
||||
|
||||
@@ -9,9 +9,9 @@ namespace telegram_tui {
|
||||
namespace {
|
||||
|
||||
void configure_tdlib_logging() {
|
||||
const char* stream_request =
|
||||
const char *stream_request =
|
||||
R"({"@type":"setLogStream","log_stream":{"@type":"logStreamEmpty"}})";
|
||||
const char* verbosity_request =
|
||||
const char *verbosity_request =
|
||||
R"({"@type":"setLogVerbosityLevel","new_verbosity_level":0})";
|
||||
|
||||
td_json_client_execute(nullptr, stream_request);
|
||||
@@ -31,14 +31,14 @@ TdClient::~TdClient() {
|
||||
}
|
||||
}
|
||||
|
||||
void TdClient::send(const json& request) {
|
||||
void TdClient::send(const json &request) {
|
||||
const std::string payload = request.dump();
|
||||
td_json_client_send(client_, payload.c_str());
|
||||
}
|
||||
|
||||
std::optional<json> TdClient::execute(const json& request) {
|
||||
std::optional<json> TdClient::execute(const json &request) {
|
||||
const std::string payload = request.dump();
|
||||
const char* raw = td_json_client_execute(client_, payload.c_str());
|
||||
const char *raw = td_json_client_execute(client_, payload.c_str());
|
||||
if (raw == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -51,7 +51,7 @@ std::optional<json> TdClient::execute(const json& request) {
|
||||
}
|
||||
|
||||
std::optional<json> TdClient::receive(double timeout_seconds) {
|
||||
const char* raw = td_json_client_receive(client_, timeout_seconds);
|
||||
const char *raw = td_json_client_receive(client_, timeout_seconds);
|
||||
if (raw == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
@@ -11,15 +11,15 @@ class TdClient {
|
||||
TdClient();
|
||||
~TdClient();
|
||||
|
||||
TdClient(const TdClient&) = delete;
|
||||
TdClient& operator=(const TdClient&) = delete;
|
||||
TdClient(const TdClient &) = delete;
|
||||
TdClient &operator=(const TdClient &) = delete;
|
||||
|
||||
void send(const json& request);
|
||||
[[nodiscard]] std::optional<json> execute(const json& request);
|
||||
void send(const json &request);
|
||||
[[nodiscard]] std::optional<json> execute(const json &request);
|
||||
[[nodiscard]] std::optional<json> receive(double timeout_seconds);
|
||||
|
||||
private:
|
||||
void* client_ = nullptr;
|
||||
void *client_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace telegram_tui
|
||||
|
||||
45
src/util.cpp
45
src/util.cpp
@@ -33,7 +33,8 @@ 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) {
|
||||
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;
|
||||
@@ -77,8 +78,8 @@ std::uint32_t decode_utf8_codepoint(const std::string& text, std::size_t offset,
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string get_env(const char* name) {
|
||||
const char* value = std::getenv(name);
|
||||
std::string get_env(const char *name) {
|
||||
const char *value = std::getenv(name);
|
||||
return value == nullptr ? std::string() : std::string(value);
|
||||
}
|
||||
|
||||
@@ -94,7 +95,7 @@ std::string trim_copy(std::string value) {
|
||||
}
|
||||
|
||||
std::string single_line(std::string text) {
|
||||
for (char& c : text) {
|
||||
for (char &c : text) {
|
||||
if (c == '\n' || c == '\r' || c == '\t') {
|
||||
c = ' ';
|
||||
}
|
||||
@@ -102,16 +103,16 @@ std::string single_line(std::string text) {
|
||||
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); });
|
||||
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') {
|
||||
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') {
|
||||
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";
|
||||
@@ -134,12 +135,12 @@ StoredConfig load_app_config() {
|
||||
safe_string(config, "api_hash"),
|
||||
config.value("auto_reload_chat_history", false),
|
||||
};
|
||||
} catch (const json::exception&) {
|
||||
} catch (const json::exception &) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
bool save_app_config(const StoredConfig& config) {
|
||||
bool save_app_config(const StoredConfig &config) {
|
||||
const std::filesystem::path path = data_root() / "config.json";
|
||||
try {
|
||||
if (path.has_parent_path()) {
|
||||
@@ -161,26 +162,26 @@ bool save_app_config(const StoredConfig& config) {
|
||||
}
|
||||
output << document.dump(2) << '\n';
|
||||
return static_cast<bool>(output);
|
||||
} catch (const std::exception&) {
|
||||
} catch (const std::exception &) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::string safe_string(const json& object, const char* key) {
|
||||
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) {
|
||||
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) {
|
||||
std::int32_t safe_i32(const json &object, const char *key) {
|
||||
if (!object.contains(key) || !object.at(key).is_number_integer()) {
|
||||
return 0;
|
||||
}
|
||||
@@ -228,7 +229,7 @@ std::string format_file_size(std::int64_t size_bytes) {
|
||||
return "?";
|
||||
}
|
||||
|
||||
static constexpr const char* units[] = {"B", "KB", "MB", "GB", "TB"};
|
||||
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)) {
|
||||
@@ -243,7 +244,7 @@ std::string format_file_size(std::int64_t size_bytes) {
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
std::vector<std::string> wrap_text(const std::string& text, int width) {
|
||||
std::vector<std::string> wrap_text(const std::string &text, int width) {
|
||||
if (width <= 1) {
|
||||
return {text};
|
||||
}
|
||||
@@ -286,7 +287,7 @@ std::vector<std::string> wrap_text(const std::string& text, int width) {
|
||||
return lines;
|
||||
}
|
||||
|
||||
std::size_t utf8_byte_index_from_utf16_offset(const std::string& text, std::size_t utf16_offset) {
|
||||
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) {
|
||||
@@ -305,7 +306,7 @@ std::size_t utf8_byte_index_from_utf16_offset(const std::string& text, std::size
|
||||
return byte_index;
|
||||
}
|
||||
|
||||
std::size_t utf8_prev_index(const std::string& text, std::size_t 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;
|
||||
}
|
||||
@@ -317,7 +318,7 @@ std::size_t utf8_prev_index(const std::string& text, std::size_t byte_index) {
|
||||
return index;
|
||||
}
|
||||
|
||||
std::size_t utf8_next_index(const std::string& text, std::size_t byte_index) {
|
||||
std::size_t utf8_next_index(const std::string &text, std::size_t byte_index) {
|
||||
if (byte_index >= text.size()) {
|
||||
return text.size();
|
||||
}
|
||||
@@ -327,7 +328,7 @@ std::size_t utf8_next_index(const std::string& text, std::size_t byte_index) {
|
||||
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) {
|
||||
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;
|
||||
@@ -351,7 +352,7 @@ int utf8_display_width(const std::string& text, std::size_t byte_limit) {
|
||||
return width;
|
||||
}
|
||||
|
||||
void pop_utf8_back(std::string& text) {
|
||||
void pop_utf8_back(std::string &text) {
|
||||
if (text.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
26
src/util.h
26
src/util.h
@@ -15,25 +15,27 @@ struct StoredConfig {
|
||||
bool auto_reload_chat_history = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] std::string get_env(const char* name);
|
||||
[[nodiscard]] std::string get_env(const char *name);
|
||||
[[nodiscard]] std::string trim_copy(std::string value);
|
||||
[[nodiscard]] std::string single_line(std::string text);
|
||||
[[nodiscard]] bool is_decimal_number(const std::string& value);
|
||||
[[nodiscard]] bool is_decimal_number(const std::string &value);
|
||||
[[nodiscard]] std::filesystem::path data_root();
|
||||
[[nodiscard]] StoredConfig load_app_config();
|
||||
bool save_app_config(const StoredConfig& config);
|
||||
[[nodiscard]] std::string safe_string(const json& object, const char* key);
|
||||
[[nodiscard]] std::int64_t safe_i64(const json& object, const char* key);
|
||||
[[nodiscard]] std::int32_t safe_i32(const json& object, const char* key);
|
||||
bool save_app_config(const StoredConfig &config);
|
||||
[[nodiscard]] std::string safe_string(const json &object, const char *key);
|
||||
[[nodiscard]] std::int64_t safe_i64(const json &object, const char *key);
|
||||
[[nodiscard]] std::int32_t safe_i32(const json &object, const char *key);
|
||||
[[nodiscard]] std::string format_time(std::int32_t unix_time);
|
||||
[[nodiscard]] std::string format_date(std::int32_t unix_time);
|
||||
[[nodiscard]] std::string format_datetime(std::int32_t unix_time);
|
||||
[[nodiscard]] std::string format_file_size(std::int64_t size_bytes);
|
||||
[[nodiscard]] std::vector<std::string> wrap_text(const std::string& text, int width);
|
||||
[[nodiscard]] std::size_t utf8_byte_index_from_utf16_offset(const std::string& text, std::size_t utf16_offset);
|
||||
[[nodiscard]] std::size_t utf8_prev_index(const std::string& text, std::size_t byte_index);
|
||||
[[nodiscard]] std::size_t utf8_next_index(const std::string& text, std::size_t byte_index);
|
||||
[[nodiscard]] int utf8_display_width(const std::string& text, std::size_t byte_limit = std::string::npos);
|
||||
void pop_utf8_back(std::string& text);
|
||||
[[nodiscard]] std::vector<std::string> wrap_text(const std::string &text, int width);
|
||||
[[nodiscard]] std::size_t utf8_byte_index_from_utf16_offset(const std::string &text,
|
||||
std::size_t utf16_offset);
|
||||
[[nodiscard]] std::size_t utf8_prev_index(const std::string &text, std::size_t byte_index);
|
||||
[[nodiscard]] std::size_t utf8_next_index(const std::string &text, std::size_t byte_index);
|
||||
[[nodiscard]] int utf8_display_width(const std::string &text,
|
||||
std::size_t byte_limit = std::string::npos);
|
||||
void pop_utf8_back(std::string &text);
|
||||
|
||||
} // namespace telegram_tui
|
||||
|
||||
Reference in New Issue
Block a user