/* fileman.c -- A tiny application which demonstrates how to use the
   GNU Readline library.  This application interactively allows users
   to manipulate files and their modes.

   NOTE: this was taken from the GNU Readline documentation and ported
   to libedit. A command to output the history list was added.
   
*/

#include <stdio.h>
#include <sys/types.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <errno.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <locale.h>
#include <time.h>

#include "editline.h"

void too_dangerous(char *caller);
void initialize_readline();
int execute_line(char *line);
int valid_argument(char *caller, char *arg);

/* The names of functions that actually do the manipulation. */
int com_list(char *);
int com_view(char *);
int com_history(char *);
int com_rename(char *);
int com_stat(char *);
int com_pwd(char *);
int com_delete(char *);
int com_help(char *);
int com_cd(char *);
int com_quit(char *);

struct cmd {
    char *name;		      /* User printable name of the function. */
    int  (*func)(char *);     /* Function to call to do the job. */
    char *doc;		      /* Documentation for this function.  */
};

struct cmd commands[] = {
    { "cd",      com_cd,      "Change to directory DIR"},
    { "delete",  com_delete,  "Delete FILE"},
    { "help",    com_help,    "Display this text"},
    { "?",       com_help,    "Synonym for `help'"},
    { "list",    com_list,    "List files in DIR"},
    { "ls",      com_list,    "Synonym for `list'"},
    { "pwd",     com_pwd,     "Print the current working directory"},
    { "quit",    com_quit,    "Quit using Fileman"},
    { "rename",  com_rename,  "Rename FILE to NEWNAME"},
    { "stat",    com_stat,    "Print out statistics on FILE"},
    { "view",    com_view,    "View the contents of FILE"},
    { "history", com_history, "List editline history"},
    { NULL, NULL, NULL },
};

/* Forward declarations. */
char *stripwhite();
struct cmd *find_command();

/* When non-zero, this means the user is done using this program. */
int done;

int main(int argc, char **argv)
{
    char *line, *s;

    setlocale(LC_CTYPE, "");

    initialize_readline();	/* Bind our completer. */

    /* Loop reading and executing lines until the user quits. */
    for (; done == 0;) {
	line = readline("FileMan: ");

	if (!line)
	    break;

	/* Remove leading and trailing whitespace from the line.
	   Then, if there is anything left, add it to the history list
	   and execute it. */
	s = stripwhite(line);
#if 0
	if (*s) {

	    char *expansion;
	    int result;

	    result = history_expand(s, &expansion);
	    if (result < 0 || result == 2) {
		fprintf(stderr, "%s\n", expansion);
	    } else {
		add_history(expansion);
		execute_line(expansion);
	    }
	    free(expansion);
	}
#else
	execute_line(s);
#endif
	free(line);
    }

    return 0;
}

/* Execute a command line. */
int execute_line(char *line)
{
    int i;
    struct cmd *command;
    char *word;

    /* Isolate the command word. */
    i = 0;
    while (line[i] && isspace(line[i]))
	i++;
    word = line + i;

    while (line[i] && !isspace(line[i]))
	i++;

    if (line[i])
	line[i++] = '\0';

    command = find_command(word);

    if (!command) {
	fprintf(stderr, "%s: No such command for FileMan.\n", word);
	return -1;
    }

    /* Get argument to command, if any. */
    while (isspace(line[i]))
	i++;

    word = line + i;

    /* Call the function. */
    return command->func(word);
}

/* Look up NAME as the name of a command, and return a pointer to that
   command.  Return a NULL pointer if NAME isn't a command name. */
struct cmd *find_command(char *name)
{
    int i;

    for (i = 0; commands[i].name; i++)
	if (strcmp(name, commands[i].name) == 0)
	    return &commands[i];

    return NULL;
}

/*
 * Strip whitespace from the start and end of STRING.  Return a pointer
 * into STRING.
 */
char *stripwhite(char *string)
{
    char *s, *t;

    for (s = string; isspace(*s); s++) ;

    if (*s == 0)
	return s;

    t = s + strlen(s) - 1;
    while (t > s && isspace(*t))
	t--;
    *++t = '\0';

    return s;
}

/* **************************************************************** */
/*                                                                  */
/*                  Interface to Readline Completion                */
/*                                                                  */
/* **************************************************************** */

char *command_generator(const char *, int);
char **fileman_completion(const char *, int, int);

/*
 * Tell the GNU Readline library how to complete.  We want to try to
 * complete on command names if this is the first word in the line, or
 * on filenames if not.
 */
void initialize_readline(void)
{
    /* Allow conditional parsing of the ~/.inputrc file. */
    rl_readline_name = "FileMan";

    /* Tell the completer that we want a crack first. */
    rl_attempted_completion_function = fileman_completion;
}

/*
 * Attempt to complete on the contents of TEXT.  START and END
 * bound the region of rl_line_buffer that contains the word to
 * complete.  TEXT is the word to complete.  We can use the entire
 * contents of rl_line_buffer in case we want to do some simple
 * parsing.  Returnthe array of matches, or NULL if there aren't any.
 */
char **fileman_completion(const char *text, int start, int end)
{
    char **matches = NULL;

    /* If this word is at the start of the line, then it is a command
       to complete.  Otherwise it is the name of a file in the current
       directory. */
    if (start == 0)
	matches = rl_completion_matches(text, command_generator);

    return matches;
}

/* Generator function for command completion.  STATE lets us
   know whether to start from scratch; without any state
   (i.e. STATE == 0), then we start at the top of the list. */
char *command_generator(const char *text, int state)
{
    static int list_index, len;
    char *name;

    /* If this is a new word to complete, initialize now.  This
       includes saving the length of TEXT for efficiency, and
       initializing the index variable to 0. */
    if (!state) {
	list_index = 0;
	len = strlen(text);
    }

    /* Return the next name which partially matches from the
       command list. */
    while ((name = commands[list_index].name)) {
	list_index++;

	if (strncmp(name, text, len) == 0)
	    return strdup(name);
    }

    /* If no names matched, then return NULL. */
    return NULL;
}

/* **************************************************************** */
/*                                                                  */
/*                       FileMan Commands                           */
/*                                                                  */
/* **************************************************************** */

/* String to pass to system ().  This is for the LIST, VIEW and RENAME
   commands. */
static char syscom[1024];

/* List the file(s) named in arg. */
int com_list(char *arg)
{
    if (!arg)
	arg = "";

    sprintf(syscom, "ls -FClg %s", arg);

    return system(syscom);
}

int com_view(char *arg)
{
    if (!valid_argument("view", arg))
	return 1;

    sprintf(syscom, "more %s", arg);

    return system(syscom);
}

int com_history(char *arg)
{
    const char *he;

    /* rewind history */
    while (el_prev_hist()) ;

    for (he = el_next_hist(); he != NULL; he = el_next_hist())
	printf("%s\n", he);

    return 0;
}

int com_rename(char *arg)
{
    too_dangerous("rename");
    return 1;
}

int com_stat(char *arg)
{
    struct stat finfo;

    if (!valid_argument("stat", arg))
	return 1;

    if (stat(arg, &finfo) == -1) {
	perror(arg);
	return 1;
    }

    printf("Statistics for `%s':\n", arg);

    printf("%s has %ld link%s, and is %lld byte%s in length.\n", arg,
	   (long)finfo.st_nlink,
	   (finfo.st_nlink == 1) ? "" : "s",
	   (long long)finfo.st_size, (finfo.st_size == 1) ? "" : "s");
    printf("Inode Last Change at: %s", ctime(&finfo.st_ctime));
    printf("      Last access at: %s", ctime(&finfo.st_atime));
    printf("    Last modified at: %s", ctime(&finfo.st_mtime));

    return 0;
}

int com_delete(char *arg)
{
    too_dangerous("delete");
    return 1;
}

/* Print out help for ARG, or for all of the commands if ARG is
   not present. */
int com_help(char *arg)
{
    int i;
    int printed = 0;

    for (i = 0; commands[i].name; i++) {
	if (!*arg || (strcmp(arg, commands[i].name) == 0)) {
	    printf("%s\t\t%s.\n", commands[i].name,
		   commands[i].doc);
	    printed++;
	}
    }

    if (!printed) {
	printf("No commands match `%s'.  Possibilties are:\n", arg);

	for (i = 0; commands[i].name; i++) {
	    /* Print in six columns. */
	    if (printed == 6) {
		printed = 0;
		printf("\n");
	    }

	    printf("%s\t", commands[i].name);
	    printed++;
	}

	if (printed)
	    printf("\n");
    }

    return 0;
}

/* Change to the directory ARG. */
int com_cd(char *arg)
{
    if (chdir(arg) == -1) {
	perror(arg);
	return 1;
    }

    com_pwd("");
    return 0;
}

/* Print out the current working directory. */
int com_pwd(char *ignore)
{
    char dir[1024], *s;

    s = getcwd(dir, sizeof(dir) - 1);
    if (!s) {
	printf("Error getting pwd: %s\n", dir);
	return 1;
    }

    printf("Current directory is %s\n", dir);
    return 0;
}

/* The user wishes to quit using this program.  Just set DONE
   non-zero. */
int com_quit(char *arg)
{
    done = 1;
    return 0;
}

/* Function which tells you that you can't do this. */
void too_dangerous(char *caller)
{
    fprintf(stderr, "%s: Too dangerous for me to distribute.\n", caller);
    fprintf(stderr, "Write it yourself.\n");
}

/* Return non-zero if ARG is a valid argument for CALLER,
   else print an error message and return zero. */
int valid_argument(char *caller, char *arg)
{
    if (!arg || !*arg) {
	fprintf(stderr, "%s: Argument required.\n", caller);
	return 0;
    }

    return 1;
}

/**
 * Local Variables:
 *  c-file-style: "k&r"
 *  c-basic-offset: 4
 * End:
 */
