"Fossies" - the Fresh Open Source Software Archive

Member "tin-2.4.5/src/group.c" (1 Dec 2020, 45167 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 "group.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    : group.c
    4  *  Author    : I. Lea & R. Skrenta
    5  *  Created   : 1991-04-01
    6  *  Updated   : 2020-06-10
    7  *  Notes     :
    8  *
    9  * Copyright (c) 1991-2021 Iain Lea <iain@bricbrac.de>, Rich Skrenta <skrenta@pbm.com>
   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 /*
   50  * Globally accessible pointer to currently active group
   51  * Any functionality accessed from group level or below can use this pointer.
   52  * Any code invoked from selection level that requires a group context will
   53  * need to manually fix this up
   54  */
   55 struct t_group *curr_group;
   56 static struct t_fmt grp_fmt;
   57 
   58 /*
   59  * Local prototypes
   60  */
   61 static int do_search(t_function func, t_bool repeat);
   62 static int enter_pager(int art, t_bool ignore_unavail);
   63 static int enter_thread(int depth, t_pagerinfo *page);
   64 static int find_new_pos(long old_artnum, int cur_pos);
   65 static int group_catchup(t_function func);
   66 static int tab_pressed(void);
   67 static t_bool prompt_getart_limit(void);
   68 static t_function group_left(void);
   69 static t_function group_right(void);
   70 static void build_sline(int i);
   71 static void build_multipart_header(char *dest, int maxlen, const char *src, int cmplen, int have, int total);
   72 static void draw_subject_arrow(void);
   73 static void show_group_title(t_bool clear_title);
   74 static void show_tagged_lines(void);
   75 static void toggle_read_unread(t_bool force);
   76 static void update_group_page(void);
   77 
   78 /*
   79  * grpmenu.curr is an index into base[] and so equates to the cursor location
   80  * (thread number) on group page
   81  * grpmenu.first is static here
   82  */
   83 t_menu grpmenu = { 0, 0, 0, show_group_page, draw_subject_arrow, build_sline };
   84 
   85 /* TODO: find a better solution */
   86 static int ret_code = 0;        /* Set to < 0 when it is time to leave the group level */
   87 
   88 static void
   89 show_tagged_lines(
   90     void)
   91 {
   92     int i, j;
   93 
   94     for (i = grpmenu.first; i < grpmenu.first + NOTESLINES && i < grpmenu.max; ++i) {
   95         if ((i != grpmenu.curr) && (j = line_is_tagged(base[i])))
   96             mark_screen(i, mark_offset - 2, tin_ltoa(j, 3));
   97     }
   98 }
   99 
  100 
  101 static t_function
  102 group_left(
  103     void)
  104 {
  105     if (curr_group->attribute->group_catchup_on_exit)
  106         return SPECIAL_CATCHUP_LEFT;        /* ie, not via 'c' or 'C' */
  107 
  108     return GLOBAL_QUIT;
  109 }
  110 
  111 
  112 static t_function
  113 group_right(
  114     void)
  115 {
  116     if (grpmenu.curr >= 0 && HAS_FOLLOWUPS(grpmenu.curr)) {
  117         if (curr_group->attribute->auto_list_thread)
  118             return GROUP_LIST_THREAD;
  119         else {
  120             int n = next_unread((int) base[grpmenu.curr]);
  121 
  122             if (n >= 0 && grpmenu.curr == which_thread(n)) {
  123                 ret_code = enter_pager(n, TRUE);
  124                 return GLOBAL_ABORT;    /* TODO: should we return something else? */
  125             }
  126         }
  127     }
  128     return GROUP_READ_BASENOTE;
  129 }
  130 
  131 
  132 /*
  133  * Return Codes:
  134  * GRP_EXIT         'Normal' return to selection menu
  135  * GRP_RETSELECT    We are en route from pager to the selection screen
  136  * GRP_QUIT         User has done a 'Q'
  137  * GRP_NEXT         User wants to move onto next group
  138  * GRP_NEXTUNREAD   User did a 'C'atchup
  139  * GRP_ENTER        'g' command has been used to set group to enter
  140  */
  141 int
  142 group_page(
  143     struct t_group *group)
  144 {
  145     char key[MAXKEYLEN];
  146     int i, n, ii;
  147     int thread_depth;   /* Starting depth in threads we enter */
  148     t_artnum old_artnum = T_ARTNUM_CONST(0);
  149     struct t_art_stat sbuf;
  150     struct t_article *art;
  151     t_bool flag;
  152     t_bool xflag = FALSE;   /* 'X'-flag */
  153     t_bool repeat_search;
  154     t_function func;
  155 
  156     /*
  157      * Set the group attributes
  158      */
  159     group->read_during_session = TRUE;
  160 
  161     curr_group = group;                 /* For global access to the current group */
  162     num_of_tagged_arts = 0;
  163     range_active = FALSE;
  164 
  165     last_resp = -1;
  166     this_resp = -1;
  167 
  168     /*
  169      * update index file. quit group level if user aborts indexing
  170      */
  171     if (!index_group(group)) {
  172         for_each_art(i) {
  173             art = &arts[i];
  174             FreeAndNull(art->refs);
  175             FreeAndNull(art->msgid);
  176         }
  177         curr_group = NULL;
  178         tin_errno = 0;
  179         return GRP_RETSELECT;
  180     }
  181 
  182     /*
  183      * Position 'grpmenu.curr' accordingly
  184      */
  185     pos_first_unread_thread();
  186     /* reset grpmenu.first */
  187     grpmenu.first = 0;
  188 
  189     clear_note_area();
  190 
  191     if (group->attribute->auto_select) {
  192         error_message(2, _(txt_autoselecting_articles), printascii(key, func_to_key(GROUP_MARK_UNSELECTED_ARTICLES_READ, group_keys)));
  193         do_auto_select_arts();                      /* 'X' command */
  194         xflag = TRUE;
  195     }
  196 
  197     show_group_page();
  198 
  199 #   ifdef DEBUG
  200     if (debug & DEBUG_NEWSRC)
  201         debug_print_bitmap(group, NULL);
  202 #   endif /* DEBUG */
  203 
  204     /* reset ret_code */
  205     ret_code = 0;
  206     while (ret_code >= 0) {
  207         set_xclick_on();
  208         if ((func = handle_keypad(group_left, group_right, global_mouse_action, group_keys)) == GLOBAL_SEARCH_REPEAT) {
  209             func = last_search;
  210             repeat_search = TRUE;
  211         } else
  212             repeat_search = FALSE;
  213 
  214         switch (func) {
  215             case GLOBAL_ABORT:
  216                 break;
  217 
  218             case DIGIT_1:
  219             case DIGIT_2:
  220             case DIGIT_3:
  221             case DIGIT_4:
  222             case DIGIT_5:
  223             case DIGIT_6:
  224             case DIGIT_7:
  225             case DIGIT_8:
  226             case DIGIT_9:
  227                 if (grpmenu.max)
  228                     prompt_item_num(func_to_key(func, group_keys), group->attribute->thread_articles == THREAD_NONE ? _(txt_select_art) : _(txt_select_thread));
  229                 break;
  230 
  231 #   ifndef NO_SHELL_ESCAPE
  232             case GLOBAL_SHELL_ESCAPE:
  233                 do_shell_escape();
  234                 break;
  235 #   endif /* !NO_SHELL_ESCAPE */
  236 
  237             case GLOBAL_FIRST_PAGE:     /* show first page of threads */
  238                 top_of_list();
  239                 break;
  240 
  241             case GLOBAL_LAST_PAGE:      /* show last page of threads */
  242                 end_of_list();
  243                 break;
  244 
  245             case GLOBAL_LAST_VIEWED:    /* go to last viewed article */
  246                 /*
  247                  * If the last art is no longer in a thread then we can't display it
  248                  */
  249                 if (this_resp < 0 || (which_thread(this_resp) == -1))
  250                     info_message(_(txt_no_last_message));
  251                 else
  252                     ret_code = enter_pager(this_resp, FALSE);
  253                 break;
  254 
  255             case GLOBAL_PIPE:       /* pipe article/thread/tagged arts to command */
  256                 if (grpmenu.curr >= 0)
  257                     feed_articles(FEED_PIPE, GROUP_LEVEL, NOT_ASSIGNED, group, (int) base[grpmenu.curr]);
  258                 break;
  259 
  260             case GROUP_MAIL:    /* mail article to somebody */
  261                 if (grpmenu.curr >= 0)
  262                     feed_articles(FEED_MAIL, GROUP_LEVEL, NOT_ASSIGNED, group, (int) base[grpmenu.curr]);
  263                 break;
  264 
  265 #ifndef DISABLE_PRINTING
  266             case GLOBAL_PRINT:  /* output art/thread/tagged arts to printer */
  267                 if (grpmenu.curr >= 0)
  268                     feed_articles(FEED_PRINT, GROUP_LEVEL, NOT_ASSIGNED, group, (int) base[grpmenu.curr]);
  269                 break;
  270 #endif /* !DISABLE_PRINTING */
  271 
  272             case GROUP_REPOST:  /* repost current article */
  273                 if (can_post) {
  274                     if (grpmenu.curr >= 0)
  275                         feed_articles(FEED_REPOST, GROUP_LEVEL, NOT_ASSIGNED, group, (int) base[grpmenu.curr]);
  276                 } else
  277                     info_message(_(txt_cannot_post));
  278                 break;
  279 
  280             case GROUP_SAVE:    /* save articles with prompting */
  281                 if (grpmenu.curr >= 0)
  282                     feed_articles(FEED_SAVE, GROUP_LEVEL, NOT_ASSIGNED, group, (int) base[grpmenu.curr]);
  283                 break;
  284 
  285             case GROUP_AUTOSAVE:    /* Auto-save articles without prompting */
  286                 if (grpmenu.curr >= 0)
  287                     feed_articles(FEED_AUTOSAVE, GROUP_LEVEL, NOT_ASSIGNED, group, (int) base[grpmenu.curr]);
  288                 break;
  289 
  290             case GLOBAL_SET_RANGE:  /* set range */
  291                 if (grpmenu.curr >= 0 && set_range(GROUP_LEVEL, 1, grpmenu.max, grpmenu.curr + 1)) {
  292                     range_active = TRUE;
  293                     show_group_page();
  294                 }
  295                 break;
  296 
  297             case GLOBAL_SEARCH_REPEAT:
  298                 info_message(_(txt_no_prev_search));
  299                 break;
  300 
  301             case GLOBAL_SEARCH_AUTHOR_FORWARD:
  302             case GLOBAL_SEARCH_AUTHOR_BACKWARD:
  303             case GLOBAL_SEARCH_SUBJECT_FORWARD:
  304             case GLOBAL_SEARCH_SUBJECT_BACKWARD:
  305                 if ((thread_depth = do_search(func, repeat_search)) != 0)
  306                     ret_code = enter_thread(thread_depth, NULL);
  307                 break;
  308 
  309             case GLOBAL_SEARCH_BODY:    /* search article body */
  310                 if (grpmenu.curr >= 0) {
  311                     if ((n = search_body(group, (int) base[grpmenu.curr], repeat_search)) != -1)
  312                         ret_code = enter_pager(n, FALSE);
  313                 } else
  314                     info_message(_(txt_no_arts));
  315                 break;
  316 
  317             case GROUP_READ_BASENOTE:   /* read current basenote */
  318                 if (grpmenu.curr >= 0)
  319                     ret_code = enter_pager((int) base[grpmenu.curr], FALSE /*TRUE*/);
  320                 else
  321                     info_message(_(txt_no_arts));
  322                 break;
  323 
  324             case GROUP_CANCEL:  /* cancel current basenote */
  325                 if (grpmenu.curr >= 0) {
  326                     if (can_post || group->attribute->mailing_list != NULL) {
  327                         int ret;
  328 
  329                         n = (int) base[grpmenu.curr];
  330                         ret = art_open(TRUE, &arts[n], group, &pgart, TRUE, _(txt_reading_article));
  331                         if (ret != ART_UNAVAILABLE && ret != ART_ABORT && cancel_article(group, &arts[n], n))
  332                             show_group_page();
  333                         art_close(&pgart);
  334                     } else
  335                         info_message(_(txt_cannot_post));
  336                 } else
  337                     info_message(_(txt_no_arts));
  338                 break;
  339 
  340             case GROUP_NEXT_UNREAD_ARTICLE_OR_GROUP:    /* goto next unread article/group */
  341                 ret_code = tab_pressed();
  342                 break;
  343 
  344             case GLOBAL_PAGE_DOWN:
  345                 page_down();
  346                 break;
  347 
  348             case GLOBAL_MENU_FILTER_SELECT:     /* auto-select article menu */
  349             case GLOBAL_MENU_FILTER_KILL:       /* kill article menu */
  350                 if (grpmenu.curr < 0) {
  351                     info_message(_(txt_no_arts));
  352                     break;
  353                 }
  354                 n = (int) base[grpmenu.curr];
  355                 if (filter_menu(func, group, &arts[n])) {
  356                     old_artnum = arts[n].artnum;
  357                     unfilter_articles(group);
  358                     filter_articles(group);
  359                     make_threads(group, FALSE);
  360                     grpmenu.curr = find_new_pos(old_artnum, grpmenu.curr);
  361                 }
  362                 show_group_page();
  363                 break;
  364 
  365             case GLOBAL_EDIT_FILTER:
  366                 if (invoke_editor(filter_file, filter_file_offset, NULL)) {
  367                     old_artnum = grpmenu.max > 0 ? arts[(int) base[grpmenu.curr]].artnum : T_ARTNUM_CONST(-1);
  368                     unfilter_articles(group);
  369                     (void) read_filter_file(filter_file);
  370                     filter_articles(group);
  371                     make_threads(group, FALSE);
  372                     grpmenu.curr = old_artnum >= T_ARTNUM_CONST(0) ? find_new_pos(old_artnum, grpmenu.curr) : grpmenu.max - 1;
  373                 }
  374                 show_group_page();
  375                 break;
  376 
  377             case GLOBAL_QUICK_FILTER_SELECT:        /* quickly auto-select article */
  378             case GLOBAL_QUICK_FILTER_KILL:          /* quickly kill article */
  379                 if (grpmenu.curr < 0) {
  380                     info_message(_(txt_no_arts));
  381                     break;
  382                 }
  383                 if ((!TINRC_CONFIRM_ACTION) || prompt_yn((func == GLOBAL_QUICK_FILTER_KILL) ? _(txt_quick_filter_kill) : _(txt_quick_filter_select), TRUE) == 1) {
  384                     n = (int) base[grpmenu.curr]; /* should this depend on show_only_unread_arts? */
  385                     if (quick_filter(func, group, &arts[n])) {
  386                         old_artnum = arts[n].artnum;
  387                         unfilter_articles(group);
  388                         filter_articles(group);
  389                         make_threads(group, FALSE);
  390                         grpmenu.curr = find_new_pos(old_artnum, grpmenu.curr);
  391                         show_group_page();
  392                         info_message((func == GLOBAL_QUICK_FILTER_KILL) ? _(txt_info_add_kill) : _(txt_info_add_select));
  393                     }
  394                 }
  395                 break;
  396 
  397             case GLOBAL_REDRAW_SCREEN:
  398                 my_retouch();
  399                 set_xclick_off();
  400                 show_group_page();
  401                 break;
  402 
  403             case GLOBAL_LINE_DOWN:
  404                 move_down();
  405                 break;
  406 
  407             case GLOBAL_LINE_UP:
  408                 move_up();
  409                 break;
  410 
  411             case GLOBAL_PAGE_UP:
  412                 page_up();
  413                 break;
  414 
  415             case GLOBAL_SCROLL_DOWN:
  416                 scroll_down();
  417                 break;
  418 
  419             case GLOBAL_SCROLL_UP:
  420                 scroll_up();
  421                 break;
  422 
  423             case SPECIAL_CATCHUP_LEFT:
  424             case CATCHUP:
  425             case CATCHUP_NEXT_UNREAD:
  426                 ret_code = group_catchup(func);
  427                 break;
  428 
  429             case GROUP_TOGGLE_SUBJECT_DISPLAY:  /* toggle display of subject & subj/author */
  430                 if (++curr_group->attribute->show_author > SHOW_FROM_BOTH)
  431                     curr_group->attribute->show_author = SHOW_FROM_NONE;
  432                 show_group_page();
  433                 break;
  434 
  435             case GROUP_GOTO:    /* choose a new group by name */
  436                 n = choose_new_group();
  437                 if (n >= 0 && n != selmenu.curr) {
  438                     selmenu.curr = n;
  439                     ret_code = GRP_ENTER;
  440                 }
  441                 break;
  442 
  443             case GLOBAL_HELP:
  444                 show_help_page(GROUP_LEVEL, _(txt_index_page_com));
  445                 show_group_page();
  446                 break;
  447 
  448             case GLOBAL_TOGGLE_HELP_DISPLAY:        /* toggle mini help menu */
  449                 toggle_mini_help(GROUP_LEVEL);
  450                 show_group_page();
  451                 break;
  452 
  453             case GLOBAL_TOGGLE_INVERSE_VIDEO:       /* toggle inverse video */
  454                 toggle_inverse_video();
  455                 show_group_page();
  456                 show_inverse_video_status();
  457                 break;
  458 
  459 #   ifdef HAVE_COLOR
  460             case GLOBAL_TOGGLE_COLOR:
  461                 if (toggle_color()) {
  462                     show_group_page();
  463                     show_color_status();
  464                 }
  465                 break;
  466 #   endif /* HAVE_COLOR */
  467 
  468             case GROUP_MARK_THREAD_READ:            /* mark current thread/range/tagged threads as read */
  469             case MARK_THREAD_UNREAD:                /* or unread */
  470                 if (grpmenu.curr < 0)
  471                     info_message(_(txt_no_arts));
  472                 else {
  473                     t_function function, type;
  474 
  475                     function = func == GROUP_MARK_THREAD_READ ? (t_function) FEED_MARK_READ : (t_function) FEED_MARK_UNREAD;
  476                     type = range_active ? FEED_RANGE : (num_of_tagged_arts && !group->attribute->mark_ignore_tags) ? NOT_ASSIGNED : FEED_THREAD;
  477                     feed_articles(function, GROUP_LEVEL, type, group, (int) base[grpmenu.curr]);
  478                 }
  479                 break;
  480 
  481             case GROUP_LIST_THREAD:             /* list articles within current thread */
  482                 ret_code = enter_thread(0, NULL);   /* Enter thread at the top */
  483                 break;
  484 
  485             case GLOBAL_LOOKUP_MESSAGEID:
  486                 if ((i = prompt_msgid()) != ART_UNAVAILABLE)
  487                     ret_code = enter_pager(i, FALSE);
  488                 break;
  489 
  490             case GLOBAL_OPTION_MENU:            /* option menu */
  491                 old_artnum = grpmenu.max > 0 ? arts[(int) base[grpmenu.curr]].artnum : T_ARTNUM_CONST(-1);
  492                 config_page(group->name, signal_context);
  493                 grpmenu.curr = old_artnum >= T_ARTNUM_CONST(0) ? find_new_pos(old_artnum, grpmenu.curr) : grpmenu.max - 1;
  494                 show_group_page();
  495                 break;
  496 
  497             case GROUP_NEXT_GROUP:          /* goto next group */
  498                 clear_message();
  499                 if (selmenu.curr + 1 >= selmenu.max)
  500                     info_message(_(txt_no_more_groups));
  501                 else {
  502                     if (xflag && TINRC_CONFIRM_SELECT && (prompt_yn(_(txt_confirm_select_on_exit), FALSE) != 1)) {
  503                         undo_auto_select_arts();
  504                         xflag = FALSE;
  505                     }
  506                     selmenu.curr++;
  507                     ret_code = GRP_NEXTUNREAD;
  508                 }
  509                 break;
  510 
  511             case GROUP_NEXT_UNREAD_ARTICLE: /* goto next unread article */
  512                 if (grpmenu.curr < 0) {
  513                     info_message(_(txt_no_next_unread_art));
  514                     break;
  515                 }
  516                 if ((n = next_unread((int) base[grpmenu.curr])) == -1)
  517                     info_message(_(txt_no_next_unread_art));
  518                 else
  519                     ret_code = enter_pager(n, FALSE);
  520                 break;
  521 
  522             case GROUP_PREVIOUS_UNREAD_ARTICLE: /* go to previous unread article */
  523                 if (grpmenu.curr < 0) {
  524                     info_message(_(txt_no_prev_unread_art));
  525                     break;
  526                 }
  527 
  528                 if ((n = prev_unread(prev_response((int) base[grpmenu.curr]))) == -1)
  529                     info_message(_(txt_no_prev_unread_art));
  530                 else
  531                     ret_code = enter_pager(n, FALSE);
  532                 break;
  533 
  534             case GROUP_PREVIOUS_GROUP:  /* previous group */
  535                 clear_message();
  536                 for (i = selmenu.curr - 1; i >= 0; i--) {
  537                     if (UNREAD_GROUP(i))
  538                         break;
  539                 }
  540                 if (i < 0)
  541                     info_message(_(txt_no_prev_group));
  542                 else {
  543                     if (xflag && TINRC_CONFIRM_SELECT && (prompt_yn(_(txt_confirm_select_on_exit), FALSE) != 1)) {
  544                         undo_auto_select_arts();
  545                         xflag = FALSE;
  546                     }
  547                     selmenu.curr = i;
  548                     ret_code = GRP_NEXTUNREAD;
  549                 }
  550                 break;
  551 
  552             case GLOBAL_QUIT:   /* return to group selection page */
  553                 if (num_of_tagged_arts && prompt_yn(_(txt_quit_despite_tags), TRUE) != 1)
  554                     break;
  555                 if (xflag && TINRC_CONFIRM_SELECT && (prompt_yn(_(txt_confirm_select_on_exit), FALSE) != 1)) {
  556                     undo_auto_select_arts();
  557                     xflag = FALSE;
  558                 }
  559                 ret_code = GRP_EXIT;
  560                 break;
  561 
  562             case GLOBAL_QUIT_TIN:       /* quit */
  563                 if (num_of_tagged_arts && prompt_yn(_(txt_quit_despite_tags), TRUE) != 1)
  564                     break;
  565                 if (xflag && TINRC_CONFIRM_SELECT && (prompt_yn(_(txt_confirm_select_on_exit), FALSE) != 1)) {
  566                     undo_auto_select_arts();
  567                     xflag = FALSE;
  568                 }
  569                 ret_code = GRP_QUIT;
  570                 break;
  571 
  572             case GROUP_TOGGLE_READ_UNREAD:
  573                 toggle_read_unread(FALSE);
  574                 show_group_page();
  575                 break;
  576 
  577             case GROUP_TOGGLE_GET_ARTICLES_LIMIT:
  578                 if (prompt_getart_limit()) {
  579                     /*
  580                      * if getart limit was given via cmd-line
  581                      * make it inactive now in order to use
  582                      * tinrc.getart_limit
  583                      */
  584                     if (cmdline.args & CMDLINE_GETART_LIMIT)
  585                         cmdline.args &= ~CMDLINE_GETART_LIMIT;
  586                     ret_code = GRP_NEXTUNREAD;
  587                 }
  588                 break;
  589 
  590             case GLOBAL_BUGREPORT:
  591                 bug_report();
  592                 break;
  593 
  594             case GROUP_TAG_PARTS: /* tag all in order */
  595                 if (0 <= grpmenu.curr) {
  596                     int old_num = num_of_tagged_arts;
  597 
  598                     if (tag_multipart(base[grpmenu.curr]) != 0) {
  599                         /*
  600                          * on success, move the pointer to the next
  601                          * untagged article just for ease of use's sake
  602                          */
  603                         n = grpmenu.curr;
  604                         update_group_page();
  605                         do {
  606                             n++;
  607                             n %= grpmenu.max;
  608                             if (arts[base[n]].tagged == 0) {
  609                                 move_to_item(n);
  610                                 break;
  611                             }
  612                         } while (n != grpmenu.curr);
  613                         if (old_num < num_of_tagged_arts)
  614                             info_message(_(txt_info_all_parts_tagged));
  615                         else
  616                             info_message(_(txt_info_all_parts_untagged));
  617                     }
  618                 }
  619                 break;
  620 
  621             case GROUP_TAG:     /* tag/untag threads for mailing/piping/printing/saving */
  622                 if (grpmenu.curr >= 0) {
  623                     t_bool tagged = TRUE;
  624 
  625                     n = (int) base[grpmenu.curr];
  626 
  627                     /*
  628                      * This loop looks for any article in the thread that
  629                      * isn't already tagged.
  630                      */
  631                     for (ii = n; ii != -1; ii = arts[ii].thread) {
  632                         if (arts[ii].tagged == 0) {
  633                             tagged = FALSE;
  634                             break;
  635                         }
  636                     }
  637 
  638                     /*
  639                      * If the whole thread is tagged, untag it. Otherwise, tag
  640                      * any untagged articles
  641                      */
  642                     if (tagged) {
  643                         /*
  644                          * Here we repeat the tagged test in both blocks
  645                          * to leave the choice of tagged/untagged
  646                          * determination politic in the previous lines.
  647                          */
  648                         for (ii = n; ii != -1; ii = arts[ii].thread) {
  649                             if (arts[ii].tagged != 0) {
  650                                 tagged = TRUE;
  651                                 untag_article(ii);
  652                             }
  653                         }
  654                     } else {
  655                         for (ii = n; ii != -1; ii = arts[ii].thread) {
  656                             if (arts[ii].tagged == 0)
  657                                 arts[ii].tagged = ++num_of_tagged_arts;
  658                         }
  659                     }
  660                     if ((ii = line_is_tagged(n)))
  661                         mark_screen(grpmenu.curr, mark_offset - 2, tin_ltoa(ii, 3));
  662                     else {
  663                         char mark[] = { '\0', '\0' };
  664 
  665                         stat_thread(grpmenu.curr, &sbuf);
  666                         mark[0] = sbuf.art_mark;
  667                         mark_screen(grpmenu.curr, mark_offset - 2, "  "); /* clear space used by tag numbering */
  668                         mark_screen(grpmenu.curr, mark_offset, mark);
  669                     }
  670                     if (tagged)
  671                         show_tagged_lines();
  672 
  673                     if (grpmenu.curr + 1 < grpmenu.max)
  674                         move_down();
  675                     else
  676                         draw_subject_arrow();
  677 
  678                     info_message(tagged ? _(txt_prefix_untagged) : _(txt_prefix_tagged), txt_thread_singular);
  679 
  680                 }
  681                 break;
  682 
  683             case GROUP_TOGGLE_THREADING:        /* Cycle through the threading types */
  684                 group->attribute->thread_articles = (group->attribute->thread_articles + 1) % (THREAD_MAX + 1);
  685                 if (grpmenu.curr >= 0) {
  686                     i = base[grpmenu.curr];                             /* Save a copy of current thread */
  687                     make_threads(group, TRUE);
  688                     find_base(group);
  689                     if ((grpmenu.curr = which_thread(i)) < 0)           /* Restore current position in group */
  690                         grpmenu.curr = 0;
  691                 }
  692                 show_group_page();
  693                 break;
  694 
  695             case GROUP_UNTAG:   /* untag all articles */
  696                 if (grpmenu.curr >= 0) {
  697                     if (untag_all_articles())
  698                         update_group_page();
  699                 }
  700                 break;
  701 
  702             case GLOBAL_VERSION:
  703                 info_message(cvers);
  704                 break;
  705 
  706             case GLOBAL_POST:   /* post an article */
  707                 if (post_article(group->name))
  708                     show_group_page();
  709                 break;
  710 
  711             case GLOBAL_POSTPONED:  /* post postponed article */
  712                 if (can_post) {
  713                     if (pickup_postponed_articles(FALSE, FALSE))
  714                         show_group_page();
  715                 } else
  716                     info_message(_(txt_cannot_post));
  717                 break;
  718 
  719             case GLOBAL_DISPLAY_POST_HISTORY:   /* display messages posted by user */
  720                 if (user_posted_messages())
  721                     show_group_page();
  722                 break;
  723 
  724             case MARK_ARTICLE_UNREAD:       /* mark base article of thread unread */
  725                 if (grpmenu.curr < 0)
  726                     info_message(_(txt_no_arts));
  727                 else {
  728                     const char *ptr;
  729 
  730                     if (range_active) {
  731                         /*
  732                          * We are tied to following base[] here, not arts[], as we operate on
  733                          * the base articles by definition.
  734                          */
  735                         for (ii = 0; ii < grpmenu.max; ++ii) {
  736                             if (arts[base[ii]].inrange) {
  737                                 arts[base[ii]].inrange = FALSE;
  738                                 art_mark(group, &arts[base[ii]], ART_WILL_RETURN);
  739                                 for_each_art_in_thread(i, ii)
  740                                     arts[i].inrange = FALSE;
  741                             }
  742                         }
  743                         range_active = FALSE;
  744                         show_group_page();
  745                         ptr = _(txt_base_article_range);
  746                     } else {
  747                         art_mark(group, &arts[base[grpmenu.curr]], ART_WILL_RETURN);
  748                         ptr = _(txt_base_article);
  749                     }
  750 
  751                     show_group_title(TRUE);
  752                     build_sline(grpmenu.curr);
  753                     draw_subject_arrow();
  754                     info_message(_(txt_marked_as_unread), ptr);
  755                 }
  756                 break;
  757 
  758             case MARK_FEED_READ:    /* mark selected articles as read */
  759                 if (grpmenu.curr >= 0)
  760                     feed_articles(FEED_MARK_READ, GROUP_LEVEL, NOT_ASSIGNED, group, (int) base[grpmenu.curr]);
  761                 break;
  762 
  763             case MARK_FEED_UNREAD:  /* mark selected articles as unread */
  764                 if (grpmenu.curr >= 0)
  765                     feed_articles(FEED_MARK_UNREAD, GROUP_LEVEL, NOT_ASSIGNED, group, (int) base[grpmenu.curr]);
  766                 break;
  767 
  768             case GROUP_SELECT_THREAD:   /* mark thread as selected */
  769             case GROUP_TOGGLE_SELECT_THREAD:    /* toggle thread */
  770                 if (grpmenu.curr < 0) {
  771                     info_message(_(txt_no_arts));
  772                     break;
  773                 }
  774 
  775                 flag = TRUE;
  776                 if (func == GROUP_TOGGLE_SELECT_THREAD) {
  777                     stat_thread(grpmenu.curr, &sbuf);
  778                     if (sbuf.selected_unread == sbuf.unread)
  779                         flag = FALSE;
  780                 }
  781                 n = 0;
  782                 for_each_art_in_thread(i, grpmenu.curr) {
  783                     arts[i].selected = flag;
  784                     ++n;
  785                 }
  786                 assert(n > 0);
  787                 {
  788                     char mark[] = { '\0', '\0' };
  789 
  790                     stat_thread(grpmenu.curr, &sbuf);
  791                     mark[0] = sbuf.art_mark;
  792                     mark_screen(grpmenu.curr, mark_offset, mark);
  793                 }
  794 
  795                 show_group_title(TRUE);
  796 
  797                 if (grpmenu.curr + 1 < grpmenu.max)
  798                     move_down();
  799                 else
  800                     draw_subject_arrow();
  801 
  802                 info_message(flag ? _(txt_thread_marked_as_selected) : _(txt_thread_marked_as_deselected));
  803                 break;
  804 
  805             case GROUP_REVERSE_SELECTIONS:  /* reverse selections */
  806                 for_each_art(i)
  807                     arts[i].selected = bool_not(arts[i].selected);
  808                 update_group_page();
  809                 show_group_title(TRUE);
  810                 break;
  811 
  812             case GROUP_UNDO_SELECTIONS: /* undo selections */
  813                 undo_selections();
  814                 xflag = FALSE;
  815                 show_group_title(TRUE);
  816                 update_group_page();
  817                 break;
  818 
  819             case GROUP_SELECT_PATTERN:  /* select matching patterns */
  820                 if (grpmenu.curr >= 0) {
  821                     char pat[128];
  822                     char *prompt;
  823                     struct regex_cache cache = { NULL, NULL };
  824 
  825                     prompt = fmt_string(_(txt_select_pattern), tinrc.default_select_pattern);
  826                     if (!(prompt_string_default(prompt, tinrc.default_select_pattern, _(txt_info_no_previous_expression), HIST_SELECT_PATTERN))) {
  827                         free(prompt);
  828                         break;
  829                     }
  830                     free(prompt);
  831 
  832                     if (STRCMPEQ(tinrc.default_select_pattern, "*")) {  /* all */
  833                         if (tinrc.wildcard)
  834                             STRCPY(pat, ".*");
  835                         else
  836                             STRCPY(pat, tinrc.default_select_pattern);
  837                     } else
  838                         snprintf(pat, sizeof(pat), REGEX_FMT, tinrc.default_select_pattern);
  839 
  840                     if (tinrc.wildcard && !(compile_regex(pat, &cache, PCRE_CASELESS)))
  841                         break;
  842 
  843                     flag = FALSE;
  844                     for (n = 0; n < grpmenu.max; n++) {
  845                         if (!match_regex(arts[base[n]].subject, pat, &cache, TRUE))
  846                             continue;
  847 
  848                         for_each_art_in_thread(i, n)
  849                             arts[i].selected = TRUE;
  850 
  851                         flag = TRUE;
  852                     }
  853                     if (flag) {
  854                         show_group_title(TRUE);
  855                         update_group_page();
  856                     }
  857                     if (tinrc.wildcard) {
  858                         FreeIfNeeded(cache.re);
  859                         FreeIfNeeded(cache.extra);
  860                     }
  861                 }
  862                 break;
  863 
  864             case GROUP_SELECT_THREAD_IF_UNREAD_SELECTED:    /* select all unread arts in thread hot if 1 is hot */
  865                 for (n = 0; n < grpmenu.max; n++) {
  866                     stat_thread(n, &sbuf);
  867                     if (!sbuf.selected_unread || sbuf.selected_unread == sbuf.unread)
  868                         continue;
  869 
  870                     for_each_art_in_thread(i, n)
  871                         arts[i].selected = TRUE;
  872                 }
  873                 show_group_title(TRUE);
  874                 break;
  875 
  876             case GROUP_MARK_UNSELECTED_ARTICLES_READ:   /* mark read all unselected arts */
  877                 if (!xflag) {
  878                     do_auto_select_arts();
  879                     xflag = TRUE;
  880                 } else {
  881                     undo_auto_select_arts();
  882                     xflag = FALSE;
  883                 }
  884                 break;
  885 
  886             case GROUP_DO_AUTOSELECT:       /* perform auto-selection on group */
  887                 for (n = 0; n < grpmenu.max; n++) {
  888                     for_each_art_in_thread(i, n)
  889                         arts[i].selected = TRUE;
  890                 }
  891                 update_group_page();
  892                 show_group_title(TRUE);
  893                 break;
  894 
  895             case GLOBAL_TOGGLE_INFO_LAST_LINE:
  896                 tinrc.info_in_last_line = bool_not(tinrc.info_in_last_line);
  897                 show_group_page();
  898                 break;
  899 
  900             default:
  901                 info_message(_(txt_bad_command), printascii(key, func_to_key(GLOBAL_HELP, group_keys)));
  902                 break;
  903         } /* switch(ch) */
  904     } /* ret_code >= 0 */
  905 
  906     set_xclick_off();
  907 
  908     clear_note_area();
  909     grp_del_mail_arts(group);
  910 
  911     art_close(&pgart);              /* Close any open art */
  912 
  913     curr_group = NULL;
  914 
  915     return ret_code;
  916 }
  917 
  918 
  919 void
  920 show_group_page(
  921     void)
  922 {
  923     int i;
  924 
  925     signal_context = cGroup;
  926     currmenu = &grpmenu;
  927 
  928     ClearScreen();
  929     set_first_screen_item();
  930     parse_format_string(curr_group->attribute->group_format, &grp_fmt);
  931     mark_offset = 0;
  932     show_group_title(FALSE);
  933 
  934     for (i = grpmenu.first; i < grpmenu.first + NOTESLINES && i < grpmenu.max; ++i)
  935         build_sline(i);
  936 
  937     show_mini_help(GROUP_LEVEL);
  938 
  939     if (grpmenu.max <= 0) {
  940         info_message(_(txt_no_arts));
  941         return;
  942     }
  943 
  944     draw_subject_arrow();
  945 }
  946 
  947 
  948 static void
  949 update_group_page(
  950     void)
  951 {
  952     int i, j;
  953     char mark[] = { '\0', '\0' };
  954     struct t_art_stat sbuf;
  955 
  956     for (i = grpmenu.first; i < grpmenu.first + NOTESLINES && i < grpmenu.max; ++i) {
  957         if ((j = line_is_tagged(base[i])))
  958             mark_screen(i, mark_offset - 2, tin_ltoa(j, 3));
  959         else {
  960             stat_thread(i, &sbuf);
  961             mark[0] = sbuf.art_mark;
  962             mark_screen(i, mark_offset - 2, "  ");  /* clear space used by tag numbering */
  963             mark_screen(i, mark_offset, mark);
  964             if (sbuf.art_mark == tinrc.art_marked_selected)
  965                 draw_mark_selected(i);
  966         }
  967     }
  968 
  969     if (grpmenu.max <= 0)
  970         return;
  971 
  972     draw_subject_arrow();
  973 }
  974 
  975 
  976 static void
  977 draw_subject_arrow(
  978     void)
  979 {
  980     draw_arrow_mark(INDEX_TOP + grpmenu.curr - grpmenu.first);
  981 
  982     if (tinrc.info_in_last_line) {
  983         struct t_art_stat statbuf;
  984 
  985         stat_thread(grpmenu.curr, &statbuf);
  986         info_message("%s", arts[(statbuf.unread ? next_unread(base[grpmenu.curr]) : base[grpmenu.curr])].subject);
  987     } else if (grpmenu.curr == grpmenu.max - 1)
  988         info_message(_(txt_end_of_arts));
  989 }
  990 
  991 
  992 void
  993 clear_note_area(
  994     void)
  995 {
  996     MoveCursor(INDEX_TOP, 0);
  997     CleartoEOS();
  998 }
  999 
 1000 
 1001 /*
 1002  * If in show_only_unread_arts mode or there are unread articles we know this
 1003  * thread will exist after toggle. Otherwise we find the next closest to
 1004  * return to. 'force' can be set to force tin to show all messages
 1005  */
 1006 static void
 1007 toggle_read_unread(
 1008     t_bool force)
 1009 {
 1010     int n, i = -1;
 1011 
 1012     /*
 1013      * Clear art->keep_in_base if switching to !show_only_unread_arts
 1014      */
 1015     if (curr_group->attribute->show_only_unread_arts) {
 1016         for_each_art(n)
 1017             arts[n].keep_in_base = FALSE;
 1018     }
 1019 
 1020     /* force currently is always false */
 1021     if (force)
 1022         curr_group->attribute->show_only_unread_arts = TRUE;    /* Yes - really, we change it in a bit */
 1023 
 1024     wait_message(0, _(txt_reading_arts),
 1025         (curr_group->attribute->show_only_unread_arts) ? _(txt_all) : _(txt_unread));
 1026 
 1027     if (grpmenu.curr >= 0) {
 1028         if (curr_group->attribute->show_only_unread_arts || new_responses(grpmenu.curr))
 1029             i = base[grpmenu.curr];
 1030         else if ((n = prev_unread((int) base[grpmenu.curr])) >= 0)
 1031             i = n;
 1032         else if ((n = next_unread((int) base[grpmenu.curr])) >= 0)
 1033             i = n;
 1034     }
 1035 
 1036     if (!force)
 1037         curr_group->attribute->show_only_unread_arts = bool_not(curr_group->attribute->show_only_unread_arts);
 1038 
 1039     find_base(curr_group);
 1040     if (i >= 0 && (n = which_thread(i)) >= 0)
 1041         grpmenu.curr = n;
 1042     else if (grpmenu.max > 0)
 1043         grpmenu.curr = grpmenu.max - 1;
 1044     clear_message();
 1045 }
 1046 
 1047 
 1048 /*
 1049  * Find new index position after a kill or unkill. Because kill can work on
 1050  * author it is impossible to know which, if any, articles will be left
 1051  * afterwards. So we make a "best attempt" to find a new index point.
 1052  */
 1053 static int
 1054 find_new_pos(
 1055     long old_artnum,
 1056     int cur_pos)
 1057 {
 1058     int i, pos;
 1059 
 1060     if ((i = find_artnum(old_artnum)) >= 0 && (pos = which_thread(i)) >= 0)
 1061         return pos;
 1062 
 1063     return ((cur_pos < grpmenu.max) ? cur_pos : (grpmenu.max - 1));
 1064 }
 1065 
 1066 
 1067 /*
 1068  * Set grpmenu.curr to the first unread or the last thread depending on
 1069  * the value of pos_first_unread
 1070  */
 1071 void
 1072 pos_first_unread_thread(
 1073     void)
 1074 {
 1075     int i;
 1076 
 1077     if (curr_group->attribute->pos_first_unread) {
 1078         for (i = 0; i < grpmenu.max; i++) {
 1079             if (new_responses(i))
 1080                 break;
 1081         }
 1082         grpmenu.curr = ((i < grpmenu.max) ? i : (grpmenu.max - 1));
 1083     } else
 1084         grpmenu.curr = grpmenu.max - 1;
 1085 }
 1086 
 1087 
 1088 void
 1089 mark_screen(
 1090     int screen_row,
 1091     int screen_col,
 1092     const char *value)
 1093 {
 1094     if (tinrc.draw_arrow) {
 1095         MoveCursor(INDEX2LNUM(screen_row), screen_col);
 1096         my_fputs(value, stdout);
 1097         stow_cursor();
 1098         my_flush();
 1099     } else {
 1100 #ifdef USE_CURSES
 1101         int y, x;
 1102         getyx(stdscr, y, x);
 1103         mvaddstr(INDEX2LNUM(screen_row), screen_col, value);
 1104         MoveCursor(y, x);
 1105 #else
 1106         int i;
 1107         for (i = 0; value[i] != '\0'; i++)
 1108             screen[INDEX2SNUM(screen_row)].col[screen_col + i] = value[i];
 1109         MoveCursor(INDEX2LNUM(screen_row), screen_col);
 1110         my_fputs(value, stdout);
 1111 #endif /* USE_CURSES */
 1112         currmenu->draw_arrow();
 1113     }
 1114 }
 1115 
 1116 
 1117 /*
 1118  *  Builds the correct header for multipart messages when sorting via
 1119  *  THREAD_MULTI.
 1120  */
 1121 static void
 1122 build_multipart_header(
 1123     char *dest,
 1124     int maxlen,
 1125     const char *src,
 1126     int cmplen,
 1127     int have,
 1128     int total)
 1129 {
 1130     const char *mark = (have == total) ? "*" : "-";
 1131     char *ss;
 1132 
 1133     if (cmplen > maxlen)
 1134         strncpy(dest, src, maxlen);
 1135     else {
 1136         strncpy(dest, src, cmplen);
 1137         ss = dest + cmplen;
 1138         snprintf(ss, maxlen - cmplen, "(%s/%d)", mark, total);
 1139     }
 1140 }
 1141 
 1142 
 1143 /*
 1144  * Build subject line given an index into base[].
 1145  *
 1146  * WARNING: some other code expects to find the article mark (ART_MARK_READ,
 1147  * ART_MARK_SELECTED, etc) at mark_offset from beginning of the line.
 1148  * So, if you change the format used in this routine, be sure to check that
 1149  * the value of mark_offset is still correct.
 1150  * Yes, this is somewhat kludgy.
 1151  */
 1152 static void
 1153 build_sline(
 1154     int i)
 1155 {
 1156     char *fmt, *buf;
 1157     char *buffer;
 1158     char arts_sub[HEADER_LEN];
 1159     char tmp_buf[8];
 1160     char tmp[LEN];
 1161     int respnum;
 1162     int j, k, n;
 1163     size_t len;
 1164     struct t_art_stat sbuf;
 1165 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
 1166     wchar_t *wtmp, *wtmp2;
 1167 #else
 1168     int fill, gap;
 1169     size_t len_start;
 1170 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
 1171 
 1172 #ifdef USE_CURSES
 1173     /*
 1174      * Allocate line buffer
 1175      * make it the same size like in !USE_CURSES case to simplify the code
 1176      */
 1177 #   if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
 1178         buffer = my_malloc(cCOLS * MB_CUR_MAX + 2);
 1179 #   else
 1180         buffer = my_malloc(cCOLS + 2);
 1181 #   endif /* MULTIBYTE_ABLE && !NO_LOCALE */
 1182 #else
 1183     buffer = screen[INDEX2SNUM(i)].col;
 1184 #endif /* USE_CURSES */
 1185 
 1186     buffer[0] = '\0';
 1187 
 1188     respnum = (int) base[i];
 1189 
 1190     stat_thread(i, &sbuf);
 1191 
 1192     /*
 1193      * Find index of first unread in this thread
 1194      */
 1195     j = (sbuf.unread) ? next_unread(respnum) : respnum;
 1196 
 1197     fmt = grp_fmt.str;
 1198 
 1199     if (tinrc.draw_arrow)
 1200         strcat(buffer, "  ");
 1201 
 1202     for (; *fmt; fmt++) {
 1203         if (*fmt != '%') {
 1204             strncat(buffer, fmt, 1);
 1205             continue;
 1206         }
 1207         switch (*++fmt) {
 1208             case '\0':
 1209                 break;
 1210 
 1211             case '%':
 1212                 strncat(buffer, fmt, 1);
 1213                 break;
 1214 
 1215             case 'D':   /* date */
 1216                 buf = my_malloc(LEN);
 1217                 if (my_strftime(buf, LEN - 1, grp_fmt.date_str, localtime((const time_t *) &arts[j].date))) {
 1218 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
 1219                     if ((wtmp = char2wchar_t(buf)) != NULL) {
 1220                         wtmp2 = wcspart(wtmp, grp_fmt.len_date_max, TRUE);
 1221                         if (wcstombs(tmp, wtmp2, sizeof(tmp) - 1) != (size_t) -1)
 1222                             strcat(buffer, tmp);
 1223 
 1224                         free(wtmp);
 1225                         free(wtmp2);
 1226                     }
 1227 #else
 1228                     strncat(buffer, buf, grp_fmt.len_date_max);
 1229 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
 1230                 }
 1231                 free(buf);
 1232                 break;
 1233 
 1234             case 'F':   /* from */
 1235                 if (curr_group->attribute->show_author != SHOW_FROM_NONE) {
 1236 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
 1237                     get_author(FALSE, &arts[j], tmp, sizeof(tmp) - 1);
 1238 
 1239                     if ((wtmp = char2wchar_t(tmp)) != NULL) {
 1240                         wtmp2 = wcspart(wtmp, grp_fmt.len_from, TRUE);
 1241                         if (wcstombs(tmp, wtmp2, sizeof(tmp) - 1) != (size_t) -1)
 1242                             strcat(buffer, tmp);
 1243 
 1244                         free(wtmp);
 1245                         free(wtmp2);
 1246                     }
 1247 #else
 1248                     len_start = strwidth(buffer);
 1249                     get_author(FALSE, &arts[j], buffer + strlen(buffer), grp_fmt.len_from);
 1250                     fill = grp_fmt.len_from - (strwidth(buffer) - len_start);
 1251                     gap = strlen(buffer);
 1252                     for (k = 0; k < fill; k++)
 1253                         buffer[gap + k] = ' ';
 1254                     buffer[gap + fill] = '\0';
 1255 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
 1256                 }
 1257                 break;
 1258 
 1259             case 'I':   /* initials */
 1260                 len = MIN(grp_fmt.len_initials, sizeof(tmp) - 1);
 1261                 get_initials(&arts[j], tmp, len);
 1262                 strcat(buffer, tmp);
 1263                 if ((k = len - strwidth(tmp)) > 0) {
 1264                     buf = buffer + strlen(buffer);
 1265                     for (; k > 0; --k)
 1266                         *buf++ = ' ';
 1267                     *buf = '\0';
 1268                 }
 1269                 break;
 1270 
 1271             case 'L':   /* lines */
 1272                 if (arts[j].line_count != -1)
 1273                     strcat(buffer, tin_ltoa(arts[j].line_count, grp_fmt.len_linecnt));
 1274                 else {
 1275                     buf = buffer + strlen(buffer);
 1276                     for (k = grp_fmt.len_linecnt; k > 1; --k)
 1277                         *buf++ = ' ';
 1278                     *buf++ = '?';
 1279                     *buf = '\0';
 1280                 }
 1281                 break;
 1282 
 1283             case 'm':   /* article flags, tag number, or whatever */
 1284                 if (!grp_fmt.mark_offset)
 1285                     grp_fmt.mark_offset = mark_offset = strwidth(buffer) + 2;
 1286                 if ((k = line_is_tagged(respnum)))
 1287                     STRCPY(tmp_buf, tin_ltoa(k, 3));
 1288                 else
 1289                     snprintf(tmp_buf, sizeof(tmp_buf), "  %c", sbuf.art_mark);
 1290                 strcat(buffer, tmp_buf);
 1291                 break;
 1292 
 1293             case 'M':   /* message-id */
 1294                 len = MIN(grp_fmt.len_msgid, sizeof(tmp) - 1);
 1295                 strncpy(tmp, arts[j].refptr ? arts[j].refptr->txt : "", len);
 1296                 tmp[len] = '\0';
 1297                 strcat(buffer, tmp);
 1298                 if ((k = len - strwidth(tmp)) > 0) {
 1299                     buf = buffer + strlen(buffer);
 1300                     for (; k > 0; --k)
 1301                         *buf++ = ' ';
 1302                     *buf = '\0';
 1303                 }
 1304                 break;
 1305 
 1306             case 'n':
 1307                 strcat(buffer, tin_ltoa(i + 1, grp_fmt.len_linenumber));
 1308                 break;
 1309 
 1310             case 'R':
 1311                 n = ((curr_group->attribute->show_only_unread_arts) ? (sbuf.unread + sbuf.seen) : sbuf.total);
 1312                 if (n > 1)
 1313                     strcat(buffer, tin_ltoa(n, grp_fmt.len_respcnt));
 1314                 else {
 1315                     buf = buffer + strlen(buffer);
 1316                     for (k = grp_fmt.len_respcnt; k > 0; --k)
 1317                         *buf++ = ' ';
 1318                     *buf = '\0';
 1319                 }
 1320                 break;
 1321 
 1322             case 'S':   /* score */
 1323                 strcat(buffer, tin_ltoa(sbuf.score, grp_fmt.len_score));
 1324                 break;
 1325 
 1326             case 's':   /* thread/subject */
 1327                 len = curr_group->attribute->show_author != SHOW_FROM_NONE ? grp_fmt.len_subj : grp_fmt.len_subj + grp_fmt.len_from;
 1328 
 1329                 if (sbuf.multipart_have > 1) /* We have a multipart msg so lets built our new header info. */
 1330                     build_multipart_header(arts_sub, len, arts[j].subject, sbuf.multipart_compare_len, sbuf.multipart_have, sbuf.multipart_total);
 1331                 else
 1332                     STRCPY(arts_sub, arts[j].subject);
 1333 
 1334 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
 1335                 if ((wtmp = char2wchar_t(arts_sub)) != NULL) {
 1336                     wtmp2 = wcspart(wtmp, len, TRUE);
 1337                     if (wcstombs(tmp, wtmp2, sizeof(tmp) - 1) != (size_t) -1)
 1338                         strcat(buffer, tmp);
 1339 
 1340                     free(wtmp);
 1341                     free(wtmp2);
 1342                 }
 1343 #else
 1344                 len_start = strwidth(buffer);
 1345                 strncat(buffer, arts_sub, len);
 1346                 fill = len - (strwidth(buffer) - len_start);
 1347                 gap = strlen(buffer);
 1348                 for (k = 0; k < fill; k++)
 1349                     buffer[gap + k] = ' ';
 1350                 buffer[gap + fill] = '\0';
 1351 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
 1352                 break;
 1353 
 1354             default:
 1355                 break;
 1356         }
 1357     }
 1358     /* protect display from non-displayable characters (e.g., form-feed) */
 1359     convert_to_printable(buffer, FALSE);
 1360 
 1361 #ifndef USE_CURSES
 1362     if (tinrc.strip_blanks)
 1363         strcat(strip_line(buffer), cCRLF);
 1364 #endif /* !USE_CURSES */
 1365 
 1366     WriteLine(INDEX2LNUM(i), buffer);
 1367 
 1368 #ifdef USE_CURSES
 1369     free(buffer);
 1370 #endif /* USE_CURSES */
 1371     if (sbuf.art_mark == tinrc.art_marked_selected)
 1372         draw_mark_selected(i);
 1373 }
 1374 
 1375 
 1376 static void
 1377 show_group_title(
 1378     t_bool clear_title)
 1379 {
 1380     char buf[LEN], tmp[LEN], flag;
 1381     int i, art_cnt = 0, recent_art_cnt = 0, selected_art_cnt = 0, read_selected_art_cnt = 0, killed_art_cnt = 0;
 1382 
 1383     for_each_art(i) {
 1384         if (arts[i].thread == ART_EXPIRED)
 1385             continue;
 1386 
 1387         if (curr_group->attribute->show_only_unread_arts) {
 1388             if (arts[i].status != ART_READ) {
 1389                 art_cnt++;
 1390                 if (tinrc.recent_time && ((time((time_t *) 0) - arts[i].date) < (tinrc.recent_time * DAY)))
 1391                     recent_art_cnt++;
 1392             }
 1393             if (arts[i].killed == ART_KILLED_UNREAD)
 1394                 killed_art_cnt++;
 1395         } else {
 1396             art_cnt++;
 1397             if (tinrc.recent_time && ((time((time_t *) 0) - arts[i].date) < (tinrc.recent_time * DAY)))
 1398                 recent_art_cnt++;
 1399 
 1400             if (arts[i].killed)
 1401                 killed_art_cnt++;
 1402         }
 1403         if (arts[i].selected) {
 1404             if (arts[i].status != ART_READ)
 1405                 selected_art_cnt++;
 1406             else
 1407                 read_selected_art_cnt++;
 1408         }
 1409     }
 1410 
 1411     /*
 1412      * build the group title
 1413      */
 1414     /* group name and thread count */
 1415     snprintf(buf, sizeof(buf), "%s (%d%c",
 1416         curr_group->name, grpmenu.max,
 1417         *txt_threading[curr_group->attribute->thread_articles]);
 1418 
 1419     /* article count */
 1420     if ((cmdline.args & CMDLINE_GETART_LIMIT) ? cmdline.getart_limit : tinrc.getart_limit)
 1421         snprintf(tmp, sizeof(tmp), " %d/%d%c",
 1422             (cmdline.args & CMDLINE_GETART_LIMIT) ? cmdline.getart_limit : tinrc.getart_limit, art_cnt,
 1423             (curr_group->attribute->show_only_unread_arts ? tinrc.art_marked_unread : tinrc.art_marked_read));
 1424     else
 1425         snprintf(tmp, sizeof(tmp), " %d%c",
 1426             art_cnt,
 1427             (curr_group->attribute->show_only_unread_arts ? tinrc.art_marked_unread : tinrc.art_marked_read));
 1428     if (sizeof(buf) > strlen(buf) + strlen(tmp))
 1429         strcat(buf, tmp);
 1430 
 1431     /* selected articles */
 1432     if (curr_group->attribute->show_only_unread_arts)
 1433         snprintf(tmp, sizeof(tmp), " %d%c",
 1434             selected_art_cnt, tinrc.art_marked_selected);
 1435     else
 1436         snprintf(tmp, sizeof(tmp), " %d%c %d%c",
 1437             selected_art_cnt, tinrc.art_marked_selected,
 1438             read_selected_art_cnt, tinrc.art_marked_read_selected);
 1439     if (sizeof(buf) > strlen(buf) + strlen(tmp))
 1440         strcat(buf, tmp);
 1441 
 1442     /* recent articles */
 1443     if (tinrc.recent_time) {
 1444         snprintf(tmp, sizeof(tmp), " %d%c",
 1445             recent_art_cnt, tinrc.art_marked_recent);
 1446 
 1447         if (sizeof(buf) > strlen(buf) + strlen(tmp))
 1448             strcat(buf, tmp);
 1449     }
 1450 
 1451     /* killed articles */
 1452     snprintf(tmp, sizeof(tmp), " %d%c",
 1453         killed_art_cnt, tinrc.art_marked_killed);
 1454     if (sizeof(buf) > strlen(buf) + strlen(tmp))
 1455         strcat(buf, tmp);
 1456 
 1457     /* group flag */
 1458     if ((flag = group_flag(curr_group->moderated)) == ' ')
 1459         snprintf(tmp, sizeof(tmp), ")");
 1460     else
 1461         snprintf(tmp, sizeof(tmp), ") %c", flag);
 1462     if (sizeof(buf) > strlen(buf) + strlen(tmp))
 1463         strcat(buf, tmp);
 1464 
 1465     if (clear_title) {
 1466         MoveCursor(0, 0);
 1467         CleartoEOLN();
 1468     }
 1469     show_title(buf);
 1470 }
 1471 
 1472 
 1473 /*
 1474  * Search for type SUBJ/AUTH in direction (TRUE = forwards)
 1475  * Return 0 if all is done, or a >0 thread_depth to enter the thread menu
 1476  */
 1477 static int
 1478 do_search(
 1479     t_function func,
 1480     t_bool repeat)
 1481 {
 1482     int start, n;
 1483 
 1484     if (grpmenu.curr < 0)
 1485         return 0;
 1486 
 1487     /*
 1488      * Not intuitive to search current thread in fwd search
 1489      */
 1490     start = ((func == GLOBAL_SEARCH_SUBJECT_FORWARD || func == GLOBAL_SEARCH_AUTHOR_FORWARD)
 1491         && grpmenu.curr < grpmenu.max - 1) ? prev_response((int) base[grpmenu.curr + 1]) : (int) base[grpmenu.curr];
 1492 
 1493     if (start >= 0 && ((n = search(func, start, repeat)) != -1)) {
 1494         grpmenu.curr = which_thread(n);
 1495 
 1496         /*
 1497          * If the search found something deeper in a thread(not the base art)
 1498          * then enter the thread
 1499          */
 1500         if ((n = which_response(n)) != 0)
 1501             return n;
 1502 
 1503         show_group_page();
 1504     }
 1505     return 0;
 1506 }
 1507 
 1508 
 1509 /*
 1510  * We don't directly invoke the pager, but pass through the thread menu
 1511  * to keep navigation sane.
 1512  * 'art' is the arts[art] we wish to read
 1513  * ignore_unavail should be set if we wish to 'keep going' after 'article unavailable'
 1514  * Return a -ve ret_code if we must exit the group menu on return
 1515  */
 1516 static int
 1517 enter_pager(
 1518     int art,
 1519     t_bool ignore_unavail)
 1520 {
 1521     t_pagerinfo page;
 1522 
 1523     page.art = art;
 1524     page.ignore_unavail = ignore_unavail;
 1525 
 1526     return enter_thread(0, &page);
 1527 }
 1528 
 1529 
 1530 /*
 1531  * Handle entry/exit with the thread menu
 1532  * Return -ve ret_code if we must exit the group menu on return
 1533  */
 1534 static int
 1535 enter_thread(
 1536     int depth,
 1537     t_pagerinfo *page)
 1538 {
 1539     int i, n;
 1540 
 1541     if (grpmenu.curr < 0) {
 1542         info_message(_(txt_no_arts));
 1543         return 0;
 1544     }
 1545 
 1546     forever {
 1547         switch (i = thread_page(curr_group, (int) base[grpmenu.curr], depth, page)) {
 1548             case GRP_QUIT:                      /* 'Q'uit */
 1549             case GRP_RETSELECT:                 /* Back to selection screen */
 1550                 return i;
 1551                 /* NOTREACHED */
 1552                 break;
 1553 
 1554             case GRP_NEXT:                      /* 'c'atchup */
 1555                 show_group_page();
 1556                 move_down();
 1557                 return 0;
 1558                 /* NOTREACHED */
 1559                 break;
 1560 
 1561             case GRP_NEXTUNREAD:                /* 'C'atchup */
 1562                 if ((n = next_unread((int) base[grpmenu.curr])) >= 0) {
 1563                     if (page)
 1564                         page->art = n;
 1565                     if ((n = which_thread(n)) >= 0) {
 1566                         grpmenu.curr = n;
 1567                         depth = 0;
 1568                         break;      /* Drop into next thread with unread */
 1569                     }
 1570                 }
 1571                 /* No more unread threads in this group, enter next group */
 1572                 grpmenu.curr = 0;
 1573                 return GRP_NEXTUNREAD;
 1574                 /* NOTREACHED */
 1575                 break;
 1576 
 1577             case GRP_KILLED:
 1578                 grpmenu.curr = 0;
 1579                 /* FALLTHROUGH */
 1580 
 1581             case GRP_EXIT:
 1582             /* case GRP_GOTOTHREAD will never make it up this far */
 1583             default:        /* ie >= 0 Shouldn't happen any more? */
 1584                 clear_note_area();
 1585                 show_group_page();
 1586                 return 0;
 1587                 /* NOTREACHED */
 1588                 break;
 1589         }
 1590     }
 1591     /* NOTREACHED */
 1592     return 0;
 1593 }
 1594 
 1595 
 1596 /*
 1597  * Return a ret_code
 1598  */
 1599 static int
 1600 tab_pressed(
 1601     void)
 1602 {
 1603     int n;
 1604 
 1605     if ((n = ((grpmenu.curr < 0) ? -1 : next_unread((int) base[grpmenu.curr]))) < 0)
 1606         return GRP_NEXTUNREAD;          /* => Enter next unread group */
 1607 
 1608     /* We still have unread arts in the current group ... */
 1609     return enter_pager(n, TRUE);
 1610 }
 1611 
 1612 
 1613 /*
 1614  * There are three ways this is called
 1615  * catchup & return to group menu
 1616  * catchup & go to next group with unread articles
 1617  * group exit via left arrow if auto-catchup is set
 1618  * Return a -ve ret_code if we're done with the group menu
 1619  */
 1620 static int
 1621 group_catchup(
 1622     t_function func)
 1623 {
 1624     char buf[LEN];
 1625     int pyn = 1;
 1626 
 1627     if (num_of_tagged_arts && prompt_yn(_(txt_catchup_despite_tags), TRUE) != 1)
 1628         return 0;
 1629 
 1630     snprintf(buf, sizeof(buf), _(txt_mark_arts_read), (func == CATCHUP_NEXT_UNREAD) ? _(txt_enter_next_unread_group) : "");
 1631 
 1632     if (!curr_group->newsrc.num_unread || (!TINRC_CONFIRM_ACTION) || (pyn = prompt_yn(buf, TRUE)) == 1)
 1633         grp_mark_read(curr_group, arts);
 1634 
 1635     switch (func) {
 1636         case CATCHUP:               /* 'c' */
 1637             if (pyn == 1)
 1638                 return GRP_NEXT;
 1639             break;
 1640 
 1641         case CATCHUP_NEXT_UNREAD:           /* 'C' */
 1642             if (pyn == 1)
 1643                 return GRP_NEXTUNREAD;
 1644             break;
 1645 
 1646         case SPECIAL_CATCHUP_LEFT:              /* <- group catchup on exit */
 1647             switch (pyn) {
 1648                 case -1:                    /* ESCAPE - do nothing */
 1649                     break;
 1650 
 1651                 case 1:                     /* We caught up - advance group */
 1652                     return GRP_NEXT;
 1653                     /* NOTREACHED */
 1654                     break;
 1655 
 1656                 default:                    /* Just leave the group */
 1657                     return GRP_EXIT;
 1658                     /* NOTREACHED */
 1659                     break;
 1660             }
 1661             /* FALLTHROUGH */
 1662         default:                            /* Should not be here */
 1663             break;
 1664     }
 1665     return 0;                               /* Stay in this menu by default */
 1666 }
 1667 
 1668 
 1669 static t_bool
 1670 prompt_getart_limit(
 1671     void)
 1672 {
 1673     char *p;
 1674     t_bool ret = FALSE;
 1675 
 1676     clear_message();
 1677     if ((p = tin_getline(_(txt_enter_getart_limit), 2, NULL, 0, FALSE, HIST_OTHER)) != NULL) {
 1678         tinrc.getart_limit = atoi(p);
 1679         ret = TRUE;
 1680     }
 1681     clear_message();
 1682     return ret;
 1683 }
 1684 
 1685 
 1686 /*
 1687  * Redraw all necessary parts of the screen after FEED_MARK_(UN)READ
 1688  * Move cursor to next unread item if needed
 1689  *
 1690  * Returns TRUE when no next unread art, FALSE otherwise
 1691  */
 1692 t_bool
 1693 group_mark_postprocess(
 1694     int function,
 1695     t_function feed_type,
 1696     int respnum)
 1697 {
 1698     int n;
 1699 
 1700     show_group_title(TRUE);
 1701     switch (function) {
 1702         case (FEED_MARK_READ):
 1703             if (feed_type == FEED_THREAD || feed_type == FEED_ARTICLE)
 1704                 build_sline(grpmenu.curr);
 1705             else
 1706                 show_group_page();
 1707 
 1708             if ((n = next_unread(next_response(respnum))) == -1) {
 1709                 draw_subject_arrow();
 1710                 return TRUE;
 1711             }
 1712 
 1713             move_to_item(which_thread(n));
 1714             break;
 1715 
 1716         case (FEED_MARK_UNREAD):
 1717             if (feed_type == FEED_THREAD || feed_type == FEED_ARTICLE)
 1718                 build_sline(grpmenu.curr);
 1719             else
 1720                 show_group_page();
 1721 
 1722             draw_subject_arrow();
 1723             break;
 1724 
 1725         default:
 1726             break;
 1727     }
 1728     return FALSE;
 1729 }