#include "elm_defs.h"
#include "elm_globals.h"
#include "s_fbrowser.h"
#include "port_stat.h"
#include "port_dirent.h"
#include <time.h>
#ifdef PWDINSYS
#  include <sys/pwd.h>
#else
#  include <pwd.h>
#endif

#define FB_CHUNK	64			/* malloc increment	*/
#define FB_DUMMYMODE	(user_level == 0)	/* more handholding	*/

#define IS_DOT(s)	((s)[0] == '.' && (s)[1] == '\0')
#define IS_DOTDOT(s)	((s)[0] == '.' && (s)[1] == '.' && (s)[2] == '\0')

/* WARNING - side effects */
#define PutRight(line, str) PutLine0((line), COLS-(strlen(str)+2), (str))

#define S_(sel, str)	catgets(elm_msg_cat, FbrowserSet, (sel), (str))


/*
 * Locations within the screen display.
 */
#define FBLINE_TITLE	0		/* page title			*/
#define FBLINE_CURR	1		/* current dir/pat display	*/
#define FBLINE_EHDR	3		/* entry column header		*/
#define FBLINE_LTOP	(FB_DUMMYMODE ? 5 : 3)
					/* top line of listing window	*/
					/* ...DUMMYMODE adds titles	*/
#define FBLINE_LBOT	(LINES - (FB_DUMMYMODE ? 6 : 5))
					/* bot line of listing window	*/
					/* ...DUMMYMODE adds more help	*/
#define FBLINE_INSTRUCT	(LINES-3)	/* instructions display		*/
#define FBLINE_INPUT	(LINES-2)	/* user data entry line		*/

/*
 * Display redraw requests.
 */
#define FBDRAW_NONE	(0)	/* nothing to redisplay			*/
#define FBDRAW_HEADER	(1<<0)	/* redisplay the screen header		*/
#define FBDRAW_LIST	(1<<1)	/* redisplay entire list of files	*/
#define FBDRAW_SELECT	(1<<2)	/* redisplay just the selection change	*/
#define FBDRAW_FOOTER	(1<<3)	/* redisplay the screen footer		*/
#define FBDRAW_ERROR	(1<<4)	/* redisplay the error line		*/
#define FBDRAW_ALL	(~0)	/* redisplay everything			*/

/*
 * Entry sorting order.
 */
#define FBSORT_TYPE_NAME	(1<<0)
#define FBSORT_TYPE_SIZE	(1<<1)
#define FBSORT_TYPE_MTIME	(1<<2)
#define FBSORT_TYPE_MASK	((1<<3)-1)
#define FBSORT_OPT_REVERSE	(1<<3)

/*
 * Entry selection/sizing/placment values.
 */
#define FB_lines_per_page	((FBLINE_LBOT-FBLINE_LTOP) + 1)
#define FB_pagenum(sel)		((sel) / FB_lines_per_page)
#define FB_line(sel)		((sel) % FB_lines_per_page)
#define FB_first_sel_of_page	(FB_pagenum(curr_sel)*FB_lines_per_page)
#define FB_last_sel_of_page	((FB_pagenum(curr_sel)+1)*FB_lines_per_page - 1)
#define FB_first_sel_of_list	(0)
#define FB_last_sel_of_list	(curr_dlist->num_entries - 1)
#define FB_num_pages		(FB_last_sel_of_list/FB_lines_per_page + 1)


/*
 * Full information on a directory being browsed.
 */
typedef struct fb_dirlist {
    struct fb_entryinfo { /* dynamically allocated list of entry recs	*/
	char *name;		/*  entry name	*/
	unsigned short mode;	/*  st_mode	*/
	unsigned short uid;	/*  st_uid	*/
	long size;		/*  st_size	*/
	time_t mtime;		/*  st_mtime	*/
    } *entry;
    int num_entries;	/* number of elements used in entry list	*/
    int alloc_entries;	/* allocated size of the entry list		*/
} FB_DIR;


/*
 * Local data.
 */
static int fb_show_dotfiles = FALSE;		/* display .dot files?	*/
static int fb_sortby = FBSORT_TYPE_NAME;	/* entry sort order	*/
static char fb_save_dir[SLEN];			/* previous dir browsed	*/
static char fb_acursor_on[]  = "->";		/* arrow cursor on	*/
static char fb_acursor_off[] = "  ";		/* arrow cursor blanked	*/

/*
 * Local procedures.
 */
static void fb_submenu_options P_((FB_DIR **, const char *, const char *));
static void fb_disp_enthdr P_((int));
static void fb_disp_entry P_((int, const FB_DIR *, int, int));
static void fb_disp_instr P_((const char *, const char *, const char *, int));
static FB_DIR *fb_start_dir P_((const char *, const char *, int));
static void fb_finish_dir P_((FB_DIR *));
static char *fb_getsorttype P_((void));
static int (*fb_getsortproc P_((void))) P_((const malloc_t, const malloc_t));
static int fbcmp_name_ascending P_((const malloc_t, const malloc_t));
static int fbcmp_name_descending P_((const malloc_t, const malloc_t));
static int fbcmp_size_ascending P_((const malloc_t, const malloc_t));
static int fbcmp_size_descending P_((const malloc_t, const malloc_t));
static int fbcmp_mtime_ascending P_((const malloc_t, const malloc_t));
static int fbcmp_mtime_descending P_((const malloc_t, const malloc_t));
static int fb_mbox_check P_((char *, int));
static int safe_copy P_((char *, const char *, int));
static int safe_mkpath P_((char *, const char *, const char *, int));


PUBLIC int fbrowser_analyze_spec(spec, ret_dir, ret_pat)
const char *spec;
char *ret_dir, *ret_pat;
{
    struct stat sbuf;
    int i;
    char *s;

    (void) strcpy(ret_dir, spec);
    trim_trailing_spaces(ret_dir);
    trim_trailing_slashes(ret_dir);
    if (ret_dir[0] == '\0')
	return FALSE;

    /* if the entry exists then it should be a directory to browse */
    if (stat(ret_dir, &sbuf) == 0) {
	if (!S_ISDIR(sbuf.st_mode)) {
	    /*
	     * This is a file (in which case we do not want to browse)
	     * or it is something that we totally cannot grok.
	     */
	    return FALSE;
	}
	(void) strcpy(ret_pat, "*");
	return TRUE;
    }

    if ((s = strrchr(ret_dir, '/')) != NULL) {
	/* maybe this is a "directory/pattern" specification */
	(void) strcpy(ret_pat, s+1);
	if (s == ret_dir)
	    ret_dir[1] = '\0';	/* dir is "/" */
	else
	    *s = '\0';
    } else {
	/* maybe this is a "pattern" specification */
	(void) strcpy(ret_pat, ret_dir);
	(void) strcpy(ret_dir, ".");
    }

    /* verify the "directory" part really is one */
    if (stat(ret_dir, &sbuf) != 0 || !S_ISDIR(sbuf.st_mode))
	return FALSE;

    /* verify the "pattern" part really is one */
    if (strpbrk(ret_pat, "*?") == NULL)
	return FALSE;

    return TRUE;
}


PUBLIC int fbrowser(ret_buf, ret_bufsiz, start_dir, start_pat, options, prompt)
char *ret_buf;			/* storage space for final selection	*/
int ret_bufsiz;			/* size of storage space		*/
const char *start_dir;		/* starting directory to browse		*/
const char *start_pat;		/* starting match pattern		*/
int options;			/* options (see FB_XXX in elm_defs.h)	*/
const char *prompt;		/* message for screen header		*/
{
    char curr_dir[SLEN];	/* current directory being browsed	*/
    char curr_pat[SLEN];	/* current match pattern for entries	*/
    FB_DIR *curr_dlist;		/* entries in the current directory	*/
    FB_DIR *new_dlist;		/* set non-NULL to switch directories	*/
    int inp_line, inp_col;	/* cursor position for user input	*/
    int do_redraw;		/* what parts of screen need redrawing	*/
    int bad_cmd;		/* TRUE if last command was bad		*/
    int cmd;			/* command from user			*/
    int curr_sel;		/* currently selected entry		*/
    int prev_sel;		/* selection at previous iteration	*/
    int rc;			/* final return status			*/
    char tmp_buf[SLEN], *np, *s;
    int n, i;
    struct stat sbuf;

    if (!safe_copy(curr_dir, start_dir, sizeof(curr_dir)))
	return FALSE;
    if (!safe_copy(curr_pat, start_pat, sizeof(curr_pat)))
	return FALSE;
    if ((new_dlist = fb_start_dir(curr_dir, curr_pat, FALSE)) == NULL) {
	error2(S_(FbrowserCannotBrowse, "Cannot browse \"%s/%s\"."),
		    curr_dir, curr_pat);
	return FALSE;
    }
    rc = FALSE;			/* initialize to failure		*/
    curr_dlist = NULL;		/* prevent fb_finish_dir() below	*/
    bad_cmd = FALSE;		/* nothing to bitch about yet		*/

    for (;;) {

	/* complain if last entry was bad */
	if (bad_cmd) {
	    Beep();
	    bad_cmd = FALSE;
	}

	/* see if we need to switch to a new directory */
	if (new_dlist != NULL) {
	    if (curr_dlist != NULL)
		fb_finish_dir(curr_dlist);
	    curr_dlist = new_dlist;
	    new_dlist = NULL;
	    prev_sel = curr_sel = FB_first_sel_of_list;
	    do_redraw = FBDRAW_ALL;
	}

	/* see if the selection moved */
	if (prev_sel != curr_sel) {
	    if (curr_sel < FB_first_sel_of_list) {
		/* adjust for movement beyond start of list */
		if ((curr_sel = FB_first_sel_of_list) == prev_sel) {
		    bad_cmd = TRUE;
		    continue;
		}
	    }
	    if (curr_sel > FB_last_sel_of_list) {
		/* adjust for movement beyond end of list */
		if ((curr_sel = FB_last_sel_of_list) == prev_sel) {
		    bad_cmd = TRUE;
		    continue;
		}
	    }
	    if (FB_pagenum(curr_sel) != FB_pagenum(prev_sel)) {
		/* page changed */
		do_redraw = FBDRAW_ALL;
	    } else {
		/* moved to a selection on this page */
		do_redraw = FBDRAW_SELECT;
	    }
	}

	/* do screen updates */
	if (do_redraw != FBDRAW_NONE) {

	    if (do_redraw == FBDRAW_ALL)
		ClearScreen();

	    /* redraw the title and selection header lines */
	    if (do_redraw & FBDRAW_HEADER) {
		if (do_redraw != FBDRAW_ALL)
		    ClearLine(FBLINE_TITLE);
		CenterLine(FBLINE_TITLE, prompt);
		sprintf(tmp_buf, S_(FbrowserHeaderPageOf, "[page %d/%d]"),
			    FB_pagenum(curr_sel)+1, FB_num_pages);
		PutLine0(FBLINE_TITLE, COLS-(strlen(tmp_buf)+2), tmp_buf);
		if (do_redraw != FBDRAW_ALL)
		    ClearLine(FBLINE_CURR);
		PutLine1(FBLINE_CURR, 0,
			    S_(FbrowserHeaderDirectory, "Directory: %s"),
			    curr_dir);
		sprintf(tmp_buf, S_(FbrowserHeaderPattern, "Pattern: %s"),
			    curr_pat);
		PutRight(FBLINE_CURR, tmp_buf);
	    }

	    /* display titles on entry list */
	    if (do_redraw == FBDRAW_ALL && FB_DUMMYMODE)
		fb_disp_enthdr(FBLINE_EHDR);

	    /* redraw the entire list of entries */
	    if (do_redraw & FBDRAW_LIST) {
		n = FB_first_sel_of_page;
		for (i = 0 ; i < FB_lines_per_page ; ++i) {
		    if (n <= FB_last_sel_of_list) {
			fb_disp_entry(FBLINE_LTOP+i, curr_dlist,
				    n, (n == curr_sel));
		    } else if (do_redraw != FBDRAW_ALL) {
			ClearLine(FBLINE_LTOP+i);
		    }
		    ++n;
		}
		do_redraw &= ~FBDRAW_SELECT;
	    }

	    /* redraw just the entries with selection changes */
	    if (do_redraw & FBDRAW_SELECT) {
		if (prev_sel >= FB_first_sel_of_page
			&& prev_sel <= FB_last_sel_of_page) {
		    if (arrow_cursor) {
			PutLine0(FBLINE_LTOP+FB_line(prev_sel), 0,
				    fb_acursor_off);
		    } else {
			fb_disp_entry(FBLINE_LTOP+FB_line(prev_sel),
				    curr_dlist, prev_sel, FALSE);
		    }
		}
		if (arrow_cursor) {
		    PutLine0(FBLINE_LTOP+FB_line(curr_sel), 0,
				fb_acursor_on);
		} else {
		    fb_disp_entry(FBLINE_LTOP+FB_line(curr_sel),
				curr_dlist, curr_sel, TRUE);
		}
	    }

	    /* redraw the instructions and prompt footer lines */
	    if (do_redraw & FBDRAW_FOOTER) {
		fb_disp_instr(
			    S_(FbrowserInstNorm, /*(*/
"Use \"jk+-\" to move, \"/=~.\" to enter name, ENTER to select, or q)uit."),
			    S_(FbrowserInstrDummy1,
"Move the highlighted selection:  j/k = down/up, +/- = down/up page"),
			    S_(FbrowserInstrDummy2, /*(*/
"Press ENTER to make selection, ? for help, or q)uit."),
			    (do_redraw != FBDRAW_ALL));
		PutLine0(FBLINE_INPUT, 0, S_(FbrowserMainPrompt, "Command: "));
		GetCursorPos(&inp_line, &inp_col);
	    }

	    if (do_redraw & FBDRAW_ERROR)
		show_last_error();

	    do_redraw = FBDRAW_NONE;

	}

	prev_sel = curr_sel;

	/* prompt for command */
	MoveCursor(inp_line, inp_col);
	CleartoEOLN();
	if ((cmd = GetKey(0)) == KEY_REDRAW) {
	    do_redraw = FBDRAW_ALL;
	    continue;
	}
	if (clear_error())
	    MoveCursor(inp_line, inp_col);

	switch (cmd) {

	case 'q':			/* quit menu */
	    goto done;

	case '?':			/* help */
	    display_helpfile("fbrowser");
	    do_redraw = FBDRAW_ALL;
	    break;

	case ctrl('L'):			/* redraw display */
	    do_redraw = FBDRAW_ALL;
	    break;

	case ctrl('R'):			/* recall previous dir */
	    if (fb_save_dir[0] == '\0') {
		error(S_(FbrowserNoDirectorySaved, "No directory saved."));
		break;
	    }
	    if (strcmp(fb_save_dir, curr_dir) == 0) {
		error1(S_(FbrowserAlreadyIn,
			    "Already in \"%s\"."), fb_save_dir);
		break;
	    }
	    new_dlist = fb_start_dir(fb_save_dir, curr_pat, FALSE);
	    if (new_dlist != NULL) {
		(void) strcpy(tmp_buf, fb_save_dir);
		(void) strcpy(fb_save_dir, curr_dir);
		(void) strcpy(curr_dir, tmp_buf);
	    }
	    break;

	case 'j':			/* down entry */
	case KEY_DOWN:
	    ++curr_sel;
	    break;

	case 'k':			/* up entry */
	case KEY_UP:
	    --curr_sel;
	    break;

	case '+':			/* down page */
	case KEY_NPAGE:
        case KEY_RIGHT:   
	    curr_sel += FB_lines_per_page;
	    break;

	case '-':			/* up page */
	case KEY_PPAGE:
        case KEY_LEFT:
	    curr_sel -= FB_lines_per_page;
	    break;

	case '1':			/* first entry */
	case KEY_HOME:
	    curr_sel = FB_first_sel_of_list;
	    break;

	case '*':			/* last entry */
	case KEY_END:
	    curr_sel = FB_last_sel_of_list;
	    break;

#ifdef ALLOW_SUBSHELL
	case '!':			/* subshell */
	    fb_disp_instr((char *)NULL, (char *)NULL, (char *)NULL, TRUE);
	    ClearLine(FBLINE_INPUT);
	    do_redraw = (subshell() ? FBDRAW_ALL : FBDRAW_FOOTER);
	    break;
#endif

#ifdef notdef /*FOO*/
	/*
	 * The "limit" command can be done with
	 * the new read-my-mind dir/file entry.
	 */
	case 'l':			/* change pattern (limit) */
	case 'p':
	    do_redraw = FBDRAW_FOOTER;
	    fb_disp_instr(
			S_(FbrowserLimitInstrNorm,
"Enter new pattern for file listing."),
			S_(FbrowserLimitInstrDummy1,
"Enter new pattern for file listing.  Only matching files will be displayed."),
			S_(FbrowserLimitInstrDummy2,
"In patterns, \"?\" means any one char, and \"*\" means any number of chars."),
			TRUE);
	    PutLine0(FBLINE_INPUT, 0, S_(FbrowserLimitPrompt, "New Pattern: "));
	    strcpy(tmp_buf, "*");
	    if (enter_string(tmp_buf, sizeof(tmp_buf), -1, -1, ESTR_REPLACE) < 0
			|| tmp_buf[0] == '\0'
			|| strcmp(tmp_buf, curr_pat) == 0) {
		error(S_(FbrowserNotChanged, "Not changed."));
		break;
	    }
	    if ((new_dlist = fb_start_dir(curr_dir, tmp_buf, TRUE)) != NULL)
		(void) strcpy(curr_pat, tmp_buf);
	    break;
#endif

#ifdef notdef /*FOO*/
	/*
	 * The "chdir" command can be done with
	 * the new read-my-mind dir/file entry.
	 */
	case 'c':			/* change dir */
	    do_redraw = FBDRAW_FOOTER;
	    fb_disp_instr(
			S_(FbrowserDirInstrNorm,
"Enter directory to browse.  \"~\" and \"=\" ok, CTRL/D to abort"),
			S_(FbrowserDirInstrDummy1,
"Enter pathname of directory to browse, or CTRL/D to abort entry."),
			S_(FbrowserDirInstrDummy2,
"You may say \"~\" for your home dir and \"=\" for your folders dir."),
			TRUE);
	    PutLine0(FBLINE_INPUT, 0, S_(FbrowserDirPrompt, "New Directory: "));
	    if (enter_string(tmp_buf, sizeof(tmp_buf), -1, -1, ESTR_ENTER) < 0
			|| tmp_buf[0] == '\0') {
		error(S_(FbrowserNotChanged, "Not changed."));
		break;
	    }
	    (void) strcat(tmp_buf, "/");	/* expand_filename silliness */
	    if (!expand_filename(tmp_buf))
		break;
	    trim_trailing_slashes(tmp_buf);	/* more silliness */
	    if ((new_dlist = fb_start_dir(tmp_buf, curr_pat, FALSE)) != NULL) {
		(void) strcpy(fb_save_dir, curr_dir);
		(void) strcpy(curr_dir, tmp_buf);
	    }
	    break;
#endif

	case 'o':			/* change options submenu */
	    fb_submenu_options(&new_dlist, curr_dir, curr_pat);
	    do_redraw = FBDRAW_ALL;
	    break;

	case '/':			/* enter value from "/" */
	    (void) strcpy(tmp_buf, "/");
	    goto enter_value;

	case '=':			/* enter value from "~/Mail/" */
	    (void) strcpy(tmp_buf, "=/");
	    if (!expand_filename(tmp_buf))
		break;
	    (void) strcat(trim_trailing_slashes(tmp_buf), "/");
	    goto enter_value;

	case '~':			/* enter value from "$HOME/" */
	    (void) strcpy(tmp_buf, "~/");
	    if (!expand_filename(tmp_buf))
		break;
	    (void) strcat(trim_trailing_slashes(tmp_buf), "/");
	    goto enter_value;

	case '.':			/* enter value from "." */
	    (void) strcpy(tmp_buf, curr_dir);
	    (void) strcat(tmp_buf, "/");

enter_value:
	    do_redraw = FBDRAW_FOOTER;
#define FbFOO 1
	    fb_disp_instr(
			S_(FbFOO,
"Enter file name (pattern ok) or directory.  CTRL/D to abort entry."),
			S_(FbFOO,
"Enter either the name of a file to select or a directory to browse."),
			S_(FbFOO,
"Press CTRL/D to abort entry and stay in current directory."),
			TRUE);
	    PutLine0(FBLINE_INPUT, 0, S_(FbFOO, "Select: "));
	    if (enter_string(tmp_buf, sizeof(tmp_buf), -1, -1, ESTR_UPDATE) < 0
			|| tmp_buf[0] == '\0') {
		error(S_(FbrowserNotChanged, "Not changed."));
		break;
	    }
	    trim_trailing_slashes(tmp_buf);

	    {
		struct stat sbuf;
		if (stat(tmp_buf, &sbuf) == 0 && S_ISREG(sbuf.st_mode)) {
		    if (!safe_copy(ret_buf, tmp_buf, ret_bufsiz))
			break;
		    if (!fb_mbox_check(ret_buf, options))
			break;
		    rc = TRUE;
		    goto done;
		}
	    }

	    {
		char tmp_dir[SLEN], tmp_pat[SLEN];
		if (!fbrowser_analyze_spec(tmp_buf, tmp_dir, tmp_pat)) {
		    error(S_(FbFOO, "Not a valid directory or pattern."));
		    break;
		}
		if ((new_dlist = fb_start_dir(tmp_dir, tmp_pat,
				streq(tmp_dir, curr_dir))) == NULL)
		    break;
		(void) strcpy(curr_dir, tmp_dir);
		(void) strcpy(curr_pat, tmp_pat);
	    }

	    (void) strcpy(fb_save_dir, curr_dir);
	    break;

	case '\r':			/* accept entry */
	case '\n':
	case ' ':
	    if (!S_ISDIR(curr_dlist->entry[curr_sel].mode)) {
		if (!safe_mkpath(ret_buf, curr_dir,
			    curr_dlist->entry[curr_sel].name, ret_bufsiz))
		    break;
		if (!fb_mbox_check(ret_buf, options))
		    break;
		rc = TRUE;
		goto done;
	    }
	    (void) strcpy(tmp_buf, curr_dir);
	    np = curr_dlist->entry[curr_sel].name;
	    if (IS_DOT(np)) {
		/* stay where we are */
		np = NULL;
	    } else if (IS_DOTDOT(np) && (s = strrchr(tmp_buf, '/')) != NULL
			    && strcmp(s, "/..") != 0) {
		/* trim "/.." from end */
		if (s == tmp_buf)
		    tmp_buf[1] = '\0';
		else
		    *s = '\0';
		np = NULL;
	    }
	    if (np != NULL) {
		/* append name to end of path */
		if (!safe_mkpath(tmp_buf, (char *)NULL, np, sizeof(tmp_buf)))
		    break;
	    }
	    if ((new_dlist = fb_start_dir(tmp_buf, curr_pat, FALSE)) != NULL) {
		(void) strcpy(fb_save_dir, curr_dir);
		(void) strcpy(curr_dir, tmp_buf);
	    }
	    break;

	default:
	    bad_cmd = TRUE;
	    break;

	}

    }

done:
    (void) strcpy(fb_save_dir, curr_dir);
    fb_finish_dir(curr_dlist);
    for (i = FBLINE_LBOT+1 ; i < LINES ; ++i)
	ClearLine(i);	/* clear out entire bottom but the error line */
    return rc;
}


#define FBOLINE_TITLE		(FBLINE_TITLE)
#define FBOLINE_OPT_DOTF	(FBOLINE_TITLE+2)
#define FBOLINE_OPT_SORT	(FBOLINE_TITLE+3)
#define FBOLINE_INPUT		(FBLINE_INPUT)
#define FBOLINE_INSTRUCT	(FBLINE_INSTRUCT)

#define FBOCOL_OPTVAL		25
#define FBOCOL_OPTHELP		(FBOCOL_OPTVAL+20)

#define FBOSEL_NONE		0
#define FBOSEL_DOTF		1
#define FBOSEL_SORT		2


static void fb_submenu_options(dl_p, curr_dir, curr_pat)
FB_DIR **dl_p;
const char *curr_dir, *curr_pat;
{
    int orig_show_dotfiles = fb_show_dotfiles;
    int orig_sortby = fb_sortby;
    int done = FALSE;
    int do_redraw = TRUE;
    int prev_sel = FBOSEL_NONE;
    int curr_sel = FBOSEL_NONE;
    int inp_line, inp_col, cmd, m;
    static char title_fmt[] = "%-22s : ";
    static char value_fmt[] = "%-20s";

    while (!done) {

	/* screen title */
	if (do_redraw) {
	    ClearScreen();
	    CenterLine(FBOLINE_TITLE, S_(FbrowserOptionsTitle,
			"File Selection Browser -- Options"));
	}

	/* fb_show_dotfiles setting */
	if (do_redraw) {
	    PutLine1(FBOLINE_OPT_DOTF, 0, title_fmt,
			S_(FbrowserOptionsTitleDotf, /*(*/
			"D)ot files displayed?"));
	}
	if (do_redraw || curr_sel == FBOSEL_DOTF) {
	    PutLine1(FBOLINE_OPT_DOTF, FBOCOL_OPTVAL, value_fmt,
			(fb_show_dotfiles
			? S_(FbrowserOptionsOn, "ON")
			: S_(FbrowserOptionsOff, "OFF")));
	}
	if (curr_sel != FBOSEL_DOTF && prev_sel == FBOSEL_DOTF && !do_redraw) {
	    MoveCursor(FBOLINE_OPT_DOTF, FBOCOL_OPTHELP);
	    CleartoEOLN();
	}
	if (curr_sel == FBOSEL_DOTF && (do_redraw || prev_sel != curr_sel)) {
	    PutRight(FBOLINE_OPT_DOTF, S_(FbrowserOptionsSpaceToToggle,
			"(SPACE to toggle)"));
	}

	/* fb_sortby setting */
	if (do_redraw) {
	    PutLine1(FBOLINE_OPT_SORT, 0, title_fmt,
			S_(FbrowserOptionsTitleSort, /*(*/
			"S)orting criteria"));
	}
	if (do_redraw || curr_sel == FBOSEL_SORT) {
	    PutLine1(FBOLINE_OPT_SORT, FBOCOL_OPTVAL, value_fmt,
			fb_getsorttype());
	}
	if (curr_sel != FBOSEL_SORT && prev_sel == FBOSEL_SORT && !do_redraw) {
	    MoveCursor(FBOLINE_OPT_DOTF, FBOCOL_OPTHELP);
	    CleartoEOLN();
	}
	if (curr_sel == FBOSEL_SORT && (do_redraw || prev_sel != curr_sel)) {
	    PutRight(FBOLINE_OPT_SORT, S_(FbrowserOptionsNextOrReverse, /*(*/
			"(SPACE for next, or r)everse)"));
	}

	/* instructions */
	if (do_redraw || curr_sel != prev_sel) {
	    if (!do_redraw)
		ClearLine(FBLINE_INSTRUCT);
	    switch (curr_sel) {
	    case FBOSEL_DOTF:
		CenterLine(FBOLINE_INSTRUCT, S_(FbrowserOptionsInstructDotf,
"Do you want filenames beginning with \".\" dot to be displayed?"));
		break;
	    case FBOSEL_SORT:
		CenterLine(FBOLINE_INSTRUCT, S_(FbrowserOptionsInstructSort,
"How should the filename listing be sorted?"));
		break;
	    case FBOSEL_NONE:
	    default:
		CenterLine(FBOLINE_INSTRUCT, S_(FbrowserOptionsInstructMain,
/*(*/ "Select option letter, or q)uit to return to File Selection Browser."));
		break;
	    }
	}

	/* command prompt */
	if (do_redraw || curr_sel != prev_sel) {
	    if (curr_sel == FBOSEL_NONE) {
		PutLine0(FBLINE_INPUT, 0, S_(FbrowserOptionsPromptCommand,
			    "Command: "));
		GetCursorPos(&inp_line, &inp_col);
		WriteChar('q');
		CleartoEOLN();
	    } else if (prev_sel == FBOSEL_NONE) {
		ClearLine(FBLINE_INPUT);
	    }
	}

	if (do_redraw)
	    show_last_error();

	switch (curr_sel) {
	case FBOSEL_DOTF:
	    MoveCursor(FBOLINE_OPT_DOTF, FBOCOL_OPTVAL);
	    break;
	case FBOSEL_SORT:
	    MoveCursor(FBOLINE_OPT_SORT, FBOCOL_OPTVAL);
	    break;
	case FBOSEL_NONE:
	    MoveCursor(inp_line, inp_col);
	    break;
	default:
	    error1(S_(FbrowserOptionsBogusSel,
			"Wierd!!  curr_sell was %d??  It's fixed now."),
			curr_sel);
	    curr_sel = FBOSEL_NONE;
	    continue;
	}

	do_redraw = FALSE;
	prev_sel = curr_sel;

	cmd = ReadCh();
	clear_error();

	switch (cmd) {
	case ctrl('D'):			/* abort without change */
	    fb_show_dotfiles = orig_show_dotfiles;
	    fb_sortby = orig_sortby;
	    set_error(S_(FbrowserOptionsNotChanged, "Options not changed."));
	    return;
	case ctrl('L'):			/* redraw */
	    do_redraw = TRUE;
	    continue;
	case ctrl('R'):			/* restore */
	    fb_show_dotfiles = orig_show_dotfiles;
	    fb_sortby = orig_sortby;
	    do_redraw = TRUE;
	    curr_sel = prev_sel = FBOSEL_NONE;
	    set_error(S_(FbrowserOptionsChangesUndone,
			"All option changes have been undone."));
	    continue;
	case '?':			/* help */
	    display_helpfile("fbrowser");
	    do_redraw = TRUE;
	    continue;
	}

	switch (curr_sel) {

	case FBOSEL_DOTF:
	    switch (cmd) {
	    case ' ':
		fb_show_dotfiles = !fb_show_dotfiles;
		break;
	    default:
		curr_sel = FBOSEL_NONE;
		break;
	    }
	    break;

	case FBOSEL_SORT:
	    switch (cmd) {
	    case ' ':
		if ((m = ((fb_sortby<<1) & FBSORT_TYPE_MASK)) == 0)
		    m = 1;
		fb_sortby = (fb_sortby & ~FBSORT_TYPE_MASK) | m;
		break;
	    case 'r':
		fb_sortby ^= FBSORT_OPT_REVERSE;
		break;
	    default:
		curr_sel = FBOSEL_NONE;
		break;
	    }
	    break;

	case FBOSEL_NONE:
	default:
	    switch (cmd) {
	    case 'd':
		curr_sel = FBOSEL_DOTF;
		break;
	    case 's':
		curr_sel = FBOSEL_SORT;
		break;
	    case 'q':
	    case ' ':
	    case '\r':
	    case '\n':
		done = TRUE;
		break;
	    default:
		Beep();
		break;
	    }
	    break;

	}

    }

    /* see if anything changed */
    if (orig_show_dotfiles != fb_show_dotfiles || orig_sortby != fb_sortby)
	*dl_p = fb_start_dir(curr_dir, curr_pat, TRUE);
    else
	set_error(S_(FbrowserOptionsNotChanged, "Options not changed."));
}


static void fb_disp_enthdr(line)
int line;
{
    int w;
    time_t tval;
    char buf[SLEN];

    ClearLine(line);
    MoveCursor(line, strlen(fb_acursor_off));
    if (!FB_DUMMYMODE) {
	PutLine1(-1, -1, "%-10.10s",
		    S_(FbrowserEnthdrPermission, "permission"));
    }
    PutLine1(-1, -1, " %-4.4s", S_(FbrowserEnthdrType, "type"));
    if (!FB_DUMMYMODE)
	PutLine1(-1, -1, " %-8.8s", S_(FbrowserEnthdrOwner, "owner"));
    PutLine1(-1, -1, " %8.8s", S_(FbrowserEnthdrSize, "size"));
    time(&tval);
    strftime(buf, sizeof(buf), S_(FbrowserEntryDateFmt, "%y-%b-%d"),
		localtime(&tval)); w = strlen(buf);
    sprintf(buf, "%-*.*s", w, w, S_(FbrowserEnthdrDate, "date"));
    PutLine1(-1, -1, "  %s", buf);
    PutLine1(-1, -1, "  %s", S_(FbrowserEnthdrFilename, "filename"));
}


static void fb_disp_entry(line, dl, n, selected)
int line;
const FB_DIR *dl;
int n, selected;
{
    int w, ech;
    unsigned mode;
    struct passwd *pw;
    char out_buf[SLEN], *etype, *bp, *np;

    static char *perms_list[] =
	{ "---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx", NULL };

    /* these are used a lot - make static to avoid re-lookup */
    static char *fbstr_entry_file;
    static char *fbstr_entry_dir;
    static char *fbstr_entry_date_fmt;
    static char *fbstr_entry_current_dir;
    static char *fbstr_entry_parrent_dir;

    if (fbstr_entry_file == NULL) {
	fbstr_entry_file = S_(FbrowserEntryFile,
	  "file");
	fbstr_entry_dir = S_(FbrowserEntryDir,
	  "dir");
	fbstr_entry_date_fmt = S_(FbrowserEntryDateFmt,
	  "%y-%b-%d");
	fbstr_entry_current_dir = S_(FbrowserEntryCurrentDir,
	  ".  [current directory]");
	fbstr_entry_parrent_dir = S_(FbrowserEntryParentDir,
	  ".. [parent directory]");
    }

    (void) strcpy(out_buf,
	(arrow_cursor && selected ? fb_acursor_on : fb_acursor_off));
    bp = out_buf + strlen(out_buf);

    mode = dl->entry[n].mode;
    switch (mode & S_IFMT) {
    case S_IFREG:
	ech = '-';
	etype = fbstr_entry_file;
	break;
    case S_IFDIR:
	ech = 'd';
	etype = fbstr_entry_dir;
	break;
    default:
	ech = '?';
	etype = "";
	break;
    }

    if (!FB_DUMMYMODE) {
	*bp++ = ech;
	(void) strcpy(bp, perms_list[(mode>>6) & 07]);
	bp += 3;
	(void) strcpy(bp, perms_list[(mode>>3) & 07]);
	bp += 3;
	(void) strcpy(bp, perms_list[ mode     & 07]);
	bp += 3;
    }

    *bp++ = ' ';
    (void) sprintf(bp, "%-4s", etype);
    bp += 4;

    if (!FB_DUMMYMODE) {
	*bp++ = ' ';
	if ((pw = fast_getpwuid(dl->entry[n].uid)) != NULL)
	    (void) sprintf(bp, "%-8.8s", pw->pw_name);
	else
	    (void) sprintf(bp, "%-8d", dl->entry[n].uid);
	bp += 8;
    }

    *bp++ = ' ';
    sprintf(bp, "%8lu", (unsigned long)dl->entry[n].size);
    bp += 8;

    *bp++ = ' ';
    *bp++ = ' ';
    bp += strftime(bp, sizeof(out_buf)-(bp-out_buf),
		fbstr_entry_date_fmt, localtime(&dl->entry[n].mtime));

    *bp++ = ' ';
    *bp++ = ' ';
    w = COLS - ((bp-out_buf)+2);
    np = dl->entry[n].name;
    if (IS_DOT(np))
	np = fbstr_entry_current_dir;
    else if (IS_DOTDOT(np))
	np = fbstr_entry_parrent_dir;
    (void) sprintf(bp, "%-*.*s", w, w, np);

    MoveCursor(line, 0);
    if (selected && !arrow_cursor)
	StartStandout();
    PutLine0(-1, -1, out_buf);
    if (selected && !arrow_cursor)
	EndStandout();
}


static void fb_disp_instr(instr_normal, instr_dummy1, instr_dummy2, do_erase)
const char *instr_normal, *instr_dummy1, *instr_dummy2;
int do_erase;
{
    if (FB_DUMMYMODE) {
	if (do_erase)
	    ClearLine(FBLINE_INSTRUCT-1);
	if (instr_dummy1 != NULL && *instr_dummy1)
	    CenterLine(FBLINE_INSTRUCT-1, instr_dummy1);
	if (do_erase)
	    ClearLine(FBLINE_INSTRUCT);
	if (instr_dummy2 != NULL && *instr_dummy2)
	    CenterLine(FBLINE_INSTRUCT, instr_dummy2);
    } else {
	if (do_erase)
	    ClearLine(FBLINE_INSTRUCT);
	if (instr_normal != NULL && *instr_normal)
	    CenterLine(FBLINE_INSTRUCT, instr_normal);
    }
}


static FB_DIR *fb_start_dir(sel_dir, sel_pat, is_rescan)
const char *sel_dir, *sel_pat;
int is_rescan;
{
    FB_DIR *dl;
    char pname[SLEN], *np;
    int nread, nsel, plen, i;
    DIR *dirp;
    struct DIRENT *dirent;
    struct stat sbuf;

    if (!safe_mkpath(pname, sel_dir, "", sizeof(pname)-32)) {
	/* don't even bother if the path buffer is close to overflowing */
	return (FB_DIR *)NULL;
    }
    plen = strlen(pname);
    np = pname+plen;

    if ((dirp = opendir(sel_dir)) == NULL) {
	error2(S_(FbrowserEnterdirCannotRead,
		    "Cannot read \"%s\".  [%s]"), sel_dir, strerror(errno));
	return (FB_DIR *)NULL;
    }

    dl = (FB_DIR *) safe_malloc(sizeof(FB_DIR));
    dl->entry = (struct fb_entryinfo *)
		safe_malloc(FB_CHUNK*sizeof(struct fb_entryinfo));
    dl->num_entries = 0;
    dl->alloc_entries = FB_CHUNK;

    nread = nsel = 0;
    error1(S_(FbrowserEnterdirScanning, "Scanning %s ..."), sel_dir);
    FlushOutput();

    while ((dirent = readdir(dirp)) != NULL) {

	++nread;
	/* FOO - this causes files to disappear mysteriously from the
	 * listing.  Maybe a warning of some sort? */
	(void) strfcpy(np, dirent->d_name, sizeof(pname)-plen);
	if (stat(pname, &sbuf) < 0)
	    continue;

	/* see if we want to accept this entry */
	switch (sbuf.st_mode & S_IFMT) {
	case S_IFDIR:
	    /*
	     * Always accept "." and "..".
	     * Accept any other dirs subject to dot_entries qualification.
	     */
	    if (np[0] == '.' && !fb_show_dotfiles
			&& !IS_DOT(np) && !IS_DOTDOT(np))
		continue;
	    break;
	case S_IFREG:
	    /*
	     * Accept this file it matches the pattern, subject to
	     * dot_entries qualification.
	     */
	    if (np[0] == '.' && !fb_show_dotfiles && sel_pat[0] != '.')
		continue;
	    if (!patmatch(sel_pat, np, PM_FANCHOR|PM_BANCHOR))
		continue;
	    break;
	default:
	    /*
	     * Reject anything other than regular files and directories.
	     */
	    continue;
	}

	/* add this entry to the list */
	if (dl->num_entries >= dl->alloc_entries) {
	    dl->alloc_entries += FB_CHUNK;
	    dl->entry = (struct fb_entryinfo *)
			safe_realloc( (malloc_t)dl->entry,
			dl->alloc_entries*sizeof(struct fb_entryinfo));
	}
	dl->entry[dl->num_entries].name = safe_strdup(np);
	dl->entry[dl->num_entries].mode = sbuf.st_mode;
	dl->entry[dl->num_entries].uid = sbuf.st_uid;
	dl->entry[dl->num_entries].size = sbuf.st_size;
	dl->entry[dl->num_entries].mtime = sbuf.st_mtime;
	++dl->num_entries;
	++nsel;

    }

    closedir(dirp);

    /* sort the list - leaving "." and ".." at the top */
    qsort((malloc_t)(dl->entry+2), dl->num_entries-2,
		sizeof(struct fb_entryinfo),
#ifdef ANSI_C
	    /*
	     * Some ANSI compilers get bent out of shape that the
	     * sort procedures use (malloc_t) instead of (void *).
	     * This cast avoids a spurious complaint.
	     */
		(int(*)(const void *, const void *))
#endif
		fb_getsortproc());

    np = (is_rescan
		? S_(FbrowserEnterdirRescan, "Rescan")
		: S_(FbrowserEnterdirScan, "Scan"));
    if (nsel == nread) {
	sprintf(pname, S_(FbrowserEnterdirSelectedAll,
		    "%s complete - selected %d entries."), np, nsel);
    } else {
	sprintf(pname, S_(FbrowserEnterdirSelectedEntries,
		    "%s complete - selected %d of %d entries."),
		    np, nsel, nread);
    }
    set_error(pname);
    return dl;
}


static void fb_finish_dir(dl)
FB_DIR *dl;
{
    int i;
    for (i = 0 ; i < dl->num_entries ; ++i)
	free((malloc_t)dl->entry[i].name);
    free((malloc_t)dl->entry);
    free((malloc_t)dl);
}


static int (*fb_getsortproc()) P_((const malloc_t, const malloc_t))
{
    switch (fb_sortby & FBSORT_TYPE_MASK) {
    case FBSORT_TYPE_NAME:
	return (!(fb_sortby & FBSORT_OPT_REVERSE)
		    ? fbcmp_name_ascending : fbcmp_name_descending);
    case FBSORT_TYPE_SIZE:
	return (!(fb_sortby & FBSORT_OPT_REVERSE)
		    ? fbcmp_size_ascending : fbcmp_size_descending);
    case FBSORT_TYPE_MTIME:
	return (!(fb_sortby & FBSORT_OPT_REVERSE)
		    ? fbcmp_mtime_ascending : fbcmp_mtime_descending);
    }
    return fbcmp_name_ascending;
}

static char *fb_getsorttype()
{
    static char sbuf[32];
    char *s;

    if (fb_sortby & FBSORT_OPT_REVERSE) {
	strcpy(sbuf, S_(FbrowserSorttypeReverse, "Reverse"));
	s = sbuf + strlen(sbuf);
	*s++ = ' ';
    } else {
	s = sbuf;
    }
    switch (fb_sortby & FBSORT_TYPE_MASK) {
    	case FBSORT_TYPE_NAME:
	    (void) strcpy(s, S_(FbrowserSorttypeName, "Name"));
	    break;
    	case FBSORT_TYPE_SIZE:
	    (void) strcpy(s, S_(FbrowserSorttypeSize, "Size"));
	    break;
    	case FBSORT_TYPE_MTIME:
	    (void) strcpy(s, S_(FbrowserSorttypeDate, "Date"));
	    break;
    	default:
	    (void) strcpy(s, S_(FbrowserSorttypeUnknown, "?Unknown?"));
	    break;
    }
    return sbuf;
}


static int fbcmp_name_ascending(p1, p2)
const malloc_t p1, p2;
{
    const struct fb_entryinfo *e1, *e2;
    e1 = (struct fb_entryinfo *) p1;
    e2 = (struct fb_entryinfo *) p2;
    return strcmp(e1->name, e2->name);
}

static int fbcmp_name_descending(p1, p2)
const malloc_t p1, p2;
{
    const struct fb_entryinfo *e1, *e2;
    e1 = (struct fb_entryinfo *) p1;
    e2 = (struct fb_entryinfo *) p2;
    return -strcmp(e1->name, e2->name);
}

static int fbcmp_size_ascending(p1, p2)
const malloc_t p1, p2;
{
    const struct fb_entryinfo *e1, *e2;
    e1 = (struct fb_entryinfo *) p1;
    e2 = (struct fb_entryinfo *) p2;
    return (int) (e1->size - e2->size);
}

static int fbcmp_size_descending(p1, p2)
const malloc_t p1, p2;
{
    const struct fb_entryinfo *e1, *e2;
    e1 = (struct fb_entryinfo *) p1;
    e2 = (struct fb_entryinfo *) p2;
    return (int) (e2->size - e1->size);
}

static int fbcmp_mtime_ascending(p1, p2)
const malloc_t p1, p2;
{
    const struct fb_entryinfo *e1, *e2;
    e1 = (struct fb_entryinfo *) p1;
    e2 = (struct fb_entryinfo *) p2;
    return (int) (e1->mtime - e2->mtime);
}

static int fbcmp_mtime_descending(p1, p2)
const malloc_t p1, p2;
{
    const struct fb_entryinfo *e1, *e2;
    e1 = (struct fb_entryinfo *) p1;
    e2 = (struct fb_entryinfo *) p2;
    return (int) (e2->mtime - e1->mtime);
}


static int fb_mbox_check(fname, options)
char *fname;
{
    char buf[SLEN];
    int ok;
    FILE *fp;
    struct stat sbuf;

    if (options & FB_READ)
	options |= FB_EXIST;	/* readable implies it exists */

    if (stat(fname, &sbuf) < 0) {
	if (options & FB_EXIST) {
	    error2(S_(FbrowserMboxCannotGetStatus,
			"Cannot get \"%s\" status.  [%s]"),
			fname, strerror(errno));
	    return FALSE;
	}
	return TRUE;
    }

    if (!S_ISREG(sbuf.st_mode)) {
	error1(S_(FbrowserMboxNotRegularFile,
		    "\"%s\" is not a regular file."), fname);
	return FALSE;
    }

    if (options & FB_READ) {
	if (sbuf.st_uid == geteuid())
	    ok = !!(sbuf.st_mode & 0400);
	else if (sbuf.st_gid == getgid())
	    ok = !!(sbuf.st_mode & 0040);
	else
	    ok = !!(sbuf.st_mode & 0004);
	if (!ok) {
	    error1(S_(FbrowserMboxNoPermissionRead,
			"No permission to read \"%s\"."), fname);
	    return FALSE;
	}
    }

    if (options & FB_WRITE) {
	if (sbuf.st_uid == geteuid())
	    ok = !!(sbuf.st_mode & 0200);
	else if (sbuf.st_gid == getgid())
	    ok = !!(sbuf.st_mode & 0020);
	else
	    ok = !!(sbuf.st_mode & 0002);
	if (!ok) {
	    error1(S_(FbrowserMboxNoPermissionWrite,
			"No permission to write \"%s\"."), fname);
	    return FALSE;
	}
    }

    if (options & FB_MBOX) {
	if ((fp = file_open(fname, "r")) == NULL)
	    return FALSE;
	while (fgets(buf, sizeof(buf), fp) != NULL && buf[0] == '\n')
	    ;
	if (file_close(fp, fname) < 0)
	    return FALSE;
#ifdef MMDF
	ok = (strcmp(buf, MSG_SEPARATOR) == 0);
#else
	ok = strbegConst(buf, "From ");
#endif
	if (!ok) {
	    error1(S_(FbrowserNotValidMailbox,
			"\"%s\" is not a valid mailbox."), fname);
	    return FALSE;
	}
    }

    return TRUE;
}


static int safe_copy(dst, src, dstsiz)
char *dst;
const char *src;
int dstsiz;
{
    if (dstsiz > 0) {
	/* strncpy() is supposed to zero-fill the target, Xenix is broke */
	dst[dstsiz-1] = '\0';
	(void) strncpy(dst, src, dstsiz);
	if (dst[dstsiz-1] == '\0')
	    return TRUE;
    }
    error(S_(FbrowserSafeCopyOverflow,
      "Internal Error - buffer not large enough to hold return result."));
    return FALSE;
}


static int safe_mkpath(pbuf, dname, fname, pbufsiz)
char *pbuf;
const char *dname, *fname;
int pbufsiz;
{
    int len;
    if (dname != NULL && !safe_copy(pbuf, dname, pbufsiz))
	return FALSE;
    len = strlen(pbuf);
    if (pbuf[0] != '/' || pbuf[1] != '\0') {
	if (!safe_copy(pbuf+len, "/", pbufsiz-len))
	    return FALSE;
	++len;
    }
    return safe_copy(pbuf+len, fname, pbufsiz-len);
}

