Format remaining C++ sources

This commit is contained in:
2026-04-24 06:35:05 +03:00
parent 70c2e77957
commit 4910fb5e8e
10 changed files with 610 additions and 554 deletions

View File

@@ -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();
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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