#include "app.h" #include #include #include #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(text.size()) <= max_width) { return text; } if (max_width <= 3) { return text.substr(0, static_cast(max_width)); } text.resize(static_cast(max_width - 3)); text += "..."; return text; } } // 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_BUILD_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(auth_label.size()) - 2); 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(footer_hint.size()) - 2); if (footer_hint_x > 1 + static_cast(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 (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 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(target_chat_ids.size())) { forward_target_index_ = static_cast(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(target_chat_ids.size())) { continue; } const std::int64_t chat_id = target_chat_ids[static_cast(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); } 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