"Fossies" - the Fresh Open Source Software Archive

Member "tin-2.4.2/src/thread.c" (8 Dec 2017, 41924 Bytes) of package /linux/misc/tin-2.4.2.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.1_vs_2.4.2.

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