/* 
 * This software is provided with no warranty  whatsoever. You may use, modify
 * and redistribute it as long as you give a credit to the original author and
 * provide a link to the original source.
 *
 * $Id: xfind.c,v 1.18 2026/04/29 15:41:39 alx Exp $
 */

#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <pwd.h>
#include <grp.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <ctype.h>
#include <stdarg.h>
#include <dirent.h>
#include <errno.h>
#include <unistd.h>
#include <fnmatch.h>
#include <Xm/Xm.h>
#include <Xm/MainW.h>
#include <Xm/RowColumn.h>
#include <Xm/CascadeBG.h>
#include <Xm/PushBG.h>
#include <Xm/PushB.h>
#include <Xm/ToggleBG.h>
#include <Xm/SeparatoG.h>
#include <Xm/List.h>
#include <Xm/TextF.h>
#include <Xm/Form.h>
#include <Xm/Label.h>
#include <Xm/LabelG.h>
#include <Xm/DialogS.h>
#include <Xm/MessageB.h>
#include <Xm/FileSB.h>
#include <Xm/SelectioB.h>
#include "util.h"

#define APP_TITLE	"XFind"
#define APP_NAME    "xfind"
#define APP_CLASS   "XFind"
#define APP_VERSION 1
#define APP_REVISION 0
#define APP_PATCHLVL 5

#include "xfind.xbm"
#include "xfind_m.xbm"

/* Initial and grow-by array sizes */
#define STACK_GROWBY 128
#define RESULTS_GROWBY 256

#define XFILE_ATOM_NAME "XFile_FileList1"

#define FILES_PER_CYCLE 16

/* Find/Stop button labels */
#define CS_RUN "Find"
#define CS_STOP "Stop"

/* Time format */
#define TIME_FMT "%d %b %Y"
#define TIME_BUFSIZ 32

/* set_gui_state state constants */
#define GS_IDLE 0
#define GS_BUSY 1

/* Tabs in results list */
#define TAB_WIDTH 2

struct app_res {
	String open_cmd;
	String show_location_cmd;
	String sort_by;
	Boolean ascending;
	Boolean recursive;
	Boolean match_dirs;
	Boolean follow_symlinks;
	Boolean follow_mntpts;
	Boolean default_is_open;
	Pixel wm_icon_color;
} app_res;

struct name_stack {
	char **names;
	unsigned int nnames;
	unsigned int nslots;
};

struct result_rec {
	char *title;
	char *path;
	struct stat st;
};

enum list_cols {
	LC_TITLE,
	LC_MODE,
	LC_OWNER,
	LC_SIZE,
	LC_TIME,
	N_LIST_COLS
};

enum sort_order {
	SO_NAME,
	SO_SUFFIX,
	SO_TIME,
	SO_SIZE
};

int result_compare(const void*, const void*);
void add_result(const char*, const char*, struct stat*);
void build_menu(void);
void update_menu(unsigned int);
Widget add_menu_item(Widget, WidgetClass, const char*,
	const char*, KeySym, Widget, XtCallbackProc);
Widget get_menu_item(const char*);
Boolean find_wp(XtPointer);
void ns_init(struct name_stack *);
void ns_clear(struct name_stack*);
int ns_push(struct name_stack*, const char*);
int ns_contains(struct name_stack*, const char*);
char* ns_pop(struct name_stack*);
void expose_results();
void set_gui_state(int);
void set_status(char*, ...);
void pass_to_input_dlg(void);
void dir_select_dlg(void);
void pass_to_input_cb(Widget,XtPointer,XtPointer);
void dir_selected_cb(Widget,XtPointer,XtPointer);
void browse_cb(Widget,XtPointer,XtPointer);
Boolean message_dialog(Boolean, const char*, const char*, ...);
void message_dialog_cb(Widget,XtPointer,XtPointer);
XmString make_label(const char*, XmStringTag);
Dimension get_average_char_width(XmRenderTable, XmStringTag);
void load_icon(const void*, const void*,
	unsigned int, unsigned int, Pixmap*, Pixmap*);
void grab_primary(Boolean);
void lose_selection_proc(Widget, Atom*);
Boolean convert_selection_proc(Widget,
	Atom*, Atom*, Atom*, XtPointer*, unsigned long*, int*);
void mbs_to_latin1(const char*, char*);
void info_dialog(const char*, const char*);
void open_cb(Widget,XtPointer,XtPointer);
void pass_to_cb(Widget,XtPointer,XtPointer);
void set_location_cb(Widget,XtPointer,XtPointer);
void show_location_cb(Widget,XtPointer,XtPointer);
void sort_ascending_cb(Widget,XtPointer,XtPointer);
void sort_by_name_cb(Widget,XtPointer,XtPointer);
void sort_by_time_cb(Widget,XtPointer,XtPointer);
void sort_by_size_cb(Widget,XtPointer,XtPointer);
void sort_by_suffix_cb(Widget,XtPointer,XtPointer);
void follow_symlinks_cb(Widget,XtPointer,XtPointer);
void match_dirs_cb(Widget,XtPointer,XtPointer);
void recursive_cb(Widget,XtPointer,XtPointer);
void follow_mntpts_cb(Widget,XtPointer,XtPointer);
void exit_cb(Widget,XtPointer,XtPointer);
void run_stop_cb(Widget,XtPointer,XtPointer);
void clear_cb(Widget,XtPointer,XtPointer);
void list_action_cb(Widget,XtPointer,XtPointer);
void list_sel_change_cb(Widget,XtPointer,XtPointer);

Display *display;
XtAppContext context;
Widget wshell;
Widget wmain;
Widget wmenu;
Widget wpath;
Widget wglob;
Widget wbrowse;
Widget wlist;
Widget wquit;
Widget wstat;
Widget wrunstop;
Widget wreset;
XmRenderTable list_rt;
XtWorkProcId wpid = None;

/* Options */
Boolean ascending = False;
Boolean match_dirs = False;
Boolean recursive = True;
Boolean follow_symlinks = False;
Boolean follow_mntpts = False;
enum sort_order sort_by = SO_NAME;

/* Search procedure state variables */
struct stat wd_stat;
struct name_stack dir_stack = { NULL, 0, 0 };
struct name_stack his_stack = { NULL, 0, 0 };
DIR *cur_dir = NULL;
char *cur_path = NULL;
char *glob_pattern = NULL;
int dir_err_count = 0;

/* Results storage */
struct result_rec *results = NULL;
size_t nresults = 0;
size_t results_size = 0;

/* List item parameters */
Dimension col_widths[N_LIST_COLS] = { 0 };
Dimension avg_char_width;

int main(int argc, char **argv)
{
	Arg args[10];
	Cardinal n;
	XmString xms;
	Widget wform;
	Widget wlabel;
	Widget wtmp;
	Pixmap icon, icon_mask;
	char tmp_name[10];
	XtCallbackRec action_cbr[2] = {NULL};
	XtCallbackRec select_cbr[2] = {NULL};
	char *init_path = NULL;
	char *pattern = NULL;
	int i;
	
	String fallback_res[] = {
		"XFind.openCommand: xfile-open",
		"XFind.showLocationCommand: xfile",
		"XFind.title: Find Files",
		"XFind.iconName: Find Files",
		"*criteria.XmTextField.marginHeight: 3",
		"*criteria.XmTextField.marginWidth: 4",
		"*criteria.pattern.columns: 40",
		"*criteria.XmPushButton.marginHeight: 3",
		"*criteria.XmPushButton.marginWidth: 10",
		"*results.visibleItemCount: 12",
		"*results.listMarginHeight: 2",
		"*results.listMarginWidth: 2",
		"*results.listSpacing: 1",
		NULL
	};
		
	
	XrmOptionDescRec options[] = {
		{ "-R", "recursive", XrmoptionNoArg, "True" },
		{ "+R", "recursive", XrmoptionNoArg, "False" },
		{ "-r", "sortAscending", XrmoptionNoArg, "True" },
		{ "+r", "sortAscending", XrmoptionNoArg, "False" },
		{ "-sn", "sortBy", XrmoptionNoArg, "name" },
		{ "-ss", "sortBy", XrmoptionNoArg, "size" },
		{ "-st", "sortBy", XrmoptionNoArg, "time" },
		{ "-sx", "sortBy", XrmoptionNoArg, "suffix" }
	};
	
	XtResource xrdb_list[] = {
		{ "recursive", "Recursive",
			XtRBoolean, sizeof(Boolean),
			XtOffset(struct app_res*, recursive),
			XmRImmediate, (void*) True
		},
		{ "followSymlinks", "FollowSymlinks",
			XtRBoolean, sizeof(Boolean),
			XtOffset(struct app_res*, follow_symlinks),
			XmRImmediate, (void*) True
		},
		{ "followMountPoints", "FollowMountPoints",
			XtRBoolean, sizeof(Boolean),
			XtOffset(struct app_res*, follow_mntpts),
			XmRImmediate, (void*) True
		},
		{ "matchDirectories", "MatchDirectories",
			XtRBoolean, sizeof(Boolean),
			XtOffset(struct app_res*, match_dirs),
			XmRImmediate, (void*) False
		},
		{ "sortBy", "SortBy",
			XmRString, sizeof(String),
			XtOffset(struct app_res*, sort_by),
			XtRImmediate, (void*) "name"
		},
		{ "sortAscending", "SortAscending",
			XtRBoolean, sizeof(Boolean),
			XtOffset(struct app_res*, ascending),
			XmRImmediate, (void*) False
		},
		{ "defaultActionIsOpen", "DefaultActionIsOpen",
			XtRBoolean, sizeof(Boolean),
			XtOffset(struct app_res*, default_is_open),
			XmRImmediate, (void*) True
		},
		{ "openCommand", "OpenCommand",
			XmRString, sizeof(String),
			XtOffset(struct app_res*, open_cmd),
			XtRImmediate, (void*) "xfile-open"
		},
		{ "showLocationCommand", "ShowLocationCommand",
			XmRString, sizeof(String),
			XtOffset(struct app_res*, show_location_cmd),
			XtRImmediate, (void*) "xfile"
		},
		{ "iconColor", "IconColor",
			XtRPixel, sizeof(Pixel),
			XtOffset(struct app_res*, wm_icon_color),
			XmRString, (void*) "wheat"
		},
	};
	
	n = 0;
	wshell = XtAppInitialize(&context, APP_CLASS, options,
		XtNumber(options), &argc, argv, fallback_res, args, n);
	
	for(i = 1; i < argc; i++) {
		if(!strcmp(argv[i], "-p")) {
			i++;
			if(i == argc) {
				fprintf(stderr, "Missing parameter for -p");
				break;
			} else {
				init_path = argv[i];
			}
		} else if(!pattern) {
			pattern = argv[i];
		} else {
			fprintf(stderr,
				"Ignoring redundant argument \'%s\'\n", argv[i]);
		}
	}
	
	if(!init_path) {
		if(! (init_path = get_working_dir())) {
			if(! (init_path = getenv("HOME"))) {
				init_path = "/";
			}
		}
	}
	
	display = XtDisplay(wshell);
	
	XtGetApplicationResources(wshell, &app_res, xrdb_list,
		XtNumber(xrdb_list), NULL, 0);
	
	load_icon(xfind_bits, xfind_m_bits,
		xfind_width, xfind_height, &icon, &icon_mask);
	n = 0;
	XtSetArg(args[n], XmNiconPixmap, icon); n++;
	XtSetArg(args[n], XmNiconMask, icon_mask); n++;
	XtSetValues(wshell, args, n);
	
	wmain = XmCreateMainWindow(wshell, "main", args, 0);
	
	build_menu();
	
	/* the search form */
	n = 0;
	XtSetArg(args[n], XmNhorizontalSpacing, 4); n++;
	XtSetArg(args[n], XmNverticalSpacing, 4); n++;
	XtSetArg(args[n], XmNmarginWidth, 4); n++;
	XtSetArg(args[n], XmNmarginHeight, 4); n++;
	wform = XmCreateForm(wmain, "criteria", args, n);
	
	n = 0;
	xms = XmStringCreateLocalized("Search In");
	XtSetArg(args[n], XmNlabelString, xms); n++;
	XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
	XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
	wlabel = XmCreateLabelGadget(wform, "pathLabel", args, n);
	XtManageChild(wlabel);

	n = 0;
	xms = XmStringCreateLocalized("...");
	action_cbr[0].callback = set_location_cb;
	XtSetArg(args[n], XmNactivateCallback, &action_cbr); n++;
	XtSetArg(args[n], XmNlabelString, xms); n++;
	XtSetArg(args[n], XmNtopAttachment, XmATTACH_WIDGET); n++;
	XtSetArg(args[n], XmNtopWidget, wlabel); n++;
	XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
	wbrowse = XmCreatePushButton(wform, "browse", args, n);
	XmStringFree(xms);
	
	n = 0;
	XtSetArg(args[n], XmNtopOffset, 0); n++;
	XtSetArg(args[n], XmNtopAttachment, XmATTACH_WIDGET); n++;
	XtSetArg(args[n], XmNtopWidget, wlabel); n++;
	XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
	XtSetArg(args[n], XmNrightAttachment, XmATTACH_WIDGET); n++;
	XtSetArg(args[n], XmNrightWidget, wbrowse); n++;
	XtSetArg(args[n], XmNvalue, init_path); n++;
	wpath = XmCreateTextField(wform, "path", args, n);
	/* nail browse button's edges to wpath's, so they'll align horizontally */
	n = 0;
	XtSetArg(args[n], XmNtopOffset, 0); n++;
	XtSetArg(args[n], XmNbottomOffset, 0); n++;
	XtSetArg(args[n], XmNtopAttachment, XmATTACH_OPPOSITE_WIDGET); n++;
	XtSetArg(args[n], XmNtopWidget, wpath); n++;
	XtSetArg(args[n], XmNbottomAttachment, XmATTACH_OPPOSITE_WIDGET); n++;
	XtSetArg(args[n], XmNbottomWidget, wpath); n++;
	XtSetValues(wbrowse, args, n);
	XtManageChild(wbrowse);
	XtManageChild(wpath);

	n = 0;
	xms = XmStringCreateLocalized("Name Pattern");
	XtSetArg(args[n], XmNlabelString, xms); n++;
	XtSetArg(args[n], XmNtopOffset, 4); n++;
	XtSetArg(args[n], XmNtopAttachment, XmATTACH_WIDGET); n++;
	XtSetArg(args[n], XmNtopWidget, wpath); n++;
	XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
	wlabel = XmCreateLabelGadget(wform, "pathLabel", args, n);
	XtManageChild(wlabel);

	n = 0;
	XtSetArg(args[n], XmNtopOffset, 0); n++;
	XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
	XtSetArg(args[n], XmNtopAttachment, XmATTACH_WIDGET); n++;
	XtSetArg(args[n], XmNtopWidget, wlabel); n++;
	XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
	if(pattern) {
		XtSetArg(args[n], XmNvalue, pattern); n++;
	}
	wglob = XmCreateTextField(wform, "pattern", args, n);
	XtManageChild(wglob);

	n = 0;
	xms = XmStringCreateLocalized(CS_RUN);
	action_cbr[0].callback = run_stop_cb;
	XtSetArg(args[n], XmNlabelString, xms); n++;
	XtSetArg(args[n], XmNtopAttachment, XmATTACH_WIDGET); n++;
	XtSetArg(args[n], XmNtopWidget, wglob); n++;
	XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
	XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
	XtSetArg(args[n], XmNactivateCallback, &action_cbr); n++;
	wrunstop = XmCreatePushButton(wform, "find", args, n);
	XmStringFree(xms);

	n = 0;
	xms = XmStringCreateLocalized("Clear");
	action_cbr[0].callback = clear_cb;
	XtSetArg(args[n], XmNlabelString, xms); n++;
	XtSetArg(args[n], XmNtopAttachment, XmATTACH_WIDGET); n++;
	XtSetArg(args[n], XmNtopWidget, wglob); n++;
	XtSetArg(args[n], XmNleftAttachment, XmATTACH_WIDGET); n++;
	XtSetArg(args[n], XmNleftWidget, wrunstop); n++;
	XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
	XtSetArg(args[n], XmNactivateCallback, &action_cbr); n++;
	wreset = XmCreatePushButton(wform, "clear", args, n);
	XmStringFree(xms);
	
	n = 0;
	XtSetArg(args[n], XmNinitialFocus, wglob); n++;
	XtSetArg(args[n], XmNdefaultButton, wrunstop); n++;
	XtSetValues(wform, args, n);
	XtSetArg(args[0], XmNdefaultButtonShadowThickness, 0); n++;
	XtSetValues(wbrowse, args, 1);	
	
	n = 0;
	select_cbr[0].callback = list_sel_change_cb;
	action_cbr[0].callback = list_action_cb;
	XtSetArg(args[n], XmNselectionPolicy, XmBROWSE_SELECT); n++;
	XtSetArg(args[n], XmNdefaultActionCallback, &action_cbr); n++;
	XtSetArg(args[n], XmNlistSizePolicy, XmCONSTANT); n++;
	XtSetArg(args[n], XmNprimaryOwnership, XmOWN_NEVER); n++;
	XtSetArg(args[n], XmNselectionPolicy, XmEXTENDED_SELECT); n++;
	XtSetArg(args[n], XmNextendedSelectionCallback, &select_cbr); n++;
	wlist = XmCreateScrolledList(wmain, "results", args, n);


	/* status widget */
	n = 0;
	xms = XmStringCreateLocalized("Ready");
	XtSetArg(args[n], XmNlabelString, xms); n++;
	XtSetArg(args[n], XmNalignment, XmALIGNMENT_BEGINNING); n++;
	wstat = XmCreateLabel(wmain, "status", args, n);
	XmStringFree(xms);

	/* main window areas */
	n = 0;
	XtSetArg(args[n], XmNmenuBar, wmenu); n++;
	XtSetArg(args[n], XmNcommandWindow, wform); n++;
	XtSetArg(args[n], XmNmessageWindow, wstat); n++;
	XtSetArg(args[n], XmNworkWindow, XtParent(wlist)); n++;
	XtSetValues(wmain, args, n);

	
	XtManageChild(wmenu);
	XtManageChild(wform);
	XtManageChild(wstat);
	XtManageChild(wlist);
	XtManageChild(wrunstop);
	XtManageChild(wreset);
	XtManageChild(wmain);
	
	/* Get default list render table we'll use for list
	 * strings with a custom tab list */
	XtSetArg(args[0], XmNrenderTable, &list_rt);
	XtGetValues(wlist, args, n);
	avg_char_width = get_average_char_width(list_rt, NULL);

	if(strcmp(app_res.sort_by, "name") &&
		strcmp(app_res.sort_by, "suffix") &&
		strcmp(app_res.sort_by, "time") &&
		strcmp(app_res.sort_by, "size")) {
		fprintf(stderr, "Invalid \'SortBy\' argument, using default.");
		app_res.sort_by = "name";
	}
	sprintf(tmp_name, "*%s", app_res.sort_by);
	wtmp = get_menu_item(tmp_name);
	XmToggleButtonGadgetSetState(wtmp, True, True);

	wtmp = get_menu_item("*descending");
	XmToggleButtonGadgetSetState(wtmp, True, True);
	wtmp = get_menu_item("*ascending");
	XmToggleButtonGadgetSetState(wtmp, app_res.ascending, True);

	wtmp = get_menu_item("*recursive");
	XmToggleButtonGadgetSetState(wtmp, app_res.recursive, True);

	wtmp = get_menu_item("*followSymlinks");
	XmToggleButtonGadgetSetState(wtmp, app_res.follow_symlinks, True);

	wtmp = get_menu_item("*followMountPoints");
	XmToggleButtonGadgetSetState(wtmp, app_res.follow_mntpts, True);

	wtmp = get_menu_item("*matchDirectories");
	XmToggleButtonGadgetSetState(wtmp, app_res.match_dirs, True);

	update_menu(0);

	XtRealizeWidget(wshell);
	XtAppMainLoop(context);
	
	return 0;
}

/* Sorts and puts everything in results array into the list widget */
void expose_results()
{
	Arg args[2];
	unsigned int i;
	size_t buf_len = 0;
	char *buffer = NULL;
	XmTab tabs[N_LIST_COLS];
	XmTabList tab_list;
	XmRenderTable nrt;
	XmRendition *ex_rend;
	XmStringTag *tags;
	XmString *entries;
	int ntags;

	if(!nresults) return;

	XmListDeleteAllItems(wlist);
	memset(col_widths, 0, sizeof(col_widths));
	update_menu(0);

	qsort(results, nresults, sizeof(struct result_rec), result_compare);
	
	entries = malloc(sizeof(XmString) * nresults);
	
	for(i = 0; i < nresults; i++) {
		size_t len;
		char size[SIZE_CS_MAX];
		char mode[MODE_CS_MAX];
		struct group *grp;
		struct passwd *pwd;
		char *user;
		char *group;
		char time[TIME_BUFSIZ];
		struct tm *file_tm;
		Dimension cch, ccw;
		char *rtag;
		XmString xms_tmp;
		
		get_size_string(results[i].st.st_size, size);
		get_mode_string(results[i].st.st_mode, mode);
		
		if( (pwd = getpwuid(results[i].st.st_uid)) ) {
			user = pwd->pw_name;		
		} else {
			static char tmp_user[16];
			snprintf(tmp_user, 16, "%d", results[i].st.st_uid);
			user = tmp_user;
		}
		
		if( (grp = getgrgid(results[i].st.st_gid)) ) {
			group = grp->gr_name;
		} else {
			static char tmp_group[16];		
			snprintf(tmp_group, 16, "%d", results[i].st.st_gid);
			group = tmp_group;
		}
		
		file_tm = localtime(&results[i].st.st_mtime);
		strftime(time, TIME_BUFSIZ, TIME_FMT, file_tm);
	
		len = snprintf(NULL, 0, "%s\t%s\t%s:%s\t%s\t%s\t%s",
			results[i].title, mode, user, group, size, time,
			results[i].path) + 2;
		
		if(len > buf_len) {
			char *new_ptr = realloc(buffer, len);
			if(!new_ptr) {
				perror("realloc");
				exit(EXIT_FAILURE);
			}
			buffer = new_ptr;
			buf_len = len;
		}
	
		snprintf(buffer, len, "%s%s\t%s\t%s:%s\t%s\t%s\t%s",
			results[i].title, (S_ISDIR(results[i].st.st_mode)) ? "/" : "",
			mode, user, group, size, time, results[i].path);

		if(S_ISDIR(results[i].st.st_mode))
			rtag = "directory";
		else if(S_ISLNK(results[i].st.st_mode))
			rtag = "symlink";
		else if(S_ISREG(results[i].st.st_mode))
			rtag = "regular";
		else
			rtag = "special";

		xms_tmp = XmStringCreate(results[i].title, rtag);
		XmStringExtent(list_rt, xms_tmp, &ccw, &cch);
		XmStringFree(xms_tmp);
		col_widths[LC_TITLE] = (col_widths[LC_TITLE] < ccw) ?
			ccw : col_widths[LC_TITLE];

		ccw = strlen(mode) * avg_char_width;
		col_widths[LC_MODE] = (col_widths[LC_MODE] < ccw) ?
			ccw : col_widths[LC_MODE];

		ccw = (strlen(user) + strlen(group)) * avg_char_width;
		col_widths[LC_OWNER] = (col_widths[LC_OWNER] < ccw) ?
			ccw : col_widths[LC_OWNER];

		ccw = strlen(size) * avg_char_width;
		col_widths[LC_SIZE] = (col_widths[LC_SIZE] < ccw) ?
			ccw : col_widths[LC_SIZE];

		ccw = strlen(time) * avg_char_width;
		col_widths[LC_TIME] = (col_widths[LC_TIME] < ccw) ?
			ccw : col_widths[LC_TIME];
		
		entries[i] = make_label(buffer, rtag);
	}
	free(buffer);
	
	XtSetArg(args[0], XmNitemCount, nresults);
	XtSetArg(args[1], XmNitems, entries);
	XtSetValues(wlist, args, 2);
	
	for(i = 0; i < nresults; i++)
		XmStringFree(entries[i]);
	
	free(entries);
	
	ntags = XmRenderTableGetTags(list_rt, &tags);
	ex_rend = XmRenderTableGetRenditions(list_rt, tags, ntags);
	for(i = 0; i < ntags; i++)
		XtFree(tags[i]);
	XtFree((char*)tags);
	
	for(i = 0; i < XtNumber(tabs); i++) {
		tabs[i] = XmTabCreate((float)col_widths[i] +
			avg_char_width * TAB_WIDTH, XmPIXELS,
			XmRELATIVE, XmALIGNMENT_BEGINNING, ".");
	}
	tab_list = XmTabListInsertTabs(NULL, tabs, XtNumber(tabs), 0);
	
	XtSetArg(args[0], XmNtabList, tab_list);
	XmRenditionUpdate(ex_rend[0], args, 1);
	nrt = XmRenderTableAddRenditions(NULL, ex_rend, ntags, XmMERGE_REPLACE);
	
	XtSetArg(args[0], XmNrenderTable, nrt);
	XtSetValues(wlist, args, 1);

	for(i = 0; i < XtNumber(tabs); i++)
		XmTabFree(tabs[i]);
	XmTabListFree(tab_list);

	for(i = 0; i < ntags; i++)
		XmRenditionFree(ex_rend[i]);
	
	list_rt = nrt;
}

/* 
 * The find work proc. We don't use multithreading, since the GUI does
 * basically nothing during the search, but we need to take care of X I/O,
 * which is done each FILES_PER_CYCLE.
 */ 
Boolean find_wp(XtPointer cdata)
{
	unsigned int i;
	static char *real_path;
	
	if(!cur_dir) {
		cur_path = ns_pop(&dir_stack);
		
		if(!cur_path) {
			expose_results();
			ns_clear(&his_stack);
			
			if(!dir_err_count)
				set_status("Finished with %d results", nresults);
			else
				set_status("Finished with %d results "
					"(%d not accessible locations)",
					nresults, dir_err_count);

			set_gui_state(GS_IDLE);
			dprintf("FINISHED\n");
			wpid = None;
			return True;
		}

		cur_dir = opendir(cur_path);
		
		if(cur_dir) {
			dprintf("entering: %s\n", cur_path);
			if( (real_path = realpath(cur_path, NULL)) ) {
				if(follow_symlinks) {
					/* make sure we haven't been here before when
					 * following symlinks */
					if(ns_contains(&his_stack, real_path)) {
						dprintf("skipping: %s\n", real_path);
						closedir(cur_dir);
						cur_dir = NULL;
						free(cur_path);
						cur_path = NULL;
						free(real_path);
						return False;
					}
					ns_push(&his_stack, real_path);
				}
				set_status("Searching %s", real_path);
			} else {
				closedir(cur_dir);
				cur_dir = NULL;
				free(cur_path);
				cur_path = NULL;
				dir_err_count++;
				return False;
			}
		} else {
			free(cur_path);
			dir_err_count++;
			return False;
		}
	}
	
	for(i = 0; i < FILES_PER_CYCLE; i++) {
		static size_t name_buf_len = 0;
		static char *name_buf = NULL;
		size_t len;
		struct stat st;
		struct dirent *ent;
		
		ent = readdir(cur_dir);
		if(!ent) {
			closedir(cur_dir);
			cur_dir = NULL;
			free(cur_path);
			cur_path = NULL;
			free(real_path);
			break;
		} else if(!strcmp(ent->d_name, "..") ||
			!strcmp(ent->d_name, ".")) continue;
		
		len = snprintf(NULL, 0, "%s/%s", real_path, ent->d_name) + 1;
		if(len > name_buf_len) {
			char *p = realloc(name_buf, len);
			if(!p) {
				perror("realloc");
				exit(EXIT_FAILURE);
			}
			name_buf = p;
			name_buf_len = len;
		}
		sprintf(name_buf, "%s/%s", real_path, ent->d_name);

		if(lstat(name_buf, &st) == -1) continue;

		if(S_ISLNK(st.st_mode)) {
			struct stat link_st;

			if(follow_symlinks && !stat(name_buf, &link_st) &&
				S_ISDIR(link_st.st_mode)) st = link_st;
		}
			
		if( S_ISDIR(st.st_mode) && (recursive || match_dirs) ) {
			if(recursive && (follow_mntpts || st.st_dev == wd_stat.st_dev) ) {
				ns_push(&dir_stack, name_buf);
			}

			if(!match_dirs)	continue;
		}
		
		if(!fnmatch(glob_pattern, ent->d_name, 0)) {
			add_result(ent->d_name, real_path, &st);
		}
	}
	return False;
}

/* Adds a file to the results array, reallocates the array if required */
void add_result(const char *title, const char *path, struct stat *st)
{
	if(nresults + 1 >= results_size) {
		void *p = realloc(results,
			(results_size + RESULTS_GROWBY) * sizeof(struct result_rec));
		if(!p) {
			perror("realloc");
			exit(EXIT_FAILURE);
		}
		results = (struct result_rec*)p;
		results_size += RESULTS_GROWBY;
	}
	results[nresults].title = strdup(title);
	results[nresults].path = strdup(path);
	results[nresults].st = *st;
	nresults++;
}

/* Clears all data within the results array */
void clear_results(void)
{
	size_t i;

	for(i = 0; i < nresults; i++) {
		free(results[i].title);
		free(results[i].path);
	}
	
	nresults = 0;
	
	XmListDeleteAllItems(wlist);
	grab_primary(False);
	update_menu(0);
}

/* Quicksort compare function for results */
int result_compare(const void *pa, const void *pb)
{
	struct result_rec *a = ((struct result_rec*)pa);
	struct result_rec *b = ((struct result_rec*)pb);
	int n = 0;
	
	n = S_ISDIR(a->st.st_mode) - S_ISDIR(b->st.st_mode);
	if(!n) {
		if(sort_by == SO_NAME) {
			n = strcmp(b->title, a->title);
		} if(sort_by == SO_SUFFIX) {
			char *sa = strrchr(a->title, '.');
			char *sb = strrchr(b->title, '.');
			if(sa && sb) {
				n = strcmp(sa, sb);
				if(n) n = strcmp(b->title, a->title);
			} else {
				n = sa ? 1 : -1;
			}
		} else if(sort_by == SO_SIZE) {
			n = a->st.st_size - b->st.st_size;
			if(!n) n = strcmp(b->title, a->title);
		} else if(sort_by == SO_TIME) {
			n = a->st.st_ctime - b->st.st_ctime;
			if(!n) n = a->st.st_mtime - b->st.st_mtime;
			if(!n) n = a->st.st_atime - b->st.st_atime;
			if(!n) n = strcmp(b->title, a->title);
		}
	}
	return ascending ? n : -n;	
}

/* Free all name stack's slots, but not the vector itself */
void ns_clear(struct name_stack *stk)
{
	unsigned int i;
	
	for( i = 0; i < stk->nnames; i++)
		free(stk->names[i]);
	
	stk->nnames = 0;
}

/* Makes a copy of given string and pushes it onto the stack */
int ns_push(struct name_stack *stk, const char *ptr)
{
	if((stk->nnames + 1 == stk->nslots) || !stk->nslots) {
		char** new_ptr = realloc(stk->names,
			sizeof(char*) * (stk->nnames + STACK_GROWBY));
		if(!new_ptr) return -1;
		
		stk->names = new_ptr;
		stk->nslots += STACK_GROWBY;
	}
	stk->names[stk->nnames] = strdup(ptr);
	if(!stk->names[stk->nnames]) return -1;
	
	stk->nnames++;
	return 0;
}

/* Removes from the stack and returns last string pushed
 * (NULL if notihing left). Caller is responsible for freeing it. */
char* ns_pop(struct name_stack *stk)
{
	if(stk->nnames) {
		stk->nnames--;
		return stk->names[stk->nnames];
	}
	return NULL;
}

/* Checks if the stack contains a name, returns 1 if so */
int ns_contains(struct name_stack *stk, const char *ptr)
{
	size_t i;
	
	for(i = 0; i < stk->nnames; i++) {
		if(!strcmp(ptr, stk->names[i])) return 1;
	}
	return 0;
}

void ns_init(struct name_stack *stk)
{
	memset(stk, 0, sizeof(struct name_stack));
}

void set_gui_state(int state)
{
	Arg args[10];
	XmString xms;

	if(state == GS_IDLE) {
		xms = XmStringCreateLocalized(CS_RUN);
		XtSetArg(args[0], XmNlabelString, xms);
		XtSetValues(wrunstop, args, 1);
		XmStringFree(xms);
		XtSetSensitive(wreset, True);
		XtSetSensitive(wpath, True);
		XtSetSensitive(wglob, True);
		XtSetSensitive(wbrowse, True);
	} else {
		xms = XmStringCreateLocalized(CS_STOP);
		XtSetArg(args[0], XmNlabelString, xms);
		XtSetValues(wrunstop, args, 1);
		XmStringFree(xms);
		XtSetSensitive(wreset, False);
		XtSetSensitive(wpath, False);
		XtSetSensitive(wglob, False);
		XtSetSensitive(wbrowse, False);
	}
}

void update_menu(unsigned int nsel_items)
{
	Widget w[3];
	int i;
	
	w[0] = get_menu_item("*open");
	w[1] = get_menu_item("*passTo");
	w[2] = get_menu_item("*showLocation");
	
	for(i = 0; i < XtNumber(w); i++)
		XtSetSensitive(w[i], (nsel_items == 1) ? True : False);
	
	XtSetSensitive(get_menu_item("*selectAll"), nresults ? True : False);
	XtSetSensitive(get_menu_item("*deselect"), nresults ? True : False);
}

/* printf like routine that sets status text */
void set_status(char *fmt, ...)
{
	Arg arg;
	XmString xms;
	char *buf;
	size_t len;
	va_list ap;

	va_start(ap, fmt);
	len = vsnprintf(NULL, 0, fmt, ap) + 1;
	va_end(ap);
	
	buf = malloc(len);

	va_start(ap, fmt);	
	vsnprintf(buf, len, fmt, ap);
	va_end(ap);

	xms = make_label(buf, NULL);
	free(buf);
	XtSetArg(arg, XmNlabelString, xms);
	XtSetValues(wstat, &arg, 1);
	XmStringFree(xms);
}

/* Starts/stops file search, updates the GUI to reflect state */
void run_stop_cb(Widget w, XtPointer pclient, XtPointer pcall)
{
	if(wpid) {
		XtRemoveWorkProc(wpid);
		wpid = None;
		
		if(cur_dir) closedir(cur_dir);
		cur_dir = NULL;
		
		ns_clear(&dir_stack);
		ns_clear(&his_stack);
		set_gui_state(GS_IDLE);
		expose_results();
		set_status("Stopped with %d results", nresults);

	} else {
		char *path;
		
		clear_results();
		
		dir_err_count = 0;
		if(glob_pattern) {
			XtFree(glob_pattern);
			glob_pattern = NULL;
		}
		
		glob_pattern = XmTextFieldGetString(wglob);
		if(!strlen(glob_pattern)) {
			message_dialog(False, APP_TITLE,
				"Specify a file name pattern to search for.");
			return;
		}
		path = XmTextFieldGetString(wpath);
		if(strlen(path) && !stat(path, &wd_stat) &&
			(wd_stat.st_mode & S_IRWXU)) {
			
			ns_push(&dir_stack, path);
			XtFree(path);
			wpid = XtAppAddWorkProc(context, find_wp, NULL);
			set_gui_state(GS_BUSY);
		} else {
			message_dialog(False, APP_TITLE,
				"Error reading path specified.\n"
				"Make sure the path is valid and the user has "
				"access permissions.");
		}
	}
}

/* Returns a fully qualified path name of a search result entry */
char *get_result_fqn(unsigned int i)
{
	size_t len;
	char *buffer;
	
	len = strlen(results[i].path) + strlen(results[i].title) + 2;
	buffer = malloc(len);
	
	if(!buffer) return NULL;
	
	sprintf(buffer, "%s/%s", results[i].path, results[i].title);
	return buffer;
}

/* Clears current search */
void clear_cb(Widget w, XtPointer pclient, XtPointer pcall)
{
	clear_results();
	set_status("Ready");
	XmTextFieldSetString(wglob, "");
	XmProcessTraversal(wglob, XmTRAVERSE_CURRENT);
}

/* Default (Enter/Double-click) results list action handler */
void list_action_cb(Widget w, XtPointer pclient, XtPointer pcall)
{
	int rv;
	XmListCallbackStruct *cbs = (XmListCallbackStruct*)pcall;
	char *args[1];
	char *cmd = (app_res.default_is_open ?
		app_res.open_cmd: app_res.show_location_cmd);
	
	args[0] = get_result_fqn(cbs->item_position - 1);
	
	if( (rv = spawn_command(cmd, args, 1)) ) {
		message_dialog(False, APP_NAME,
			"Error executing command \'%s\'.\n%s.",
			app_res.open_cmd, strerror(rv));
	}
}

void list_sel_change_cb(Widget w, XtPointer pclient, XtPointer pcall)
{
	XmListCallbackStruct *cbs = (XmListCallbackStruct*)pcall;
	update_menu(cbs->selected_item_count);
	
	grab_primary((cbs->selected_item_count) ? True : False);
}

/*
 * Menu callbacks
 */
void open_cb(Widget w, XtPointer pclient, XtPointer pcall)
{
	Arg args[2];
	int nsel;
	unsigned int *sel_pos;
	char *cmd_args[1];
	int rv;
	
	XtSetArg(args[0], XmNselectedPositionCount, &nsel);
	XtSetArg(args[1], XmNselectedPositions, &sel_pos);
	XtGetValues(wlist, args, 2);
	
	if(nsel != 1) return;
	
	cmd_args[0] = get_result_fqn(sel_pos[0] - 1);
	
	if( (rv = spawn_command(app_res.open_cmd, cmd_args, 1)) ) {
		message_dialog(False, APP_NAME,
			"Error executing command \'%s\'.\n%s.",
			app_res.open_cmd, strerror(rv));
	}

	free(cmd_args[0]);
}

void pass_to_cb(Widget w, XtPointer pclient, XtPointer pcall)
{
	Arg args[2];
	int nsel;
	unsigned int *sel_pos;
	
	XtSetArg(args[0], XmNselectedPositionCount, &nsel);
	XtSetArg(args[1], XmNselectedPositions, &sel_pos);
	XtGetValues(wlist, args, 2);
	
	if(nsel == 1) pass_to_input_dlg();
}

void show_location_cb(Widget w, XtPointer pclient, XtPointer pcall)
{
	Arg args[2];
	int nsel;
	unsigned int *sel_pos;
	char *cmd_args[1];
	int rv;
	
	XtSetArg(args[0], XmNselectedPositionCount, &nsel);
	XtSetArg(args[1], XmNselectedPositions, &sel_pos);
	XtGetValues(wlist, args, 2);
	
	if(nsel != 1) return;
	
	cmd_args[0] = results[sel_pos[0] - 1].path;
	
	if( (rv = spawn_command(app_res.show_location_cmd, cmd_args, 1)) ) {
		message_dialog(False, APP_NAME,
			"Error executing command \'%s\'.\n%s.",
			app_res.show_location_cmd, strerror(rv));
	}
}

void select_all_cb(Widget w, XtPointer pclient, XtPointer pcall)
{
	Arg args[2];
	unsigned int i, *sel_pos;
	
	if(!nresults) return;

	XmListDeselectAllItems(wlist);
	
	sel_pos = malloc(sizeof(unsigned int) * nresults);
	if(!sel_pos) return;
	
	for(i = 0; i < nresults; i++)
		sel_pos[i] = i + 1;

	XtSetArg(args[0], XmNselectedPositionCount, nresults);
	XtSetArg(args[1], XmNselectedPositions, sel_pos);
	XtSetValues(wlist, args, 2);
	
	free(sel_pos);
	grab_primary(True);
}

void deselect_cb(Widget w, XtPointer pclient, XtPointer pcall)
{
	XmListDeselectAllItems(wlist);
	grab_primary(False);
}

void set_location_cb(Widget w, XtPointer pclient, XtPointer pcall)
{
	dir_select_dlg();
}

void sort_ascending_cb(Widget w, XtPointer pclient, XtPointer pcall)
{
	XmToggleButtonCallbackStruct *cbs =
		(XmToggleButtonCallbackStruct*) pcall;
	ascending = cbs->set;
	expose_results();
}


void sort_by_name_cb(Widget w, XtPointer pclient, XtPointer pcall)
{
	XmToggleButtonCallbackStruct *cbs =
		(XmToggleButtonCallbackStruct*) pcall;

	if(cbs->set) sort_by = SO_NAME;
	expose_results();
}


void sort_by_time_cb(Widget w, XtPointer pclient, XtPointer pcall)
{
	XmToggleButtonCallbackStruct *cbs =
		(XmToggleButtonCallbackStruct*) pcall;

	if(cbs->set) sort_by = SO_TIME;
	expose_results();
}


void sort_by_size_cb(Widget w, XtPointer pclient, XtPointer pcall)
{
	XmToggleButtonCallbackStruct *cbs =
		(XmToggleButtonCallbackStruct*) pcall;

	if(cbs->set) sort_by = SO_SIZE;
	expose_results();
}


void sort_by_suffix_cb(Widget w, XtPointer pclient, XtPointer pcall)
{
	XmToggleButtonCallbackStruct *cbs =
		(XmToggleButtonCallbackStruct*) pcall;
	if(cbs->set) sort_by = SO_SUFFIX;
	expose_results();
}


void follow_symlinks_cb(Widget w, XtPointer pclient, XtPointer pcall)
{
	XmToggleButtonCallbackStruct *cbs =
		(XmToggleButtonCallbackStruct*) pcall;
	follow_symlinks = cbs->set;
}


void match_dirs_cb(Widget w, XtPointer pclient, XtPointer pcall)
{
	XmToggleButtonCallbackStruct *cbs =
		(XmToggleButtonCallbackStruct*) pcall;
	match_dirs = cbs->set;
}


void recursive_cb(Widget w, XtPointer pclient, XtPointer pcall)
{
	XmToggleButtonCallbackStruct *cbs =
		(XmToggleButtonCallbackStruct*) pcall;
	recursive = cbs->set;
}


void follow_mntpts_cb(Widget w, XtPointer pclient, XtPointer pcall)
{
	XmToggleButtonCallbackStruct *cbs =
		(XmToggleButtonCallbackStruct*) pcall;
	follow_mntpts = cbs->set;
}

void exit_cb(Widget w, XtPointer pclient, XtPointer pcall)
{
	XtAppSetExitFlag(context);
}

void about_cb(Widget w, XtPointer pclient, XtPointer pcall)
{
	char *info_sz;
	size_t len;
	char format_sz[] = "%s - Version %d.%d.%d\n\n%s";
	char sz_copy[] =
		"This software is provided with no warranty  whatsoever.\n"
		"You may use, modify and redistribute it as long as you\n"
		"give a credit to the original author and provide a link\n"
		"to the original source.\n\n"
		"http://fastestcode.org/xfile.html alx@fastestcode.org";
	
	len = snprintf(NULL, 0, format_sz,
		APP_TITLE, APP_VERSION, APP_REVISION, APP_PATCHLVL, sz_copy) + 1;
	info_sz = malloc(len);
	snprintf(info_sz, len, format_sz,
		APP_TITLE, APP_VERSION, APP_REVISION, APP_PATCHLVL, sz_copy);
	
	info_dialog("About...", info_sz);
	
	free(info_sz);
}

Widget get_menu_item(const char *name)
{
	Widget w = XtNameToWidget(wmenu, name);
	if(!w) fprintf(stderr, "Invalid menu widget name: %s\n", name);
	
	return w;
}

/* Adds a widget to a bar or popup menu. If class specified is a cascade
 * button, sub may point to a popup to set subMenuId for it */
Widget add_menu_item(Widget menu, WidgetClass class, const char *name,
	const char *title, KeySym mnemonic, Widget sub, XtCallbackProc cb_proc)
{
	Arg args[4];
	unsigned int i = 0;
	XmString xms;
	Widget w;
	XtCallbackRec cbr[] = {
		{ cb_proc, NULL }, { NULL, NULL }
	};
	
	if(title) {
		xms = XmStringCreateLocalized((char*)title);
		XtSetArg(args[i], XmNlabelString, xms);
		i++;
		XtSetArg(args[i], XmNmnemonic, mnemonic);
		i++;
	}
	if(cb_proc) {
		if(class == xmToggleButtonGadgetClass)
			XtSetArg(args[i], XmNvalueChangedCallback, &cbr);
		else
			XtSetArg(args[i], XmNactivateCallback, &cbr);
		i++;
	}
	if(sub) {
		XtSetArg(args[i], XmNsubMenuId, sub);
		i++;
	}
	w = XtCreateWidget(name, class, menu, args, i);
	XtManageChild(w);
	if(title) XmStringFree(xms);
	
	return w;
}

/* Creates the main menu */
void build_menu(void)
{
	Arg args[2];
	Widget wpd;
	Widget wsub;
	
	wmenu = XmCreateMenuBar(wmain, "menu", NULL, 0);

	wpd = XmCreatePulldownMenu(wmenu, "fileMenu", NULL, 0);
	add_menu_item(wmenu, xmCascadeButtonGadgetClass,
		"file", "File", 'F', wpd, NULL);
	add_menu_item(wpd, xmPushButtonGadgetClass,
		"open", "Open", 'O', NULL, open_cb);
	add_menu_item(wpd, xmPushButtonGadgetClass,
		"passTo", "Pass To...", 'P', NULL, pass_to_cb);
	add_menu_item(wpd, xmPushButtonGadgetClass,
		"showLocation", "Show Location", 'L', NULL, show_location_cb);
	add_menu_item(wpd, xmSeparatorGadgetClass, "separator",
		NULL, 0, NULL, NULL);
	add_menu_item(wpd, xmPushButtonGadgetClass,
		"exit", "Exit", 'x', NULL, exit_cb);
		
	wpd = XmCreatePulldownMenu(wmenu, "editMenu", NULL, 0);
	add_menu_item(wmenu, xmCascadeButtonGadgetClass,
		"edit", "Edit", 'E', wpd, NULL);
	add_menu_item(wpd, xmPushButtonGadgetClass,
		"selectAll", "Select All", 'A', NULL, select_all_cb);
	add_menu_item(wpd, xmPushButtonGadgetClass,
		"deselect", "Deselect", 'D', NULL, deselect_cb);

	wpd = XmCreatePulldownMenu(wmenu, "viewMenu", NULL, 0);
	add_menu_item(wmenu, xmCascadeButtonGadgetClass,
		"view", "View", 'V', wpd, NULL);
	
	XtSetArg(args[0], XmNradioBehavior, True);
	wsub = XmCreatePulldownMenu(wpd, "sortByMenu", args, 1);
	add_menu_item(wpd, xmCascadeButtonGadgetClass,
		"sortBy", "Sort By", 'S', wsub, NULL);
	add_menu_item(wsub, xmToggleButtonGadgetClass,
		"name", "Name", 'N', NULL, sort_by_name_cb);
	add_menu_item(wsub, xmToggleButtonGadgetClass,
		"suffix", "Suffix", 'D', NULL, sort_by_suffix_cb);
	add_menu_item(wsub, xmToggleButtonGadgetClass,
		"time", "Time", 'T', NULL, sort_by_time_cb);
	add_menu_item(wsub, xmToggleButtonGadgetClass,
		"size", "Size", 'S', NULL, sort_by_size_cb);

	XtSetArg(args[0], XmNradioBehavior, True);
	wsub = XmCreatePulldownMenu(wpd, "sortDirectionMenu", args, 1);
	add_menu_item(wpd, xmCascadeButtonGadgetClass,
		"sortDirection", "Sort Direction", 'D', wsub, NULL);
	add_menu_item(wsub, xmToggleButtonGadgetClass,
		"ascending", "Ascending", 'A', NULL, sort_ascending_cb);
	add_menu_item(wsub, xmToggleButtonGadgetClass,
		"descending", "Descending", 'D', NULL, NULL);

	wpd = XmCreatePulldownMenu(wmenu, "optionsMenu", NULL, 0);
	add_menu_item(wmenu, xmCascadeButtonGadgetClass,
		"options", "Options", 'O', wpd, NULL);
	add_menu_item(wpd, xmToggleButtonGadgetClass,
		"recursive", "Recursive", 'R',
		NULL, recursive_cb);
	add_menu_item(wpd, xmToggleButtonGadgetClass,
		"followSymlinks", "Follow Symlinks", 'S',
		NULL, follow_symlinks_cb);
	add_menu_item(wpd, xmToggleButtonGadgetClass,
		"followMountPoints", "Follow Mount Points", 'M',
		NULL, follow_mntpts_cb);
	add_menu_item(wpd, xmToggleButtonGadgetClass,
		"matchDirectories", "Match Directories", 'D',
		NULL, match_dirs_cb);

	wpd = XmCreatePulldownMenu(wmenu, "helpMenu", NULL, 0);
	wsub = add_menu_item(wmenu, xmCascadeButtonGadgetClass,
		"help", "Help", 'H', wpd, NULL);
	add_menu_item(wpd, xmPushButtonGadgetClass,
		"about", "About", 'A', NULL, about_cb);

	XtSetArg(args[0], XmNmenuHelpWidget, wsub);
	XtSetValues(wmenu, args, 1);

	XtManageChild(wmenu);
}

/*
 * Displays a blocking message dialog. If 'confirm' is True, the dialog will
 * have OK+Cancel buttons, OK only otherwise. Returns True if OK was chosen.
 */
Boolean message_dialog(Boolean confirm,
	const char *title, const char *message_fmt, ...)
{
	Widget wdlg;
	XmString xm_message_str;
	Arg args[8];
	int n = 0;
	int result = (-1);
	XmString xm_title;
	XtCallbackRec callback[] = {
		{ (XtCallbackProc)message_dialog_cb, (XtPointer)&result },
		{ (XtCallbackProc)NULL, (XtPointer)NULL }
	};
	va_list ap;
	char *msg_buf;
	
	va_start(ap, message_fmt);
	n = vsnprintf(NULL, 0, message_fmt, ap) + 1;
	msg_buf = malloc(n + 1);
	va_end(ap);

	va_start(ap, message_fmt);
	vsnprintf(msg_buf, n, message_fmt, ap);
	va_end(ap);

	xm_message_str = XmStringCreateLocalized(msg_buf);
	free(msg_buf);
	
	xm_title = XmStringCreateLocalized((char*)title);

	n = 0;
	XtSetArg(args[n], XmNokCallback, callback); n++;
	XtSetArg(args[n], XmNcancelCallback, callback); n++;
	XtSetArg(args[n], XmNdialogStyle, XmDIALOG_FULL_APPLICATION_MODAL); n++;
	XtSetArg(args[n], XmNdialogTitle, xm_title); n++;
	XtSetArg(args[n], XmNmessageString, xm_message_str); n++;

	wdlg = XmCreateMessageDialog(wshell, "messageDialog", args, n);

	n = 0;
	XtSetArg(args[n], XmNdialogType,
		confirm ? XmDIALOG_QUESTION : XmDIALOG_INFORMATION); n++;
	XtSetArg(args[n], XmNdefaultButtonType,
		confirm ? XmDIALOG_CANCEL_BUTTON : XmDIALOG_OK_BUTTON); n++;
	XtSetValues(wdlg, args, n);
	
	XtSetArg(args[0], XmNdeleteResponse, XmUNMAP);
	XtSetValues(XtParent(wdlg), args, 1);

	XmStringFree(xm_title);
	XmStringFree(xm_message_str);

	if(!confirm) XtUnmanageChild(
		XmMessageBoxGetChild(wdlg, XmDIALOG_CANCEL_BUTTON));
	XtUnmanageChild(XmMessageBoxGetChild(wdlg, XmDIALOG_HELP_BUTTON));

	XtManageChild(wdlg);

	while(XtIsManaged(wdlg), result == (-1)){
		XtAppProcessEvent(context, XtIMAll);
	}

	XtDestroyWidget(wdlg);
	XSync(XtDisplay(wshell), False);
	XmUpdateDisplay(wshell);

	return (result > 0) ? True : False;
}

/*
 * message_dialog dialog callback
 */
void message_dialog_cb(Widget w, XtPointer client_data,
	XtPointer call_data)
{
	XmSelectionBoxCallbackStruct *cbs=
		(XmSelectionBoxCallbackStruct*)call_data;
	char *result = (Boolean*)client_data;

	*result = (cbs->reason == XmCR_OK) ? 1 : 0;
}

/*
 * Display a directory selection dialog.
 */
void dir_select_dlg(void)
{
	static Widget wdlg = None;
	Arg arg[8];
	int i = 0;
	
	if(!wdlg) {
		char *home_path = getenv("HOME");
		XmString init_path = NULL;
		if(home_path) init_path = XmStringCreateLocalized((String)home_path);
		
		XtSetArg(arg[i], XmNautoUnmanage, True); i++;
		XtSetArg(arg[i], XmNfileTypeMask, XmFILE_DIRECTORY); i++;
		XtSetArg(arg[i], XmNpathMode, XmPATH_MODE_FULL); i++;
		XtSetArg(arg[i], XmNresizePolicy, XmRESIZE_GROW); i++;
		XtSetArg(arg[i], XmNdirectory, init_path); i++;
		XtSetArg(arg[i], XmNtitle, "Select Directory"); i++;
		XtSetArg(arg[i], XmNdialogStyle,
			XmDIALOG_PRIMARY_APPLICATION_MODAL); i++;

		wdlg = XmCreateFileSelectionDialog(wshell,
			"directorySelection", arg, i);
		if(init_path) XmStringFree(init_path);
		XtUnmanageChild(XmFileSelectionBoxGetChild(wdlg, XmDIALOG_LIST_LABEL));
		XtUnmanageChild(XtParent(
			XmFileSelectionBoxGetChild(wdlg, XmDIALOG_LIST)));
		XtAddCallback(wdlg, XmNokCallback, dir_selected_cb, NULL);
		XtUnmanageChild(XmFileSelectionBoxGetChild(wdlg, XmDIALOG_HELP_BUTTON));
	}
	
	XtManageChild(wdlg);
}

void dir_selected_cb(Widget w, XtPointer client, XtPointer call)
{
	XmFileSelectionBoxCallbackStruct *fscb=
		(XmFileSelectionBoxCallbackStruct*)call;
	char *name;

	if(fscb->reason == XmCR_OK) {
		name = (char*)XmStringUnparse(fscb->value, NULL,
			XmMULTIBYTE_TEXT, XmMULTIBYTE_TEXT, NULL, 0, XmOUTPUT_ALL);
		
		if(name) {
			if(strlen(name))
				XmTextFieldSetString(wpath, name);
			XtFree(name);
		}
	}
}


/*
 * Displays a blocking command input dialog.
 * Returns a valid string or NULL if cancelled.
 */
void pass_to_input_dlg(void)
{
	static Widget wdlg = None;
	static Widget wtext = None;

	if(!wdlg) {
		Arg arg[8];
		XmString label_string;
		int i = 0;
		Widget wlabel;
		char alt_tt_src[] = 
			":s <Key>osfEndLine: end-of-line(extend)\n"
			":s <Key>osfBeginLine: beginning-of-line(extend)\n"
			":<Key>osfEndLine: end-of-line()\n"
			":<Key>osfBeginLine: beginning-of-line()\n";
		XtTranslations alt_tt = NULL;

		XtSetArg(arg[i], XmNautoUnmanage, True); i++;
		XtSetArg(arg[i], XmNdialogType, XmDIALOG_PROMPT); i++;
		XtSetArg(arg[i], XmNtitle, "Pass To..."); i++;
		XtSetArg(arg[i], XmNdialogStyle,
			XmDIALOG_PRIMARY_APPLICATION_MODAL); i++;
	
		wdlg = XmCreatePromptDialog(wshell,"passToDialog", arg ,i);
		XtAddCallback(wdlg, XmNokCallback, pass_to_input_cb, NULL);
		
		XtUnmanageChild(XmSelectionBoxGetChild(wdlg, XmDIALOG_HELP_BUTTON));

		wlabel = XmSelectionBoxGetChild(wdlg, XmDIALOG_SELECTION_LABEL);
		wtext = XmSelectionBoxGetChild(wdlg, XmDIALOG_TEXT);
		
		alt_tt = XtParseTranslationTable(alt_tt_src);
		if(alt_tt) XtOverrideTranslations(wtext, alt_tt);
		
		label_string = XmStringCreateLocalized(
			"Specify a command to pass the file name to.");
		XtSetArg(arg[0], XmNlabelString, label_string);
		XtSetValues(wlabel, arg, 1);
		XmStringFree(label_string);
	}

	XtManageChild(wdlg);
}

void pass_to_input_cb(Widget w, XtPointer client, XtPointer call)
{
	XmFileSelectionBoxCallbackStruct *fscb=
		(XmFileSelectionBoxCallbackStruct*)call;
	char *cmd;

	if(fscb->reason == XmCR_OK) {
		cmd = (char*)XmStringUnparse(fscb->value, NULL,
			XmMULTIBYTE_TEXT, XmMULTIBYTE_TEXT, NULL, 0, XmOUTPUT_ALL);

		if(cmd && strlen(cmd)) {
			Arg args[2];
			int nsel;
			unsigned int *sel_pos;
			char *cmd_args[1];
			int rv;

			XtSetArg(args[0], XmNselectedPositionCount, &nsel);
			XtSetArg(args[1], XmNselectedPositions, &sel_pos);
			XtGetValues(wlist, args, 2);

			if(nsel != 1) return;

			cmd_args[0] = get_result_fqn(sel_pos[0] - 1);

			if( (rv = spawn_command(cmd, cmd_args, 1)) ) {
				message_dialog(False, APP_NAME,
					"Error executing command \'%s\'.\n%s.", cmd, strerror(rv));
			}
			free(cmd_args[0]);
		}

		if(cmd) XtFree(cmd);
	}
}

void info_dialog(const char *title_sz, const char *info_sz)
{
	static Widget wdlg = None;
	Arg args[4];
	int n = 0;
	XmString xm_text;
	XmString xm_title;
	
	if(!wdlg) {
		wdlg = XmCreateInformationDialog(wshell,"informationDialog", NULL, 0);
		n = 0;
		XtSetArg(args[n], XmNdialogStyle, XmDIALOG_FULL_APPLICATION_MODAL); n++;
		XtSetArg(args[n], XmNdialogType, XmDIALOG_INFORMATION); n++;
		XtSetArg(args[n], XmNdefaultButtonType, XmDIALOG_OK_BUTTON); n++;
		XtSetValues(wdlg, args, n);
		
		XtUnmanageChild(XmMessageBoxGetChild(wdlg, XmDIALOG_CANCEL_BUTTON));
		XtUnmanageChild(XmMessageBoxGetChild(wdlg, XmDIALOG_HELP_BUTTON));
	}

	xm_title = XmStringCreateLocalized((char*)title_sz);
	xm_text = XmStringCreateLocalized((char*)info_sz);
	
	n = 0;
	XtSetArg(args[n], XmNdialogTitle, xm_title); n++;
	XtSetArg(args[n], XmNmessageString, xm_text); n++;
	XtSetValues(wdlg, args, n);

	XmStringFree(xm_text);
	XmStringFree(xm_title);
	XtManageChild(wdlg);
}


/* Build a masked icon pixmap from the xbm data */
void load_icon(const void *bits, const void *mask_bits,
	unsigned int width, unsigned int height, Pixmap *icon, Pixmap *mask)
{
	Pixel fg_color = 0, bg_color = 0;
	Window root;
	int depth, screen;
	Screen *pscreen;
	
	pscreen = XtScreen(wshell);
	screen = XScreenNumberOfScreen(pscreen);
	root = RootWindowOfScreen(pscreen);
	depth = DefaultDepth(display, screen);
	fg_color = XBlackPixel(display, screen);
	bg_color = app_res.wm_icon_color; /* XWhitePixel(display, screen); */
	*icon = XCreatePixmapFromBitmapData(display, root,
		(char*)bits, width, height, fg_color, bg_color, depth);
	*mask = XCreatePixmapFromBitmapData(display, root,
		(char*)mask_bits ,width, height, 1, 0, 1);
}

/* 
 * Converts the string to an XmString while replacing control characters
 * with whitespace, and discarding any invalid multibyte sequences.
 * Returns new XmString allocated from heap, or NULL on error.
 */
XmString make_label(const char *src, XmStringTag rtag)
{
	size_t nbytes = strlen(src);
	size_t is = 0;
	size_t id = 0;
	int ns;
	char *dest;
	XmString result;

	dest = malloc(nbytes + 3);
	if(!dest) return NULL;

	mblen(NULL, 0);

	while(src[is]) {
		ns = mblen(src + is, nbytes - is);
		if(ns == -1) {
			dest[id] = '?';
			id++;
			is++;
			continue;
		} else if(ns == 1 && iscntrl(src[is]) && src[is] != '\t') {
			dest[id] = ' ';
		} else {
			memcpy(dest + id, src + is, ns);
		}
		id += ns;
		is += ns;
	}
	dest[id] = '\0';
	
	result = XmStringGenerate(dest, NULL, XmCHARSET_TEXT, rtag);
	free(dest);

	return result;
}

/*
 * Computes average character width from ASCII set using
 * the given render-table and rendition tag
 */
Dimension get_average_char_width(XmRenderTable rt, XmStringTag tag)
{
	static char chrs[96] = { '\0' };
	XmString xms;
	Dimension width;
	Dimension height;
	
	if(chrs[0] == '\0') {
		char i;

		for(i = 32; i < 127; i++) {
			chrs[i - 32] = i;
		}
		chrs[i - 32] = '\0';
	}
	xms = XmStringGenerate(chrs, NULL, XmCHARSET_TEXT, tag);
	XmStringExtent(rt, xms, &width, &height);
	XmStringFree(xms);
	
	return (width / 90);
}

/* Grabs primary selection */
void grab_primary(Boolean grab)
{
	if(grab) {
		XtOwnSelection(wshell, XA_PRIMARY,
			XtLastTimestampProcessed(display),
			convert_selection_proc, lose_selection_proc, NULL);
	} else {
		XtDisownSelection(wshell, XA_PRIMARY,
			XtLastTimestampProcessed(display));
	}
}

void lose_selection_proc(Widget w, Atom *sel)
{
	/* TBD: maybe change list's selection color to reflect state? */
}

/*
 * Primary selection converter.
 * Returns space separated list of selected files, if any, to the requestor.
 */
Boolean convert_selection_proc(Widget w,
	Atom *sel, Atom *tgt, Atom *type_ret, XtPointer *val_ret,
	unsigned long *len_ret, int *fmt_ret)
{
	static Boolean initial = True;
	static Atom XA_TEXT = None;
	static Atom XA_UTF8_STRING = None;
	static Atom XA_TARGETS = None;
	static Atom XA_FILE_LIST = None;
	Arg args[2];
	unsigned int i;
	int nsel;
	unsigned int *sel_pos, *tmp_sel_pos;
	
	if(initial) {
		initial = False;
		
		XA_TEXT = XInternAtom(display, "TEXT", False);
		XA_UTF8_STRING = XInternAtom(display, "UTF8_STRING", False);
		XA_TARGETS = XInternAtom(display, "TARGETS", False);
		XA_FILE_LIST = XInternAtom(display, XFILE_ATOM_NAME, False);
	}
	
	XtSetArg(args[0], XmNselectedPositionCount, &nsel);
	XtSetArg(args[1], XmNselectedPositions, &tmp_sel_pos);
	XtGetValues(wlist, args, 2);

	if(!nsel) return False;
	
	/* for some weird reason, list selections are 1 based, and we cannot
	 * modify the returned list, because it's list widget's internal */
	sel_pos = malloc(sizeof(unsigned int*) * nsel);
	if(!sel_pos) {
		perror("malloc");
		return False;
	}

	for(i = 0; i < nsel; i++)
		sel_pos[i] = tmp_sel_pos[i] - 1;
	
	if(*tgt == XA_TARGETS) {
		Atom targets[] = {
			XA_STRING, XA_TEXT,
			XA_UTF8_STRING, XA_FILE_LIST
		};
		
		char *data;
		
		data = XtMalloc(sizeof(targets)); /* Xt will XtFree this when done */
		if(!data) return False;

		memcpy(data, targets, sizeof(targets));
		*type_ret = *tgt;
		*fmt_ret = 32;
		*len_ret = sizeof(targets) / sizeof(Atom);
		*val_ret = data;

	} else if(*tgt == XA_TEXT || *tgt == XA_STRING || *tgt == XA_UTF8_STRING) {
		unsigned long len = 0;
		char *data;
		
		for(i = 0; i < nsel; i++) {
			len += strlen(results[sel_pos[i]].path) +
				strlen(results[sel_pos[i]].title) + 2;
		}
		
		data = XtMalloc(len + 1); /* Xt will XtFree this when done */
		if(!data) {
			fprintf(stderr, "Memory allocation error\n");
			free(sel_pos);
			return False;
		}
		
		data[0] = '\0';
		
		for(i = 0; i < nsel; i++) {
			strcat(data, results[sel_pos[i]].path);
			strcat(data, "/");
			strcat(data, results[sel_pos[i]].title);
			strcat(data, " ");
		}
		data[len - 1] = '\0';
		
		if(*tgt == XA_TEXT || *tgt == XA_STRING)
			mbs_to_latin1(data, data);

		*type_ret = *tgt;
		*fmt_ret = 8;
		*len_ret = strlen(data);
		*val_ret = data;
		
	} else if(*tgt == XA_FILE_LIST) {
		unsigned int i;
		unsigned long len = 0;
		char *data;
		char *pos;
		
		len = 2;
		for(i = 0; i < nsel; i++) {
			len += strlen(results[sel_pos[i]].path) + 
				strlen(results[sel_pos[i]].title) + 2;
		}
		
		data = XtMalloc(len + 1); /* Xt will XtFree this when done */
		if(!data) {
			fprintf(stderr, "Memory allocation error\n");
			free(sel_pos);
			return False;
		}
		
		/* the first entry in the list must specify the path the rest of
		 * the list descends from, which in our case always is root */
		strcpy(data, "/");
		pos = data + 2;
		
		for(i = 0; i < nsel; i++) {
			/* names in the list are FQN, so remove leading / */
			strcpy(pos, results[sel_pos[i]].path + 1);
			pos += strlen(results[sel_pos[i]].path + 1);
			*pos = '/';
			pos++;
			strcpy(pos, results[sel_pos[i]].title);
			pos += strlen(results[sel_pos[i]].title);
			*pos = '\0';
			pos++;
		}
		*pos = '\0';
		
		*type_ret = *tgt;
		*fmt_ret = 8;
		*len_ret = len;
		*val_ret = data;
	} else {
		free(sel_pos);
		return False;
	}

	free(sel_pos);
	return True;
}

/*
 * Converts a multibyte string to iso8859-1, substituting out of
 * code page characters with a question mark. The resulting string
 * is placed in dest, which may point to the same buffer as src.
 */
void mbs_to_latin1(const char *src, char *dest)
{
	size_t nbytes = strlen(src);
	size_t is = 0;
	size_t id = 0;
	int ns;

	mblen(NULL, 0);

	while(src[is]) {
		ns = mblen(src + is, nbytes - is);
		if(ns == -1) {
			dest[id] = '?';
			id++;
			is++;
			continue;
		} else if(ns > 2) {
			dest[id] = '?';
		} else if(ns == 2) {
			dest[id] = (src[is] & 0x1C) ? '?' :
				((src[is] << 6) | (src[is + 1] & 0x3F));
		} else {
			dest[id] = src[is];
		}
		id++;
		is += ns;
	}
	dest[id] = '\0';
}
