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