409 lines
13 KiB
C++
409 lines
13 KiB
C++
#include "app.h"
|
|
|
|
#include <algorithm>
|
|
#include <clocale>
|
|
#include <sstream>
|
|
|
|
#include <curses.h>
|
|
|
|
#include "app_ui.h"
|
|
#include "build_config.h"
|
|
#include "util.h"
|
|
|
|
namespace telegram_tui {
|
|
|
|
namespace {
|
|
|
|
std::string truncate_to_width(std::string text, int max_width) {
|
|
if (max_width <= 0) {
|
|
return {};
|
|
}
|
|
if (static_cast<int>(text.size()) <= max_width) {
|
|
return text;
|
|
}
|
|
if (max_width <= 3) {
|
|
return text.substr(0, static_cast<std::size_t>(max_width));
|
|
}
|
|
text.resize(static_cast<std::size_t>(max_width - 3));
|
|
text += "...";
|
|
return text;
|
|
}
|
|
|
|
std::vector<std::string> split_preview_lines(const std::string &text) {
|
|
std::vector<std::string> lines;
|
|
std::stringstream stream(text);
|
|
std::string line;
|
|
while (std::getline(stream, line)) {
|
|
if (!line.empty() && line.back() == '\r') {
|
|
line.pop_back();
|
|
}
|
|
lines.push_back(line);
|
|
}
|
|
if (lines.empty()) {
|
|
lines.push_back(text);
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void App::init_curses() {
|
|
std::setlocale(LC_ALL, "");
|
|
initscr();
|
|
init_colors();
|
|
cbreak();
|
|
noecho();
|
|
keypad(stdscr, TRUE);
|
|
#ifdef NCURSES_VERSION
|
|
set_escdelay(25);
|
|
#endif
|
|
timeout(kPollTimeoutMs);
|
|
curs_set(0);
|
|
}
|
|
|
|
void App::init_colors() {
|
|
if (!has_colors()) {
|
|
return;
|
|
}
|
|
|
|
start_color();
|
|
use_default_colors();
|
|
init_pair(kColorPairSenderBlue, COLOR_BLUE, -1);
|
|
init_pair(kColorPairSenderCyan, COLOR_CYAN, -1);
|
|
init_pair(kColorPairSenderGreen, COLOR_GREEN, -1);
|
|
init_pair(kColorPairSenderYellow, COLOR_YELLOW, -1);
|
|
init_pair(kColorPairSenderMagenta, COLOR_MAGENTA, -1);
|
|
init_pair(kColorPairSenderRed, COLOR_RED, -1);
|
|
init_pair(kColorPairLink, COLOR_CYAN, -1);
|
|
init_pair(kColorPairMarkdown, COLOR_MAGENTA, -1);
|
|
init_pair(kColorPairTimestamp, COLOR_YELLOW, -1);
|
|
init_pair(kColorPairOpenChat, COLOR_GREEN, -1);
|
|
}
|
|
|
|
void App::shutdown_curses() {
|
|
endwin();
|
|
}
|
|
|
|
void App::draw() {
|
|
erase();
|
|
|
|
int height = 0;
|
|
int width = 0;
|
|
getmaxyx(stdscr, height, width);
|
|
if (height < 8 || width < 40) {
|
|
mvprintw(0, 0, "Terminal too small.");
|
|
refresh();
|
|
return;
|
|
}
|
|
|
|
const int header_y = 0;
|
|
const int content_top = 1;
|
|
const int footer_y = height - 2;
|
|
const int input_y = height - 1;
|
|
const int content_height = footer_y - content_top;
|
|
const int chat_width = std::max(24, width / 3);
|
|
const int message_width = width - chat_width - 1;
|
|
|
|
attron(A_REVERSE);
|
|
mvhline(header_y, 0, ' ', width);
|
|
std::string header_label = std::string("shinoa ") + TELEGRAM_TUI_PROJECT_VERSION;
|
|
if (use_test_dc_) {
|
|
header_label += " [TEST DC]";
|
|
}
|
|
mvprintw(header_y, 1, "%s", header_label.c_str());
|
|
const std::string auth_label = authorized_ ? "ready" : current_auth_label();
|
|
const int auth_x = std::max(1, width - static_cast<int>(auth_label.size()) - 2);
|
|
if (!update_notice_.empty()) {
|
|
const std::string centered_notice =
|
|
truncate_to_width(update_notice_, std::max(1, width - 4));
|
|
const int notice_x =
|
|
std::max(1, (width - static_cast<int>(centered_notice.size())) / 2);
|
|
if (notice_x > 1 + static_cast<int>(header_label.size()) &&
|
|
notice_x + static_cast<int>(centered_notice.size()) < auth_x - 1) {
|
|
attron(A_BOLD);
|
|
mvprintw(header_y, notice_x, "%s", centered_notice.c_str());
|
|
attroff(A_BOLD);
|
|
}
|
|
}
|
|
mvprintw(header_y, auth_x, "%s", auth_label.c_str());
|
|
attroff(A_REVERSE);
|
|
|
|
mvvline(content_top, chat_width, ACS_VLINE, content_height);
|
|
draw_chat_pane(content_top, content_height, chat_width);
|
|
draw_message_pane(content_top, content_height, chat_width + 1, message_width);
|
|
|
|
attron(A_REVERSE);
|
|
mvhline(footer_y, 0, ' ', width);
|
|
const std::string footer_status = use_test_dc_ ? "[TEST DC] " + status_line_ : status_line_;
|
|
mvprintw(footer_y, 1, "%s", footer_status.c_str());
|
|
const std::string footer_hint = "? for help";
|
|
const int footer_hint_x = std::max(1, width - static_cast<int>(footer_hint.size()) - 2);
|
|
if (footer_hint_x > 1 + static_cast<int>(footer_status.size())) {
|
|
attron(A_DIM);
|
|
mvprintw(footer_y, footer_hint_x, "%s", footer_hint.c_str());
|
|
attroff(A_DIM);
|
|
}
|
|
attroff(A_REVERSE);
|
|
|
|
const std::string help =
|
|
input_mode_ == InputMode::None
|
|
? "? for help"
|
|
: input_prompt_ + ": " +
|
|
(input_hidden_ ? std::string(input_buffer_.size(), '*')
|
|
: input_buffer_);
|
|
mvhline(input_y, 0, ' ', width);
|
|
mvprintw(input_y, 0, "%s", help.c_str());
|
|
if (input_mode_ != InputMode::None) {
|
|
const std::string prefix = input_prompt_ + ": ";
|
|
const int cursor_x = std::min(
|
|
width - 1, utf8_display_width(prefix) +
|
|
utf8_display_width(input_buffer_, input_cursor_));
|
|
move(input_y, std::max(0, cursor_x));
|
|
}
|
|
|
|
refresh();
|
|
|
|
if (attachment_viewer_open_) {
|
|
draw_attachment_viewer(height, width);
|
|
} else if (attachment_action_menu_open_) {
|
|
draw_attachment_action_menu(height, width);
|
|
} else if (saved_animation_menu_open_) {
|
|
clear_attachment_preview_graphics();
|
|
draw_saved_animation_menu(height, width);
|
|
} else if (attachments_menu_open_) {
|
|
draw_attachments_menu(height, width);
|
|
} else if (forward_target_menu_open_) {
|
|
draw_forward_target_menu(height, width);
|
|
} else if (help_menu_open_) {
|
|
draw_help_menu(height, width);
|
|
} else {
|
|
clear_attachment_preview_graphics();
|
|
}
|
|
}
|
|
|
|
void App::draw_forward_target_menu(int height, int width) {
|
|
const std::vector<std::int64_t> target_chat_ids = forward_target_chat_ids();
|
|
if (target_chat_ids.empty()) {
|
|
forward_target_menu_open_ = false;
|
|
status_line_ = "No non-channel chats available to forward to.";
|
|
return;
|
|
}
|
|
|
|
const int menu_width = std::min(width - 4, 72);
|
|
const int menu_height = std::min(height - 4, 20);
|
|
const int top = std::max(1, (height - menu_height) / 2);
|
|
const int left = std::max(1, (width - menu_width) / 2);
|
|
|
|
WINDOW *window = newwin(menu_height, menu_width, top, left);
|
|
if (window == nullptr) {
|
|
return;
|
|
}
|
|
|
|
if (forward_target_index_ < 0) {
|
|
forward_target_index_ = 0;
|
|
}
|
|
if (forward_target_index_ >= static_cast<int>(target_chat_ids.size())) {
|
|
forward_target_index_ = static_cast<int>(target_chat_ids.size()) - 1;
|
|
}
|
|
|
|
box(window, 0, 0);
|
|
mvwprintw(window, 0, 2, " Forward To ");
|
|
|
|
std::string source_label =
|
|
forward_message_ids_.size() == 1
|
|
? "1 message"
|
|
: (std::to_string(forward_message_ids_.size()) + " messages");
|
|
const auto source_chat_it = chats_.find(forward_source_chat_id_);
|
|
if (source_chat_it != chats_.end()) {
|
|
if (forward_message_ids_.size() == 1) {
|
|
const auto message_index = find_message_index(source_chat_it->second,
|
|
forward_message_ids_.front());
|
|
if (message_index.has_value()) {
|
|
source_label =
|
|
"Message [" + std::to_string(*message_index + 1) + "]";
|
|
}
|
|
}
|
|
if (!source_chat_it->second.title.empty()) {
|
|
source_label += " from " + source_chat_it->second.title;
|
|
}
|
|
}
|
|
mvwaddnstr(window, 1, 2, truncate_to_width(source_label, menu_width - 4).c_str(),
|
|
menu_width - 4);
|
|
mvwhline(window, 2, 1, ACS_HLINE, menu_width - 2);
|
|
|
|
const int list_top = 3;
|
|
const int list_height = std::max(1, menu_height - 5);
|
|
int first_index = 0;
|
|
if (forward_target_index_ >= list_height) {
|
|
first_index = forward_target_index_ - list_height + 1;
|
|
}
|
|
|
|
for (int row = 0; row < list_height; ++row) {
|
|
const int item_index = first_index + row;
|
|
const int y = list_top + row;
|
|
mvwhline(window, y, 1, ' ', menu_width - 2);
|
|
if (item_index >= static_cast<int>(target_chat_ids.size())) {
|
|
continue;
|
|
}
|
|
|
|
const std::int64_t chat_id = target_chat_ids[static_cast<std::size_t>(item_index)];
|
|
const auto chat_it = chats_.find(chat_id);
|
|
std::string label = chat_it != chats_.end() ? chat_it->second.title
|
|
: ("Chat " + std::to_string(chat_id));
|
|
if (chat_id == forward_source_chat_id_) {
|
|
label += " (current)";
|
|
}
|
|
if (item_index == forward_target_index_) {
|
|
wattron(window, A_REVERSE | A_BOLD);
|
|
}
|
|
mvwaddnstr(window, y, 2, truncate_to_width(label, menu_width - 4).c_str(),
|
|
menu_width - 4);
|
|
if (item_index == forward_target_index_) {
|
|
wattroff(window, A_REVERSE | A_BOLD);
|
|
}
|
|
}
|
|
|
|
mvwaddnstr(window, menu_height - 1, 2, "Enter forward Esc cancel Up/Down move",
|
|
menu_width - 4);
|
|
wrefresh(window);
|
|
delwin(window);
|
|
}
|
|
|
|
void App::draw_saved_animation_menu(int height, int width) {
|
|
const int menu_width = std::min(width - 4, 110);
|
|
const int menu_height = std::min(height - 4, 28);
|
|
const int top = std::max(1, (height - menu_height) / 2);
|
|
const int left = std::max(1, (width - menu_width) / 2);
|
|
|
|
WINDOW *window = newwin(menu_height, menu_width, top, left);
|
|
if (window == nullptr) {
|
|
return;
|
|
}
|
|
|
|
if (saved_animation_selection_index_ < 0) {
|
|
saved_animation_selection_index_ = 0;
|
|
}
|
|
if (saved_animation_selection_index_ >= static_cast<int>(saved_animations_.size())) {
|
|
saved_animation_selection_index_ =
|
|
std::max(0, static_cast<int>(saved_animations_.size()) - 1);
|
|
}
|
|
|
|
box(window, 0, 0);
|
|
mvwprintw(window, 0, 2, " Saved GIFs ");
|
|
std::string subtitle = saved_animations_loading_
|
|
? "Loading..."
|
|
: std::to_string(saved_animations_.size()) + " saved";
|
|
mvwaddnstr(window, 1, 2, subtitle.c_str(), menu_width - 4);
|
|
mvwhline(window, 2, 1, ACS_HLINE, menu_width - 2);
|
|
|
|
const int list_width = std::max(28, menu_width / 2 - 1);
|
|
const int preview_left = list_width + 2;
|
|
const int preview_width = std::max(10, menu_width - preview_left - 2);
|
|
const int list_top = 3;
|
|
const int list_height = std::max(1, menu_height - 6);
|
|
int first_index = 0;
|
|
if (saved_animation_selection_index_ >= list_height) {
|
|
first_index = saved_animation_selection_index_ - list_height + 1;
|
|
}
|
|
|
|
for (int row = 0; row < list_height; ++row) {
|
|
const int item_index = first_index + row;
|
|
const int y = list_top + row;
|
|
mvwhline(window, y, 1, ' ', list_width);
|
|
if (item_index >= static_cast<int>(saved_animations_.size())) {
|
|
continue;
|
|
}
|
|
|
|
const SavedAnimationInfo &animation =
|
|
saved_animations_[static_cast<std::size_t>(item_index)];
|
|
std::string label = animation.name;
|
|
if (animation.is_downloading_active && !animation.is_downloaded) {
|
|
label += " [dl]";
|
|
} else if (animation.is_downloaded) {
|
|
label += " [ready]";
|
|
}
|
|
if (item_index == saved_animation_selection_index_) {
|
|
wattron(window, A_REVERSE | A_BOLD);
|
|
}
|
|
mvwaddnstr(window, y, 2, truncate_to_width(label, list_width - 2).c_str(),
|
|
list_width - 2);
|
|
if (item_index == saved_animation_selection_index_) {
|
|
wattroff(window, A_REVERSE | A_BOLD);
|
|
}
|
|
}
|
|
|
|
mvwvline(window, 3, preview_left - 1, ACS_VLINE, menu_height - 4);
|
|
|
|
if (saved_animations_.empty()) {
|
|
mvwaddnstr(window, 4, preview_left, "No saved GIFs on this account.", preview_width);
|
|
} else {
|
|
const SavedAnimationInfo &animation = saved_animations_[static_cast<std::size_t>(
|
|
saved_animation_selection_index_)];
|
|
const std::string title = truncate_to_width(animation.name, preview_width);
|
|
mvwaddnstr(window, 3, preview_left, title.c_str(), preview_width);
|
|
const std::string meta =
|
|
truncate_to_width(format_file_size(animation.size_bytes) + " " +
|
|
std::to_string(std::max(0, animation.width)) + "x" +
|
|
std::to_string(std::max(0, animation.height)) + " " +
|
|
std::to_string(std::max(0, animation.duration)) + "s",
|
|
preview_width);
|
|
mvwaddnstr(window, 4, preview_left, meta.c_str(), preview_width);
|
|
mvwhline(window, 5, preview_left, ACS_HLINE, preview_width);
|
|
|
|
AttachmentInfo preview_attachment;
|
|
preview_attachment.type = AttachmentType::Animation;
|
|
preview_attachment.name = animation.name;
|
|
preview_attachment.size_bytes = animation.size_bytes;
|
|
preview_attachment.downloaded_size = animation.downloaded_size;
|
|
preview_attachment.file_id = animation.file_id;
|
|
preview_attachment.local_path = animation.local_path;
|
|
preview_attachment.is_downloading_active = animation.is_downloading_active;
|
|
preview_attachment.can_be_downloaded = animation.can_be_downloaded;
|
|
preview_attachment.can_be_deleted = animation.can_be_deleted;
|
|
preview_attachment.is_downloaded = animation.is_downloaded;
|
|
|
|
const std::string preview = render_attachment_preview(
|
|
preview_attachment, preview_width, std::max(4, list_height - 4));
|
|
const std::vector<std::string> preview_lines = split_preview_lines(preview);
|
|
for (std::size_t i = 0; i < preview_lines.size() &&
|
|
static_cast<int>(i) < list_height - 3;
|
|
++i) {
|
|
mvwaddnstr(window, 6 + static_cast<int>(i), preview_left,
|
|
truncate_to_width(preview_lines[i], preview_width).c_str(), preview_width);
|
|
}
|
|
}
|
|
|
|
mvwaddnstr(window, menu_height - 2, 2,
|
|
"Enter send r refresh Up/Down move Esc close", menu_width - 4);
|
|
wrefresh(window);
|
|
delwin(window);
|
|
}
|
|
|
|
std::string App::current_auth_label() const {
|
|
const std::string type = safe_string(authorization_state_, "@type");
|
|
if (type == "authorizationStateWaitTdlibParameters") {
|
|
return "need API";
|
|
}
|
|
if (type == "authorizationStateWaitPhoneNumber") {
|
|
return "need phone";
|
|
}
|
|
if (type == "authorizationStateWaitCode") {
|
|
return "need code";
|
|
}
|
|
if (type == "authorizationStateWaitEncryptionKey") {
|
|
return "unlock db";
|
|
}
|
|
if (type == "authorizationStateWaitPassword") {
|
|
return "need password";
|
|
}
|
|
if (type == "authorizationStateReady") {
|
|
return "ready";
|
|
}
|
|
if (type.empty()) {
|
|
return "starting";
|
|
}
|
|
return type;
|
|
}
|
|
|
|
} // namespace telegram_tui
|