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