"Fossies" - the Fresh Open Source Software Archive

Member "tin-2.4.4/src/thread.c" (20 Nov 2019, 42023 Bytes) of package /linux/misc/tin-2.4.4.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.4.3_vs_2.4.4.

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