"Fossies" - the Fresh Open Source Software Archive

Member "tin-2.6.1/src/thread.c" (22 Dec 2021, 45309 Bytes) of package /linux/misc/tin-2.6.1.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.0_vs_2.6.1.

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