/*
** command2.C - the main command loop and some of the commands themselves.
**
**               Command1.C and command2.C
**               are concatenated during the make into
**               commands.C, which consists of the main
**               command loop and all commands called from
**               within that loop.
**
** command2.C 1.42   Delta\'d: 09:49:45 9/28/92   Mike Lijewski, CNSF
**
** Copyright \(c\) 1991, 1992 Cornell University
** All rights reserved.
**
** Redistribution and use in source and binary forms are permitted
** provided that: \(1\) source distributions retain this entire copyright
** notice and comment, and \(2\) distributions including binaries display
** the following acknowledgement:  ``This product includes software
** developed by Cornell University\'\' in the documentation or other
** materials provided with the distribution and in all advertising
** materials mentioning features or use of this software. Neither the
** name of the University nor the names of its contributors may be used
** to endorse or promote products derived from this software without
** specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED ``AS IS\'\' AND WITHOUT ANY EXPRESS OR
** IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
*/

/*
** delete_file - delete the current file which is a regular file.
**               Update the window appropriately.
*/

static void delete_file(DirList *dl, const char *file)
{
    if (unlink(file) != -1)
        remove_listing_line(dl, dl->savedYPos());
    else
    {
        message("`deletion' failed");
        move_cursor(dl->savedYPos(), dl->savedXPos());
    }
}

/*
** delete_directory - delete the current file which is a directory.
**                    Update the window appropriately.
*/

static void delete_directory(DirList *dl, const char *dirname)
{
    if (rmdir(dirname) != -1)
        remove_listing_line(dl, dl->savedYPos());
    else
    {
        message("`deletion' failed");
        move_cursor(dl->savedYPos(), dl->savedXPos());
    }
}

/*
** delete_current_file - attempt to delete current file.
*/

static void delete_current_file(DirList *dl)
{
    const char *file = get_file_name(dl);
    
    if (strcmp(file, ".")  == 0 || strcmp(file, "..") == 0)
    {
        message("`deletion' of `.' and `..' is not allowed");
        move_cursor(dl->savedYPos(), dl->savedXPos());
        synch_display();
        return;
    }

    String msg = String("delete `") + file + "'? (y|n) ";

    if (is_directory(file) && yes_or_no(msg))
        delete_directory(dl, file);
    else if (yes_or_no(msg))
        delete_file(dl, file);
    else
        //
        // Don\'t do the delete.
        //
        move_cursor(dl->savedYPos(), dl->savedXPos());
    
    synch_display();
}


/*
** update_current_line - updates the current line in the listing
**                       and it\'s representation on the screen.
*/

static void update_current_line(DirList *dl, char *new_line)
{
    if (strlen(new_line) <= columns() && dl->currLine()->length() <= columns())
    {
        //
        // The most common case.
        //
        update_screen_line(dl->currLine()->line(), new_line, dl->savedYPos());
        dl->currLine()->update(&new_line);
        dl->saveYXPos(dl->savedYPos(), goal_column(dl));
        move_cursor(dl->savedYPos(), dl->savedXPos());
    }
    else
    {
        //
        // Either the old or the new line must be shifted.
        // Probably not worth trying to use update_screen_line\(\).
        //
        dl->currLine()->update(&new_line);
        dl->saveYXPos(dl->savedYPos(), goal_column(dl));
        
        if (dl->currLine()->length() > columns())
            leftshift_current_line(dl);
        else
        {
            move_cursor(dl->savedYPos(), 0);
            clear_to_end_of_line();
            display_string(new_line);
            move_cursor(dl->savedYPos(), dl->savedXPos());
        }
    }
}

/*
** rename_file - rename src to dest and update window appropriately.
*/

static void rename_file(DirList *dl, const char *src, const char *dest)
{
    if (rename(src, dest) != -1)
    {
        message("`rename' was successful");
        if (in_same_directory(dl, dest))
        {
            //
            // The important point to note about the following code
            // is that if `dest\' is a directory, we need to add a `d\'
            // to the ls command as in
            //
            //   ls -ld source
            //
            int is_dir = is_directory(dest);
            size_t size = strlen(ls_cmd[the_sort_order()]) + strlen(dest) + 3;

            if (is_dir) size++;

            String command = ls_cmd[the_sort_order()];
            if (is_dir) command += "-d";
            command += String("'") + dest + "'";

            FILE *fp = popen(command, "r");
            if (fp == 0)
                error("File %s, line %d: popen(%s) failed",
                      __FILE__, __LINE__, (const char *)command);

            char *new_line = fgetline(fp);
            if (fp == 0)
                error("File %s, line %d: fgetline() failed",
                      __FILE__, __LINE__);
            (void)pclose(fp);

            update_current_line(dl, new_line);
        }
        else
            remove_listing_line(dl, dl->savedYPos());
    }
    else
    {
        message("`rename' failed");
        move_cursor(dl->savedYPos(), dl->savedXPos());
    }
}

/*
** rename_current_file - attempt to rename the current file
*/

static void rename_current_file(DirList *dl)
{
    const char *from_file = get_file_name(dl);
    
    if (strcmp(from_file, ".")  == 0 || strcmp(from_file, "..") == 0)
    {
        message("`renaming' of `.' and `..' is not allowed");
        move_cursor(dl->savedYPos(), dl->savedXPos());
        synch_display();
        return;
    }
    
    const char *to_file = prompt("Rename to: ");
    
    if (to_file == 0)
    {
        move_cursor(dl->savedYPos(), dl->savedXPos());
        synch_display();
        return;
    }
    else if (*to_file == 0)
    {
        message("not a valid file name");
        move_cursor(dl->savedYPos(), dl->savedXPos());
        synch_display();
        return;
    }

    if (*to_file == '~') to_file = expand_tilde(to_file);
    rename_file(dl, from_file, to_file);
    synch_display();
}

/*
** compress_file - compress the current file and update the window.
*/

static void compress_file(DirList *dl, const char *file)
{
    String cmd = String("compress") + " -f '" + file + "' 1>/dev/null 2>&1";

    if (!system(cmd))
    {
        message("Compressing ... ");

        String command = String(ls_cmd[the_sort_order()]) + "'" + file +
                         ".Z' 2>/dev/null";

        FILE *fp = popen(command, "r");
        if (fp == 0)
            error("File %s, line %d: popen(%s) failed",
                  __FILE__, __LINE__, (const char *)command);

        char *new_line = fgetline(fp);
        if (new_line)
            //
            // Only update the line if we were able to read the line.
            // Some versions of compress don\'t properly return a failure
            // code if they don\'t compress the file.  Hence we probably
            // still have the uncompressed file lying around.
            //
            update_current_line(dl, new_line);
        (void)pclose(fp);

        message("Compressing ... done");
    }
    else
        message("`compress' failed");
}

/*
** compress_current_file - attempt to compress current file
*/

static void compress_current_file(DirList *dl)
{
    const char *file = get_file_name(dl);

    //
    // Disallow compressing of symbollically linked files.
    //
    if (strstr(&(dl->currLine()->line())[dl->savedXPos()], " -> "))
    {
        message("compress'ing symbollically linked files not allowed");
        move_cursor(dl->savedYPos(), dl->savedXPos());
        synch_display();
        return;
    }
    
    if (file[strlen(file)-1] == 'Z' && file[strlen(file) - 2] == '.')
    {
        message("file appears to already be compressed");
        move_cursor(dl->savedYPos(), dl->savedXPos());
        synch_display();
        return;
    }
    
    if (is_regular_file(file))
        compress_file(dl, file);
    else
        message("can only `compress' regular files");
    move_cursor(dl->savedYPos(), dl->savedXPos());
    synch_display();
}

/*
** chgrp_file - change the group of the current file.
**              Update the window appropriately.
*/

static void chgrp_file(DirList *dl, const char *file, const char *group)
{
    const char *chgrp  = "chgrp";
    const char *args[4];
    args[0] = chgrp; args[1] = group; args[2] = file; args[3] = 0;

    if (execute(chgrp, args))
    {
        String command = ls_cmd[the_sort_order()];
        if (is_directory(file)) command += "-d";
        command += String("'") + file + "'";

        FILE *fp = popen(command, "r");
        if (fp == 0)
            error("File %s, line %d: popen(%s) failed",
                  __FILE__, __LINE__, (const char *)command);

        char *new_line = fgetline(fp);
        if (fp == 0)
            error("File %s, line %d: fgetline() failed", __FILE__, __LINE__);
        (void)pclose(fp);

        update_current_line(dl, new_line);
    }
    else
        message("`chgrp' failed");
}

/*
** chgrp_current_file - attempt to chgrp current file
*/

static void chgrp_current_file(DirList *dl)
{
    const char *file  = get_file_name(dl);
    const char *group = prompt("Change to group: ");
    
    if (group == 0)
    {
        move_cursor(dl->savedYPos(), dl->savedXPos());
        synch_display();
        return;
    }
    else if (*group == 0)
    {
        message("not a valid group");
        move_cursor(dl->savedYPos(), dl->savedXPos());
        synch_display();
        return;
    }
    
    chgrp_file(dl, file, group);
    move_cursor(dl->savedYPos(), dl->savedXPos());
    synch_display();
}

/*
** chmod_file - change the mode of the current file.
**              Update the window appropriately.
*/

static void chmod_file(DirList *dl, const char *file, const char *mode)
{
    const char *chmod = "chmod";
    const char *args[4];
    args[0] = chmod; args[1] = mode; args[2] = file; args[3] = 0;

    if (execute(chmod, args))
    {
        String command = ls_cmd[the_sort_order()];
        if (is_directory(file)) command += "-d";
        command += String("'") + file + "'";

        FILE *fp = popen(command, "r");
        if (fp == 0)
            error("File %s, line %d: popen(%s) failed",
                  __FILE__, __LINE__, (const char *)command);

        char *new_line = fgetline(fp);
        if (fp == 0)
            error("File %s, line %d: fgetline() failed", __FILE__, __LINE__);
        (void)pclose(fp);

        update_current_line(dl, new_line);
    }
    else
        message("`chmod' failed");
}

/*
** chmod_current_file - attempt to chmod the current file
*/

static void chmod_current_file(DirList *dl)
{
    const char *file = get_file_name(dl);
    const char *mode = prompt("Change to mode: ");
    
    if (mode == 0)
    {
        move_cursor(dl->savedYPos(), dl->savedXPos());
        synch_display();
        return;
    }
    else if (*mode == 0)
    {
        message("not a valid file mode");
        move_cursor(dl->savedYPos(), dl->savedXPos());
        synch_display();
        return;
    }
    
    chmod_file(dl, file, mode);
    
    move_cursor(dl->savedYPos(), dl->savedXPos());
    synch_display();
}

/*
** print_current_file - attempt to print current file
*/

static void print_current_file(DirList *dl)
{
    const char *file = get_file_name(dl);
    char *printer    = getenv("DIREDPRT");
    if (printer == 0) printer = "lpr";
    String cmd = String(printer) + " '" + file + "'";
    
    if (is_regular_file(file))
        if (!system(cmd))
            message("`%s' printed successfully", file);
        else
            message("print of `%s' failed", file);
    else
        message("`%s' isn't a regular file", file);
    
    move_cursor(dl->savedYPos(), dl->savedXPos());
    synch_display();
}

/*
** uncompress_file - uncompress the file at line y in the DirList.
**                   Update the window appropriately.
*/

static void uncompress_file(DirList *dl, const char *file)
{
    String cmd = String("uncompress") + " '" + file + "' 1>/dev/null 2>&1";

    if (!system(cmd))
    {
        message("Uncompressing ... ");

        char *dot = strrchr(file, '.');
        *dot = 0;  // remove .Z suffix

        String command = String(ls_cmd[the_sort_order()]) + "'" + file +
                         "' 2>/dev/null";

        FILE *fp = popen(command, "r");
        if (fp == 0)
           error("File %s, line %d: popen(%s) failed",
                  __FILE__, __LINE__, (const char *)command);            

        char *new_line = fgetline(fp);
        if (new_line)
            //
            // Only update the line if we were able to read the line.
            // Some versions of compress don\'t properly return a failure
            // code if they don\'t uncompress the file.  Hence we probably
            // still have the .Z file lying around.
            //
            update_current_line(dl, new_line);
        (void)pclose(fp);

        message("Uncompressing ... done");
    }
    else
        message("`uncompress' failed");
}

/*
** uncompress_current_file - attempt to uncompress current file
*/

static void uncompress_current_file(DirList *dl)
{
    const char *file = get_file_name(dl);
    
    if (file[strlen(file)-1] != 'Z' && file[strlen(file)-1] != '.')
    {
        message("can only `uncompress' compressed files");
        move_cursor(dl->savedYPos(), dl->savedXPos());
        synch_display();
        return;
    }
    
    uncompress_file(dl, file);

    move_cursor(dl->savedYPos(), dl->savedXPos());
    synch_display();
}

/*
** search_forward - search forward for file or directory matching string.
**                  The search always starts with the file following
**                  the current file.  If a match occurs, the cursor
**                  is placed on the new current file, with that line
**                  placed at the top of the window.
*/

static void search_forward(DirList *dl)
{
    const char *str = prompt("Search forward for: ");
    if (str == 0)
    {
        move_cursor(dl->savedYPos(), dl->savedXPos());
        synch_display();
        return;
    }                  
    
    DirLine *ln = dl->currLine();
    if (ln == dl->tail())
    {
        message("no match");
        move_cursor(dl->savedYPos(), dl->savedXPos());
        return;
    }
    
    int pos = dl->savedYPos()+1;
    DirLine *found = search(ln->next(), str, &pos);
    if (found)
    {
        if (dl->currLine()->length() > columns()) rightshift_current_line(dl);
        dl->setCurrLine(found);
    }
    
    if (found && pos < rows()-2)
    {
        //
        // We found a match in our current window.
        // Only need to update the cursor position.
        //
        dl->saveYXPos(pos, goal_column(dl));
        if (dl->currLine()->length() > columns())
            leftshift_current_line(dl);
        else
            move_cursor(pos, dl->savedXPos());
    }
    else if (found)
    {
        //
        // We found a match, but it is out of our current view.
        // Place the matched line at the top of the window.
        //
        ln = found;
        dl->setFirst(ln);
        cursor_home();
        for (int i = 0; i < rows() - 2 && ln; i++, ln = ln->next())
        {
            clear_to_end_of_line();
            display_string(ln->line(), ln->length());
        }
        move_cursor(i, 0);
        for (; i < rows() - 2; i++)
        {
            clear_to_end_of_line();
            cursor_down();
        }

        ln ? dl->setLast(ln->prev()) : dl->setLast(dl->tail());
        dl->saveYXPos(0, goal_column(dl));
        if (dl->currLine()->length() > columns())
            leftshift_current_line(dl);
        else
            move_cursor(0, dl->savedXPos());
    }
    else
    {
        message("no match");
        move_cursor(dl->savedYPos(), dl->savedXPos());
    }
    
    synch_display();
}

/*
** search_backward - search backward for file or directory matching string.
**                   The search always starts with the file following
**                   the current file.  If a match occurs, the cursor
**                   is placed on the new current file, with that line
**                   placed at the top of the window.
*/

static void search_backward(DirList *dl)
{
    const char *str = prompt("Search backward for: ");
    if (str == 0)
    {
        move_cursor(dl->savedYPos(), dl->savedXPos());
        synch_display();
        return;
    }
    
    DirLine *ln = dl->currLine();
    if (ln == dl->head()) return;
    
    int pos = dl->savedYPos()-1;
    DirLine *found = search(ln->prev(), str, &pos, 0);
    if (found)
    {
        if (dl->currLine()->length() > columns()) rightshift_current_line(dl);
        dl->setCurrLine(found);
    }
    
    if (found && pos >= 0)
    {
        //
        // We found a match in our current window.
        // Only need to update the cursor position.
        //
        dl->saveYXPos(pos, goal_column(dl));
        if (dl->currLine()->length() > columns())
            leftshift_current_line(dl);
        else
            move_cursor(pos, dl->savedXPos());
    }
    else if (found)
    {
        //
        // We found a match, but it is out of our
        // current view.  Place the matched line at the top
        // of the window.  Since we\'re searching backwards
        // and the match is out of our present window, we
        // must have a whole new window to display.
        //
        dl->setFirst(ln = found);
        cursor_home();
        for (int i = 0; i < rows() - 2 && ln->next(); i++, ln = ln->next())
        {
            clear_to_end_of_line();
            display_string(ln->line(), ln->length());
        }
        
        dl->setLast(ln->prev());
        dl->saveYXPos(0, goal_column(dl));

        if (dl->currLine()->length() > columns())
            leftshift_current_line(dl);
        else
            move_cursor(0, dl->savedXPos());
    }
    else
    {
        message("no match");
        move_cursor(dl->savedYPos(), dl->savedXPos());
    }
    synch_display();
}

//
// Help message for the message window when displaying help.
//
const char *const HELP_MSG[] = {
    "Space scrolls forward.  Other keys quit.",
    "Space forward, Backspace backward.  Other keys quit.",
    "Backspace scrolls backward.  Other keys quit."
};

/*
** help - give some help.  Deal with SIGWINCH and SIGTSTP.
*/

static void help(DirList *dl)
{
    update_modeline("----- HELP");
    
    int position = 0;
    char key;
    do
    {
        cursor_home();
        for (int i = 0; i < rows() - 2 && i + position < HELP_FILE_DIM; i++)
        {
            clear_to_end_of_line();
            display_string(help_file[position + i]);
        }
        move_cursor(i, 0);
        for (; i < rows() - 2; i++)
        {
            clear_to_end_of_line();
            cursor_down();
        }
        clear_message_line();

        if (position + rows() -2 >= HELP_FILE_DIM)
            //
            // The tail of the help message.
            //
            (void)fputs(HELP_MSG[2], stdout);
        else if (position == 0)
            //
            // The head of the help message.
            //
            (void)fputs(HELP_MSG[0], stdout);
        else
            //
            // Somewhere in between.
            //
            (void)fputs(HELP_MSG[1], stdout);

        synch_display();

        if (read(0, &key, 1) < 0 // assume fails only when errno == EINTR
#ifdef SIGWINCH
            || win_size_changed
#endif
            )
        {
#ifdef SIGWINCH
            if (win_size_changed)
            {
                win_size_changed = 0;
                adjust_window();
                redisplay();
            }
#endif
        }
        else if (key == KEY_SPC)
        {
            if (position >= HELP_FILE_DIM - 1) goto finished;
            position += rows() - 2;
        }
        else if (key == *BC)
        {
            if (position == 0) goto finished;
            position -= rows() - 2;
        }
        else 
            goto finished;  // return to the listing
    }
    while (position < HELP_FILE_DIM - 1);

  finished:
    update_modeline(modeline_prefix, dl->name());
    redisplay();
}

/*
** reread_current_directory - reread the current directory and
**                            put it up fresh on the screen.  We
**                            attempt to put the cursor back onto
**                            the previous current file, if that file
**                            still exists.
*/

static void reread_current_directory(DirList * &dlr)
{
    char *dirname = new char[strlen(dlr->name()) + 1];
    (void)strcpy(dirname, dlr->name());
    const char *old_current_file = get_file_name(dlr);

    int old_pos;  // position in old DirList of old_current_file
    DirLine *ln = dlr->head();
    for (old_pos = 0; ln != dlr->currLine(); ln = ln->next(), old_pos++) ;

    delete dir_stack->pop();
    
    DirList *dl = get_directory_listing(dirname);
    if (dl == 0)
        error("File %s, line %d: couldn't read directory `%s'",
              __FILE__, __LINE__, dirname);

    dlr = dl;  // update dir_list in read_commands\(\)

    dir_stack->push(dl);
    
    ln = dl->head();
    int pos = 0;
    const char *name;
    int namelen = 0;
    while (ln)
    {

#ifndef NO_SYMLINKS
        if ((name = strstr(ln->line(), " -> ")) != 0)
            // we have a symbolic link
            --name;
        else
#endif
            name = ln->line() + ln->length();

        for (namelen = 0; isspace(*name) == 0; name--, namelen++) ;

        if (strncmp(name + 1, old_current_file, namelen) == 0) break;

        ln = ln->next();
        pos++;
        namelen = 0;
    }

    if (ln == 0)
    {
        //
        // Update ln and pos to refer to a suitable file on
        // which to place cursor.
        //
        ln = dl->head();
        for (pos = 0; pos < old_pos && ln; ln = ln->next(), pos++) ;
    }

    dl->setCurrLine(ln);

    if (pos < rows() - 2)
    {
        //
        // Display starting at the head.
        //
        dl->setFirst(ln = dl->head());
        dl->saveYXPos(pos, goal_column(dl));
    }
    else
    {
        //
        // Center dl->currLine.
        //
        ln = dl->currLine();
        for (int i = 0; i < (rows() - 1)/2; i++, ln = ln->prev()) ;
        dl->setFirst(ln);
        dl->saveYXPos((rows() - 1)/2, goal_column(dl));
    }

    cursor_home();
    for (int i = 0; i < rows() - 2 && ln; ln = ln->next(), i++)
    {
        clear_to_end_of_line();
        display_string(ln->line(), ln->length());
    }
    move_cursor(i, 0);
    for (; i < rows() - 2; i++)
    {
        clear_to_end_of_line();
        cursor_down();
    }

    ln ? dl->setLast(ln->prev()) : dl->setLast(dl->tail());
    
    if (dl->currLine()->length() > columns())
        leftshift_current_line(dl);
    else
        move_cursor(dl->savedYPos(), dl->savedXPos());
    
    synch_display();
}

/*
** expand_percent - expand any percent signs in cmd to file.  The result is
**                  stored in volatile storage.
*/

static const char *expand_percent(const char *cmd, const char *file)
{
    static char *expanded_cmd;
    DELETE expanded_cmd;

    if (!strchr(cmd, '%')) return cmd;

    expanded_cmd  = new char[strlen(cmd) + 1];
    (void)strcpy(expanded_cmd, cmd);
    char *pos = strchr(expanded_cmd, '%');

    while (pos)
    {
        char *full_cmd = new char[strlen(expanded_cmd) + strlen(file)];
        (void)strncpy(full_cmd, expanded_cmd, pos - expanded_cmd);
        full_cmd[pos - expanded_cmd] = '\0';
        (void)strcat(full_cmd, file);
        (void)strcat(full_cmd, pos + 1);
        DELETE expanded_cmd;
        expanded_cmd = full_cmd;
        pos = strchr(expanded_cmd, '%');
    }

    return expanded_cmd;
}

/*
** shell_command - execute a shell command.  % is expanded to
**                 the current filename.
**                 If *cmd == 0, start up a shell.
**                 If *cmd == !, reexecute most recent shell command.
*/

static void shell_command(DirList *dl)
{
    static String original_cmd;
    static String saved_cmd;
    static String saved_shell;
    const char *file = get_file_name(dl);
    const char *cmd  = prompt("!");

    if (cmd == 0)
    {
        //
        // command aborted
        //
        move_cursor(dl->savedYPos(), dl->savedXPos());
        synch_display();
        return;
    }
    else if (*cmd == '\0')
    {
        //
        // start up a shell
        //
        if (saved_shell == "") saved_shell = getenv("SHELL");
        if (saved_shell == "") saved_shell = "sh";
        saved_cmd = original_cmd = saved_shell;

        const char *slash = strrchr(saved_shell, '/');
        const char *args[2];
        args[0] = slash ? slash + 1 : (const char *)saved_shell;
        args[1] = 0;

        message("Starting interactive shell ...");

        cursor_wrap();
        synch_display();
        unsetraw();
        unset_signals();

        execute(saved_shell, args, 0);

        set_signals();
        setraw();
    }
    else if (*cmd == '[')
    {
        //
        // Re-expand previous original command, if it contains a %.
        // Otherwise, re-execute previous saved command since the original
        // and saved will be the same.
        //
        if (original_cmd != "")
        {
            //
            // expand the `%\'
            //
            saved_cmd = expand_percent(original_cmd, file);
            message(saved_cmd);
            cursor_wrap();
            synch_display();
            exec_with_system(saved_cmd);
        }
        else
        {
            message("No Previous Shell Command");
            move_cursor(dl->savedYPos(), dl->savedXPos());
            synch_display();
            return;
        }
    }
    else if (*cmd == '!')
    {
        //
        //  re-execute previously saved command
        //
        if (saved_cmd != "")
        {
            message(saved_cmd);
            cursor_wrap();
            synch_display();
            exec_with_system(saved_cmd);
        }
        else
        {
            message("No Previous Shell Command");
            move_cursor(dl->savedYPos(), dl->savedXPos());
            synch_display();
            return;
        }
    }
    else
    {
        //
        // expand and then execute command
        //
        original_cmd = cmd;
        saved_cmd = expand_percent(original_cmd, file);
        message(saved_cmd);
        cursor_wrap();
        synch_display();
        exec_with_system(saved_cmd);
    }
    redisplay();
}

/*
** change_sort_order - query the user for a new sorting order - expects to
**                     read a single character - and change the sort order
**                     to that value, if it is a valid value.  Deals with
**                     SIGWINCH and SIGTSTP.
*/

static void change_sort_order(DirList *dl)
{
    const char *msg = "Set sort order to? [a, c, t, u]: ";
    message(msg);

    char key;
    while (1)
        if (read(0, &key, 1) < 0 // assume fails only when errno == EINTR
#ifdef SIGWINCH
            || win_size_changed
#endif
            )
        {
#ifdef SIGWINCH
            if (win_size_changed)
            {
                win_size_changed = 0;
                adjust_window();
                redisplay();
            }
#endif
            //
            // Redisplay the message - termstop\(\) takes care of the rest
            // in the case of SIGTSTP.
            //
            message(msg);
        }
    else
        switch(key)
        {
          case 'a':
            set_sort_order(ALPHABETICALLY);
            message("Sorting `alphabetically' now.");
            goto end;
          case 'c':
            set_sort_order(INODE_CHANGE_TIME);
            message("Sorting by `inode-change time' now.");
            goto end;
          case 't':
            set_sort_order(MODIFICATION_TIME);
            message("Sorting by `modification time' now.");
            goto end;
          case 'u':
            set_sort_order(ACCESS_TIME);
            message("Sorting by `access time' now.");
            goto end;
          default:
            message("Sort order not changed.");
            goto end;
        }
  end:
    move_cursor(dl->savedYPos(), dl->savedXPos());
    synch_display();
}

/*
** read_commands - the command loop
*/

void read_commands(DirList *dir_list)
{
    int key;
    for (;;)
    {
        switch (key = get_key(dir_list))
        {
          case KEY_j         :
          case KEY_n         :
          case KEY_CTL_N     :
          case KEY_SPC       :
          case KEY_CR        :
          case KEY_ARROW_DOWN:

              if (dir_list->atEndOfList()) break;

              if (dir_list->currLine()->length() > columns()) 
                  rightshift_current_line(dir_list);
    
              if (dir_list->savedYPos() < rows() - 3)
              {
                  //
                  // There are still more lines below us in the window
                  // so we just move the cursor down one line.
                  //
                  dir_list->setCurrLine(dir_list->currLine()->next());
                  int x = goal_column(dir_list);
                  if (x == dir_list->savedXPos())
                      cursor_down();
                  else
                      move_cursor(dir_list->savedYPos() + 1, x);
                  dir_list->saveYXPos(dir_list->savedYPos() + 1, x);
              }
              else
                  //
                  // We are on the last line on the screen and there
                  // are more lines to display.  Scroll up one line
                  // and leave the cursor on the next logical line.
                  //
                  scroll_up_one_line(dir_list);

              if (dir_list->currLine()->length() > columns())
                  leftshift_current_line(dir_list);

              synch_display();
              break;

          case KEY_k       :
          case KEY_p       :
          case KEY_CTL_P   :
          case KEY_CTL_Y   :
          case KEY_ARROW_UP:

              if (dir_list->atBegOfList()) break;

              if (dir_list->currLine()->length() > columns())
                  rightshift_current_line(dir_list);

              if (dir_list->savedYPos() != 0)
              {
                  // We aren\'t at the top of the window so can mOve up.
                  dir_list->setCurrLine(dir_list->currLine()->prev());
                  int x = goal_column(dir_list);
                  if (x == dir_list->savedXPos() && UP)
                      cursor_up();
                  else
                      move_cursor(dir_list->savedYPos() - 1, x);
                  dir_list->saveYXPos(dir_list->savedYPos() - 1, x);
              }
              else
                  //
                  // We are on the first line of the window and there are
                  // lines preceding us in the directory listing.
                  //
                  scroll_down_one_line(dir_list);

              if (dir_list->currLine()->length() > columns())
                  leftshift_current_line(dir_list);

              synch_display();
              break;

          case KEY_CTL_F:
          case KEY_CTL_V:

              if (dir_list->lastLine() == dir_list->tail()) break;
              scroll_up_full_window(dir_list);

              break;
          case KEY_b    :
          case KEY_CTL_B:

              if (dir_list->firstLine() == dir_list->head()) break;
              scroll_down_full_window(dir_list);

              break;
          case KEY_CTL_D:

              if (dir_list->lastLine() == dir_list->tail()) break;
              scroll_up_half_window(dir_list, dir_list->savedYPos());

              break;
          case KEY_CTL_U:

              if (dir_list->firstLine() == dir_list->head()) break;
              scroll_down_half_window(dir_list, dir_list->savedYPos());

              break;
          case KEY_TOP:

              if (dir_list->atBegOfList()) break;
              goto_first(dir_list);

              break;
          case KEY_BOT:

              if (dir_list->atEndOfList()) break;
              goto_last(dir_list);

              break;
          case KEY_e:
          case KEY_f:

              edit_current_file(dir_list); break;

          case KEY_m:
          case KEY_v:

              page_current_file(dir_list); break;

          case KEY_c:

              copy_current_file(dir_list); break;

          case KEY_d:

              delete_current_file(dir_list); break;

          case KEY_r:

              rename_current_file(dir_list); break;

          case KEY_C:

              compress_current_file(dir_list); break;

          case KEY_E:

              edit_prompted_for_directory(dir_list); break;

          case KEY_G:

              chgrp_current_file(dir_list); break;

          case KEY_QM:
          case KEY_H :

              help(dir_list); break;

          case KEY_L:

              link_current_file(dir_list); break;

          case KEY_M:

              chmod_current_file(dir_list); break;

          case KEY_O:

              change_sort_order(dir_list); break;

          case KEY_P:

              print_current_file(dir_list); break;

          case KEY_g:
          case KEY_R:

              reread_current_directory(dir_list); break;

#ifndef NO_SYMLINKS
          case KEY_S:

              symlink_current_file(dir_list); break;
#endif

          case KEY_U:

              uncompress_current_file(dir_list); break;

          case KEY_SLASH:

              search_forward(dir_list); break;

          case KEY_BKSLASH:

              search_backward(dir_list); break;

          case KEY_BANG:

              shell_command(dir_list); break;

          case KEY_V:

              message(version);
              move_cursor(dir_list->savedYPos(), dir_list->savedXPos());
              synch_display();
              break;

          case KEY_CTL_L:

              redisplay(); break;

          case KEY_q:

              if (dir_stack->nelems() > 1)
              {
                  delete dir_stack->pop();
                  dir_list = dir_stack->top();

                  // update our current directory
                  if (chdir(dir_list->name()) < 0)
                      error("File %s, line %d: couldn't chdir() to `%s'",
                            __FILE__, __LINE__, dir_list->name());
                  //
                  // We track the CWD and PWD variables if they\'re defined,
                  // so that applications, such as emacs, which use them
                  // will work properly.
                  //
                  if (getenv("CWD"))
                  {
                      static String str;
                      static String ostr;
                      str = String("CWD=") + dir_list->name();
                      if (putenv(str) < 0)
                          error("File %s, line %d: putenv(%s) failed.",
                                __FILE__, __LINE__, dir_list->name());
                      ostr = str;
                  }
                  if (getenv("PWD"))
                  {
                      static String str;
                      static String ostr;
                      str = String("PWD=") + dir_list->name();
                      if (putenv(str) < 0)
                          error("File %s, line %d: putenv(%s) failed.",
                                __FILE__, __LINE__, dir_list->name());
                      ostr = str;
                  }
                  
                  update_modeline(modeline_prefix, dir_list->name());
                  redisplay();
              }
              else
              {
                  term_display();
                  exit(0);
              }
              break;

          case KEY_ESC:

              // some Emacs ESC key bindings

              switch(get_key(dir_list))
              {
                case KEY_v:
                  if (dir_list->firstLine() == dir_list->head()) break;
                  scroll_down_full_window(dir_list);
                  break;
                case KEY_TOP:
                  if (dir_list->atBegOfList()) break;
                  goto_first(dir_list);
                  break;
                case KEY_BOT:
                  if (dir_list->atEndOfList()) break;
                  goto_last(dir_list);
                  break;
                default:
                  ding();
                  break;
              }
          break;

          case KEY_Q:

              term_display(); exit(0); break;

          default:

              ding(); break;
        }
    }
}
