LCOV - code coverage report
Current view: top level - src/main - tg_tui.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 14.1 % 576 81
Test Date: 2026-04-20 19:54:22 Functions: 13.3 % 30 4

            Line data    Source code
       1              : /* SPDX-License-Identifier: GPL-3.0-or-later */
       2              : /* Copyright 2026 Peter Csaszar */
       3              : 
       4              : /**
       5              :  * @file main/tg_tui.c
       6              :  * @brief tg-tui — interactive Telegram client entry point.
       7              :  *
       8              :  * V1 is an interactive command shell (readline-backed). Since P5-03/06
       9              :  * landed, the TUI also links tg-domain-write and exposes send / reply /
      10              :  * edit / delete / forward / read commands. A full curses-style TUI with
      11              :  * panes and live redraw is tracked under US-11 v2.
      12              :  */
      13              : 
      14              : #include "app/bootstrap.h"
      15              : #include "app/auth_flow.h"
      16              : #include "app/config_wizard.h"
      17              : #include "app/credentials.h"
      18              : #include "app/dc_config.h"
      19              : #include "app/session_store.h"
      20              : #include "infrastructure/auth_logout.h"
      21              : #include "logger.h"
      22              : 
      23              : #include "readline.h"
      24              : #include "platform/terminal.h"
      25              : #include "arg_parse.h"
      26              : 
      27              : #include "domain/read/self.h"
      28              : #include "domain/read/dialogs.h"
      29              : #include "domain/read/history.h"
      30              : #include "domain/read/updates.h"
      31              : #include "domain/read/user_info.h"
      32              : #include "domain/read/contacts.h"
      33              : #include "domain/read/search.h"
      34              : #include "domain/read/media.h"
      35              : 
      36              : #include "infrastructure/media_index.h"
      37              : 
      38              : #include "platform/path.h"
      39              : #include "fs_util.h"
      40              : 
      41              : #include "domain/write/send.h"
      42              : #include "domain/write/edit.h"
      43              : #include "domain/write/delete.h"
      44              : #include "domain/write/forward.h"
      45              : #include "domain/write/read_history.h"
      46              : #include "domain/write/upload.h"
      47              : 
      48              : #include "tui/app.h"
      49              : 
      50              : #include <stdio.h>
      51              : #include <stdlib.h>
      52              : #include <string.h>
      53              : 
      54              : /* ---- Credential callbacks: batch values fall back to interactive prompts ---- */
      55              : 
      56              : typedef struct {
      57              :     LineHistory *hist;
      58              :     const char  *phone;     /**< from --phone (may be NULL → interactive) */
      59              :     const char  *code;      /**< from --code  (may be NULL → interactive) */
      60              :     const char  *password;  /**< from --password (may be NULL → interactive) */
      61              : } PromptCtx;
      62              : 
      63            0 : static int tui_read_line(const char *label, char *out, size_t cap,
      64              :                           LineHistory *hist) {
      65              :     char prompt[64];
      66            0 :     snprintf(prompt, sizeof(prompt), "%s: ", label);
      67            0 :     int n = rl_readline(prompt, out, cap, hist);
      68            0 :     return (n < 0) ? -1 : 0;
      69              : }
      70              : 
      71            0 : static int cb_get_phone(void *u, char *out, size_t cap) {
      72            0 :     PromptCtx *c = (PromptCtx *)u;
      73            0 :     if (c->phone) { snprintf(out, cap, "%s", c->phone); return 0; }
      74            0 :     return tui_read_line("phone (+...)", out, cap, c->hist);
      75              : }
      76            0 : static int cb_get_code(void *u, char *out, size_t cap) {
      77            0 :     PromptCtx *c = (PromptCtx *)u;
      78            0 :     if (c->code) { snprintf(out, cap, "%s", c->code); return 0; }
      79            0 :     return tui_read_line("code", out, cap, c->hist);
      80              : }
      81            0 : static int cb_get_password(void *u, char *out, size_t cap) {
      82            0 :     PromptCtx *c = (PromptCtx *)u;
      83            0 :     if (c->password) { snprintf(out, cap, "%s", c->password); return 0; }
      84            0 :     return tui_read_line("2FA password", out, cap, c->hist);
      85              : }
      86              : 
      87              : /* ---- Forward label helpers ---- */
      88              : 
      89              : static const char *kind_label(DialogPeerKind k);
      90              : static const char *resolved_label(ResolvedKind k);
      91              : 
      92              : /* ---- Commands dispatched from the interactive loop ---- */
      93              : 
      94            0 : static void do_me(const ApiConfig *cfg, MtProtoSession *s, Transport *t) {
      95            0 :     SelfInfo me = {0};
      96            0 :     if (domain_get_self(cfg, s, t, &me) != 0) {
      97            0 :         puts("me: request failed");
      98            0 :         return;
      99              :     }
     100            0 :     printf("id=%lld  @%s  %s %s  +%s  premium=%s\n",
     101            0 :            (long long)me.id,
     102            0 :            me.username[0] ? me.username : "(none)",
     103              :            me.first_name, me.last_name,
     104            0 :            me.phone[0] ? me.phone : "(hidden)",
     105            0 :            me.is_premium ? "yes" : "no");
     106              : }
     107              : 
     108            0 : static void do_dialogs(const ApiConfig *cfg, MtProtoSession *s, Transport *t,
     109              :                        int limit) {
     110            0 :     if (limit <= 0 || limit > 100) limit = 20;
     111            0 :     DialogEntry entries[100] = {0};
     112            0 :     int count = 0;
     113            0 :     if (domain_get_dialogs(cfg, s, t, limit, 0, entries, &count, NULL) != 0) {
     114            0 :         puts("dialogs: request failed");
     115            0 :         return;
     116              :     }
     117            0 :     printf("%-8s %6s %-32s %s\n", "type", "unread", "title", "@username / id");
     118            0 :     for (int i = 0; i < count; i++) {
     119            0 :         const char *title = entries[i].title[0] ? entries[i].title : "(no title)";
     120            0 :         if (entries[i].username[0]) {
     121            0 :             printf("%-8s %6d %-32s @%s\n",
     122              :                    kind_label(entries[i].kind),
     123            0 :                    entries[i].unread_count, title, entries[i].username);
     124              :         } else {
     125            0 :             printf("%-8s %6d %-32s %lld\n",
     126              :                    kind_label(entries[i].kind),
     127              :                    entries[i].unread_count, title,
     128            0 :                    (long long)entries[i].peer_id);
     129              :         }
     130              :     }
     131              : }
     132              : 
     133            0 : static const char *kind_label(DialogPeerKind k) {
     134            0 :     switch (k) {
     135            0 :     case DIALOG_PEER_USER:    return "user";
     136            0 :     case DIALOG_PEER_CHAT:    return "chat";
     137            0 :     case DIALOG_PEER_CHANNEL: return "channel";
     138            0 :     default:                  return "unknown";
     139              :     }
     140              : }
     141              : 
     142            0 : static const char *resolved_label(ResolvedKind k) {
     143            0 :     switch (k) {
     144            0 :     case RESOLVED_KIND_USER:    return "user";
     145            0 :     case RESOLVED_KIND_CHAT:    return "chat";
     146            0 :     case RESOLVED_KIND_CHANNEL: return "channel";
     147            0 :     default:                    return "unknown";
     148              :     }
     149              : }
     150              : 
     151            0 : static int resolve_history_peer(const ApiConfig *cfg, MtProtoSession *s,
     152              :                                  Transport *t, const char *arg,
     153              :                                  HistoryPeer *out) {
     154            0 :     if (!arg || !*arg || !strcmp(arg, "self")) {
     155            0 :         out->kind = HISTORY_PEER_SELF; out->peer_id = 0; out->access_hash = 0;
     156            0 :         return 0;
     157              :     }
     158            0 :     ResolvedPeer rp = {0};
     159            0 :     if (domain_resolve_username(cfg, s, t, arg, &rp) != 0) return -1;
     160            0 :     switch (rp.kind) {
     161            0 :     case RESOLVED_KIND_USER:    out->kind = HISTORY_PEER_USER;    break;
     162            0 :     case RESOLVED_KIND_CHANNEL: out->kind = HISTORY_PEER_CHANNEL; break;
     163            0 :     case RESOLVED_KIND_CHAT:    out->kind = HISTORY_PEER_CHAT;    break;
     164            0 :     default: return -1;
     165              :     }
     166            0 :     out->peer_id = rp.id;
     167            0 :     out->access_hash = rp.access_hash;
     168            0 :     if ((out->kind == HISTORY_PEER_USER || out->kind == HISTORY_PEER_CHANNEL)
     169            0 :         && !rp.have_hash) return -1;
     170            0 :     return 0;
     171              : }
     172              : 
     173            0 : static void do_history_any(const ApiConfig *cfg, MtProtoSession *s,
     174              :                             Transport *t, const char *arg, int limit) {
     175            0 :     if (limit <= 0 || limit > 100) limit = 20;
     176            0 :     HistoryPeer peer = {0};
     177            0 :     if (resolve_history_peer(cfg, s, t, arg, &peer) != 0) {
     178            0 :         printf("history: could not resolve '%s'\n", arg ? arg : "self");
     179            0 :         return;
     180              :     }
     181            0 :     HistoryEntry entries[100] = {0};
     182            0 :     int count = 0;
     183            0 :     if (domain_get_history(cfg, s, t, &peer, 0, limit, entries, &count) != 0) {
     184            0 :         puts("history: request failed");
     185            0 :         return;
     186              :     }
     187            0 :     for (int i = 0; i < count; i++) {
     188            0 :         printf("[%d] %s %lld %s\n",
     189              :                entries[i].id,
     190            0 :                entries[i].out ? ">" : "<",
     191            0 :                (long long)entries[i].date,
     192            0 :                entries[i].complex ? "(complex — text not parsed)"
     193              :                                    : entries[i].text);
     194              :     }
     195            0 :     if (count == 0) puts("(no messages)");
     196              : }
     197              : 
     198            0 : static void do_contacts(const ApiConfig *cfg, MtProtoSession *s, Transport *t) {
     199            0 :     ContactEntry entries[64] = {0};
     200            0 :     int count = 0;
     201            0 :     if (domain_get_contacts(cfg, s, t, entries, 64, &count) != 0) {
     202            0 :         puts("contacts: request failed");
     203            0 :         return;
     204              :     }
     205            0 :     printf("%-18s %s\n", "user_id", "mutual");
     206            0 :     for (int i = 0; i < count; i++) {
     207            0 :         printf("%-18lld %s\n",
     208            0 :                (long long)entries[i].user_id,
     209            0 :                entries[i].mutual ? "yes" : "no");
     210              :     }
     211            0 :     if (count == 0) puts("(no contacts)");
     212              : }
     213              : 
     214            0 : static void do_info(const ApiConfig *cfg, MtProtoSession *s, Transport *t,
     215              :                     const char *peer) {
     216            0 :     if (!peer || !*peer) { puts("usage: info <@name>"); return; }
     217            0 :     ResolvedPeer r = {0};
     218            0 :     if (domain_resolve_username(cfg, s, t, peer, &r) != 0) {
     219            0 :         puts("info: resolve failed");
     220            0 :         return;
     221              :     }
     222            0 :     printf("type:         %s\nid:           %lld\nusername:     @%s\n"
     223              :            "access_hash:  %s\n",
     224              :            resolved_label(r.kind),
     225            0 :            (long long)r.id,
     226            0 :            r.username[0] ? r.username : "",
     227            0 :            r.have_hash ? "present" : "none");
     228              : }
     229              : 
     230              : /* Returns 1 if the token looks like a peer specifier:
     231              :  *   @username, a numeric id, or the literal "self". */
     232            0 : static int is_peer_token(const char *tok) {
     233            0 :     if (!tok || !*tok) return 0;
     234            0 :     if (*tok == '@') return tok[1] != '\0';
     235            0 :     if (!strcmp(tok, "self")) return 1;
     236              :     /* Pure numeric (possibly negative) → peer id */
     237            0 :     const char *p = tok;
     238            0 :     if (*p == '-') p++;
     239            0 :     if (!*p) return 0;
     240            0 :     while (*p >= '0' && *p <= '9') p++;
     241            0 :     return (*p == '\0');
     242              : }
     243              : 
     244            0 : static void do_search(const ApiConfig *cfg, MtProtoSession *s, Transport *t,
     245              :                       char *arg) {
     246            0 :     if (!arg || !*arg) {
     247            0 :         puts("usage: search [<peer>] <query>\n"
     248              :              "  <peer>  @username, numeric id, or 'self' (omit for global search)");
     249            0 :         return;
     250              :     }
     251              : 
     252              :     /* Split arg on the first whitespace to check for an optional peer token. */
     253            0 :     char peer_buf[128] = "";
     254            0 :     const char *query = arg;
     255            0 :     char *space = arg;
     256            0 :     while (*space && *space != ' ' && *space != '\t') space++;
     257            0 :     if (*space) {
     258              :         /* There is a second token — check if the first is a peer. */
     259            0 :         size_t pn = (size_t)(space - arg);
     260            0 :         char first[128] = "";
     261            0 :         if (pn < sizeof(first)) { memcpy(first, arg, pn); first[pn] = '\0'; }
     262            0 :         if (is_peer_token(first)) {
     263            0 :             memcpy(peer_buf, first, pn + 1);
     264            0 :             query = space;
     265            0 :             while (*query == ' ' || *query == '\t') query++;
     266              :         }
     267              :     }
     268              : 
     269            0 :     if (!*query) {
     270            0 :         puts("usage: search [<peer>] <query>");
     271            0 :         return;
     272              :     }
     273              : 
     274            0 :     HistoryEntry e[20] = {0};
     275            0 :     int n = 0;
     276              : 
     277            0 :     if (peer_buf[0]) {
     278              :         /* Per-peer search */
     279            0 :         HistoryPeer peer = {0};
     280            0 :         if (resolve_history_peer(cfg, s, t, peer_buf, &peer) != 0) {
     281            0 :             printf("search: cannot resolve '%s'\n", peer_buf);
     282            0 :             return;
     283              :         }
     284            0 :         if (domain_search_peer(cfg, s, t, &peer, query, 20, e, &n) != 0) {
     285            0 :             puts("search: request failed");
     286            0 :             return;
     287              :         }
     288              :     } else {
     289              :         /* Global search */
     290            0 :         if (domain_search_global(cfg, s, t, query, 20, e, &n) != 0) {
     291            0 :             puts("search: request failed");
     292            0 :             return;
     293              :         }
     294              :     }
     295              : 
     296            0 :     for (int i = 0; i < n; i++) {
     297            0 :         printf("%d  %lld  %s\n", e[i].id, (long long)e[i].date,
     298            0 :                e[i].complex ? "(complex)" : e[i].text);
     299              :     }
     300            0 :     if (n == 0) puts("(no matches)");
     301              : }
     302              : 
     303              : /* ---- Write commands ---- */
     304              : 
     305              : /* Split "arg" on the first whitespace. Returns pointer to the remainder
     306              :  * (may be empty). The first token is NUL-terminated in place. */
     307            0 : static char *split_rest(char *arg) {
     308            0 :     while (*arg && *arg != ' ' && *arg != '\t') arg++;
     309            0 :     if (!*arg) return arg;
     310            0 :     *arg++ = '\0';
     311            0 :     while (*arg == ' ' || *arg == '\t') arg++;
     312            0 :     return arg;
     313              : }
     314              : 
     315            0 : static void do_send(const ApiConfig *cfg, MtProtoSession *s, Transport *t,
     316              :                      char *arg) {
     317            0 :     if (!arg || !*arg) { puts("usage: send <peer> <text>"); return; }
     318            0 :     char *text = split_rest(arg);
     319            0 :     if (!*text) { puts("usage: send <peer> <text>"); return; }
     320              : 
     321            0 :     HistoryPeer peer = {0};
     322            0 :     if (resolve_history_peer(cfg, s, t, arg, &peer) != 0) {
     323            0 :         printf("send: cannot resolve '%s'\n", arg);
     324            0 :         return;
     325              :     }
     326            0 :     int32_t new_id = 0;
     327            0 :     RpcError err = {0};
     328            0 :     if (domain_send_message(cfg, s, t, &peer, text, &new_id, &err) != 0) {
     329            0 :         printf("send: failed (%d: %s)\n", err.error_code, err.error_msg);
     330            0 :         return;
     331              :     }
     332            0 :     if (new_id > 0) printf("sent, id=%d\n", new_id);
     333            0 :     else            puts("sent");
     334              : }
     335              : 
     336            0 : static void do_reply(const ApiConfig *cfg, MtProtoSession *s, Transport *t,
     337              :                       char *arg) {
     338            0 :     if (!arg || !*arg) { puts("usage: reply <peer> <msg_id> <text>"); return; }
     339            0 :     char *rest = split_rest(arg);
     340            0 :     if (!*rest) { puts("usage: reply <peer> <msg_id> <text>"); return; }
     341            0 :     char *text = split_rest(rest);
     342            0 :     if (!*text) { puts("usage: reply <peer> <msg_id> <text>"); return; }
     343            0 :     int32_t mid = atoi(rest);
     344            0 :     if (mid <= 0) { puts("reply: <msg_id> must be positive"); return; }
     345              : 
     346            0 :     HistoryPeer peer = {0};
     347            0 :     if (resolve_history_peer(cfg, s, t, arg, &peer) != 0) {
     348            0 :         printf("reply: cannot resolve '%s'\n", arg); return;
     349              :     }
     350            0 :     int32_t new_id = 0; RpcError err = {0};
     351            0 :     if (domain_send_message_reply(cfg, s, t, &peer, text, mid,
     352              :                                     &new_id, &err) != 0) {
     353            0 :         printf("reply: failed (%d: %s)\n", err.error_code, err.error_msg);
     354            0 :         return;
     355              :     }
     356            0 :     if (new_id > 0) printf("sent, id=%d (reply to %d)\n", new_id, mid);
     357            0 :     else            printf("sent (reply to %d)\n", mid);
     358              : }
     359              : 
     360            0 : static void do_edit(const ApiConfig *cfg, MtProtoSession *s, Transport *t,
     361              :                      char *arg) {
     362            0 :     if (!arg || !*arg) { puts("usage: edit <peer> <msg_id> <text>"); return; }
     363            0 :     char *rest = split_rest(arg);
     364            0 :     if (!*rest) { puts("usage: edit <peer> <msg_id> <text>"); return; }
     365            0 :     char *text = split_rest(rest);
     366            0 :     if (!*text) { puts("usage: edit <peer> <msg_id> <text>"); return; }
     367            0 :     int32_t mid = atoi(rest);
     368            0 :     if (mid <= 0) { puts("edit: <msg_id> must be positive"); return; }
     369              : 
     370            0 :     HistoryPeer peer = {0};
     371            0 :     if (resolve_history_peer(cfg, s, t, arg, &peer) != 0) {
     372            0 :         printf("edit: cannot resolve '%s'\n", arg); return;
     373              :     }
     374            0 :     RpcError err = {0};
     375            0 :     if (domain_edit_message(cfg, s, t, &peer, mid, text, &err) != 0) {
     376            0 :         printf("edit: failed (%d: %s)\n", err.error_code, err.error_msg);
     377            0 :         return;
     378              :     }
     379            0 :     printf("edited %d\n", mid);
     380              : }
     381              : 
     382            0 : static void do_delete(const ApiConfig *cfg, MtProtoSession *s, Transport *t,
     383              :                        char *arg) {
     384            0 :     if (!arg || !*arg) { puts("usage: delete <peer> <msg_id> [revoke]"); return; }
     385            0 :     char *rest = split_rest(arg);
     386            0 :     if (!*rest) { puts("usage: delete <peer> <msg_id> [revoke]"); return; }
     387            0 :     int revoke = 0;
     388            0 :     char *id_tok = rest;
     389            0 :     char *extra = split_rest(rest);
     390            0 :     if (*extra && !strcmp(extra, "revoke")) revoke = 1;
     391            0 :     int32_t mid = atoi(id_tok);
     392            0 :     if (mid <= 0) { puts("delete: <msg_id> must be positive"); return; }
     393              : 
     394            0 :     HistoryPeer peer = {0};
     395            0 :     if (resolve_history_peer(cfg, s, t, arg, &peer) != 0) {
     396            0 :         printf("delete: cannot resolve '%s'\n", arg); return;
     397              :     }
     398            0 :     int32_t ids[1] = { mid };
     399            0 :     RpcError err = {0};
     400            0 :     if (domain_delete_messages(cfg, s, t, &peer, ids, 1, revoke, &err) != 0) {
     401            0 :         printf("delete: failed (%d: %s)\n", err.error_code, err.error_msg);
     402            0 :         return;
     403              :     }
     404            0 :     printf("deleted %d%s\n", mid, revoke ? " (revoke)" : "");
     405              : }
     406              : 
     407            0 : static void do_forward(const ApiConfig *cfg, MtProtoSession *s, Transport *t,
     408              :                         char *arg) {
     409            0 :     if (!arg || !*arg) { puts("usage: forward <from> <to> <msg_id>"); return; }
     410            0 :     char *rest = split_rest(arg);
     411            0 :     if (!*rest) { puts("usage: forward <from> <to> <msg_id>"); return; }
     412            0 :     char *id_tok = split_rest(rest);
     413            0 :     if (!*id_tok) { puts("usage: forward <from> <to> <msg_id>"); return; }
     414            0 :     int32_t mid = atoi(id_tok);
     415            0 :     if (mid <= 0) { puts("forward: <msg_id> must be positive"); return; }
     416              : 
     417            0 :     HistoryPeer from = {0}, to = {0};
     418            0 :     if (resolve_history_peer(cfg, s, t, arg, &from) != 0
     419            0 :         || resolve_history_peer(cfg, s, t, rest, &to) != 0) {
     420            0 :         puts("forward: cannot resolve peers"); return;
     421              :     }
     422            0 :     int32_t ids[1] = { mid };
     423            0 :     RpcError err = {0};
     424            0 :     if (domain_forward_messages(cfg, s, t, &from, &to, ids, 1, &err) != 0) {
     425            0 :         printf("forward: failed (%d: %s)\n", err.error_code, err.error_msg);
     426            0 :         return;
     427              :     }
     428            0 :     printf("forwarded %d\n", mid);
     429              : }
     430              : 
     431            0 : static void do_upload(const ApiConfig *cfg, MtProtoSession *s, Transport *t,
     432              :                        char *arg) {
     433            0 :     if (!arg || !*arg) { puts("usage: upload <peer> <path> [caption]"); return; }
     434            0 :     char *path = split_rest(arg);
     435            0 :     if (!*path) { puts("usage: upload <peer> <path> [caption]"); return; }
     436            0 :     char *caption = split_rest(path);
     437            0 :     HistoryPeer peer = {0};
     438            0 :     if (resolve_history_peer(cfg, s, t, arg, &peer) != 0) {
     439            0 :         printf("upload: cannot resolve '%s'\n", arg); return;
     440              :     }
     441            0 :     RpcError err = {0};
     442            0 :     int as_photo = domain_path_is_image(path);
     443            0 :     int rc = as_photo
     444            0 :         ? domain_send_photo(cfg, s, t, &peer, path,
     445            0 :                              *caption ? caption : NULL, &err)
     446            0 :         : domain_send_file (cfg, s, t, &peer, path,
     447            0 :                              *caption ? caption : NULL, NULL, &err);
     448            0 :     if (rc != 0) {
     449            0 :         printf("upload: failed (%d: %s)\n", err.error_code, err.error_msg);
     450            0 :         return;
     451              :     }
     452            0 :     printf("uploaded %s as %s\n", path, as_photo ? "photo" : "document");
     453              : }
     454              : 
     455            0 : static void do_download(const ApiConfig *cfg, MtProtoSession *s, Transport *t,
     456              :                          char *arg) {
     457              :     /* Syntax: download <peer> <msg_id> [out] */
     458            0 :     if (!arg || !*arg) {
     459            0 :         puts("usage: download <peer> <msg_id> [out]");
     460            0 :         return;
     461              :     }
     462            0 :     char *rest = split_rest(arg);
     463            0 :     if (!*rest) { puts("usage: download <peer> <msg_id> [out]"); return; }
     464            0 :     char *out_arg = split_rest(rest);
     465              : 
     466            0 :     int32_t mid = atoi(rest);
     467            0 :     if (mid <= 0) { puts("download: <msg_id> must be positive"); return; }
     468              : 
     469            0 :     HistoryPeer peer = {0};
     470            0 :     if (resolve_history_peer(cfg, s, t, arg, &peer) != 0) {
     471            0 :         printf("download: cannot resolve '%s'\n", arg);
     472            0 :         return;
     473              :     }
     474              : 
     475              :     /* Fetch the single message using offset_id = msg_id + 1, limit = 1. */
     476            0 :     HistoryEntry entry = {0};
     477            0 :     int count = 0;
     478            0 :     if (domain_get_history(cfg, s, t, &peer, mid + 1, 1, &entry, &count) != 0
     479            0 :         || count == 0 || entry.id != mid) {
     480            0 :         printf("download: message %d not found in this peer\n", mid);
     481            0 :         return;
     482              :     }
     483            0 :     if (entry.media != MEDIA_PHOTO && entry.media != MEDIA_DOCUMENT) {
     484            0 :         printf("download: message %d has no downloadable photo/document "
     485            0 :                "(media kind=%d)\n", mid, (int)entry.media);
     486            0 :         return;
     487              :     }
     488            0 :     if (entry.media_info.access_hash == 0
     489            0 :         || entry.media_info.file_reference_len == 0) {
     490            0 :         puts("download: missing access_hash or file_reference");
     491            0 :         return;
     492              :     }
     493              : 
     494              :     /* Compose output path: explicit [out] > default cache path. */
     495              :     char path_buf[2048];
     496            0 :     const char *out_path = (*out_arg) ? out_arg : NULL;
     497            0 :     if (!out_path) {
     498            0 :         const char *cache = platform_cache_dir();
     499            0 :         if (!cache) cache = "/tmp";
     500              :         char dir_buf[1536];
     501            0 :         snprintf(dir_buf, sizeof(dir_buf), "%s/tg-cli/downloads", cache);
     502            0 :         fs_mkdir_p(dir_buf, 0700);
     503            0 :         if (entry.media == MEDIA_DOCUMENT) {
     504            0 :             const char *fn = entry.media_info.document_filename;
     505            0 :             if (fn[0]) {
     506            0 :                 snprintf(path_buf, sizeof(path_buf), "%s/%s", dir_buf, fn);
     507              :             } else {
     508            0 :                 snprintf(path_buf, sizeof(path_buf), "%s/doc-%lld",
     509            0 :                          dir_buf, (long long)entry.media_info.document_id);
     510              :             }
     511              :         } else {
     512            0 :             snprintf(path_buf, sizeof(path_buf), "%s/photo-%lld.jpg",
     513            0 :                      dir_buf, (long long)entry.media_info.photo_id);
     514              :         }
     515            0 :         out_path = path_buf;
     516              :     }
     517              : 
     518            0 :     if (domain_download_media_cross_dc(cfg, s, t, &entry.media_info,
     519              :                                         out_path) != 0) {
     520            0 :         puts("download: failed (see logs)");
     521            0 :         return;
     522              :     }
     523              : 
     524              :     /* Record in the media index so `history` can show inline paths. */
     525            0 :     int64_t media_id = (entry.media == MEDIA_DOCUMENT)
     526              :                      ? entry.media_info.document_id
     527            0 :                      : entry.media_info.photo_id;
     528            0 :     if (media_index_put(media_id, out_path) != 0) {
     529            0 :         puts("download: warning: failed to update media index");
     530              :     }
     531              : 
     532            0 :     printf("saved: %s\n", out_path);
     533              : }
     534              : 
     535            0 : static void do_read(const ApiConfig *cfg, MtProtoSession *s, Transport *t,
     536              :                      const char *arg) {
     537            0 :     if (!arg || !*arg) { puts("usage: read <peer>"); return; }
     538            0 :     HistoryPeer peer = {0};
     539            0 :     if (resolve_history_peer(cfg, s, t, arg, &peer) != 0) {
     540            0 :         printf("read: cannot resolve '%s'\n", arg); return;
     541              :     }
     542            0 :     RpcError err = {0};
     543            0 :     if (domain_mark_read(cfg, s, t, &peer, 0, &err) != 0) {
     544            0 :         printf("read: failed (%d: %s)\n", err.error_code, err.error_msg);
     545            0 :         return;
     546              :     }
     547            0 :     puts("marked as read");
     548              : }
     549              : 
     550            0 : static void do_poll(const ApiConfig *cfg, MtProtoSession *s, Transport *t) {
     551            0 :     UpdatesState st = {0};
     552            0 :     if (domain_updates_state(cfg, s, t, &st) != 0) {
     553            0 :         puts("poll: getState failed");
     554            0 :         return;
     555              :     }
     556            0 :     UpdatesDifference diff = {0};
     557            0 :     if (domain_updates_difference(cfg, s, t, &st, &diff) != 0) {
     558            0 :         puts("poll: getDifference failed");
     559            0 :         return;
     560              :     }
     561            0 :     printf("pts=%d  new_messages=%d  empty=%d\n",
     562              :            diff.next_state.pts, diff.new_messages_count, diff.is_empty);
     563              : }
     564              : 
     565              : /* ---- Help ---- */
     566              : 
     567            0 : static void print_help(void) {
     568            0 :     puts(
     569              :         "Commands (accept '/' prefix too):\n"
     570              :         "\n"
     571              :         "Read commands:\n"
     572              :         "  me, self                     Show own profile\n"
     573              :         "  dialogs, list [N]            List up to N dialogs (default 20)\n"
     574              :         "  history [<peer>] [N]         Saved Messages by default, or <peer>\n"
     575              :         "  contacts                     List my contacts\n"
     576              :         "  info <@peer>                 Resolve peer info\n"
     577              :         "  search [<peer>] <query>       Message search: per-peer or global (top 20)\n"
     578              :         "  poll                         One-shot updates.getDifference\n"
     579              :         "  read <peer>                  Mark peer's history as read\n"
     580              :         "  download <peer> <msg_id> [out]  Download photo or document from message\n"
     581              :         "\n"
     582              :         "Write commands:\n"
     583              :         "  send <peer> <text>           Send a text message\n"
     584              :         "  reply <peer> <msg_id> <text> Send as a reply to msg_id\n"
     585              :         "  edit <peer> <msg_id> <text>  Edit a previously sent message\n"
     586              :         "  delete, del <peer> <msg_id> [revoke]  Delete a message\n"
     587              :         "  forward, fwd <from> <to> <msg_id>     Forward one message\n"
     588              :         "  upload <peer> <path> [caption] Upload a file (document)\n"
     589              :         "\n"
     590              :         "Session:\n"
     591              :         "  help, ?                      Show this help\n"
     592              :         "  quit, exit, :q               Leave the TUI (Ctrl-D also exits)\n"
     593              :         "\n"
     594              :         "Launch flags (pass on the command line, not inside the REPL):\n"
     595              :         "  --help, -h                   Show this help and exit\n"
     596              :         "  --version, -v                Show version and exit\n"
     597              :         "  --tui                        Curses-style three-pane UI instead of REPL\n"
     598              :         "  --phone <number>             Pre-fill login phone (E.164)\n"
     599              :         "  --code <digits>              Pre-fill SMS/app code\n"
     600              :         "  --password <pass>            Pre-fill 2FA password\n"
     601              :         "  --logout                     Clear persisted session and exit\n"
     602              :         "  login [--api-id N --api-hash HEX] [--force]  First-run config wizard\n"
     603              :         "      Interactive when stdin is a TTY. From a script, pass both\n"
     604              :         "      flags; otherwise the command exits with 1 (never blocks on input).\n"
     605              :         "\n"
     606              :         "See man tg-tui(1) for the full reference.\n"
     607              :     );
     608            0 : }
     609              : 
     610              : /* ---- REPL ---- */
     611              : 
     612            1 : static int repl(const ApiConfig *cfg, MtProtoSession *s, Transport *t,
     613              :                  LineHistory *hist) {
     614              :     char line[512];
     615            0 :     for (;;) {
     616            1 :         int n = rl_readline("tg> ", line, sizeof(line), hist);
     617            1 :         if (n < 0) return 0; /* EOF / Ctrl-C */
     618            0 :         if (n == 0) continue;
     619              : 
     620            0 :         rl_history_add(hist, line);
     621              : 
     622              :         /* Parse the very small command language in-place. */
     623            0 :         char *cmd = line;
     624            0 :         while (*cmd == ' ' || *cmd == '\t') cmd++;
     625            0 :         if (*cmd == '\0') continue;
     626              : 
     627            0 :         char *arg = cmd;
     628            0 :         while (*arg && *arg != ' ' && *arg != '\t') arg++;
     629            0 :         if (*arg) { *arg++ = '\0'; while (*arg == ' ') arg++; }
     630              : 
     631              :         /* Allow an optional leading '/' for IRC-style commands. */
     632            0 :         if (*cmd == '/') cmd++;
     633              : 
     634            0 :         if (!strcmp(cmd, "quit") || !strcmp(cmd, "exit") ||
     635            0 :             !strcmp(cmd, ":q")) return 0;
     636            0 :         if (!strcmp(cmd, "help") || !strcmp(cmd, "?")) { print_help(); continue; }
     637            0 :         if (!strcmp(cmd, "me") || !strcmp(cmd, "self")) { do_me(cfg, s, t); continue; }
     638            0 :         if (!strcmp(cmd, "dialogs") || !strcmp(cmd, "list")) {
     639            0 :             do_dialogs(cfg, s, t, atoi(arg));
     640            0 :             continue;
     641              :         }
     642            0 :         if (!strcmp(cmd, "history")) {
     643              :             /* Accept "history <peer> <N>" or "history <N>" or "history". */
     644            0 :             char peer[128] = "";
     645            0 :             int lim = 0;
     646            0 :             if (*arg) {
     647            0 :                 char *space = strpbrk(arg, " \t");
     648            0 :                 if (space) {
     649            0 :                     size_t pn = (size_t)(space - arg);
     650            0 :                     if (pn >= sizeof(peer)) pn = sizeof(peer) - 1;
     651            0 :                     memcpy(peer, arg, pn);
     652            0 :                     peer[pn] = '\0';
     653            0 :                     while (*space == ' ' || *space == '\t') space++;
     654            0 :                     lim = atoi(space);
     655              :                 } else {
     656            0 :                     if (*arg >= '0' && *arg <= '9') {
     657            0 :                         lim = atoi(arg);
     658              :                     } else {
     659            0 :                         size_t an = strlen(arg);
     660            0 :                         if (an >= sizeof(peer)) an = sizeof(peer) - 1;
     661            0 :                         memcpy(peer, arg, an);
     662            0 :                         peer[an] = '\0';
     663              :                     }
     664              :                 }
     665              :             }
     666            0 :             do_history_any(cfg, s, t, peer[0] ? peer : NULL, lim);
     667            0 :             continue;
     668              :         }
     669            0 :         if (!strcmp(cmd, "contacts")) { do_contacts(cfg, s, t); continue; }
     670            0 :         if (!strcmp(cmd, "info"))     { do_info(cfg, s, t, arg); continue; }
     671            0 :         if (!strcmp(cmd, "search"))   { do_search(cfg, s, t, arg); continue; }  /* arg is mutable */
     672            0 :         if (!strcmp(cmd, "poll"))     { do_poll(cfg, s, t); continue; }
     673            0 :         if (!strcmp(cmd, "send"))     { do_send(cfg, s, t, arg); continue; }
     674            0 :         if (!strcmp(cmd, "reply"))    { do_reply(cfg, s, t, arg); continue; }
     675            0 :         if (!strcmp(cmd, "edit"))     { do_edit(cfg, s, t, arg); continue; }
     676            0 :         if (!strcmp(cmd, "delete") || !strcmp(cmd, "del")) {
     677            0 :             do_delete(cfg, s, t, arg); continue;
     678              :         }
     679            0 :         if (!strcmp(cmd, "forward") || !strcmp(cmd, "fwd")) {
     680            0 :             do_forward(cfg, s, t, arg); continue;
     681              :         }
     682            0 :         if (!strcmp(cmd, "read"))     { do_read(cfg, s, t, arg); continue; }
     683            0 :         if (!strcmp(cmd, "upload"))   { do_upload(cfg, s, t, arg); continue; }
     684            0 :         if (!strcmp(cmd, "download")) { do_download(cfg, s, t, arg); continue; }
     685              : 
     686            0 :         printf("unknown command: %s  (try 'help')\n", cmd);
     687              :     }
     688              : }
     689              : 
     690              : 
     691              : /* Map a DialogEntry to a HistoryPeer. Legacy groups don't need an
     692              :  * access_hash; users/channels do, and since TUI-08 the DialogEntry
     693              :  * carries it when the server sent one. */
     694            0 : static int dialog_to_history_peer(const DialogEntry *d, HistoryPeer *out) {
     695            0 :     memset(out, 0, sizeof(*out));
     696            0 :     switch (d->kind) {
     697            0 :     case DIALOG_PEER_CHAT:
     698            0 :         out->kind = HISTORY_PEER_CHAT;
     699            0 :         out->peer_id = d->peer_id;
     700            0 :         return 0;
     701            0 :     case DIALOG_PEER_USER:
     702            0 :         if (!d->have_access_hash) return -1;
     703            0 :         out->kind = HISTORY_PEER_USER;
     704            0 :         out->peer_id = d->peer_id;
     705            0 :         out->access_hash = d->access_hash;
     706            0 :         return 0;
     707            0 :     case DIALOG_PEER_CHANNEL:
     708            0 :         if (!d->have_access_hash) return -1;
     709            0 :         out->kind = HISTORY_PEER_CHANNEL;
     710            0 :         out->peer_id = d->peer_id;
     711            0 :         out->access_hash = d->access_hash;
     712            0 :         return 0;
     713            0 :     default:
     714            0 :         return -1;
     715              :     }
     716              : }
     717              : 
     718              : /* Curses-style TUI loop (US-11 v2). Returns 0 on clean exit. */
     719            4 : static int run_tui_loop(const ApiConfig *cfg,
     720              :                          MtProtoSession *s, Transport *t) {
     721            4 :     int rows = terminal_rows(); if (rows < 3)  rows = 24;
     722            4 :     int cols = terminal_cols(); if (cols < 40) cols = 80;
     723              : 
     724              :     TuiApp app;
     725            4 :     if (tui_app_init(&app, rows, cols) != 0) {
     726            0 :         fprintf(stderr, "tg-tui: cannot initialize TUI (size %dx%d)\n",
     727              :                 rows, cols);
     728            0 :         return 1;
     729              :     }
     730              : 
     731              :     /* Prime the dialog list. Failure is non-fatal; the user sees the
     732              :      * empty-placeholder and can still quit. */
     733            4 :     if (dialog_pane_refresh(&app.dialogs, cfg, s, t) != 0) {
     734            0 :         status_row_set_message(&app.status, "dialogs: load failed");
     735              :     }
     736              :     /* dialog_pane_set_entries resets the list_view but forgets the
     737              :      * viewport height set in tui_app_init; restore it. */
     738            4 :     app.dialogs.lv.rows_visible = app.layout.dialogs.rows;
     739              : 
     740            8 :     RAII_TERM_RAW TermRawState *raw = terminal_raw_enter();
     741            4 :     if (!raw) {
     742            0 :         fprintf(stderr, "tg-tui: cannot enter raw mode\n");
     743            0 :         tui_app_free(&app);
     744            0 :         return 1;
     745              :     }
     746              :     /* Ensure SIGTERM / SIGHUP / SIGINT restore the terminal before exiting,
     747              :      * even if the default handler bypasses our RAII cleanup. */
     748            4 :     terminal_install_cleanup_handlers(raw);
     749            4 :     screen_cursor_visible(&app.screen, 0);
     750              : 
     751            4 :     terminal_enable_resize_notifications();
     752              : 
     753              :     /* Prime updates polling. If getState fails (eg. offline or server hiccup)
     754              :      * we silently skip live polling — the pane UI still works. */
     755            4 :     UpdatesState upd_state = {0};
     756            4 :     int upd_active = (domain_updates_state(cfg, s, t, &upd_state) == 0);
     757              : 
     758            4 :     tui_app_paint(&app);
     759            4 :     screen_flip(&app.screen);
     760              : 
     761              :     /* Poll cadence for updates.getDifference. 5 seconds is low enough to
     762              :      * feel live and high enough not to rate-limit ourselves. */
     763            4 :     const int POLL_INTERVAL_MS = 5000;
     764              : 
     765            4 :     int rc = 0;
     766            2 :     for (;;) {
     767            6 :         int ready = terminal_wait_key(POLL_INTERVAL_MS);
     768              : 
     769              :         /* SIGWINCH interrupts poll(): pick up the new size and repaint. */
     770            6 :         if (terminal_consume_resize()) {
     771            2 :             int nr = terminal_rows(); if (nr < 3)  nr = app.rows;
     772            2 :             int nc = terminal_cols(); if (nc < 40) nc = app.cols;
     773            2 :             if (nr != app.rows || nc != app.cols) {
     774            2 :                 if (tui_app_resize(&app, nr, nc) == 0) {
     775            2 :                     screen_cursor_visible(&app.screen, 0);
     776            2 :                     tui_app_paint(&app);
     777            2 :                     screen_flip(&app.screen);
     778              :                 }
     779              :             }
     780            2 :             if (ready < 0) continue;   /* poll returned -1 (EINTR) */
     781              :         }
     782              : 
     783            4 :         if (ready == 0) {
     784              :             /* No keystroke within the poll window: consult the server
     785              :              * for any changes since we last asked. If anything came in,
     786              :              * refresh the dialog pane (titles / unread counts) and, if a
     787              :              * dialog is currently open, its history too. */
     788            0 :             if (upd_active) {
     789            0 :                 UpdatesDifference diff = {0};
     790            0 :                 if (domain_updates_difference(cfg, s, t,
     791              :                                                &upd_state, &diff) == 0) {
     792            0 :                     upd_state = diff.next_state;
     793            0 :                     int changed = !diff.is_empty
     794            0 :                                && (diff.new_messages_count > 0
     795            0 :                                    || diff.other_updates_count > 0);
     796            0 :                     if (changed) {
     797            0 :                         dialog_pane_refresh(&app.dialogs, cfg, s, t);
     798            0 :                         app.dialogs.lv.rows_visible = app.layout.dialogs.rows;
     799            0 :                         if (app.history.peer_loaded) {
     800            0 :                             history_pane_load(&app.history, cfg, s, t,
     801              :                                               &app.history.peer);
     802            0 :                             app.history.lv.rows_visible = app.layout.history.rows;
     803              :                         }
     804            0 :                         tui_app_paint(&app);
     805            0 :                         screen_flip(&app.screen);
     806              :                     }
     807              :                 }
     808              :             }
     809            0 :             continue;
     810              :         }
     811              : 
     812            4 :         if (ready < 0) continue;   /* interrupted before any key — retry */
     813              : 
     814            4 :         TermKey key = terminal_read_key();
     815              :         TuiEvent ev;
     816            4 :         if (key == TERM_KEY_IGNORE) {
     817            3 :             ev = tui_app_handle_char(&app, terminal_last_printable());
     818              :         } else {
     819            1 :             ev = tui_app_handle_key(&app, key);
     820              :         }
     821              : 
     822            4 :         if (ev == TUI_EVENT_QUIT) break;
     823              : 
     824            0 :         if (ev == TUI_EVENT_OPEN_DIALOG) {
     825            0 :             const DialogEntry *d = dialog_pane_selected(&app.dialogs);
     826            0 :             if (d) {
     827              :                 HistoryPeer peer;
     828            0 :                 if (dialog_to_history_peer(d, &peer) == 0) {
     829            0 :                     status_row_set_message(&app.status, "loading…");
     830            0 :                     tui_app_paint(&app);
     831            0 :                     screen_flip(&app.screen);
     832            0 :                     if (history_pane_load(&app.history, cfg, s, t, &peer) == 0) {
     833              :                         /* Restore the viewport height lost to the
     834              :                          * history_pane_set_entries reset. */
     835            0 :                         app.history.lv.rows_visible = app.layout.history.rows;
     836            0 :                         status_row_set_message(&app.status, NULL);
     837              :                     } else {
     838            0 :                         status_row_set_message(&app.status, "history: load failed");
     839              :                     }
     840              :                 } else {
     841            0 :                     status_row_set_message(&app.status,
     842              :                                            "cannot open (access_hash missing)");
     843              :                 }
     844              :             }
     845              :         }
     846              : 
     847            0 :         if (ev != TUI_EVENT_NONE) {
     848            0 :             tui_app_paint(&app);
     849            0 :             screen_flip(&app.screen);
     850              :         }
     851              :     }
     852              : 
     853              :     /* Reset the terminal for the shell prompt. */
     854            4 :     screen_cursor_visible(&app.screen, 1);
     855            4 :     screen_cursor(&app.screen, app.rows, 1);
     856            4 :     fputs("\r\n", stdout);
     857            4 :     fflush(stdout);
     858            4 :     tui_app_free(&app);
     859            4 :     return rc;
     860              : }
     861              : 
     862              : /* Scan argv for --tui flag (used after full arg parse to decide mode). */
     863            5 : static int has_tui_flag(int argc, char **argv) {
     864            5 :     for (int i = 1; i < argc; i++) {
     865            4 :         if (strcmp(argv[i], "--tui") == 0) return 1;
     866              :     }
     867            1 :     return 0;
     868              : }
     869              : 
     870            5 : int main(int argc, char **argv) {
     871            5 :     platform_normalize_argv(&argc, &argv);
     872              :     AppContext ctx;
     873            5 :     if (app_bootstrap(&ctx, "tg-tui") != 0) {
     874            0 :         fprintf(stderr, "tg-tui: bootstrap failed\n");
     875            0 :         return 1;
     876              :     }
     877              : 
     878              :     /* Drop the session-scoped resolver cache when the user logs out so a
     879              :      * subsequent login does not see stale @peer → id mappings. */
     880            5 :     auth_logout_set_cache_flush_cb(resolve_cache_flush);
     881              : 
     882              :     /* --logout: invalidate session server-side, then wipe the local file. */
     883            9 :     for (int i = 1; i < argc; i++) {
     884            4 :         if (strcmp(argv[i], "--logout") == 0) {
     885              :             ApiConfig cfg;
     886              :             MtProtoSession s;
     887              :             Transport t;
     888            0 :             if (credentials_load(&cfg) == 0) {
     889            0 :                 transport_init(&t);
     890            0 :                 mtproto_session_init(&s);
     891            0 :                 int loaded_dc = 0;
     892            0 :                 if (session_store_load(&s, &loaded_dc) == 0) {
     893            0 :                     const DcEndpoint *ep = dc_lookup(loaded_dc);
     894            0 :                     if (ep && transport_connect(&t, ep->host, ep->port) == 0) {
     895            0 :                         t.dc_id = loaded_dc;
     896            0 :                         auth_logout(&cfg, &s, &t);
     897            0 :                         transport_close(&t);
     898              :                     } else {
     899            0 :                         logger_log(LOG_WARN,
     900              :                             "tg-tui: logout: cannot connect to DC%d, clearing local session",
     901              :                             loaded_dc);
     902            0 :                         session_store_clear();
     903              :                     }
     904              :                 } else {
     905            0 :                     session_store_clear();
     906              :                 }
     907              :             } else {
     908            0 :                 session_store_clear();
     909              :             }
     910            0 :             fprintf(stderr, "tg-tui: persisted session cleared.\n");
     911            0 :             app_shutdown(&ctx);
     912            0 :             return 0;
     913              :         }
     914              :     }
     915              : 
     916              :     /* Handle `login` subcommand before credentials are loaded.
     917              :      * Syntax: tg-tui login [--api-id N --api-hash HEX [--force]] */
     918            9 :     for (int i = 1; i < argc; i++) {
     919            4 :         if (strcmp(argv[i], "login") == 0) {
     920              :             /* Parse any --api-id / --api-hash / --force flags after "login". */
     921            0 :             const char *api_id_str  = NULL;
     922            0 :             const char *api_hash_str = NULL;
     923            0 :             int force = 0;
     924            0 :             for (int j = i + 1; j < argc; j++) {
     925            0 :                 if (strcmp(argv[j], "--api-id") == 0 && j + 1 < argc)
     926            0 :                     { api_id_str = argv[++j]; }
     927            0 :                 else if (strcmp(argv[j], "--api-hash") == 0 && j + 1 < argc)
     928            0 :                     { api_hash_str = argv[++j]; }
     929            0 :                 else if (strcmp(argv[j], "--force") == 0)
     930            0 :                     { force = 1; }
     931              :             }
     932              :             int wrc;
     933            0 :             if (api_id_str || api_hash_str)
     934            0 :                 wrc = config_wizard_run_batch(api_id_str, api_hash_str, force);
     935              :             else
     936            0 :                 wrc = config_wizard_run_interactive();
     937            0 :             app_shutdown(&ctx);
     938            0 :             return wrc != 0 ? 1 : 0;
     939              :         }
     940              :     }
     941              : 
     942              :     /* Handle --help / --version before anything else. */
     943            9 :     for (int i = 1; i < argc; i++) {
     944            4 :         if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
     945            0 :             print_help();
     946            0 :             app_shutdown(&ctx);
     947            0 :             return 0;
     948              :         }
     949            4 :         if (strcmp(argv[i], "--version") == 0 || strcmp(argv[i], "-v") == 0) {
     950            0 :             arg_print_version();
     951            0 :             app_shutdown(&ctx);
     952            0 :             return 0;
     953              :         }
     954              :     }
     955              : 
     956              :     /* Parse --phone / --code / --password / --tui flags. */
     957            5 :     const char *opt_phone    = NULL;
     958            5 :     const char *opt_code     = NULL;
     959            5 :     const char *opt_password = NULL;
     960            5 :     int tui_mode = has_tui_flag(argc, argv);
     961              : 
     962            9 :     for (int i = 1; i < argc; i++) {
     963            4 :         if (strcmp(argv[i], "--phone") == 0 && i + 1 < argc) {
     964            0 :             opt_phone = argv[++i];
     965            4 :         } else if (strcmp(argv[i], "--code") == 0 && i + 1 < argc) {
     966            0 :             opt_code = argv[++i];
     967            4 :         } else if (strcmp(argv[i], "--password") == 0 && i + 1 < argc) {
     968            0 :             opt_password = argv[++i];
     969              :         }
     970              :     }
     971              : 
     972              :     ApiConfig cfg;
     973            5 :     if (credentials_load(&cfg) != 0) {
     974            0 :         app_shutdown(&ctx);
     975            0 :         return 1;
     976              :     }
     977              : 
     978              :     LineHistory hist;
     979            5 :     rl_history_init(&hist);
     980              : 
     981            5 :     PromptCtx pctx = {
     982              :         .hist     = &hist,
     983              :         .phone    = opt_phone,
     984              :         .code     = opt_code,
     985              :         .password = opt_password,
     986              :     };
     987            5 :     AuthFlowCallbacks cb = {
     988              :         .get_phone    = cb_get_phone,
     989              :         .get_code     = cb_get_code,
     990              :         .get_password = cb_get_password,
     991              :         .user         = &pctx,
     992              :     };
     993              : 
     994              :     MtProtoSession s;
     995              :     Transport t;
     996            5 :     transport_init(&t);
     997            5 :     mtproto_session_init(&s);
     998              : 
     999            5 :     if (!tui_mode) {
    1000            1 :         fputs("tg-tui — interactive Telegram client. "
    1001              :               "Type 'help' for commands (or run with --tui for pane view).\n",
    1002              :               stdout);
    1003              :     }
    1004              : 
    1005            5 :     if (auth_flow_login(&cfg, &cb, &t, &s, NULL) != 0) {
    1006            0 :         fprintf(stderr, "tg-tui: login failed\n");
    1007            0 :         transport_close(&t);
    1008            0 :         app_shutdown(&ctx);
    1009            0 :         return 1;
    1010              :     }
    1011              : 
    1012            4 :     int rc = tui_mode ? run_tui_loop(&cfg, &s, &t)
    1013            5 :                       : repl(&cfg, &s, &t, &hist);
    1014            5 :     transport_close(&t);
    1015            5 :     app_shutdown(&ctx);
    1016            5 :     return rc;
    1017              : }
        

Generated by: LCOV version 2.0-1