% Emulation of trn's wumpus thread tree display, in slrn % Copyright (C) 2001 Peter Maydell % Available under the GNU General Public License (v2 or later). % The only way slrn allows us to deal with articles is % by using the message-ID to refer to an article. We % use these functions to abstract away this detail. % An article variable may also be NULL to indicate % no article. % To use: hook do_wumpus into the read_article hook, eg: % define read_article_hook() % { % do_wumpus(); % } % TODO: % (1) Our definition of get_sibling is a bit ropy and % may claim two articles not to be brothers when they are. % % (2) last_article_read() doesn't work. Disabled because % call("goto_last_read") causes read_article hook to be % called recursively. % % (3) tree_indent is currently an array of strings and % ought probably to be an array of characters % % (4) the subject line to letter mapping should perhaps % strip off "Re: " prefixes. [What does trn do here?] % % (5) There must be a better way than using sprintf % to turn a character into a string! % % (6) whole handling of tree_indent and in particular % the loop that copies it into tree_buff is a bit nasty % % (7) we need to write code to actually display the wumpus properly! define make_current(article) { % Make sure that specified article is slrn's % 'current' article () = locate_header_by_msgid(article,0); } define get_current() { % Return the article slrn thinks is current return extract_article_header("Message-Id"); } define get_first_child(article) { % Return the first child of this article, % or NULL if it has none make_current(article); if (thread_size() < 2) return NULL; header_down(1); return get_current(); } define get_parent(article) { % return the parent of this article, NULL if none make_current(article); % get_parent_header will barf if we try to go up too far, so catch it ERROR_BLOCK { _clear_error(); } call("get_parent_header"); variable a = get_current(); if (a == article) return NULL; return a; } define get_sibling(article) { % Return the sibling of this article, % or NULL if it has none make_current(article); variable ts = thread_size(); if (header_down(ts) != ts) return NULL; variable a = get_current(); variable p = get_parent(a); % XXX this is wrong: if a and p have the same parent % but it has expired, we'll treat them as separate subtrees % because p == get_parent(article) == NULL and this % is indistinguishable from two unrelated thread-tops % (short of playing silly games with References) if (p == NULL) return NULL; if (p != get_parent(article)) return NULL; return a; } define get_ultimate_parent(article) { % Return the article at the top of the tree % which this article is in variable a; make_current(article); forever { a = get_parent(article); if (a == NULL) return article; article = a; } } define has_ref_or_parent(article) { % return true if this article has a % parent or has a References header % (indicating a parent which has expired, or similar) make_current(article); if (extract_article_header("References") != "") return 1; if (get_parent(article) != NULL) return 1; return 0; } define is_read(article) { % return true if this article has been read make_current(article); if (get_header_flags() & HEADER_READ) return 1; else return 0; } define is_selected(article) { % return true if this article has been selected make_current(article); if (get_header_flags() & HEADER_TAGGED) return 1; else return 0; } variable last_art_read = NULL; define last_article_read() { % return the last article read (ie what we % would go to if the user did goto_last_read) % We cache the last-read article so we don't keep % re-querying it in the inner loop. return last_art_read; } define cache_last_article_read() { % XXX doesn't work! When we goto_last_read it treats it as a new % article to read and calls the read_article hook again, so we % infinitely recurse... % call("goto_last_read"); % last_art_read = get_current(); last_art_read = NULL; } % Wumpus algorithm proper % User tweakable parameters variable max_tree_lines = 6; % Max 11, as per size of tree_lines/tree_indent % Other variables variable max_depth; variable max_line = -1; variable my_depth; variable my_line; variable first_depth; variable first_line; variable node_line_cnt; variable line_num; variable header_indent; variable tree_buff; variable tree_lines = String_Type [11]; variable tree_article; variable node_on_line; variable thread; % I don't know why this array is the size it is. -- PMM variable ind_size = 28*5 + 2; % XXX this ought probably to be a string of characters? variable tree_indent = String_Type [ind_size]; tree_indent[*] = " "; tree_indent[[1:ind_size:5]] = "X"; define dump_ind() { variable i,c; () = fprintf(stderr, "tree_indent[]"); for (i = 0; i < ind_size; i++) { c = tree_indent[i]; if (c == "X") c = "0"; () = fprintf(stderr, ":%s", c); } () = fprintf(stderr, "\n"); } define dump_thread_data() { % Print thread display to stderr, for debugging variable i; for (i = 0; i <= max_line; i++) () = fprintf(stderr, "%s\n", tree_lines[i]); } define find_depth(article, depth); define find_depth(article, depth) { % Recursive function to find out how big the tree % is and where we are on it. Sets max_depth, max_line, % my_depth, my_line if (depth >= max_depth) max_depth = depth; while (article != NULL) { if (article == tree_article) { my_depth = depth; my_line = max_line; } variable c = get_first_child(article); if (c != NULL) find_depth(c, depth + 1); else max_line++; article = get_sibling(article); } } % The wumpus display includes a distinct letter/number % for each different subject line in the thread. % We implement this with a hash table that we look the % subject line up in. % XXX strip off "Re:" style prefixes? variable thread_letter_array = Assoc_Type [String_Type]; variable max_lets = 9+26+26+1; variable letters = Char_Type[max_lets]; init_char_array(letters,"123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+"); define thread_letter(article) { make_current(article); variable subj = extract_article_header("Subject"); if (assoc_key_exists(thread_letter_array, subj) == 0) return "?"; return thread_letter_array[subj]; } define setup_thread_letters() { % For each article in the thread, put its subject in % the hashtable. % Clear the current table thread_letter_array = Assoc_Type [String_Type]; variable a = thread; make_current(thread); variable ts = thread_size(); variable i = 0; variable num = 0; do { variable subj = extract_article_header("Subject"); % () = fprintf(stderr, "handling article with subj %s\n", subj); if (assoc_key_exists(thread_letter_array, subj) == 0) { % New subject, assign it a new number % XXX is this really the best way to turn a character into a string? thread_letter_array[subj] = sprintf("%c", letters[num]); num++; if (num >= max_lets) num = max_lets - 1; } header_down(1); i++; } while (i < ts); } define cache_tree(ap, depth, cp); define cache_tree(ap, depth, cp) { % This recursive routine actually writes the display % into the tree_lines[] array of strings % ap is the article currently being dealt with, % depth our depth in the tree, and cp is used to % keep track of the correct string to use to indent % when the first node in this line of the display % is not at the far left of the display % () = fprintf(stderr,"cache_tree(%s, %d, %d)\n", ap, depth, cp); %dump_ind(); variable depth_mode; tree_indent[cp+1] = " "; if (depth >= first_depth and depth <= max_depth) { cp += 5; depth_mode = 1; } else if (depth + 1 == first_depth) { depth_mode = 2; } else { cp = 0; depth_mode = 0; } forever { % In slrn this is expensive, so calculate it % once now. [In trn, this is just a pointer deref.] variable ap_c1 = get_first_child(ap); switch (depth_mode) { case 1 : variable ch; if (has_ref_or_parent(ap) != 0) tree_buff += "-"; else tree_buff += " "; if (ap == tree_article) tree_buff += "*"; if (is_read(ap)) { tree_buff += "("; ch = ")"; } else if (is_selected(ap)) { tree_buff += "["; ch = "]"; } else { tree_buff += "<"; ch = ">"; } if (ap == last_article_read() and ap != tree_article) tree_buff += "@"; tree_buff += thread_letter(ap); tree_buff += ch; if (ap_c1 != NULL) { if (get_sibling(ap_c1) != NULL) tree_buff += "+"; else tree_buff += "-"; } if (get_sibling(ap) != NULL) { tree_indent[cp] = "|"; } else { tree_indent[cp] = " "; } node_on_line = 1; } { case 2 : if (ap_c1 == NULL) { tree_buff += " "; } else { if (get_sibling(ap_c1) != NULL) tree_buff += "+"; else tree_buff += "-"; } } if (ap_c1 != NULL) { % If we have children, recurse into them cache_tree(ap_c1, depth+1, cp); tree_indent[cp+1] = "X"; } else { % This node must be the last on this line, % write completed tree_buff string to tree_lines % if necessary if (node_on_line == 0 and first_line == line_num) first_line++; if (line_num >= first_line) { % delete trailing " " from tree_buff if any tree_buff = strtrim_end(tree_buff, " "); tree_lines[line_num - first_line] = tree_buff; if (node_on_line != 0) { node_line_cnt = line_num - first_line; } } line_num++; node_on_line = 0; } % loop round for any sibling articles ap = get_sibling(ap); if (ap == NULL or line_num > max_line) break; if (get_sibling(ap) == NULL) { tree_indent[cp] = "\\"; } if (first_depth == 0) { tree_indent[5] = " "; } % () = fprintf(stderr, "Starting tree_buff with indent\n"); tree_buff = ""; % XXX There must be a nicer way to do this! variable i; for (i = 5; tree_indent[i] != "X"; i++) { tree_buff += tree_indent[i]; } % () = fprintf(stderr, "Done, proceeding to sibling\n"); } } define init_tree() { % Build up a set of strings defining the % wumpus display for the current article. % We set max_line, max_depth and tree_lines[]. % NB: trashes slrn's current article! % () = fprintf(stderr, "init_tree...\n"); tree_article = get_current(); % return if various things broken if (tree_article == NULL) return; % () = fprintf(stderr, "foo\n"); thread = get_ultimate_parent(tree_article); if (thread == NULL) return; % () = fprintf(stderr, "this = %s, thread top = %s\n", tree_article, thread); cache_last_article_read(); % associate a letter with each new subject in thread setup_thread_letters(); % () = fprintf(stderr, "done thread letters\n"); max_depth = 0; max_line = 0; my_line =0; node_line_cnt = 0; % Determine the size of the thread tree and our position within it find_depth(thread, 0); % () = fprintf(stderr, "max depth %d, max line %d, my depth %d, my line %d\n", max_depth,max_line,my_depth,my_line); % Adjust for special cases where the portion of the tree we're % displaying is near the edge if (max_depth <= 5) { first_depth = 0; } else { if (my_depth + 2 > max_depth) first_depth = max_depth - 5; else { first_depth = my_depth - 3; if (first_depth < 0) first_depth = 0; } max_depth = first_depth + 5; } max_line--; if (max_line < max_tree_lines) first_line = 0; else { if (my_line + max_tree_lines/2 > max_line) first_line = max_line - (max_tree_lines-1); else { first_line = my_line - (max_tree_lines-1)/2; if (first_line < 0) first_line = 0; max_line = first_line + max_tree_lines - 1; } } % () = fprintf(stderr, "ADJ: max depth %d, max line %d, my depth %d, my line %d\n", max_depth,max_line,my_depth,my_line); % () = fprintf(stderr, "first_line = %d, first_depth = %d\n", first_line, first_depth); tree_buff = " "; node_on_line = 0; line_num = 0; cache_tree(thread, 0, 0); % () = fprintf(stderr, "done\n"); % Turn depth into width in characters max_depth = (max_depth - first_depth + 1) * 5; % Turn max_line into a line count max_line -= first_line; % Shorten tree if lower lines aren't visible if (node_line_cnt < max_line) max_line = node_line_cnt + 1; % Just dump strings to stderr for now % dump_thread_data(); } define do_wumpus() { variable this_article = get_current(); init_tree(); make_current(this_article); % () = fprintf(stderr, "init_tree done\n"); % At this point all we need to do is draw the wumpus display % to the right of the headers of this article. % tree_lines[0..max_lines] are the various lines of the % display, and max_depth is the width of the display in % characters. The strings can be printed as-is, except that % the magic characters @ and * indicate desired highlighting: % @ means 'highlight next character' and * means 'highlight % next three characters' (neither @ nor * are to be printed themselves) % [These are used to mark current and last node: % [1]--*[2]--[@3] % Here node 2 is current (highlight entire "[2]" section) and % node 3 is 'last' (highlight just the number) % This is a really dumb implementation that just puts the % display at the top, above the headers. And it puts in % some leading blanks so slrn thinks the real headers are % body now. FIXME FIXME FIXME! variable the_article = article_as_string(); variable new_article = "\n\n"; variable i, s; for (i = 0; i <= max_line; i++) new_article += tree_lines[i] + "\n"; new_article += the_article; replace_article(new_article); }