"Fossies" - the Fresh Open Source Software Archive

Member "tin-2.4.5/src/thread.c" (1 Dec 2020, 42557 Bytes) of package /linux/misc/tin-2.4.5.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.4_vs_2.4.5.

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