"Fossies" - the Fresh Open Source Software Archive

Member "tin-2.6.3/src/thread.c" (23 Dec 2023, 47331 Bytes) of package /linux/misc/tin-2.6.3.tar.xz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) C and C++ source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "thread.c" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 2.6.2_vs_2.6.3.

    1 /*
    2  *  Project   : tin - a Usenet reader
    3  *  Module    : thread.c
    4  *  Author    : I. Lea
    5  *  Created   : 1991-04-01
    6  *  Updated   : 2023-11-14
    7  *  Notes     :
    8  *
    9  * Copyright (c) 1991-2024 Iain Lea <iain@bricbrac.de>
   10  * All rights reserved.
   11  *
   12  * Redistribution and use in source and binary forms, with or without
   13  * modification, are permitted provided that the following conditions
   14  * are met:
   15  *
   16  * 1. Redistributions of source code must retain the above copyright notice,
   17  *    this list of conditions and the following disclaimer.
   18  *
   19  * 2. Redistributions in binary form must reproduce the above copyright
   20  *    notice, this list of conditions and the following disclaimer in the
   21  *    documentation and/or other materials provided with the distribution.
   22  *
   23  * 3. Neither the name of the copyright holder nor the names of its
   24  *    contributors may be used to endorse or promote products derived from
   25  *    this software without specific prior written permission.
   26  *
   27  * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   28  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   29  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
   30  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
   31  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
   32  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
   33  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
   34  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
   35  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
   36  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   37  * POSSIBILITY OF SUCH DAMAGE.
   38  */
   39 
   40 
   41 #ifndef TIN_H
   42 #   include "tin.h"
   43 #endif /* !TIN_H */
   44 #ifndef TCURSES_H
   45 #   include "tcurses.h"
   46 #endif /* !TCURSES_H */
   47 
   48 
   49 #define IS_EXPIRED(a) ((a)->article == ART_UNAVAILABLE || arts[(a)->article].thread == ART_EXPIRED)
   50 
   51 int thread_basenote = 0;                /* Index in base[] of basenote */
   52 static int thread_respnum = 0;          /* Index in arts[] of basenote ie base[thread_basenote] */
   53 static struct t_fmt thrd_fmt;
   54 t_bool show_subject;
   55 
   56 /*
   57  * Local prototypes
   58  */
   59 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
   60     static wchar_t get_art_mark(struct t_article *art);
   61 #else
   62     static char get_art_mark(struct t_article *art);
   63 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
   64 static int enter_pager(int art, t_bool ignore_unavail, int level);
   65 static int thread_catchup(t_function func, struct t_group *group);
   66 static int thread_tab_pressed(void);
   67 static t_bool find_unexpired(struct t_msgid *ptr);
   68 static t_bool has_sibling(struct t_msgid *ptr);
   69 static t_function thread_left(void);
   70 static t_function thread_right(void);
   71 static void build_tline(int l, struct t_article *art);
   72 static void draw_thread_arrow(void);
   73 static void draw_thread_item(int item);
   74 static void make_prefix(struct t_msgid *art, char *prefix, int maxlen);
   75 static void show_thread_page(void);
   76 static void update_thread_page(void);
   77 
   78 
   79 /*
   80  * thdmenu.curr     Current screen cursor position in thread
   81  * thdmenu.max      Essentially = # threaded arts in current thread
   82  * thdmenu.first    Response # at top of screen
   83  */
   84 static t_menu thdmenu = {0, 0, 0, show_thread_page, draw_thread_arrow, draw_thread_item };
   85 
   86 /* TODO: find a better solution */
   87 static int ret_code = 0;        /* Set to < 0 when it is time to leave this menu */
   88 
   89 /*
   90  * returns the mark which should be used for this article
   91  */
   92 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
   93     static wchar_t
   94 #else
   95     static char
   96 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
   97 get_art_mark(
   98     struct t_article *art)
   99 {
  100     if (art->inrange) {
  101         return tinrc.art_marked_inrange;
  102     } else if (art->status == ART_UNREAD) {
  103         return (art->selected ? tinrc.art_marked_selected : (tinrc.recent_time && ((time((time_t *) 0) - art->date) < (tinrc.recent_time * DAY))) ? tinrc.art_marked_recent : tinrc.art_marked_unread);
  104     } else if (art->status == ART_WILL_RETURN) {
  105         return tinrc.art_marked_return;
  106     } else if (art->killed && tinrc.kill_level != KILL_NOTHREAD) {
  107         return tinrc.art_marked_killed;
  108     } else {
  109         if (/* tinrc.kill_level != KILL_UNREAD && */ art->score >= tinrc.score_select)
  110             return tinrc.art_marked_read_selected; /* read hot chil^H^H^H^H article */
  111         else
  112             return tinrc.art_marked_read;
  113     }
  114 }
  115 
  116 
  117 /*
  118  * Build one line of the thread page display. Looks long winded, but
  119  * there are a lot of variables in the format for the output
  120  *
  121  * WARNING: some other code expects to find the article mark (ART_MARK_READ,
  122  * ART_MARK_SELECTED, etc) at mark_offset from beginning of the line.
  123  * So, if you change the format used in this routine, be sure to check that
  124  * the value of mark_offset is still correct.
  125  * Yes, this is somewhat kludgy.
  126  */
  127 static void
  128 build_tline(
  129     int l,
  130     struct t_article *art)
  131 {
  132     int gap, fill, i;
  133     size_t len, len_start, len_end;
  134     struct t_msgid *ptr;
  135     char *buffer, *buf;
  136     char *fmt = thrd_fmt.str;
  137     char tmp[LEN];
  138 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
  139     char markbuf[sizeof(wchar_t) + 4];
  140     wchar_t *wtmp, *wtmp2;
  141     wchar_t mark[] = { L'\0', L'\0' };
  142 #else
  143     char mark[] = { '\0', '\0' };
  144 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
  145 
  146 #ifdef USE_CURSES
  147     /*
  148      * Allocate line buffer
  149      * make it the same size like in !USE_CURSES case to simplify some code
  150      */
  151 #   if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
  152         buffer = my_malloc(cCOLS * MB_CUR_MAX + 2);
  153 #   else
  154         buffer = my_malloc(cCOLS + 2);
  155 #   endif /* MULTIBYTE_ABLE && !NO_LOCALE */
  156 #else
  157     buffer = screen[INDEX2SNUM(l)].col;
  158 #endif /* USE_CURSES */
  159 
  160     buffer[0] = '\0';
  161 
  162     if (tinrc.draw_arrow)
  163         strcat(buffer, "  ");
  164 
  165     for (; *fmt; fmt++) {
  166         if (*fmt != '%') {
  167             strncat(buffer, fmt, 1);
  168             continue;
  169         }
  170         switch (*++fmt) {
  171             case '\0':
  172                 break;
  173 
  174             case '%':
  175                 strncat(buffer, fmt, 1);
  176                 break;
  177 
  178             case 'D':   /* date */
  179                 buf = my_malloc(LEN);
  180                 if (my_strftime(buf, LEN - 1, thrd_fmt.date_str, localtime(&art->date))) {
  181 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
  182                     if ((wtmp = char2wchar_t(buf)) != NULL) {
  183                         wtmp2 = wcspart(wtmp, (int) thrd_fmt.len_date_max, TRUE);
  184                         if (wcstombs(tmp, wtmp2, sizeof(tmp) - 1) != (size_t) -1)
  185                             strcat(buffer, tmp);
  186 
  187                         free(wtmp);
  188                         free(wtmp2);
  189                     }
  190 #else
  191                     strncat(buffer, buf, thrd_fmt.len_date_max);
  192 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
  193                 }
  194                 free(buf);
  195                 break;
  196 
  197             case 'F':   /* from */
  198 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
  199                 get_author(TRUE, art, tmp, sizeof(tmp) - 1);
  200 
  201                 if ((wtmp = char2wchar_t(tmp)) != NULL) {
  202                     wtmp2 = wcspart(wtmp, (int) thrd_fmt.len_from, TRUE);
  203                     if (wcstombs(tmp, wtmp2, sizeof(tmp) - 1) != (size_t) -1)
  204                         strcat(buffer, tmp);
  205 
  206                     free(wtmp);
  207                     free(wtmp2);
  208                 }
  209 #else
  210                 if (curr_group->attribute->show_author != SHOW_FROM_NONE) {
  211                     len_start = strwidth(buffer);
  212                     get_author(TRUE, art, buffer + strlen(buffer), thrd_fmt.len_from);
  213                     fill = thrd_fmt.len_from - (strwidth(buffer) - len_start);
  214                     gap = strlen(buffer);
  215                     for (i = 0; i < fill; i++)
  216                         buffer[gap + i] = ' ';
  217                     buffer[gap + fill] = '\0';
  218                 }
  219 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
  220                 break;
  221 
  222             case 'I':   /* initials */
  223                 len = MIN(thrd_fmt.len_initials, sizeof(tmp) - 1);
  224                 get_initials(art, tmp, (int) len);
  225                 strcat(buffer, tmp);
  226                 if ((i = (int) (len - (size_t) strwidth(tmp))) > 0) {
  227                     buf = buffer + strlen(buffer);
  228                     for (; i > 0; --i)
  229                         *buf++ = ' ';
  230                     *buf = '\0';
  231                 }
  232                 break;
  233 
  234             case 'L':   /* lines */
  235                 if (art->line_count != -1)
  236                     strcat(buffer, tin_ltoa(art->line_count, (int) thrd_fmt.len_linecnt));
  237                 else {
  238                     buf = buffer + strlen(buffer);
  239                     for (i = (int) thrd_fmt.len_linecnt; i > 1; --i)
  240                         *buf++ = ' ';
  241                     *buf++ = '?';
  242                     *buf = '\0';
  243                 }
  244                 break;
  245 
  246             case 'm':   /* article flags, tag number, or whatever */
  247                 if (!thrd_fmt.mark_offset)
  248                     thrd_fmt.mark_offset = (size_t) (mark_offset = strwidth(buffer) + 2);
  249                 if (art->tagged) {
  250 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
  251                     if (art_mark_width > 1)
  252                         strcat(buffer, " ");
  253 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
  254                     strcat(buffer, tin_ltoa(art->tagged, 3));
  255 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
  256                     mark[0] = L'\0';
  257 #else
  258                     mark[0] = '\0';
  259 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
  260                 } else {
  261                     mark[0] = get_art_mark(art);
  262 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
  263                     snprintf(markbuf, sizeof(markbuf), "%s%lc", art_mark_width > wcwidth(mark[0]) ? "   " : "  ", mark[0]);
  264                     strcat(buffer, markbuf);
  265 #else
  266                     strcat(buffer, "   ");
  267                     buffer[strlen(buffer) - 1] = mark[0];       /* insert mark */
  268 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
  269                 }
  270                 break;
  271 
  272             case 'M':   /* message-id */
  273                 len = MIN(thrd_fmt.len_msgid, sizeof(tmp) - 1);
  274                 strncpy(tmp, art->refptr ? art->refptr->txt : "", len);
  275                 tmp[len] = '\0';
  276                 strcat(buffer, tmp);
  277                 if ((i = (int) (len - (size_t) strwidth(tmp))) > 0) {
  278                     buf = buffer + strlen(buffer);
  279                     for (; i > 0; --i)
  280                         *buf++ = ' ';
  281                     *buf = '\0';
  282                 }
  283                 break;
  284 
  285             case 'n':
  286                 strcat(buffer, tin_ltoa(l + 1, (int) thrd_fmt.len_linenumber));
  287                 break;
  288 
  289             case 'S':   /* score */
  290                 strcat(buffer, tin_ltoa(art->score, (int) thrd_fmt.len_score));
  291                 break;
  292 
  293             case 'T':   /* thread/subject */
  294                 len = curr_group->attribute->show_author != SHOW_FROM_NONE ? thrd_fmt.len_subj : thrd_fmt.len_subj + thrd_fmt.len_from;
  295                 len_start = (size_t) strwidth(buffer);
  296 
  297                 switch (curr_group->attribute->thread_articles) {
  298                     case THREAD_REFS:
  299                     case THREAD_BOTH:
  300                         /*
  301                          * Mutt-like thread tree. by sjpark@sparcs.kaist.ac.kr
  302                          * Insert tree-structure strings "`->", "+->", ...
  303                          */
  304 
  305                         if (art->refptr) {
  306                             make_prefix(art->refptr, buffer + strlen(buffer), (int) len);
  307 
  308                             len_end = (size_t) strwidth(buffer);
  309 
  310                             /*
  311                              * Copy in the subject up to where the author (if any) starts
  312                              */
  313                             gap = (len - (len_end - len_start));
  314 
  315                             /*
  316                              * Mutt-like thread tree. by sjpark@sparcs.kaist.ac.kr
  317                              * Hide subject if same as parent's.
  318                              */
  319                             if (gap > 0) {
  320                                 for (ptr = art->refptr->parent; ptr && IS_EXPIRED(ptr); ptr = ptr->parent)
  321                                     ;
  322                                 if (!(ptr && arts[ptr->article].subject == art->subject))
  323 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
  324                                 {
  325                                     if ((wtmp = char2wchar_t(art->subject)) != NULL) {
  326                                         wtmp2 = wcspart(wtmp, gap, TRUE);
  327                                         if (wcstombs(tmp, wtmp2, sizeof(tmp) - 1) != (size_t) -1)
  328                                             strcat(buffer, tmp);
  329 
  330                                         free(wtmp);
  331                                         free(wtmp2);
  332                                     }
  333                                 }
  334 #else
  335                                 {
  336                                     strncat(buffer, art->subject, gap);
  337                                 }
  338                                 buffer[len_end + gap] = '\0';   /* Just in case */
  339 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
  340                             }
  341                         }
  342                         break;
  343 
  344                     case THREAD_NONE:
  345                     case THREAD_SUBJ:
  346                     case THREAD_MULTI:
  347                     case THREAD_PERC:
  348                         len_end = (size_t) strwidth(buffer);
  349                         gap = (len - (len_end - len_start));
  350                         if (gap > 0) {
  351 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
  352                             {
  353                                 if ((wtmp = char2wchar_t(art->subject)) != NULL) {
  354                                     wtmp2 = wcspart(wtmp, gap, TRUE);
  355                                     if (wcstombs(tmp, wtmp2, sizeof(tmp) - 1) != (size_t) -1)
  356                                         strcat(buffer, tmp);
  357 
  358                                     free(wtmp);
  359                                     free(wtmp2);
  360                                 }
  361                             }
  362 #else
  363                             {
  364                                 strncat(buffer, art->subject, gap);
  365                             }
  366                             buffer[len_end + gap] = '\0';   /* Just in case */
  367 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
  368                         }
  369                         break;
  370 
  371                     default:
  372                         break;
  373                 }
  374 
  375                 /* pad out */
  376                 fill = (len - ((size_t) strwidth(buffer) - len_start));
  377                 gap = (int) strlen(buffer);
  378                 for (i = 0; i < fill; i++)
  379                     buffer[gap + i] = ' ';
  380                 buffer[gap + fill] = '\0';
  381                 break;
  382 
  383             default:
  384                 break;
  385         }
  386     }
  387     /* protect display from non-displayable characters (e.g., form-feed) */
  388     convert_to_printable(buffer, FALSE);
  389 
  390 #ifndef USE_CURSES
  391     if (tinrc.strip_blanks)
  392         strcat(strip_line(buffer), cCRLF);
  393 #endif /* !USE_CURSES */
  394 
  395     WriteLine(INDEX2LNUM(l), buffer);
  396 
  397 #ifdef USE_CURSES
  398     free(buffer);
  399 #endif /* USE_CURSES */
  400 
  401     if (mark[0] == tinrc.art_marked_selected)
  402         draw_mark_selected(l);
  403     my_flush();
  404 }
  405 
  406 
  407 static void
  408 draw_thread_item(
  409     int item)
  410 {
  411     build_tline(item, &arts[find_response(thread_basenote, item)]);
  412 }
  413 
  414 
  415 static t_function
  416 thread_left(
  417     void)
  418 {
  419     if (curr_group->attribute->thread_catchup_on_exit)
  420         return SPECIAL_CATCHUP_LEFT;            /* ie, not via 'c' or 'C' */
  421     else
  422         return GLOBAL_QUIT;
  423 }
  424 
  425 
  426 static t_function
  427 thread_right(
  428     void)
  429 {
  430     return THREAD_READ_ARTICLE;
  431 }
  432 
  433 
  434 /*
  435  * Show current thread.
  436  * If threaded on Subject: show
  437  *   <respnum> <name>
  438  * If threaded on References:
  439  *   <respnum> <subject> <name>
  440  * Return values:
  441  *      GRP_RETSELECT   Return to selection screen
  442  *      GRP_QUIT        'Q'uit all the way out
  443  *      GRP_NEXT        Catchup goto next group
  444  *      GRP_NEXTUNREAD  Catchup enter next unread thread
  445  *      GRP_KILLED      Thread was killed at art level?
  446  *      GRP_EXIT        Return to group menu
  447  */
  448 int
  449 thread_page(
  450     struct t_group *group,
  451     int respnum,                /* base[] article of thread to view */
  452     int thread_depth,           /* initial depth in thread */
  453     t_pagerinfo *page)          /* !NULL if we must go direct to the pager */
  454 {
  455     char key[MAXKEYLEN];
  456 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
  457     wchar_t mark[] = { L'\0', L'\0' };
  458     wchar_t *wtmp;
  459 #else
  460     char mark[] = { '\0', '\0' };
  461 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
  462     int i, n;
  463     t_artnum old_artnum;
  464     t_bool repeat_search;
  465     t_function func;
  466 
  467     thread_respnum = respnum;       /* Bodge to make this variable global */
  468 
  469     if ((n = which_thread(thread_respnum)) >= 0)
  470         thread_basenote = n;
  471     if ((thdmenu.max = num_of_responses(thread_basenote) + 1) <= 0) {
  472         info_message(_(txt_no_resps_in_thread));
  473         return GRP_EXIT;
  474     }
  475 
  476     /*
  477      * Set the cursor to the last response unless pos_first_unread is on
  478      * or an explicit thread_depth has been specified
  479      */
  480     thdmenu.curr = thdmenu.max;
  481     /* reset the first item on screen to 0 */
  482     thdmenu.first = 0;
  483 
  484     if (thread_depth)
  485         thdmenu.curr = thread_depth;
  486     else {
  487         if (group->attribute->pos_first_unread) {
  488             if (new_responses(thread_basenote)) {
  489                 for (n = 0, i = (int) base[thread_basenote]; i >= 0; i = arts[i].thread, n++) {
  490                     if (arts[i].status == ART_UNREAD || arts[i].status == ART_WILL_RETURN) {
  491                         if (arts[i].thread == ART_EXPIRED)
  492                             art_mark(group, &arts[i], ART_READ);
  493                         else
  494                             thdmenu.curr = n;
  495                         break;
  496                     }
  497                 }
  498             }
  499         }
  500     }
  501 
  502     if (thdmenu.curr < 0)
  503         thdmenu.curr = 0;
  504 
  505     /*
  506      * See if we're on a direct call from the group menu to the pager
  507      */
  508     if (page) {
  509         if ((ret_code = enter_pager(page->art, page->ignore_unavail, GROUP_LEVEL)) != 0)
  510             return ret_code;
  511         /* else fall through to stay in thread level */
  512     }
  513 
  514     /* Now we know where the cursor is, actually put something on the screen */
  515     show_thread_page();
  516 
  517     /* reset ret_code */
  518     ret_code = 0;
  519     while (ret_code >= 0) {
  520         set_xclick_on();
  521         if ((func = handle_keypad(thread_left, thread_right, global_mouse_action, thread_keys)) == GLOBAL_SEARCH_REPEAT) {
  522             func = last_search;
  523             repeat_search = TRUE;
  524         } else
  525             repeat_search = FALSE;
  526 
  527         switch (func) {
  528             case GLOBAL_ABORT:          /* Abort */
  529                 break;
  530 
  531             case DIGIT_1:
  532             case DIGIT_2:
  533             case DIGIT_3:
  534             case DIGIT_4:
  535             case DIGIT_5:
  536             case DIGIT_6:
  537             case DIGIT_7:
  538             case DIGIT_8:
  539             case DIGIT_9:
  540                 if (thdmenu.max == 1)
  541                     info_message(_(txt_no_responses));
  542                 else
  543                     prompt_item_num(func_to_key(func, thread_keys), _(txt_select_art));
  544                 break;
  545 
  546 #ifndef NO_SHELL_ESCAPE
  547             case GLOBAL_SHELL_ESCAPE:
  548                 do_shell_escape();
  549                 break;
  550 #endif /* !NO_SHELL_ESCAPE */
  551 
  552             case GLOBAL_FIRST_PAGE:     /* show first page of articles */
  553                 top_of_list();
  554                 break;
  555 
  556             case GLOBAL_LAST_PAGE:      /* show last page of articles */
  557                 end_of_list();
  558                 break;
  559 
  560             case GLOBAL_LAST_VIEWED:    /* show last viewed article */
  561                 if (this_resp < 0 || (which_thread(this_resp) == -1)) {
  562                     info_message(_(txt_no_last_message));
  563                     break;
  564                 }
  565                 ret_code = enter_pager(this_resp, FALSE, THREAD_LEVEL);
  566                 break;
  567 
  568             case GLOBAL_SET_RANGE:      /* set range */
  569                 if (set_range(THREAD_LEVEL, 1, thdmenu.max, thdmenu.curr + 1)) {
  570                     range_active = TRUE;
  571                     show_thread_page();
  572                 }
  573                 break;
  574 
  575             case GLOBAL_PIPE:           /* pipe article(s) to command */
  576                 if (thread_basenote >= 0)
  577                     feed_articles(FEED_PIPE, THREAD_LEVEL, NOT_ASSIGNED, group, find_response(thread_basenote, thdmenu.curr));
  578                 break;
  579 
  580 #ifndef DISABLE_PRINTING
  581             case GLOBAL_PRINT:          /* print article(s) */
  582                 if (thread_basenote >= 0)
  583                     feed_articles(FEED_PRINT, THREAD_LEVEL, NOT_ASSIGNED, group, find_response(thread_basenote, thdmenu.curr));
  584                 break;
  585 #endif /* !DISABLE_PRINTING */
  586 
  587             case THREAD_MAIL:   /* mail article(s) to somebody */
  588                 if (thread_basenote >= 0)
  589                     feed_articles(FEED_MAIL, THREAD_LEVEL, NOT_ASSIGNED, group, find_response(thread_basenote, thdmenu.curr));
  590                 break;
  591 
  592             case THREAD_SAVE:   /* save articles with prompting */
  593                 if (thread_basenote >= 0)
  594                     feed_articles(FEED_SAVE, THREAD_LEVEL, NOT_ASSIGNED, group, find_response(thread_basenote, thdmenu.curr));
  595                 break;
  596 
  597             case THREAD_AUTOSAVE:   /* Auto-save articles without prompting */
  598                 if (thread_basenote >= 0)
  599                     feed_articles(FEED_AUTOSAVE, THREAD_LEVEL, NOT_ASSIGNED, group, (int) base[grpmenu.curr]);
  600                 break;
  601 
  602             case MARK_FEED_READ:    /* mark selected articles as read */
  603                 if (thread_basenote >= 0)
  604                     ret_code = feed_articles(FEED_MARK_READ, THREAD_LEVEL, NOT_ASSIGNED, group, find_response(thread_basenote, thdmenu.curr));
  605                 break;
  606 
  607             case MARK_FEED_UNREAD:  /* mark selected articles as unread */
  608                 if (thread_basenote >= 0)
  609                     feed_articles(FEED_MARK_UNREAD, THREAD_LEVEL, NOT_ASSIGNED, group, find_response(thread_basenote, thdmenu.curr));
  610                 break;
  611 
  612             case GLOBAL_MENU_FILTER_SELECT:
  613             case GLOBAL_MENU_FILTER_KILL:
  614                 n = find_response(thread_basenote, thdmenu.curr);
  615                 if (filter_menu(func, group, &arts[n])) {
  616                     old_artnum = arts[n].artnum;
  617                     unfilter_articles(group);
  618                     filter_articles(group);
  619                     make_threads(group, FALSE);
  620                     if ((n = find_artnum(old_artnum)) == -1 || which_thread(n) == -1) { /* We have lost the thread */
  621                         ret_code = GRP_KILLED;
  622                         break;
  623                     }
  624                     fixup_thread(n, TRUE);
  625                 }
  626                 show_thread_page();
  627                 break;
  628 
  629             case GLOBAL_EDIT_FILTER:
  630                 if (invoke_editor(filter_file, filter_file_offset, NULL)) {
  631                     old_artnum = arts[find_response(thread_basenote, thdmenu.curr)].artnum;
  632                     unfilter_articles(group);
  633                     (void) read_filter_file(filter_file);
  634                     filter_articles(group);
  635                     make_threads(group, FALSE);
  636                     if ((n = find_artnum(old_artnum)) == -1 || which_thread(n) == -1) { /* We have lost the thread */
  637                         ret_code = GRP_KILLED;
  638                         break;
  639                     }
  640                     fixup_thread(n, TRUE);
  641                 }
  642                 show_thread_page();
  643                 break;
  644 
  645             case THREAD_READ_ARTICLE:   /* read current article within thread */
  646                 ret_code = enter_pager(find_response(thread_basenote, thdmenu.curr), FALSE, THREAD_LEVEL);
  647                 break;
  648 
  649 /*
  650             case THREAD_FOLLOWUP_QUOTE_HEADERS:
  651             may need
  652                     if (func == THREAD_FOLLOWUP_QUOTE_HEADERS)
  653                         resize_article(TRUE, &pgart);
  654             but as '^W' is already taken by MARK_FEED_UNREAD
  655             we leave that function out for now
  656 */
  657             case THREAD_FOLLOWUP_QUOTE:
  658             case THREAD_FOLLOWUP:
  659                 if (can_post || group->attribute->mailing_list != NULL) {
  660                     int ret;
  661 
  662                     n = find_response(thread_basenote, thdmenu.curr);
  663                     ret = art_open(TRUE, &arts[n], group, &pgart, TRUE, _(txt_reading_article));
  664                     if (ret != ART_UNAVAILABLE && ret != ART_ABORT && n >= 0) {
  665                         post_response(group->name, n, (func == THREAD_FOLLOWUP_QUOTE) ? TRUE : FALSE, FALSE, FALSE);
  666                         show_thread_page();
  667                     }
  668                     art_close(&pgart);
  669                 }
  670                 break;
  671 
  672             case THREAD_READ_NEXT_ARTICLE_OR_THREAD:
  673                 ret_code = thread_tab_pressed();
  674                 break;
  675 
  676             case THREAD_CANCEL:     /* cancel current article */
  677                 if (can_post || group->attribute->mailing_list != NULL) {
  678                     int ret;
  679 
  680                     n = find_response(thread_basenote, thdmenu.curr);
  681                     ret = art_open(TRUE, &arts[n], group, &pgart, TRUE, _(txt_reading_article));
  682                     if (ret != ART_UNAVAILABLE && ret != ART_ABORT && cancel_article(group, &arts[n], n))
  683                         show_thread_page();
  684                     art_close(&pgart);
  685                 } else
  686                     info_message(_(txt_cannot_post));
  687                 break;
  688 
  689             case GLOBAL_POST:       /* post a basenote */
  690                 if (post_article(group->name))
  691                     show_thread_page();
  692                 break;
  693 
  694             case GLOBAL_REDRAW_SCREEN:  /* redraw screen */
  695                 my_retouch();
  696                 set_xclick_off();
  697                 show_thread_page();
  698                 break;
  699 
  700             case GLOBAL_LINE_DOWN:
  701                 move_down();
  702                 break;
  703 
  704             case GLOBAL_LINE_UP:
  705                 move_up();
  706                 break;
  707 
  708             case GLOBAL_PAGE_UP:
  709                 page_up();
  710                 break;
  711 
  712             case GLOBAL_PAGE_DOWN:
  713                 page_down();
  714                 break;
  715 
  716             case GLOBAL_SCROLL_DOWN:
  717                 scroll_down();
  718                 break;
  719 
  720             case GLOBAL_SCROLL_UP:
  721                 scroll_up();
  722                 break;
  723 
  724             case SPECIAL_CATCHUP_LEFT:              /* come here when exiting thread via <- */
  725             case CATCHUP:               /* catchup thread, move to next one */
  726             case CATCHUP_NEXT_UNREAD:   /* -> next with unread arts */
  727                 ret_code = thread_catchup(func, group);
  728                 break;
  729 
  730             case THREAD_MARK_ARTICLE_READ:  /* mark current article/range/tagged articles as read */
  731             case MARK_ARTICLE_UNREAD:       /* or unread */
  732                 if (thread_basenote >= 0) {
  733                     t_function function, type;
  734 
  735                     function = func == THREAD_MARK_ARTICLE_READ ? (t_function) FEED_MARK_READ : (t_function) FEED_MARK_UNREAD;
  736                     type = range_active ? FEED_RANGE : (num_of_tagged_arts && !group->attribute->mark_ignore_tags) ? NOT_ASSIGNED : FEED_ARTICLE;
  737                     if (feed_articles(function, THREAD_LEVEL, type, group, find_response(thread_basenote, thdmenu.curr)) == 1)
  738                         ret_code = GRP_EXIT;
  739                 }
  740                 break;
  741 
  742             case THREAD_TOGGLE_SUBJECT_DISPLAY: /* toggle display of subject & subj/author */
  743                 if (show_subject) {
  744                     if (++curr_group->attribute->show_author > SHOW_FROM_BOTH)
  745                         curr_group->attribute->show_author = SHOW_FROM_NONE;
  746                     show_thread_page();
  747                 }
  748                 break;
  749 
  750             case GLOBAL_OPTION_MENU:
  751                 n = find_response(thread_basenote, thdmenu.curr);
  752                 old_artnum = arts[n].artnum;
  753                 config_page(group->name, signal_context);
  754                 if ((n = find_artnum(old_artnum)) == -1 || which_thread(n) == -1) { /* We have lost the thread */
  755                     pos_first_unread_thread();
  756                     ret_code = GRP_EXIT;
  757                 } else {
  758                     fixup_thread(n, FALSE);
  759                     thdmenu.curr = which_response(n);
  760                     show_thread_page();
  761                 }
  762                 break;
  763 
  764             case GLOBAL_HELP:                   /* help */
  765                 show_help_page(THREAD_LEVEL, _(txt_thread_com));
  766                 show_thread_page();
  767                 break;
  768 
  769             case GLOBAL_CONNECTION_INFO:
  770                 show_connection_page();
  771                 show_thread_page();
  772                 break;
  773 
  774             case GLOBAL_LOOKUP_MESSAGEID:
  775                 if ((n = prompt_msgid()) != ART_UNAVAILABLE)
  776                     ret_code = enter_pager(n, FALSE, THREAD_LEVEL);
  777                 break;
  778 
  779             case GLOBAL_SEARCH_REPEAT:
  780                 info_message(_(txt_no_prev_search));
  781                 break;
  782 
  783             case GLOBAL_SEARCH_BODY:            /* search article body */
  784                 if ((n = search_body(group, find_response(thread_basenote, thdmenu.curr), repeat_search)) != -1) {
  785                     fixup_thread(n, FALSE);
  786                     ret_code = enter_pager(n, FALSE, THREAD_LEVEL);
  787                 }
  788                 break;
  789 
  790             case GLOBAL_SEARCH_AUTHOR_FORWARD:          /* author search */
  791             case GLOBAL_SEARCH_AUTHOR_BACKWARD:
  792             case GLOBAL_SEARCH_SUBJECT_FORWARD:         /* subject search */
  793             case GLOBAL_SEARCH_SUBJECT_BACKWARD:
  794                 if ((n = search(func, find_response(thread_basenote, thdmenu.curr), repeat_search)) != -1)
  795                     fixup_thread(n, TRUE);
  796                 break;
  797 
  798             case GLOBAL_TOGGLE_HELP_DISPLAY:        /* toggle mini help menu */
  799                 toggle_mini_help(THREAD_LEVEL);
  800                 show_thread_page();
  801                 break;
  802 
  803             case GLOBAL_TOGGLE_INVERSE_VIDEO:   /* toggle inverse video */
  804                 toggle_inverse_video();
  805                 show_thread_page();
  806                 show_inverse_video_status();
  807                 break;
  808 
  809 #ifdef HAVE_COLOR
  810             case GLOBAL_TOGGLE_COLOR:       /* toggle color */
  811                 if (toggle_color()) {
  812                     show_thread_page();
  813                     show_color_status();
  814                 }
  815                 break;
  816 #endif /* HAVE_COLOR */
  817 
  818             case GLOBAL_QUIT:           /* return to previous level */
  819                 ret_code = GRP_EXIT;
  820                 break;
  821 
  822             case GLOBAL_QUIT_TIN:           /* quit */
  823                 ret_code = GRP_QUIT;
  824                 break;
  825 
  826             case THREAD_TAG_PARTS:          /* tag/untag article */
  827                 /* Find index of current article */
  828                 if ((n = find_response(thread_basenote, thdmenu.curr)) < 0)
  829                     break;
  830                 else {
  831                     int old_num = num_of_tagged_arts;
  832 
  833                     if (tag_multipart(n) != 0) {
  834                         update_thread_page();
  835 
  836                         if (old_num < num_of_tagged_arts)
  837                             info_message(_(txt_info_all_parts_tagged));
  838                         else
  839                             info_message(_(txt_info_all_parts_untagged));
  840                     }
  841                 }
  842                 break;
  843 
  844             case THREAD_TAG:            /* tag/untag article */
  845                 /* Find index of current article */
  846                 if ((n = find_response(thread_basenote, thdmenu.curr)) < 0)
  847                     break;
  848                 else {
  849                     t_bool tagged;
  850 
  851                     if ((tagged = tag_article(n))) {
  852 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
  853                         if ((wtmp = char2wchar_t(tin_ltoa((&arts[n])->tagged, 3)))) {
  854                             mark_screen(thdmenu.curr, mark_offset - (3 - art_mark_width), wtmp);
  855                             free(wtmp);
  856                         }
  857 #else
  858                         mark_screen(thdmenu.curr, mark_offset - 2, tin_ltoa((&arts[n])->tagged, 3));
  859 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
  860                     } else
  861                         update_thread_page();                       /* Must update whole page */
  862 
  863                     /* Automatically advance to next art if not at end of thread */
  864                     if (thdmenu.curr + 1 < thdmenu.max)
  865                         move_down();
  866                     else
  867                         draw_thread_arrow();
  868 
  869                     info_message(tagged ? _(txt_prefix_tagged) : _(txt_prefix_untagged), txt_article_singular);
  870                 }
  871                 break;
  872 
  873             case GLOBAL_BUGREPORT:
  874                 bug_report();
  875                 break;
  876 
  877             case THREAD_UNTAG:          /* untag all articles */
  878                 if (grpmenu.curr >= 0 && untag_all_articles())
  879                     update_thread_page();
  880                 break;
  881 
  882             case GLOBAL_VERSION:            /* version */
  883                 info_message(cvers);
  884                 break;
  885 
  886             case MARK_THREAD_UNREAD:        /* mark thread as unread */
  887                 thd_mark_unread(group, base[thread_basenote]);
  888                 update_thread_page();
  889                 info_message(_(txt_marked_as_unread), _(txt_thread_upper));
  890                 break;
  891 
  892             case THREAD_SELECT_ARTICLE:     /* mark article as selected */
  893             case THREAD_TOGGLE_ARTICLE_SELECTION:       /* toggle article as selected */
  894                 if ((n = find_response(thread_basenote, thdmenu.curr)) < 0)
  895                     break;
  896                 arts[n].selected = (!(func == THREAD_TOGGLE_ARTICLE_SELECTION && arts[n].selected));    /* TODO: optimise? */
  897 /*              update_thread_page(); */
  898                 mark[0] = get_art_mark(&arts[n]);
  899 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
  900                 mark_screen(thdmenu.curr, mark_offset + (art_mark_width - wcwidth(mark[0])), mark);
  901 #else
  902                 mark_screen(thdmenu.curr, mark_offset, mark);
  903 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
  904                 if (thdmenu.curr + 1 < thdmenu.max)
  905                     move_down();
  906                 else
  907                     draw_thread_arrow();
  908                 break;
  909 
  910             case THREAD_REVERSE_SELECTIONS:     /* reverse selections */
  911                 for_each_art_in_thread(i, thread_basenote)
  912                     arts[i].selected = bool_not(arts[i].selected);
  913                 update_thread_page();
  914                 break;
  915 
  916             case THREAD_UNDO_SELECTIONS:        /* undo selections */
  917                 for_each_art_in_thread(i, thread_basenote)
  918                     arts[i].selected = FALSE;
  919                 update_thread_page();
  920                 break;
  921 
  922             case GLOBAL_POSTPONED:      /* post postponed article */
  923                 if (can_post) {
  924                     if (pickup_postponed_articles(FALSE, FALSE))
  925                         show_thread_page();
  926                 } else
  927                     info_message(_(txt_cannot_post));
  928                 break;
  929 
  930             case GLOBAL_DISPLAY_POST_HISTORY:   /* display messages posted by user */
  931                 if (post_hist_page())
  932                     return GRP_EXIT;
  933                 break;
  934 
  935             case GLOBAL_TOGGLE_INFO_LAST_LINE:      /* display subject in last line */
  936                 tinrc.info_in_last_line = bool_not(tinrc.info_in_last_line);
  937                 show_thread_page();
  938                 break;
  939 
  940             default:
  941                 info_message(_(txt_bad_command), PrintFuncKey(key, GLOBAL_HELP, thread_keys));
  942         }
  943     } /* ret_code >= 0 */
  944 
  945     set_xclick_off();
  946     clear_note_area();
  947 
  948     return ret_code;
  949 }
  950 
  951 
  952 static void
  953 show_thread_page(
  954     void)
  955 {
  956     char keyhelp[MAXKEYLEN];
  957     char *title;
  958     int i, art, keyhelplen;
  959     size_t len;
  960 
  961     signal_context = cThread;
  962     currmenu = &thdmenu;
  963     show_subject = FALSE;
  964 
  965     ClearScreen();
  966     set_first_screen_item();
  967 
  968     parse_format_string(curr_group->attribute->thread_format, &thrd_fmt);
  969     mark_offset = 0;
  970 
  971     if (show_subject)
  972         title = fmt_string(_(txt_stp_list_thread), grpmenu.curr + 1, grpmenu.max);
  973     else {
  974         /*
  975          * determine max. length for centered title
  976          * txt_stp_thread: "Thread (%.*s)"
  977          *                          ^^^^
  978          * Subtracting 4 from strwidth results in a trimmed long title with
  979          * a space on the right and no space on the left edge of the screen
  980          * --> subtracting only 3 makes it symmetrical
  981          */
  982         if (tinrc.show_help_mail_sign != SHOW_SIGN_NONE) {
  983             if (tinrc.show_help_mail_sign == SHOW_SIGN_MAIL)
  984                 len = cCOLS - (2 * strwidth(_(txt_you_have_mail))) - (strwidth(_(txt_stp_thread)) - 3);
  985             else {
  986                 PrintFuncKey(keyhelp, GLOBAL_HELP, thread_keys);
  987                 keyhelplen = strwidth(keyhelp);
  988                 if (tinrc.show_help_mail_sign == SHOW_SIGN_HELP)
  989                     len = cCOLS - (2 * (strwidth(_(txt_type_h_for_help)) - 2 + keyhelplen)) - (strwidth(_(txt_stp_thread)) - 3);
  990                 else
  991                     len = cCOLS - (2 * MAX(strwidth(_(txt_type_h_for_help)) - 2 + keyhelplen, strwidth(_(txt_you_have_mail)))) - (strwidth(_(txt_stp_thread)) - 3);
  992             }
  993         } else
  994             len = cCOLS - (strwidth(_(txt_stp_thread)) - 3);
  995 
  996         title = fmt_string(_(txt_stp_thread), len, arts[thread_respnum].subject);
  997     }
  998 
  999     show_title(title);
 1000     free(title);
 1001 
 1002     art = find_response(thread_basenote, thdmenu.first);
 1003     for (i = thdmenu.first; i < thdmenu.first + NOTESLINES && i < thdmenu.max; ++i) {
 1004         build_tline(i, &arts[art]);
 1005         if ((art = next_response(art)) < 0)
 1006             break;
 1007     }
 1008 
 1009     show_mini_help(THREAD_LEVEL);
 1010     draw_thread_arrow();
 1011 }
 1012 
 1013 
 1014 static void
 1015 update_thread_page(
 1016     void)
 1017 {
 1018 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
 1019     wchar_t mark[] = { L'\0', L'\0' };
 1020     wchar_t *wtmp;
 1021 #else
 1022     char mark[] = { '\0', '\0' };
 1023 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
 1024     int i, the_index;
 1025 
 1026     the_index = find_response(thread_basenote, thdmenu.first);
 1027     assert(thdmenu.first != 0 || the_index == thread_respnum);
 1028 
 1029     for (i = thdmenu.first; i < thdmenu.first + NOTESLINES && i < thdmenu.max; ++i) {
 1030         if ((&arts[the_index])->tagged) {
 1031 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
 1032             if ((wtmp = char2wchar_t(tin_ltoa((&arts[the_index])->tagged, 3)))) {
 1033                 mark_screen(i, mark_offset - (3 - art_mark_width), wtmp);
 1034                 free(wtmp);
 1035             }
 1036 #else
 1037             mark_screen(i, mark_offset - 2, tin_ltoa((&arts[the_index])->tagged, 3));
 1038 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
 1039         } else {
 1040             mark[0] = get_art_mark(&arts[the_index]);
 1041 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
 1042             mark_screen(i, mark_offset - (3 - art_mark_width), L"   "); /* clear space used by tag numbering */
 1043             mark_screen(i, mark_offset + (art_mark_width - wcwidth(mark[0])), mark);
 1044 #else
 1045             mark_screen(i, mark_offset - 2, "  ");  /* clear space used by tag numbering */
 1046             mark_screen(i, mark_offset, mark);
 1047 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
 1048             if (mark[0] == tinrc.art_marked_selected)
 1049                 draw_mark_selected(i);
 1050         }
 1051         if ((the_index = next_response(the_index)) == -1)
 1052             break;
 1053     }
 1054 
 1055     draw_thread_arrow();
 1056 }
 1057 
 1058 
 1059 static void
 1060 draw_thread_arrow(
 1061     void)
 1062 {
 1063     draw_arrow_mark(INDEX_TOP + thdmenu.curr - thdmenu.first);
 1064 
 1065     if (tinrc.info_in_last_line)
 1066         info_message("%s", arts[find_response(thread_basenote, thdmenu.curr)].subject);
 1067     else if (thdmenu.curr == thdmenu.max - 1)
 1068         info_message(_(txt_end_of_thread));
 1069 }
 1070 
 1071 
 1072 /*
 1073  * Fix all the internal pointers if the current thread/response has
 1074  * changed.
 1075  */
 1076 void
 1077 fixup_thread(
 1078     int respnum,
 1079     t_bool redraw)
 1080 {
 1081     int basenote = which_thread(respnum);
 1082     int old_thread_basenote = thread_basenote;
 1083 
 1084     if (basenote >= 0) {
 1085         thread_basenote = basenote;
 1086         thdmenu.max = num_of_responses(thread_basenote) + 1;
 1087         thread_respnum = (int) base[thread_basenote];
 1088         grpmenu.curr = basenote;
 1089         if (redraw && basenote != old_thread_basenote)
 1090             show_thread_page();
 1091     }
 1092 
 1093     if (redraw)
 1094         move_to_item(which_response(respnum));      /* Redraw screen etc.. */
 1095 }
 1096 
 1097 
 1098 /*
 1099  * Return the number of unread articles there are within a thread
 1100  */
 1101 int
 1102 new_responses(
 1103     int thread)
 1104 {
 1105     int i;
 1106     int sum = 0;
 1107 
 1108     for_each_art_in_thread(i, thread) {
 1109         if (arts[i].status != ART_READ)
 1110             sum++;
 1111     }
 1112 
 1113     return sum;
 1114 }
 1115 
 1116 
 1117 /*
 1118  * Which base note (an index into base[]) does a respnum (an index into
 1119  * arts[]) correspond to?
 1120  *
 1121  * In other words, base[] points to an entry in arts[] which is the head of
 1122  * a thread, linked with arts[].thread. For any q: arts[q], find i such that
 1123  * base[i]->arts[n]->arts[o]->...->arts[q]
 1124  *
 1125  * Note that which_thread() can return -1 if in show_read_only mode and the
 1126  * article of interest has been read as well as all other articles in the
 1127  * thread, thus resulting in no base[] entry for it.
 1128  */
 1129 int
 1130 which_thread(
 1131     int n)
 1132 {
 1133     int i, j;
 1134 
 1135     /* Move to top of thread */
 1136     for (i = n; arts[i].prev >= 0; i = arts[i].prev)
 1137         ;
 1138     /* Find in base[] */
 1139     for (j = 0; j < grpmenu.max; j++) {
 1140         if (base[j] == i)
 1141             return j;
 1142     }
 1143 
 1144 #ifdef DEBUG
 1145     if (debug & (DEBUG_FILTER | DEBUG_REFS))
 1146         error_message(2, _(txt_cannot_find_base_art), n);
 1147 #endif /* DEBUG */
 1148     return -1;
 1149 }
 1150 
 1151 
 1152 /*
 1153  * Find how deep in its' thread arts[n] is. Start counting at zero
 1154  */
 1155 int
 1156 which_response(
 1157     int n)
 1158 {
 1159     int i, j;
 1160     int num = 0;
 1161 
 1162     if ((i = which_thread(n)) == -1)
 1163         return 0;
 1164 
 1165     for_each_art_in_thread(j, i) {
 1166         if (j == n)
 1167             break;
 1168         else
 1169             num++;
 1170     }
 1171 
 1172     return num;
 1173 }
 1174 
 1175 
 1176 /*
 1177  * Given an index into base[], find the number of responses for
 1178  * that basenote
 1179  */
 1180 int
 1181 num_of_responses(
 1182     int n)
 1183 {
 1184     int i;
 1185     int sum = 0;
 1186 #ifndef NDEBUG
 1187     int oldi = -3;
 1188 
 1189     assert(n < grpmenu.max && n >= 0);
 1190 #endif /* !NDEBUG */
 1191 
 1192     for_each_art_in_thread(i, n) {
 1193 #ifndef NDEBUG
 1194         assert(i != ART_EXPIRED);
 1195         assert(i != oldi);
 1196         oldi = i;
 1197 #endif /* !NDEBUG */
 1198         sum++;
 1199     }
 1200 
 1201     return sum - 1;
 1202 }
 1203 
 1204 
 1205 /*
 1206  * Calculating the score of a thread has been extracted from stat_thread()
 1207  * because we need it also in art.c to sort base[].
 1208  * get_score_of_thread expects the number of the first article of a thread.
 1209  */
 1210 int
 1211 get_score_of_thread(
 1212     int n)
 1213 {
 1214     int i;
 1215     int j = 0;
 1216     int score = 0;
 1217 
 1218     for (i = n; i >= 0; i = arts[i].thread) {
 1219         /*
 1220          * TODO: do we want to take the score of read articles into account?
 1221          */
 1222         if (arts[i].status != ART_READ || arts[i].killed == ART_KILLED_UNREAD /* || tinrc.kill_level == KILL_THREAD */) {
 1223             if (tinrc.thread_score == THREAD_SCORE_MAX) {
 1224                 /* we use the maximum article score for the complete thread */
 1225                 if ((arts[i].score > score) && (arts[i].score > 0))
 1226                     score = arts[i].score;
 1227                 else {
 1228                     if ((arts[i].score < score) && (score <= 0))
 1229                         score = arts[i].score;
 1230                 }
 1231             } else { /* tinrc.thread_score >= THREAD_SCORE_SUM */
 1232                 /* sum scores of unread arts and count num. arts */
 1233                 score += arts[i].score;
 1234                 j++;
 1235             }
 1236         }
 1237     }
 1238     if (j && tinrc.thread_score == THREAD_SCORE_WEIGHT)
 1239         score /= j;
 1240 
 1241     return score;
 1242 }
 1243 
 1244 
 1245 /*
 1246  * Given an index into base[], return relevant statistics
 1247  */
 1248 int
 1249 stat_thread(
 1250     int n,
 1251     struct t_art_stat *sbuf) /* return value is always ignored */
 1252 {
 1253     int i;
 1254     MultiPartInfo minfo = {0};
 1255 
 1256     sbuf->total = 0;
 1257     sbuf->unread = 0;
 1258     sbuf->seen = 0;
 1259     sbuf->deleted = 0;
 1260     sbuf->inrange = 0;
 1261     sbuf->selected_total = 0;
 1262     sbuf->selected_unread = 0;
 1263     sbuf->selected_seen = 0;
 1264     sbuf->killed = 0;
 1265     sbuf->art_mark = tinrc.art_marked_read;
 1266     sbuf->score = 0 /* -(SCORE_MAX) */;
 1267     sbuf->time = 0;
 1268     sbuf->multipart_compare_len = 0;
 1269     sbuf->multipart_total = 0;
 1270     sbuf->multipart_have = 0;
 1271 
 1272     for_each_art_in_thread(i, n) {
 1273         ++sbuf->total;
 1274         if (arts[i].inrange)
 1275             ++sbuf->inrange;
 1276 
 1277         if (arts[i].delete_it)
 1278             ++sbuf->deleted;
 1279 
 1280         if (arts[i].status == ART_UNREAD) {
 1281             ++sbuf->unread;
 1282 
 1283             if (arts[i].date > sbuf->time)
 1284                 sbuf->time = arts[i].date;
 1285         } else if (arts[i].status == ART_WILL_RETURN)
 1286             ++sbuf->seen;
 1287 
 1288         if (arts[i].selected) {
 1289             ++sbuf->selected_total;
 1290             if (arts[i].status == ART_UNREAD)
 1291                 ++sbuf->selected_unread;
 1292             else if (arts[i].status == ART_WILL_RETURN)
 1293                 ++sbuf->selected_seen;
 1294         }
 1295 
 1296         if (arts[i].killed)
 1297             ++sbuf->killed;
 1298 
 1299         if ((curr_group->attribute->thread_articles == THREAD_MULTI) && global_get_multipart_info(i, &minfo) && (minfo.total >= 1)) {
 1300             sbuf->multipart_compare_len = minfo.subject_compare_len;
 1301             sbuf->multipart_total = minfo.total;
 1302             sbuf->multipart_have++;
 1303         }
 1304     }
 1305 
 1306     sbuf->score = get_score_of_thread((int) base[n]);
 1307 
 1308     if (sbuf->inrange)
 1309         sbuf->art_mark = tinrc.art_marked_inrange;
 1310     else if (sbuf->deleted)
 1311         sbuf->art_mark = tinrc.art_marked_deleted;
 1312     else if (sbuf->selected_unread)
 1313         sbuf->art_mark = tinrc.art_marked_selected;
 1314     else if (sbuf->unread) {
 1315         if (tinrc.recent_time && (time((time_t *) 0) - sbuf->time) < (tinrc.recent_time * DAY))
 1316             sbuf->art_mark = tinrc.art_marked_recent;
 1317         else
 1318             sbuf->art_mark = tinrc.art_marked_unread;
 1319     }
 1320     else if (sbuf->seen)
 1321         sbuf->art_mark = tinrc.art_marked_return;
 1322     else if (sbuf->selected_total)
 1323         sbuf->art_mark = tinrc.art_marked_read_selected;
 1324     else if (sbuf->killed == sbuf->total)
 1325         sbuf->art_mark = tinrc.art_marked_killed;
 1326     else
 1327         sbuf->art_mark = tinrc.art_marked_read;
 1328     return sbuf->total;
 1329 }
 1330 
 1331 
 1332 /*
 1333  * Find the next response to arts[n]. Go to the next basenote if there
 1334  * are no more responses in this thread
 1335  */
 1336 int
 1337 next_response(
 1338     int n)
 1339 {
 1340     int i;
 1341 
 1342     if (arts[n].thread >= 0)
 1343         return arts[n].thread;
 1344 
 1345     i = which_thread(n) + 1;
 1346 
 1347     if (i >= grpmenu.max)
 1348         return -1;
 1349 
 1350     return (int) base[i];
 1351 }
 1352 
 1353 
 1354 /*
 1355  * Given a respnum (index into arts[]), find the respnum of the
 1356  * next basenote
 1357  */
 1358 int
 1359 next_thread(
 1360     int n)
 1361 {
 1362     int i;
 1363 
 1364     i = which_thread(n) + 1;
 1365     if (i >= grpmenu.max)
 1366         return -1;
 1367 
 1368     return (int) base[i];
 1369 }
 1370 
 1371 
 1372 /*
 1373  * Find the previous response. Go to the last response in the previous
 1374  * thread if we go past the beginning of this thread.
 1375  * Return -1 if we are at the start of the group
 1376  */
 1377 int
 1378 prev_response(
 1379     int n)
 1380 {
 1381     int i;
 1382 
 1383     if (arts[n].prev >= 0)
 1384         return arts[n].prev;
 1385 
 1386     i = which_thread(n) - 1;
 1387 
 1388     if (i < 0)
 1389         return -1;
 1390 
 1391     return find_response(i, num_of_responses(i));
 1392 }
 1393 
 1394 
 1395 /*
 1396  * return index in arts[] of the 'n'th response in thread base 'i'
 1397  */
 1398 int
 1399 find_response(
 1400     int i,
 1401     int n)
 1402 {
 1403     int j;
 1404 
 1405     j = (int) base[i];
 1406 
 1407     while (n-- > 0 && arts[j].thread >= 0)
 1408         j = arts[j].thread;
 1409 
 1410     return j;
 1411 }
 1412 
 1413 
 1414 /*
 1415  * Find the next unread response to art[n] in this group. If no response is
 1416  * found from current point to the end restart from beginning of articles.
 1417  * If no more responses can be found, return -1
 1418  */
 1419 int
 1420 next_unread(
 1421     int n)
 1422 {
 1423     int cur_base_art = n;
 1424 
 1425     while (n >= 0) {
 1426         if (((arts[n].status == ART_UNREAD) || (arts[n].status == ART_WILL_RETURN)) && arts[n].thread != ART_EXPIRED)
 1427             return n;
 1428 
 1429         n = next_response(n);
 1430     }
 1431 
 1432     if (curr_group->attribute->wrap_on_next_unread) {
 1433         n = (int) base[0];
 1434         while (n != cur_base_art && n >= 0) {
 1435             if (((arts[n].status == ART_UNREAD) || (arts[n].status == ART_WILL_RETURN)) && arts[n].thread != ART_EXPIRED)
 1436                 return n;
 1437 
 1438             n = next_response(n);
 1439         }
 1440     }
 1441 
 1442     return -1;
 1443 }
 1444 
 1445 
 1446 /*
 1447  * Find the previous unread response in this thread
 1448  */
 1449 int
 1450 prev_unread(
 1451     int n)
 1452 {
 1453     while (n >= 0) {
 1454         if (arts[n].status != ART_READ && arts[n].thread != ART_EXPIRED)
 1455             return n;
 1456 
 1457         n = prev_response(n);
 1458     }
 1459 
 1460     return -1;
 1461 }
 1462 
 1463 
 1464 static t_bool
 1465 find_unexpired(
 1466     struct t_msgid *ptr)
 1467 {
 1468     return ptr && (!IS_EXPIRED(ptr) || find_unexpired(ptr->child) || find_unexpired(ptr->sibling));
 1469 }
 1470 
 1471 
 1472 static t_bool
 1473 has_sibling(
 1474     struct t_msgid *ptr)
 1475 {
 1476     do {
 1477         if (find_unexpired(ptr->sibling))
 1478             return TRUE;
 1479         ptr = ptr->parent;
 1480     } while (ptr && IS_EXPIRED(ptr));
 1481     return FALSE;
 1482 }
 1483 
 1484 
 1485 /*
 1486  * mutt-like subject according. by sjpark@sparcs.kaist.ac.kr
 1487  * string in prefix will be overwritten up to length len prefix will always
 1488  * be terminated with \0
 1489  * make sure prefix is at least len+1 bytes long (to hold the terminating
 1490  * null byte)
 1491  */
 1492 static void
 1493 make_prefix(
 1494     struct t_msgid *art,
 1495     char *prefix,
 1496     int maxlen)
 1497 {
 1498 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
 1499     char *result;
 1500     wchar_t *buf, *buf2;
 1501 #else
 1502     char *buf;
 1503 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
 1504     int prefix_ptr;
 1505     int depth = 0;
 1506     int depth_level = 0;
 1507     struct t_msgid *ptr;
 1508 
 1509     for (ptr = art->parent; ptr; ptr = ptr->parent)
 1510         depth += (!IS_EXPIRED(ptr) ? 1 : 0);
 1511 
 1512     if ((depth == 0) || (maxlen < 1)) {
 1513         prefix[0] = '\0';
 1514         return;
 1515     }
 1516 
 1517     prefix_ptr = depth * 2 - 1;
 1518 
 1519     if (prefix_ptr > maxlen - 1 - !(maxlen % 2)) {
 1520         int odd = ((maxlen % 2) ? 0 : 1);
 1521 
 1522         prefix_ptr -= maxlen - ++depth_level - 2 - odd;
 1523 
 1524         while (prefix_ptr > maxlen - 2 - odd) {
 1525             if (depth_level < maxlen / 5)
 1526                 depth_level++;
 1527             prefix_ptr -= maxlen - depth_level - 2 - odd;
 1528             odd = (odd ? 0 : 1);
 1529         }
 1530     }
 1531 
 1532 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
 1533     buf = my_malloc(sizeof(wchar_t) * (size_t) prefix_ptr + 3 * sizeof(wchar_t));
 1534     buf[prefix_ptr + 2] = (wchar_t) '\0';
 1535 #else
 1536     buf = my_malloc(prefix_ptr + 3);
 1537     buf[prefix_ptr + 2] = '\0';
 1538 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
 1539     buf[prefix_ptr + 1] = TREE_ARROW;
 1540     buf[prefix_ptr] = TREE_HORIZ;
 1541     buf[--prefix_ptr] = (has_sibling(art) ? TREE_VERT_RIGHT : TREE_UP_RIGHT);
 1542 
 1543     for (ptr = art->parent; ptr && prefix_ptr > 1; ptr = ptr->parent) {
 1544         if (IS_EXPIRED(ptr))
 1545             continue;
 1546         buf[--prefix_ptr] = TREE_BLANK;
 1547         buf[--prefix_ptr] = (has_sibling(ptr) ? TREE_VERT : TREE_BLANK);
 1548     }
 1549 
 1550     while (depth_level)
 1551         buf[--depth_level] = TREE_ARROW_WRAP;
 1552 
 1553 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
 1554     buf2 = wcspart(buf, maxlen, FALSE);
 1555     result = wchar_t2char(buf2);
 1556     strcpy(prefix, result);
 1557     free(buf);
 1558     FreeIfNeeded(buf2);
 1559     FreeIfNeeded(result);
 1560 #else
 1561     strncpy(prefix, buf, maxlen);
 1562     prefix[maxlen] = '\0'; /* just in case strlen(buf) > maxlen */
 1563     free(buf);
 1564 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
 1565 }
 1566 
 1567 
 1568 /*
 1569  * There are 3 catchup methods:
 1570  * When exiting thread via <-
 1571  * Catchup thread, move to next one
 1572  * Catchup thread and enter next one with unread arts
 1573  * Return a suitable ret_code
 1574  */
 1575 static int
 1576 thread_catchup(
 1577     t_function func,
 1578     struct t_group *group)
 1579 {
 1580     char buf[LEN];
 1581     int i, n;
 1582     int pyn = 1;
 1583 
 1584     /* Find first unread art in this thread */
 1585     n = ((thdmenu.curr == 0) ? thread_respnum : find_response(thread_basenote, 0));
 1586     for (i = n; i != -1; i = arts[i].thread) {
 1587         if ((arts[i].status == ART_UNREAD) || (arts[i].status == ART_WILL_RETURN))
 1588             break;
 1589     }
 1590 
 1591     if (i != -1) {              /* still unread arts in this thread */
 1592         if (group->attribute->thread_articles == THREAD_NONE)
 1593             snprintf(buf, sizeof(buf), _(txt_mark_art_read), (func == CATCHUP_NEXT_UNREAD) ? _(txt_enter_next_unread_art) : "");
 1594         else
 1595             snprintf(buf, sizeof(buf), _(txt_mark_thread_read), (func == CATCHUP_NEXT_UNREAD) ? _(txt_enter_next_thread) : "");
 1596         if ((!TINRC_CONFIRM_ACTION) || (pyn = prompt_yn(buf, TRUE)) == 1)
 1597             thd_mark_read(curr_group, base[thread_basenote]);
 1598     }
 1599 
 1600     switch (func) {
 1601         case CATCHUP:               /* 'c' */
 1602             if (pyn == 1)
 1603                 return GRP_NEXT;
 1604             break;
 1605 
 1606         case CATCHUP_NEXT_UNREAD:   /* 'C' */
 1607             if (pyn == 1)
 1608                 return GRP_NEXTUNREAD;
 1609             break;
 1610 
 1611         case SPECIAL_CATCHUP_LEFT:          /* <- thread catchup on exit */
 1612             switch (pyn) {
 1613                 case -1:                /* ESC from prompt, stay in group */
 1614                     break;
 1615 
 1616                 case 1:                 /* We caught up - advance group */
 1617                     return GRP_NEXT;
 1618 
 1619                 default:                /* Just leave the group */
 1620                     return GRP_EXIT;
 1621             }
 1622             /* FALLTHROUGH */
 1623         default:
 1624             break;
 1625     }
 1626     return 0;                           /* Default is to stay in current screen */
 1627 }
 1628 
 1629 
 1630 /*
 1631  * This is the single entry point into the article pager
 1632  * art
 1633  *      is the arts[art] we wish to read
 1634  * ignore_unavail
 1635  *      should be set if we wish to keep going after article unavailable
 1636  * level
 1637  *      is the menu from which we came. This should be only be GROUP or THREAD
 1638  *      it is used to set the return code to go back to the calling menu when
 1639  *      not explicitly set
 1640  * Return:
 1641  *  <0 to quit to group menu
 1642  *   0 to stay in thread menu
 1643  *  >0 after normal exit from pager to return to previous menu level
 1644  */
 1645 static int
 1646 enter_pager(
 1647     int art,
 1648     t_bool ignore_unavail,
 1649     int level)
 1650 {
 1651     int i;
 1652 
 1653 again:
 1654     switch ((i = show_page(curr_group, art, &thdmenu.curr))) {
 1655         /* These exit to previous menu level */
 1656         case GRP_QUIT:              /* 'Q' all the way out */
 1657         case GRP_EXIT:              /*     back to group menu */
 1658         case GRP_RETSELECT:         /* 'T' back to select menu */
 1659         case GRP_NEXT:              /* 'c' Move to next thread on group menu */
 1660         case GRP_NEXTUNREAD:        /* 'C' */
 1661         case GRP_KILLED:            /*     article/thread was killed at page level */
 1662             break;
 1663 
 1664         case GRP_ARTABORT:          /* user 'q'uit load of article */
 1665             /* break forces return to group menu */
 1666             if (level == GROUP_LEVEL)
 1667                 break;
 1668             /* else stay on thread menu */
 1669             show_thread_page();
 1670             return 0;
 1671 
 1672         /* Keeps us in thread menu */
 1673         case GRP_ARTUNAVAIL:
 1674             if (ignore_unavail && (art = next_unread(art)) != -1)
 1675                 goto again;
 1676             else if (level == GROUP_LEVEL)
 1677                 return GRP_ARTABORT;
 1678             /* back to thread menu */
 1679             show_thread_page();
 1680             return 0;
 1681 
 1682         case GRP_GOTOTHREAD:        /* 'l' from pager */
 1683             show_thread_page();
 1684             move_to_item(which_response(this_resp));
 1685             return 0;
 1686 
 1687         default:                    /* >=0 normal exit, new basenote */
 1688             fixup_thread(this_resp, FALSE);
 1689 
 1690             if (currmenu != &grpmenu)   /* group menu will redraw itself */
 1691                 currmenu->redraw();
 1692 
 1693             return 1;               /* Must return any +ve integer */
 1694     }
 1695     return i;
 1696 }
 1697 
 1698 
 1699 /*
 1700  * Find index in arts[] of next unread article _IN_THIS_THREAD_
 1701  * Page it or return GRP_NEXTUNREAD if thread is all read
 1702  * (to tell group menu to skip to next thread)
 1703  */
 1704 static int
 1705 thread_tab_pressed(
 1706     void)
 1707 {
 1708     int i, n;
 1709 
 1710     /*
 1711      * Find current position in thread
 1712      */
 1713     n = ((thdmenu.curr == 0) ? thread_respnum : find_response(thread_basenote, thdmenu.curr));
 1714 
 1715     /*
 1716      * Find and display next unread
 1717      */
 1718     for (i = n; i != -1; i = arts[i].thread) {
 1719         if ((arts[i].status == ART_UNREAD) || (arts[i].status == ART_WILL_RETURN))
 1720             return (enter_pager(i, TRUE, THREAD_LEVEL));
 1721     }
 1722 
 1723     /*
 1724      * We ran out of thread, tell group.c to enter the next with unread
 1725      */
 1726     return GRP_NEXTUNREAD;
 1727 }
 1728 
 1729 
 1730 /*
 1731  * Redraw all necessary parts of the screen after FEED_MARK_(UN)READ
 1732  * Move cursor to next unread item if needed
 1733  *
 1734  * Returns TRUE when no next unread art, FALSE otherwise
 1735  */
 1736 t_bool
 1737 thread_mark_postprocess(
 1738     int function,
 1739     t_function feed_type,
 1740     int respnum)
 1741 {
 1742 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
 1743     wchar_t mark[] = { L'\0', L'\0' };
 1744 #else
 1745     char mark[] = { '\0', '\0' };
 1746 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
 1747     int n;
 1748 
 1749     switch (function) {
 1750         case (FEED_MARK_READ):
 1751             if (feed_type == FEED_ARTICLE) {
 1752                 mark[0] = get_art_mark(&arts[respnum]);
 1753 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
 1754                 mark_screen(thdmenu.curr, mark_offset - (3 - art_mark_width), L"   ");
 1755                 mark_screen(thdmenu.curr, mark_offset + (art_mark_width - wcwidth(mark[0])), mark);
 1756 #else
 1757                 mark_screen(thdmenu.curr, mark_offset, mark);
 1758 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
 1759             } else
 1760                 show_thread_page();
 1761 
 1762             if ((n = next_unread(respnum)) == -1)   /* no more unread articles */
 1763                 return TRUE;
 1764             else
 1765                 fixup_thread(n, TRUE);  /* We may be in the next thread now */
 1766             break;
 1767 
 1768         case (FEED_MARK_UNREAD):
 1769             if (feed_type == FEED_ARTICLE) {
 1770                 mark[0] = get_art_mark(&arts[respnum]);
 1771 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
 1772                 mark_screen(thdmenu.curr, mark_offset - (3 - art_mark_width), L"   ");
 1773                 mark_screen(thdmenu.curr, mark_offset + (art_mark_width - wcwidth(mark[0])), mark);
 1774 #else
 1775                 mark_screen(thdmenu.curr, mark_offset, mark);
 1776 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
 1777                 draw_thread_arrow();
 1778             } else
 1779                 show_thread_page();
 1780             break;
 1781 
 1782         default:
 1783             break;
 1784     }
 1785     return FALSE;
 1786 }