3 * $Id: follow_dlg.c,v 1.20 2002/01/21 07:37:41 guy Exp $
5 * Ethereal - Network traffic analyzer
6 * By Gerald Combs <gerald@ethereal.com>
7 * Copyright 2000 Gerald Combs
9 * This program is free software; you can redistribute it and/or
10 * modify it under the terms of the GNU General Public License
11 * as published by the Free Software Foundation; either version 2
12 * of the License, or (at your option) any later version.
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
36 #ifdef HAVE_SYS_TYPES_H
37 #include <sys/types.h>
40 #ifdef HAVE_SYS_STAT_H
45 #include <io.h> /* open/close on win32 */
52 #ifdef NEED_SNPRINTF_H
53 # include "snprintf.h"
59 #include "color_utils.h"
61 #include "follow_dlg.h"
63 #include "dlg_utils.h"
66 #include "gtkglobals.h"
68 #include "simple_dialog.h"
69 #include "packet-ipv6.h"
71 #include <epan/resolv.h>
74 #include <epan/epan_dissect.h>
91 show_stream_t show_stream;
92 show_type_t show_type;
93 char data_out_filename[128 + 1];
97 GtkWidget *hexdump_bt;
98 GtkWidget *follow_save_as_w;
102 static void follow_destroy_cb(GtkWidget * win, gpointer data);
103 static void follow_charset_toggle_cb(GtkWidget * w, gpointer parent_w);
104 static void follow_load_text(follow_info_t *follow_info);
105 static void follow_print_stream(GtkWidget * w, gpointer parent_w);
106 static void follow_save_as_cmd_cb(GtkWidget * w, gpointer data);
107 static void follow_save_as_ok_cb(GtkWidget * w, GtkFileSelection * fs);
108 static void follow_save_as_destroy_cb(GtkWidget * win, gpointer user_data);
109 static void follow_stream_om_both(GtkWidget * w, gpointer data);
110 static void follow_stream_om_client(GtkWidget * w, gpointer data);
111 static void follow_stream_om_server(GtkWidget * w, gpointer data);
114 FILE *data_out_file = NULL;
117 #define E_FOLLOW_INFO_KEY "follow_info_key"
119 /* List of "follow_info_t" structures for all "Follow TCP Stream" windows,
120 so we can redraw them all if the colors or font changes. */
121 static GList *follow_infos;
123 /* Add a "follow_info_t" structure to the list. */
125 remember_follow_info(follow_info_t *follow_info)
127 follow_infos = g_list_append(follow_infos, follow_info);
130 /* Remove a "follow_info_t" structure from the list. */
132 forget_follow_info(follow_info_t *follow_info)
134 follow_infos = g_list_remove(follow_infos, follow_info);
138 follow_redraw(gpointer data, gpointer user_data)
140 follow_load_text((follow_info_t *)data);
143 /* Redraw the text in all "Follow TCP Stream" windows. */
145 follow_redraw_all(void)
147 g_list_foreach(follow_infos, follow_redraw, NULL);
150 /* Follow the TCP stream, if any, to which the last packet that we called
151 a dissection routine on belongs (this might be the most recently
152 selected packet, or it might be the last packet in the file). */
154 follow_stream_cb(GtkWidget * w, gpointer data)
156 GtkWidget *streamwindow, *vbox, *txt_scrollw, *text, *filter_te;
157 GtkWidget *hbox, *button, *radio_bt;
158 GtkWidget *stream_om, *stream_menu, *stream_mi;
160 gchar *follow_filter;
161 const char *hostname0, *hostname1;
164 follow_tcp_stats_t stats;
165 follow_info_t *follow_info;
167 /* we got tcp so we can follow */
168 if (cfile.edt->pi.ipproto != 6) {
169 simple_dialog(ESD_TYPE_CRIT, NULL,
170 "Error following stream. Please make\n"
171 "sure you have a TCP packet selected.");
175 follow_info = g_new0(follow_info_t, 1);
177 /* Create a temporary file into which to dump the reassembled data
178 from the TCP stream, and set "data_out_file" to refer to it, so
179 that the TCP code will write to it.
181 XXX - it might be nicer to just have the TCP code directly
182 append stuff to the text widget for the TCP stream window,
183 if we can arrange that said window not pop up until we're
185 tmp_fd = create_tempfile(follow_info->data_out_filename,
186 sizeof follow_info->data_out_filename, "follow");
189 simple_dialog(ESD_TYPE_WARN, NULL,
190 "Could not create temporary file %s: %s",
191 follow_info->data_out_filename, strerror(errno));
196 data_out_file = fdopen(tmp_fd, "wb");
197 if (data_out_file == NULL) {
198 simple_dialog(ESD_TYPE_WARN, NULL,
199 "Could not create temporary file %s: %s",
200 follow_info->data_out_filename, strerror(errno));
202 unlink(follow_info->data_out_filename);
207 /* Create a new filter that matches all packets in the TCP stream,
208 and set the display filter entry accordingly */
209 reset_tcp_reassembly();
210 follow_filter = build_follow_filter(&cfile.edt->pi);
212 /* Set the display filter entry accordingly */
213 filter_te = gtk_object_get_data(GTK_OBJECT(w), E_DFILTER_TE_KEY);
214 gtk_entry_set_text(GTK_ENTRY(filter_te), follow_filter);
216 /* Run the display filter so it goes in effect. */
217 filter_packets(&cfile, follow_filter);
219 /* The data_out_file should now be full of the streams information */
220 fclose(data_out_file);
222 /* The data_out_filename file now has all the text that was in the session */
223 streamwindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
224 gtk_widget_set_name(streamwindow, "TCP stream window");
226 gtk_signal_connect(GTK_OBJECT(streamwindow), "destroy",
227 GTK_SIGNAL_FUNC(follow_destroy_cb), NULL);
229 gtk_signal_connect (GTK_OBJECT (streamwindow), "realize",
230 GTK_SIGNAL_FUNC (window_icon_realize_cb), NULL);
231 if (incomplete_tcp_stream) {
232 gtk_window_set_title(GTK_WINDOW(streamwindow),
233 "Contents of TCP stream (incomplete)");
235 gtk_window_set_title(GTK_WINDOW(streamwindow),
236 "Contents of TCP stream");
238 gtk_widget_set_usize(GTK_WIDGET(streamwindow), DEF_WIDTH,
240 gtk_container_border_width(GTK_CONTAINER(streamwindow), 2);
242 /* setup the container */
243 vbox = gtk_vbox_new(FALSE, 0);
244 gtk_container_add(GTK_CONTAINER(streamwindow), vbox);
246 /* create a scrolled window for the text */
247 txt_scrollw = scrolled_window_new(NULL, NULL);
248 gtk_box_pack_start(GTK_BOX(vbox), txt_scrollw, TRUE, TRUE, 0);
249 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(txt_scrollw),
253 /* create a text box */
254 text = gtk_text_new(NULL, NULL);
255 gtk_text_set_editable(GTK_TEXT(text), FALSE);
256 gtk_container_add(GTK_CONTAINER(txt_scrollw), text);
257 follow_info->text = text;
260 hbox = gtk_hbox_new(FALSE, 1);
261 gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
265 follow_tcp_stats(&stats);
268 struct e_in6_addr ipaddr;
269 memcpy(&ipaddr, stats.ip_address[0], 16);
270 hostname0 = get_hostname6(&ipaddr);
271 memcpy(&ipaddr, stats.ip_address[0], 16);
272 hostname1 = get_hostname6(&ipaddr);
275 memcpy(&ipaddr, stats.ip_address[0], 4);
276 hostname0 = get_hostname(ipaddr);
277 memcpy(&ipaddr, stats.ip_address[1], 4);
278 hostname1 = get_hostname(ipaddr);
281 port0 = get_tcp_port(stats.tcp_port[0]);
282 port1 = get_tcp_port(stats.tcp_port[1]);
284 follow_info->is_ipv6 = stats.is_ipv6;
286 stream_om = gtk_option_menu_new();
287 stream_menu = gtk_menu_new();
290 snprintf(string, sizeof(string),
291 "Entire conversation (%u bytes)",
292 stats.bytes_written[0] + stats.bytes_written[1]);
293 stream_mi = gtk_menu_item_new_with_label(string);
294 gtk_signal_connect(GTK_OBJECT(stream_mi), "activate",
295 GTK_SIGNAL_FUNC(follow_stream_om_both), follow_info);
296 gtk_menu_append(GTK_MENU(stream_menu), stream_mi);
297 gtk_widget_show(stream_mi);
298 follow_info->show_stream = BOTH_HOSTS;
300 /* Host 0 --> Host 1 */
301 snprintf(string, sizeof(string), "%s:%s --> %s:%s (%u bytes)",
302 hostname0, port0, hostname1, port1,
303 stats.bytes_written[0]);
304 stream_mi = gtk_menu_item_new_with_label(string);
305 gtk_signal_connect(GTK_OBJECT(stream_mi), "activate",
306 GTK_SIGNAL_FUNC(follow_stream_om_client), follow_info);
307 gtk_menu_append(GTK_MENU(stream_menu), stream_mi);
308 gtk_widget_show(stream_mi);
310 /* Host 1 --> Host 0 */
311 snprintf(string, sizeof(string), "%s:%s --> %s:%s (%u bytes)",
312 hostname1, port1, hostname0, port0,
313 stats.bytes_written[1]);
314 stream_mi = gtk_menu_item_new_with_label(string);
315 gtk_signal_connect(GTK_OBJECT(stream_mi), "activate",
316 GTK_SIGNAL_FUNC(follow_stream_om_server), follow_info);
317 gtk_menu_append(GTK_MENU(stream_menu), stream_mi);
318 gtk_widget_show(stream_mi);
320 gtk_option_menu_set_menu(GTK_OPTION_MENU(stream_om), stream_menu);
321 /* Set history to 0th item, i.e., the first item. */
322 gtk_option_menu_set_history(GTK_OPTION_MENU(stream_om), 0);
323 gtk_box_pack_start(GTK_BOX(hbox), stream_om, FALSE, FALSE, 0);
325 /* ASCII radio button */
326 radio_bt = gtk_radio_button_new_with_label(NULL, "ASCII");
327 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(radio_bt), TRUE);
328 gtk_box_pack_start(GTK_BOX(hbox), radio_bt, FALSE, FALSE, 0);
329 gtk_signal_connect(GTK_OBJECT(radio_bt), "toggled",
330 GTK_SIGNAL_FUNC(follow_charset_toggle_cb),
332 follow_info->ascii_bt = radio_bt;
333 follow_info->show_type = SHOW_ASCII;
335 /* EBCDIC radio button */
336 radio_bt = gtk_radio_button_new_with_label(gtk_radio_button_group
337 (GTK_RADIO_BUTTON(radio_bt)),
339 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(radio_bt), FALSE);
340 gtk_box_pack_start(GTK_BOX(hbox), radio_bt, FALSE, FALSE, 0);
341 gtk_signal_connect(GTK_OBJECT(radio_bt), "toggled",
342 GTK_SIGNAL_FUNC(follow_charset_toggle_cb),
344 follow_info->ebcdic_bt = radio_bt;
346 /* HEX DUMP radio button */
347 radio_bt = gtk_radio_button_new_with_label(gtk_radio_button_group
348 (GTK_RADIO_BUTTON(radio_bt)),
350 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(radio_bt), FALSE);
351 gtk_box_pack_start(GTK_BOX(hbox), radio_bt, FALSE, FALSE, 0);
352 gtk_signal_connect(GTK_OBJECT(radio_bt), "toggled",
353 GTK_SIGNAL_FUNC(follow_charset_toggle_cb),
355 follow_info->hexdump_bt = radio_bt;
357 /* Create Close Button */
358 button = gtk_button_new_with_label("Close");
359 gtk_signal_connect_object(GTK_OBJECT(button), "clicked",
360 GTK_SIGNAL_FUNC(gtk_widget_destroy),
361 GTK_OBJECT(streamwindow));
362 gtk_box_pack_end(GTK_BOX(hbox), button, FALSE, FALSE, 0);
364 /* Catch the "key_press_event" signal in the window, so that we can catch
365 the ESC key being pressed and act as if the "Cancel" button had
367 dlg_set_cancel(streamwindow, button);
369 /* Create Save As Button */
370 button = gtk_button_new_with_label("Save As");
371 gtk_signal_connect(GTK_OBJECT(button), "clicked",
372 GTK_SIGNAL_FUNC(follow_save_as_cmd_cb),
374 gtk_box_pack_end(GTK_BOX(hbox), button, FALSE, FALSE, 0);
376 /* Create Print Button */
377 button = gtk_button_new_with_label("Print");
378 gtk_signal_connect(GTK_OBJECT(button), "clicked",
379 GTK_SIGNAL_FUNC(follow_print_stream), follow_info);
380 gtk_box_pack_end(GTK_BOX(hbox), button, FALSE, FALSE, 0);
383 /* Tuck away the follow_info object into the window */
384 gtk_object_set_data(GTK_OBJECT(streamwindow), E_FOLLOW_INFO_KEY,
387 follow_load_text(follow_info);
388 remember_follow_info(follow_info);
390 data_out_file = NULL;
392 /* Make sure this widget gets destroyed if we quit the main loop,
393 so that if we exit, we clean up any temporary files we have
394 for "Follow TCP Stream" windows. */
395 gtk_quit_add_destroy(gtk_main_level(), GTK_OBJECT(streamwindow));
396 gtk_widget_show_all(streamwindow);
399 /* The destroy call back has the responsibility of
400 * unlinking the temporary file */
402 follow_destroy_cb(GtkWidget *w, gpointer data)
404 follow_info_t *follow_info;
406 follow_info = gtk_object_get_data(GTK_OBJECT(w), E_FOLLOW_INFO_KEY);
407 unlink(follow_info->data_out_filename);
408 gtk_widget_destroy(w);
409 forget_follow_info(follow_info);
413 /* XXX - can I emulate follow_charset_toggle_cb() instead of having
414 * 3 different functions here? */
416 follow_stream_om_both(GtkWidget *w, gpointer data)
418 follow_info_t *follow_info = data;
419 follow_info->show_stream = BOTH_HOSTS;
420 follow_load_text(follow_info);
424 follow_stream_om_client(GtkWidget *w, gpointer data)
426 follow_info_t *follow_info = data;
427 follow_info->show_stream = FROM_CLIENT;
428 follow_load_text(follow_info);
432 follow_stream_om_server(GtkWidget *w, gpointer data)
434 follow_info_t *follow_info = data;
435 follow_info->show_stream = FROM_SERVER;
436 follow_load_text(follow_info);
440 /* Handles the ASCII/EBCDIC toggling */
442 follow_charset_toggle_cb(GtkWidget * w, gpointer data)
444 follow_info_t *follow_info = data;
446 if (GTK_TOGGLE_BUTTON(follow_info->ebcdic_bt)->active)
447 follow_info->show_type = SHOW_EBCDIC;
448 else if (GTK_TOGGLE_BUTTON(follow_info->hexdump_bt)->active)
449 follow_info->show_type = SHOW_HEXDUMP;
450 else if (GTK_TOGGLE_BUTTON(follow_info->ascii_bt)->active)
451 follow_info->show_type = SHOW_ASCII;
453 g_assert_not_reached();
455 follow_load_text(follow_info);
458 #define FLT_BUF_SIZE 1024
460 follow_read_stream(follow_info_t *follow_info,
461 void (*print_line) (char *, int, gboolean, void *),
466 guint8 client_addr[MAX_IPADDR_LEN];
467 guint16 client_port = 0;
469 guint16 current_pos, global_client_pos = 0, global_server_pos = 0;
473 iplen = (follow_info->is_ipv6) ? 16 : 4;
475 data_out_file = fopen(follow_info->data_out_filename, "rb");
477 char buffer[FLT_BUF_SIZE];
479 while (fread(&sc, 1, sizeof(sc), data_out_file)) {
480 if (client_port == 0) {
481 memcpy(client_addr, sc.src_addr, iplen);
482 client_port = sc.src_port;
485 if (memcmp(client_addr, sc.src_addr, iplen) == 0 &&
486 client_port == sc.src_port) {
488 global_pos = &global_client_pos;
489 if (follow_info->show_stream == FROM_SERVER) {
495 global_pos = &global_server_pos;
496 if (follow_info->show_stream == FROM_CLIENT) {
501 while (sc.dlen > 0) {
502 bcount = (sc.dlen < FLT_BUF_SIZE) ? sc.dlen : FLT_BUF_SIZE;
503 nchars = fread(buffer, 1, bcount, data_out_file);
508 switch (follow_info->show_type) {
510 /* If our native arch is ASCII, call: */
511 EBCDIC_to_ASCII(buffer, nchars);
512 (*print_line) (buffer, nchars, is_server, arg);
515 /* If our native arch is EBCDIC, call:
516 * ASCII_TO_EBCDIC(buffer, nchars);
518 (*print_line) (buffer, nchars, is_server, arg);
522 while (current_pos < nchars) {
524 gchar hexchars[] = "0123456789abcdef";
526 /* is_server indentation : put 63 spaces at the begenning
528 sprintf(hexbuf, (is_server &&
529 follow_info->show_stream == BOTH_HOSTS) ?
532 "%08X ", *global_pos);
533 cur = strlen(hexbuf);
534 for (i = 0; i < 16 && current_pos + i < nchars;
537 hexchars[(buffer[current_pos + i] & 0xf0)
540 hexchars[buffer[current_pos + i] & 0x0f];
547 /* Fill it up if column isn't complete */
551 for (j = i; j < 16; j++) {
561 /* Now dump bytes as text */
562 for (i = 0; i < 16 && current_pos + i < nchars;
565 (isprint((guchar)buffer[current_pos + i]) ?
566 buffer[current_pos + i] : '.' );
573 hexbuf[cur++] = '\n';
575 (*print_line) (hexbuf, strlen(hexbuf), is_server, arg);
582 if (ferror(data_out_file)) {
583 simple_dialog(ESD_TYPE_WARN, NULL,
584 "Error reading temporary file %s: %s", follow_info->data_out_filename,
587 fclose(data_out_file);
588 data_out_file = NULL;
590 simple_dialog(ESD_TYPE_WARN, NULL,
591 "Could not open temporary file %s: %s", follow_info->data_out_filename,
597 * XXX - for text printing, we probably want to wrap lines at 80 characters;
598 * for PostScript printing, we probably want to wrap them at the appropriate
599 * width, and perhaps put some kind of dingbat (to use the technical term)
600 * to indicate a wrapped line, along the lines of what's done when displaying
601 * this in a window, as per Warren Young's suggestion.
603 * For now, we support only text printing.
606 follow_print_text(char *buffer, int nchars, gboolean is_server, void *arg)
610 fwrite(buffer, nchars, 1, fh);
614 follow_print_stream(GtkWidget * w, gpointer data)
619 follow_info_t *follow_info = data;
621 switch (prefs.pr_dest) {
623 print_dest = prefs.pr_cmd;
628 print_dest = prefs.pr_file;
631 default: /* "Can't happen" */
632 simple_dialog(ESD_TYPE_CRIT, NULL,
633 "Couldn't figure out where to send the print "
634 "job. Check your preferences.");
638 fh = open_print_dest(to_file, print_dest);
642 simple_dialog(ESD_TYPE_WARN, NULL,
643 "Couldn't run print command %s.", prefs.pr_cmd);
647 simple_dialog(ESD_TYPE_WARN, NULL,
648 file_write_error_message(errno), prefs.pr_file);
654 print_preamble(fh, PR_FMT_TEXT);
655 follow_read_stream(follow_info, follow_print_text, fh);
656 print_finale(fh, PR_FMT_TEXT);
657 close_print_dest(to_file, fh);
661 follow_add_to_gtk_text(char *buffer, int nchars, gboolean is_server,
664 GtkWidget *text = arg;
668 /* While our isprint() hack is in place, we
669 * have to use convert some chars to '.' in order
670 * to be able to see the data we *should* see
671 * in the GtkText widget.
675 for (i = 0; i < nchars; i++) {
676 if (buffer[i] == 0x0a || buffer[i] == 0x0d) {
679 else if (! isprint(buffer[i])) {
686 color_t_to_gdkcolor(&fg, &prefs.st_server_fg);
687 color_t_to_gdkcolor(&bg, &prefs.st_server_bg);
689 color_t_to_gdkcolor(&fg, &prefs.st_client_fg);
690 color_t_to_gdkcolor(&bg, &prefs.st_client_bg);
692 gtk_text_insert(GTK_TEXT(text), m_r_font, &fg, &bg, buffer, nchars);
696 follow_load_text(follow_info_t *follow_info)
700 /* Delete any info already in text box */
701 bytes_already = gtk_text_get_length(GTK_TEXT(follow_info->text));
702 if (bytes_already > 0) {
703 gtk_text_set_point(GTK_TEXT(follow_info->text), 0);
704 gtk_text_forward_delete(GTK_TEXT(follow_info->text), bytes_already);
707 /* stop the updates while we fill the text box */
708 gtk_text_freeze(GTK_TEXT(follow_info->text));
709 follow_read_stream(follow_info, follow_add_to_gtk_text, follow_info->text);
710 gtk_text_thaw(GTK_TEXT(follow_info->text));
715 * Keep a static pointer to the current "Save TCP Follow Stream As" window, if
716 * any, so that if somebody tries to do "Save"
717 * while there's already a "Save TCP Follow Stream" window up, we just pop
718 * up the existing one, rather than creating a new one.
721 follow_save_as_cmd_cb(GtkWidget *w, gpointer data)
723 GtkWidget *ok_bt, *new_win;
724 follow_info_t *follow_info = data;
726 if (follow_info->follow_save_as_w != NULL) {
727 /* There's already a dialog box; reactivate it. */
728 reactivate_window(follow_info->follow_save_as_w);
732 new_win = gtk_file_selection_new("Ethereal: Save TCP Follow Stream As");
733 follow_info->follow_save_as_w = new_win;
734 gtk_signal_connect(GTK_OBJECT(new_win), "destroy",
735 GTK_SIGNAL_FUNC(follow_save_as_destroy_cb), follow_info);
737 /* Tuck away the follow_info object into the window */
738 gtk_object_set_data(GTK_OBJECT(new_win), E_FOLLOW_INFO_KEY,
741 /* If we've opened a file, start out by showing the files in the directory
742 in which that file resided. */
744 gtk_file_selection_complete(GTK_FILE_SELECTION(new_win),
747 /* Connect the ok_button to file_save_as_ok_cb function and pass along a
748 pointer to the file selection box widget */
749 ok_bt = GTK_FILE_SELECTION(new_win)->ok_button;
750 gtk_signal_connect(GTK_OBJECT(ok_bt), "clicked",
751 (GtkSignalFunc) follow_save_as_ok_cb,
754 /* Connect the cancel_button to destroy the widget */
755 gtk_signal_connect_object(GTK_OBJECT(GTK_FILE_SELECTION
756 (new_win)->cancel_button),
758 (GtkSignalFunc) gtk_widget_destroy,
759 GTK_OBJECT(new_win));
761 /* Catch the "key_press_event" signal in the window, so that we can catch
762 the ESC key being pressed and act as if the "Cancel" button had
764 dlg_set_cancel(new_win,
765 GTK_FILE_SELECTION(new_win)->cancel_button);
767 gtk_file_selection_set_filename(GTK_FILE_SELECTION(new_win), "");
768 gtk_widget_show_all(new_win);
773 follow_save_as_ok_cb(GtkWidget * w, GtkFileSelection * fs)
776 follow_info_t *follow_info;
779 to_name = g_strdup(gtk_file_selection_get_filename(GTK_FILE_SELECTION(fs)));
781 gtk_widget_hide(GTK_WIDGET(fs));
782 follow_info = gtk_object_get_data(GTK_OBJECT(fs), E_FOLLOW_INFO_KEY);
783 gtk_widget_destroy(GTK_WIDGET(fs));
785 fh = fopen(to_name, "wb");
787 simple_dialog(ESD_TYPE_WARN, NULL,
788 file_write_error_message(errno), to_name);
792 follow_read_stream(follow_info, follow_print_text, fh);
798 follow_save_as_destroy_cb(GtkWidget * win, gpointer data)
800 follow_info_t *follow_info = data;
802 /* Note that we no longer have a dialog box. */
803 follow_info->follow_save_as_w = NULL;