"Fossies" - the Fresh Open Source Software Archive

Member "tin-2.4.1/src/thread.c" (12 Oct 2016, 41942 Bytes) of archive /linux/misc/tin-2.4.1.tar.gz:


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.0_vs_2.4.1.

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