/*
** xcd.c
**
** (Powered Up) Replacement for Shell's CD command
**
** $Id: xcd.c,v 1.7 94/08/09 17:22:48 GF Exp Locker: GF $
**
**************************************************************************
**
** Copyright © 1994 by Gabriele Falcioni. All Rights Reserved.
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
**
** For inquiries, comments, donations, bug reports, write to:
**
** Gabriele Falcioni
** via E. Cialdini, 50
** I-60122 Ancona
** ITALY
**
**************************************************************************
**
** XCD: eXtended Change current Directory
**
** REQUIREMENTS
**
** · "exec.library" V37+
** · "dos.library"  V37+
**
** · SAS DevSystem V6.x
** · Startup: "shellcres2.o"
*/

#define _CMDNAME	"XCD"
#define _VER_REV	"1.7"
#define _CREATION	"(17.05.94)"
#define _TEMPLATE "DIR,PROCESS/K/N,SHOW/S,FIND/S,LAST/S,GUI/S,QUIET/S"

/*
 * COMMAND
 * XCD - Replacement for Shell's CD command
 *
 * TEMPLATE
 * DIR,PROCESS/K/N,SHOW/S,FIND/S,LAST/S,GUI/S,QUIET/S
 *
 * DESCRIPTION
 * This command replaces the resident CD command and it offers more features
 * than the standard one. XCD allows the user to either view/change the
 * working directory of current CLI/Shell process or view/CD to the working
 * dir of another CLI/Shell process. As an added bonus, XCD remembers (it
 * saves in a local ENV variable) the path of the last used working dir, it
 * offers a GUI to navigate around the dir tree, it can scan the complete
 * tree to find a directory, it can use standard AmigaDOS regular expressions
 * to specify partial dir nodes or names, and much more for *only* 2464 bytes
 * of residentable code (does someone offer more? :-)
 *
 * Default action is to change working dir based on the option(s) specified.
 * If the switch SHOW/S is specified, the full path of the requested dir is
 * printed to stdio *and* working dir is *not* changed. If no arguments are
 * specified, XCD will print to stdio the full path of your current dir.
 * In general, it's possible to specify a standard AmigaDOS regular
 * expression in arguments accepting strings. If multiple directories match
 * the given pattern, the first matching dir is chosen, its path is printed
 * to stdout to notify the user and a CD is performed to it.
 *
 * ARGUMENTS
 * DIR         - name of the new directory, like the same name parameter
 *               of system's CD command. The path can contain standard
 *               AmigaDOS regular expression.
 * PROCESS/K/N	- CLI/Shell process number. XCD gets new directory from it.
 * SHOW/S      - display new directory name instead of CD to it. This switch
 *               is accepted with any other argument specified.
 * FIND/S      - if dir is not found the complete file system tree below the
 *               current working dir is searched for a directory matching
 *               the DIR argument.
 * LAST/S      - return to last directory remembered.
 * GUI/S       - display a directory requester.
 * QUIET/S     - turn off every message that is not an error. This switch is
 *               accepted with any other argument specified.
 */

/* -------------------------------------------------------------------- */

#include <string.h>

#include <exec/types.h>
#include <exec/memory.h>
#include <libraries/asl.h>
#include <dos/var.h>
#include <dos/stdio.h>
#include <dos/dosasl.h>
#include <dos/rdargs.h>
#include <dos/dosextens.h>

#include <proto/dos.h>
#include <proto/exec.h>
#include <proto/asl.h>

/* --- My favourite memory macros ------------------------------------- */

#define STDMEM (MEMF_CLEAR|MEMF_PUBLIC)

#define ConStruct(type)	((type *)AllocMem(sizeof(type),STDMEM))
#define DiStruct(ptr)	FreeMem((ptr),sizeof(*(ptr)))

/* -------------------------------------------------------------------- */

/* last directory is saved in this local variable */
#define LAST_DIRECTORY_VAR	"xcd_last_dir"

/* this is the size in bytes of the work buffers */
#define BUFLEN 512

/* magic constant used to mark current directory */
#define THISDIR	((BPTR)(-1L))

/* a *slightly* nonstandard version string */
static UBYTE const EmbeddedVersionID[] = {
	"$VER: " _CMDNAME " " _VER_REV " " _CREATION
	" Copyright © 1994 by Gabriele Falcioni. FREE Software."
};

/* opened only if the GUI/S switch is used */
struct Library *AslBase;

struct RDArgs *MyRdargs;
struct {
	UBYTE *dirname;
	ULONG *clinum;
	ULONG show;
	ULONG find;
	ULONG last;
	ULONG gui;
	ULONG quiet;
} MyArgs;

/*
 * if this flag is TRUE the new directory pathname is printed to stdout.
 * NOTE: this can be set indipendently from the SHOW/S switch.
 */

BOOL flag_PrintDir;

/* -------------------------------------------------------------------- */

static BPTR do_gui(VOID)
{
	BPTR newdir = NULL;

	if (AslBase = OpenLibrary("asl.library",37)) {
		struct FileRequester *fr;

		if (fr = AllocAslRequestTags(ASL_FileRequest,
				ASL_Hail,		"Set Current Directory",
				ASL_ExtFlags1,	FIL1F_NOFILES,
				ASL_OKText,		"CD",
				TAG_END)) {

			if (AslRequestTags((APTR)fr,TAG_END))
				if (!(newdir = Lock(fr->rf_Dir,SHARED_LOCK)))
					PrintFault(IoErr(),"dir node not found");

			FreeAslRequest(fr);
		} else PrintFault(ERROR_NO_FREE_STORE,"can't create dir requester");

	} else Printf("can't open \"asl.library\" V37+\n");

	return newdir;
}

static BPTR do_search(VOID)
{
	LONG errcode = 0;
	BPTR newdir = NULL;
	BOOL quit = FALSE;

	UBYTE *pat;
	struct AnchorPath *ap;

	if (ap = ConStruct(struct AnchorPath)) {
		ap->ap_BreakBits = SIGBREAKF_CTRL_C;
	} else errcode = ERROR_NO_FREE_STORE;

	if (pat = AllocMem(BUFLEN*2+2,0)) {
		if (ParsePatternNoCase(MyArgs.dirname,pat,BUFLEN*2+2) == -1)
			errcode = IoErr();
	} else errcode = ERROR_NO_FREE_STORE;

	if (ap && pat && !errcode) {

		if (!MyArgs.quiet)
			Printf("Looking for \"%s\" ...\n",MyArgs.dirname);

		(VOID)MatchFirst("#?",ap);

		do {

			switch (errcode = IoErr()) {

				case 0:
					if (ap->ap_Info.fib_DirEntryType > 0) {

						if (ap->ap_Flags & APF_DIDDIR) {
							ap->ap_Flags &= ~APF_DIDDIR;
						} else if (MatchPatternNoCase(pat,
								ap->ap_Info.fib_FileName)) {

							BPTR olddir;

							olddir = CurrentDir(ap->ap_Current->an_Lock);

								if (!(newdir = Lock(ap->ap_Info.fib_FileName,
										SHARED_LOCK)))
									errcode = IoErr();

							(VOID)CurrentDir(olddir);

							/*
							 * Tell the user about the dir we have discovered,
							 * the full dir pathname will be printed to stdio.
							 */

							flag_PrintDir = TRUE;

							quit = TRUE;

						} else if (!(errcode = IoErr()))
							ap->ap_Flags |= APF_DODIR;

					} /* files are ignored */
					break;

				case ERROR_NO_MORE_ENTRIES:
					errcode = ERROR_OBJECT_NOT_FOUND;
					/* going to default */

				default:
					quit = TRUE;
					break;

			}	/* end switch */

			if (!quit) (VOID)MatchNext(ap);

		} while (!quit);

		MatchEnd(ap);
	}

	if (pat) FreeMem(pat,BUFLEN*2+2);
	if (ap)  DiStruct(ap);

	if (errcode) PrintFault(errcode,"search failed");

	return newdir;
}

static BPTR lock_by_dirname(VOID)
{
	LONG errcode;
	BPTR newdir = NULL;
	struct AnchorPath *ap;

	if (ap = ConStruct(struct AnchorPath)) {
		ap->ap_BreakBits = SIGBREAKF_CTRL_C;

		(VOID)MatchFirst(MyArgs.dirname,ap);
		errcode = IoErr();

		switch (errcode) {
			case 0:
				if (ap->ap_Info.fib_DirEntryType > 0) {
					BPTR olddir;

					olddir = CurrentDir(ap->ap_Current->an_Lock);

					if (!(newdir = Lock(ap->ap_Info.fib_FileName,SHARED_LOCK)))
						errcode = IoErr();

					(VOID)CurrentDir(olddir);

					/*
					 * If more than one object matches the pattern,
					 * the full dir pathname will be printed to stdio.
					 */

					if (!MatchNext(ap)) flag_PrintDir = TRUE;

				} else errcode = ERROR_OBJECT_WRONG_TYPE;
				break;

			case ERROR_NO_MORE_ENTRIES:
				errcode = ERROR_OBJECT_NOT_FOUND;
				break;

		}	/* end switch */

		MatchEnd(ap);
		DiStruct(ap);
	} else errcode = ERROR_NO_FREE_STORE;

	if (errcode)
		if (errcode == ERROR_OBJECT_NOT_FOUND
		&& MyArgs.find)
			newdir = do_search();
		else PrintFault(errcode,"dir node not found");

	return newdir;
}

static BPTR lock_by_clinum(VOID)
{
	BPTR newdir = NULL;
	struct Process *proc;

	Forbid();

	if (proc = FindCliProc(*MyArgs.clinum)) {

		if (!(newdir = DupLock(proc->pr_CurrentDir)))
			PrintFault(IoErr(),"can't duplicate CLI dir lock");

	} else Printf("CLI process %ld not found\n",*MyArgs.clinum);

	Permit();

	/*
	 * Because the user may have forgot the directory path,
	 * it will be printed to stdio as useful memo.
	 */

	flag_PrintDir = TRUE;

	return newdir;
}

static BPTR lock_last_dir(VOID)
{
	LONG errcode = 0;
	BPTR newdir = NULL;
	UBYTE *bufptr;

	if (bufptr = AllocMem(BUFLEN,0)) {

		if (GetVar(
				LAST_DIRECTORY_VAR,
				bufptr, BUFLEN, LV_VAR|GVF_LOCAL_ONLY) > 0) {

			if (!(newdir = Lock(bufptr,SHARED_LOCK)))
				errcode = IoErr();

		} else errcode = IoErr();

		FreeMem(bufptr,BUFLEN);
	} else errcode = ERROR_NO_FREE_STORE;

	if (errcode) PrintFault(errcode,"can't find last dir");

	/*
	 * Because the user may have forgot the directory path,
	 * it will be printed to stdio as useful memo.
	 */

	flag_PrintDir = TRUE;

	return newdir;
}

static BOOL change_directory(BPTR newdir)
{
	LONG errcode = 0;
	UBYTE *bufptr,*cdname;

	if (bufptr = AllocMem(BUFLEN,0L)) {
		if (cdname = AllocMem(BUFLEN,0L)) {

			if (GetCurrentDirName(cdname,BUFLEN)) {

				if (NameFromLock(newdir,bufptr,BUFLEN)
				&& SetCurrentDirName(bufptr)) {

					UnLock( CurrentDir(newdir) );

					if (!SetVar(
							LAST_DIRECTORY_VAR,
							cdname, -1, LV_VAR|GVF_LOCAL_ONLY)) {

						PrintFault(IoErr(),"can't create local variable");
						SetIoErr(0);
					}

				} else {
					errcode = IoErr();
					SetCurrentDirName(cdname);	/* restore old name */
				}

			} else errcode = ERROR_LINE_TOO_LONG;

			FreeMem(cdname,BUFLEN);
		} else errcode = ERROR_NO_FREE_STORE;

		FreeMem(bufptr,BUFLEN);
	} else errcode = ERROR_NO_FREE_STORE;

	if (errcode) {
		PrintFault(errcode,"can't change current dir");
		return FALSE;
	}

	return TRUE;
}

static VOID print_directory(BPTR dirlk)
{
	LONG errcode = 0;
	UBYTE *bufptr;

	if (bufptr = AllocMem(BUFLEN,0L)) {

		if (dirlk == THISDIR) {
			if (!GetCurrentDirName(bufptr,BUFLEN))
				errcode = ERROR_LINE_TOO_LONG;
		} else {
			if (!NameFromLock(dirlk,bufptr,BUFLEN))
				errcode = IoErr();
		}

		if (!errcode) Printf("%s\n",bufptr);

		FreeMem(bufptr,BUFLEN);
	} else errcode = ERROR_NO_FREE_STORE;

	if (errcode) PrintFault(errcode,"can't print dir name");
}

/* -------------------------------------------------------------------- */

/*
 * this is from file: "shellcres2.a" (cres-like custom startup module)
 *
 * Features:
 * · Smaller than standard one
 * · Makes C programs resident-able
 * · Permits Shell execution only
 * · Initializes SysBase and DOSBase
 * · Requires V37+ ROMs
 * (of course practical *only* for really small Shell utilities)
 *
 */

void late_exit(long);

/*
 * custom exit function: void early_exit(long exitcode)
 * this function calls the "shellcres2.o" exit code
 */

void early_exit(long code)
{
	if (AslBase) CloseLibrary(AslBase);

	late_exit(code);
}

/*
 * custom main function: void early_main(void)
 * called by "shellcres2.o" (custom startup module, see above)
 */

void __stdargs early_main(void)
{
	BPTR newdir;

	if (MyRdargs = ReadArgs(_TEMPLATE,(LONG *)&MyArgs,NULL)) {

		flag_PrintDir = FALSE;	/* default: do not print dir name */

		if (MyArgs.gui)				newdir = do_gui();
		else if (MyArgs.dirname)	newdir = lock_by_dirname();
		else if (MyArgs.clinum)		newdir = lock_by_clinum();
		else if (MyArgs.last)		newdir = lock_last_dir();
		else {
			newdir = THISDIR;
			MyArgs.show = TRUE;	/* force a display op */
		}

		if (MyArgs.show) flag_PrintDir = TRUE;

		if (newdir) {
			if (flag_PrintDir && !MyArgs.quiet) {
				if (!MyArgs.show) Printf("CD to ");
				print_directory(newdir);
			}

			if (!MyArgs.show)
				if (change_directory(newdir)) newdir = NULL;
		}

		if (newdir && newdir != THISDIR) UnLock(newdir);

		FreeArgs(MyRdargs);
	} else PrintFault(IoErr(),"Bad Args");

	early_exit(IoErr() ? RETURN_FAIL : RETURN_OK);
}
