/* File: cdargs.cc
 *
 *     This file is part of cdargs
 *
 *     Copyright (C) 2001-2003 by Stefan Kamphausen
 *     Author: Stefan Kamphausen <http://www.skamphausen.de>
 *
 *     Time-stamp: <26-Feb-2006 18:06:47 ska>
 * 
 *     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.
 */ 

/********************************************************************/
/* MAN, IF YOU DIDN'T TRY TO FIX THIS CODE YOU DON'T KNWO HOW MUCH  */
/*                    IT NEEDS A COMPLETE REWRITE                   */
/*                           (the author)                           */
/********************************************************************/


// damn, if you remove this, you get lots of trouble that _I_ don't
// wanna get involved in. I've been reading include files for more
// than one hour now, and that's my decision.  The code is a mess
// anyway. 
#ifndef _GNU_SOURCE
# define _GNU_SOURCE
#endif

/* C++ Includes */
# include <iostream>
# include <string>
# include <vector>
# include <algorithm>
# include <fstream>
# include <map>
using namespace std;

/* C Includes */
# include <stdio.h>
# include <stdlib.h>
# include <sys/types.h>
# include <sys/stat.h>
# include <dirent.h>
# include <getopt.h>
# include <unistd.h>
# include <signal.h>
# include <string.h>

//# if defined(USE_NCURSES) && !defined(RENAMED_NCURSES)
# if defined(HAVE_NCURSES_H)
# include <ncurses.h>
# else
# include <curses.h>
# endif

# include "cdargs.h"

/* GLOBALS */
// FIXME: this file has too many globals. Especially the
// offset/position handling is UGLY!
//

// Change this if you want different length (actually width) for the
// description:
# define DESCRIPTION_MAXLENGTH 18
# define DESCRIPTION_BROWSELENGTH 8

const char* DefaultListfile = ".cdargs";
const char* DefaultResultfile = "~/.cdargsresult";

const char* Needle = NULL;
bool NeedleGiven = false;

int CurrPosition = 0;
map<const char*, int, ltstr> LastPositions;

// terminal coordinates and other curses stuff
int  terminal_width, terminal_height;
int  xmax;
int  display_area_ymax;
int  display_area_ymin;
int  modeliney;
int  msg_area_y;
int  yoffset = 0;
bool curses_running = false;
int shorties_offset = 0;

// cdargs modi
int  mode = LIST;
bool show_hidden_files = false;
bool opt_no_wrap = false;
bool opt_no_resolve = false;
bool listfile_empty = false;
bool opt_cwd = false;

string opt_resultfile="";
string opt_listfile="";
string opt_user="";

int main(int argc, char** argv) {
    int c;
    /* parse command line args */
    while (1) {
        int option_index = 0;
        static struct option long_options[] =
            {
                {"add"        , 1, 0, 0},
                {"file"       , 1, 0, 0},
                {"user"       , 1, 0, 0},
                {"browse"     , 0, 0, 0},
                {"nowrap"     , 0, 0, 0},
                {"noresolve"  , 0, 0, 0},
                {"cwd"        , 0, 0, 0},
                {"output"     , 1, 0, 0},
                {"version"    , 0, 0, 0},
                {"help"       , 0, 0, 0},
                {0, 0, 0, 0}
            };
        c = getopt_long (argc, argv, "a:f:u:brco:vh",
                         long_options, &option_index);
        if (c == -1) {
            break;
        }
        string optname;
        string argument;
        switch (c)
            {
            case 0:
                optname = string(long_options[option_index].name);
                if (optname == "help") {
                    version();
                    usage();
                    exit(0);
                }
                if (optname == "version") {
                    version();
                    exit(0);
                }
                if (optname == "add") {
                    argument = string(optarg);
                    add_to_list_file(argument);
                    exit(0);
                }
                if (optname == "file") {
                    opt_listfile = string(optarg);
                }
                if (optname == "user") {
                    opt_user = string(optarg);
                }
                if(optname == "browse") {
                    mode = BROWSE;
                }
                if (optname == "nowrap") {
                    opt_no_wrap = true;
                }
                if (optname == "noresolve") {
                    opt_no_resolve = true;
                }
                if (optname == "cwd") {
                    opt_cwd = true;
                }
                if (optname == "output") {
                    opt_resultfile = string(optarg);
                }
                break;             
            case 'a':
                argument = string(optarg);
                add_to_list_file(argument);
                exit(0);
                break;
            case 'f':
                opt_listfile = string(optarg);
                break;
            case 'u':
                opt_user = string(optarg);
                break;
            case 'b':
                mode = BROWSE;
                break;
            case 'r':
                opt_no_resolve = true;
                break;
            case 'c':
                opt_cwd = true;
                break;
            case 'o':
                opt_resultfile = string(optarg);
                break;
            case 'h':
                version();
                usage();
                exit(0);
                break;
            case 'v':
                version();
                exit(0);
            default:
                usage();
                exit(1);
            }
    }

    if (optind < argc) {
        Needle = argv[optind];
        if(strlen(Needle) > 0) {
            NeedleGiven = true;
        } 
        if(strlen(Needle) == 1 && isdigit(Needle[0])) {
            CurrPosition = atoi(Needle);
            Needle = NULL;
        }
    }
    // leave terminal tidy
    // FIXME: what other signals do I need to catch?
    signal(SIGINT, terminate);
    signal(SIGTERM, terminate);
    signal(SIGSEGV, terminate);
    init_curses();
    // answer to terminal reizing
    signal(SIGWINCH, resizeevent);
    /* get list from file or start in browse mode */
    if (!list_from_file()) {
        /* doesn't exist. browse current dir */
        mode = BROWSE;
    }
    // if we're browsing read the CWD
    if(mode == BROWSE) {
        list_from_dir();
    }

    /* main event loop */
    /* determines the entry to use */
    while (1) {
        display_list();
        c = getch();
        if (!user_interaction(c)) {
            break;
        }
    }
   
    finish(current_entry(), true);
    exit(1);
}

void toggle_mode(void) {
    if (mode == LIST) {
        mode = BROWSE;
        list_from_dir(".");
    } else {
        if(listfile_empty) { // list was empty at start
            if(!default_list.empty()) { // but isn't now 
                list_to_file();
                list_from_file();
                mode = LIST;
            } else {                   // ok, we have nothing to show here
                message("No List entry. Staying in BROWSE mode");
                mode = BROWSE;
                list_from_dir(".");
            }
        } else {         // the "normal" case;
            // disable Needle and reload the list!
            NeedleGiven = false;
            Needle = NULL;
            list_from_file();
            yoffset = 0;
            shorties_offset = 0;
            CurrPosition = 0;
            mode = LIST;
        }
    }
}

void toggle_hidden(void) {
    show_hidden_files = !show_hidden_files;
    if (mode == BROWSE) {
        list_from_dir();
    }
}

bool user_interaction(int c) {
    int num;
    string curen;
    switch(c) {
        // ==== Exits
    case CTRL('['): // vi
    case CTRL('g'): // emacs
        abort_cdargs();
        break;
    case 'q':
        finish(".", false);
        break;
    case 13: // ENTER
        // choose dir at cur pos
        return false;
        break;
    case '0':
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9':
        num =  c-'0'+shorties_offset;
        if(mode == LIST) {
            if (entry_nr_exists(num)) {
                CurrPosition = num+yoffset;
                return opt_no_resolve?true:false;
            }
        } else {
            CurrPosition = num+yoffset;
            return true;
        }
        break;      

        // ==== Modes
    case '.':
        // show hidden files
        toggle_hidden();
        break;
    case '\t': // TAB
        toggle_mode();
        break;      

        // ==== Navigate The List
    case 'j':       // vi
    case CTRL('n'): // emacs
#if TERMINFO
    case KEY_DOWN:
#endif
        // navigate list++
        cur_pos_adjust(+1);
        break;
    case 'k':       // vi
    case CTRL('p'): // emacs
#if TERMINFO
    case KEY_UP:
#endif
        // navigate list--
        cur_pos_adjust(-1);
        break;

    case '^':       // vi?
    case CTRL('a'): // emacs
#if TERMINFO
    case KEY_HOME:
#endif
        // go to top
        CurrPosition = 0;
        yoffset = 0;
        break;

    case '$':       // vi
    case CTRL('e'): // emacs
# if TERMINFO
    case KEY_END:
#endif
        // go to end
        if (mode == BROWSE) {
            CurrPosition = cur_list.size()-1;
        }
        else {
            CurrPosition = default_list.size()-1;
        }
        yoffset = max_yoffset();
        break;
      
        //==== move the shortcut digits ('shorties')
        // FIXME case ??:  //vi
        // FIXME maybe change the scrolling behaviour to not take the window but the whole
        // list? That means recentering when shorties leave the screen (adjust yoffset and
        // CurrPosition) 
    case CTRL('v'): // emacs
#if TERMINFO      
    case KEY_NPAGE:
#endif
        for(int i=0;i<10;i++) {
            cur_pos_adjust(+1,false);
        }
        break;

        // fixme: vi?
        //case CTRL(''): // FIXME: META(x)??
#if TERMINFO
    case KEY_PPAGE:
#endif
        for(int i=0;i<10;i++) {
            cur_pos_adjust(-1,false);
        }
        break;      

    case 'h':       // vi
    case CTRL('b'): // emacs
#if TERMINFO
    case KEY_LEFT:
#endif
        // up dir
        if (mode == BROWSE) {
//             LastPositions[get_current_dir_name()] = CurrPosition;
            LastPositions[get_cwd_as_charp()] = CurrPosition;
        } else {
            mode = BROWSE;
        }
        list_from_dir("..");
        break;
    case 'l':       // vi
    case CTRL('f'): // emacs
#if TERMINFO
    case KEY_RIGHT:
#endif
        // descend dir at cur pos
        curen = current_entry();
        if (mode == BROWSE) {
            LastPositions[get_cwd_as_charp()] = CurrPosition;
        } else {
            mode = BROWSE;
        }
        list_from_dir(curen.c_str());
        break;
    case 'H':
    case '?':
        helpscreen();
        break;

    case 'd':
    case CTRL('d'): // emacs
#if TERMINFO
    case KEY_BACKSPACE:
#endif
        // delete dir acp
        if (mode == LIST && !NeedleGiven) {
            delete_from_default_list(CurrPosition);
        } else {
            beep();
        }
        break;

#if TERMINFO
    case KEY_IC:
#endif
    case 'a':
        // add dir acp (if in browse mode)
        if(!NeedleGiven) {
            if (mode == BROWSE) {
                add_to_default_list(current_entry());
            } else {
                add_to_default_list(get_cwd_as_string());
            }
        } else {
            beep();
        }
        break;
    case 'A':
        if(!NeedleGiven) {
            // add dir acp (if in browse mode) (ask for desc)
            if (mode == BROWSE) {
                add_to_default_list(current_entry(),"",true);
            } else {
                add_to_default_list(get_cwd_as_string(),"",true);
            }
        } else {
            beep();
        }
        break;
    case 'c':
        if(!NeedleGiven) {
            // add the current dir in every mode
            add_to_default_list(get_cwd_as_string());
        } else {
            beep();
        }
        break;
    case 'C':
        if(!NeedleGiven) {
            // add the current dir in every mode (ask for desc)
            add_to_default_list(get_cwd_as_string(),"",true);
        } else {
            beep();
        }
        break;

        // ==== EDIT the list
    case 'v':
    case 'e':
    {
        if(!NeedleGiven) {
            edit_list_file();
        } else {
            beep();
        }
        break;
    }
    case 'm':
        swap_two_entries(1);
        break;
    case 'M':
        swap_two_entries(-1);
        break;
    case 't':
    case 's':
        swap_two_entries(0);
        break;

        // ==== Filesystem Hotspots
    case '~':
        mode = BROWSE;
        list_from_dir(getenv("HOME"));
        break;
    case '/':
        mode = BROWSE;
        list_from_dir("/");
        break;
    default:
        beep();
        message("unknown command");
    }
    return true;
}

void
swap_two_entries(int advance_afterwards) {
    int switch_index;
    if(advance_afterwards >= 0) {
        switch_index = CurrPosition + 1;
    } else {
        switch_index = CurrPosition -1;
    }
    // ranges check:
    if(switch_index >= int(default_list.size()) || switch_index < 0) {
        beep();
        return;
    }
    if (mode == LIST && !NeedleGiven) {
        pair<string, string> tmp = default_list[CurrPosition];
        default_list[CurrPosition] = default_list[switch_index];
        default_list[switch_index] = tmp;
        cur_pos_adjust(advance_afterwards);
        display_list();
    } else {
        beep();
    }
}

string get_description_from_user(void) {
    char desc[DESCRIPTION_MAXLENGTH+1];
    move(msg_area_y, 0);
    clrtoeol();
    printw("Enter description (<ENTER> to quit): ");
    refresh();
    echo();
    getnstr(desc, DESCRIPTION_MAXLENGTH);
    noecho();
    move(msg_area_y, 0);
    clrtoeol();

    return string(desc);
}

bool
list_from_file(void) {
   
    string desc;
    string path;
    int linecount=0;
    default_list.clear();
    string mylistfile = get_listfile();
    ifstream listfile(mylistfile.c_str());
    if (!listfile) {
        return false;
    }
    string cwd = get_cwd_as_string();
    while (! listfile.eof()) {
        string line;
        getline(listfile, line);
        if(line == "") continue;
        // comments ... are not saved later so we simply don't allow
        // them
//       if (line[0] == '#') {
//          continue;
//       }
        // detect path and description at the leading slash of the
        // absolute path:
        desc = line.substr(0,line.find('/')-1);
        path = line.substr(line.find('/'));
        if(opt_cwd && cwd == path) {
            CurrPosition = linecount;
        }
        // counting the lines: if only one, no resolving should take place
        linecount++;

        // if we got an exact match and  not opt_no_resolve the first entry is
        // the result!
        if(NeedleGiven && Needle && Needle == desc && !opt_no_resolve) {
            finish(path,true);
        }
        // filtered?
        if (do_not_show(desc.c_str()) && do_not_show(path.c_str())) {
            continue;
        }
        default_list.push_back(make_pair(desc, path));
    }
    listfile.close();
    // some magic:
    switch(linecount) {
    case 0:
        listfile_empty = true;
        break;
    case 1:
        opt_no_resolve=true;
        break;
    }
    shorties_offset = 0;
    if(default_list.empty()) {
        return false;
    }
    return true;
}

// default: name="."
void
list_from_dir(const char* name) {

    string previous_dir = "";
    if(name == "..") {
        previous_dir = get_cwd_as_string();
    }
    // Checking, Changing and Reading
    if (chdir(name) < 0) {
        string msg = "couldn't change to dir: ";
        msg += name;
        message(msg.c_str());
    }
   
    DIR* THISDIR;
    string cwd = get_cwd_as_string();
    THISDIR = opendir(cwd.c_str());
   
    if (THISDIR == NULL) {
        string msg = "couldn't read dir: ";
        msg += cwd;
        message(msg.c_str());
        sleep(1);
        abort_cdargs();
    }
    yoffset = 0;
    shorties_offset = 1;
    struct dirent* en;
    struct stat buf;

    // cleanup
    cur_list.clear();

    CurrPosition = 1;

    // put the current directory on top ef every listing
    string desc = ".";
    cur_list.push_back(make_pair(desc,cwd));

    // read home dir
    while ((en = readdir(THISDIR)) != NULL) {
        /* filter dirs */
        if (do_not_show(en->d_name)) continue;
        string fullname;
        fullname = cwd + string("/") + string(en->d_name);

        fullname=canonify_filename(fullname);
      
        stat(fullname.c_str(), &buf);
        if (!S_ISDIR(buf.st_mode)) continue;
      
        string lastdir = last_dirname(fullname);
        cur_list.push_back(make_pair(lastdir, fullname));
    }
    // empty directory listing: .. and .
    desc="..";
    string path=get_cwd_as_string();
    if(cur_list.size() == 1) {
        path = path.substr(0,path.find(last_dirname(path)));
        cur_list.push_back(make_pair(desc,path));
      
    }
    sort(cur_list.begin(),cur_list.end());

    // have we remembered a current position for this dir?
    if (LastPositions.count(cwd.c_str())) {
        CurrPosition = LastPositions[cwd.c_str()];
    } else {
        //CurrPosition = 1;
        if(previous_dir.length() > 0) {
            int count = 0;
            for(listit it=cur_list.begin();
                it!=cur_list.end();++it, count++) {
                if(it->second == previous_dir) {
                    CurrPosition = count;
                    break;
                }
            }
        }
    }
    if(!visible(CurrPosition)) {
        yoffset = CurrPosition - 1;
    }

    closedir(THISDIR);
}

void list_to_file(void) {
    // don't write empty files FIXME: unlink file?
    string mylistfile = get_listfile();
    if(default_list.empty()) {
        unlink(mylistfile.c_str());
        return;
    }
    // never touch the listfile of another user
    if(opt_user.size() > 0) {
        return;
    }
    string backup_file = mylistfile + "~";
    if(rename(mylistfile.c_str(),backup_file.c_str()) == -1) {
        fprintf(stderr,"warning: could not create backupfile\n");
    }
    ofstream listfile(mylistfile.c_str());
    if (!listfile) {
        fatal_exit("couldn't write listfile");
    }
    for (listit li = default_list.begin();li != default_list.end(); ++li) {
        listfile << li->first << " " << li->second << endl;
    }
    listfile.close();
}

bool do_not_show(const char* name) {
    /* don't show current, up, hidden if not flag */
    if (mode == BROWSE) {
        if (strcmp(name, "..") == 0)
            return true;
        if (!show_hidden_files && name[0] == '.')
            return true;
    } else {
        if (!NeedleGiven) {
            return false;
        }
        if(!Needle)  return false;
        /* FIXME case-insensitive comparison */
        //if (strcasecmp(name, Needle)) {
        if (strstr(name, Needle)) {
            return false;
        } else {
            return true;
        }
    }
    return false;
}

// default: n=0,
//          wraparound=true
void cur_pos_adjust(int n, bool wraparound) {
    int newpos = CurrPosition + n;
    int max, min;
   
    if (mode == LIST) {
        max = default_list.size() - 1;
    } else {
        max = cur_list.size() - 1;
    }
    min = 0;   
    if (newpos < min) {
        if (opt_no_wrap || !wraparound) return;
        newpos = max;
        yoffset = max_yoffset();
    }
    if (newpos > max) {
        if (opt_no_wrap || !wraparound) return;
        newpos = min;
        yoffset = 0;
    }
    CurrPosition = newpos;
    // scrolling...
    if (CurrPosition-yoffset >= display_area_ymax) {
        yoffset++;
    }
    if (yoffset > 0 && CurrPosition == yoffset) {
        yoffset--;
    }

}

bool entry_nr_exists(unsigned int nr) {
    if (mode == LIST) {
        return nr<default_list.size() ? true:false;
    }
    if (mode == BROWSE) {
        return nr<cur_list.size() ? true:false;
    }
    return false;
}

string current_entry(void) {
    string res;
    if (mode == LIST) {
        if (default_list.empty()) {
            return string(".");
        }
        res = default_list[CurrPosition].second;
    }
    if (mode == BROWSE) {
        if (cur_list.empty()) {
            return string(".");
        }
        res = cur_list[CurrPosition].second;
    }
    return (res == "") ? string("."):res;
}

// default: description=""
// default: ask_for_desc=false
void add_to_default_list(string path,
                         string description,
                         bool ask_for_desc) {

    //
    if (cur_list.empty() && curses_running) {
        path = get_cwd_as_string();
    }

    // get the description (either from user or generic)
    string desc;
    if(description.size() == 0) {
        if(ask_for_desc && curses_running) {
            desc = get_description_from_user();
            if(desc.size()==0) { // empty string is quit
                return;
            }
        } else {
            desc = last_dirname(path);
        }
    } else {
        desc = description;
    }
   
    string msg = "added :" +desc+":" + path;
    default_list.push_back(make_pair(desc, path));
    message(msg.c_str());
}

void add_to_list_file(string path) {
    // get rid of leading = if there
    if (path.at(0) == '=') {
        path = path.substr(1);
    }   
    // the syntax for passing descriptions from the command line is:
    // --add=:desc:/absolute/path
    string desc;
    if(path.at(0) == ':') {
        int colon2_at;
        colon2_at = path.find(":",1);
        if(colon2_at>(DESCRIPTION_MAXLENGTH+1)) {
            fprintf(stderr,"description too long! max %d chars\n", DESCRIPTION_MAXLENGTH);
            exit(-4);
        } else {
            desc = path.substr(1,colon2_at-1);
        }
        path = path.substr(colon2_at+1);
    }
    // FIXME: check for existance here?
    if(path.at(0) != '/') {
        fprintf(stderr,"this is not an absolute path:\n%s\n",path.c_str());
        exit(-2);
    }
    list_from_file();
    add_to_default_list(path,desc,false);
    list_to_file();
}

void delete_from_default_list(int pos) {
    int count = 0;
    for (listit li = default_list.begin();li != default_list.end(); ++li) {
        if (count == pos) {
            default_list.erase(li);
            cur_pos_adjust(0,false);
            // we want no one-entry-in-list-magic anymore
            opt_no_resolve=true;
            return;
        } else {
            count++;
        }
    }
}

void edit_list_file(void) {
    char* editor = getenv("EDITOR");
    if (!editor) {
        // FIXME: how to detect debian and the /usr/bin/editor rule?
        struct stat buf;
        if(stat("/usr/bin/editor",&buf) == 0)
            editor = "/usr/bin/editor";
        else
            editor = "vi";
    }
    endwin();
    list_to_file();
    system((string(editor) + " \"" + get_listfile() + "\"").c_str());
    list_from_file();
    refresh();
    init_curses();
    display_list();
}

string get_resultfile(void) {
    if(opt_resultfile.size() >0) {
        return opt_resultfile;
    }
    return string(DefaultResultfile);
}

char* get_cwd_as_charp(void) {
    char buf[PATH_MAX];
    char* result = getcwd(buf, sizeof(buf));

    if(result == NULL) {
        message("cannot determine current working directory.exit.");
        sleep(1);
        abort_cdargs();
    }
    /* this can be a memleak if not freed again */
    /* but then ... how long does cdargs usually run? */
    return strdup(buf);
}

string get_cwd_as_string(void) {
    char* cwd = get_cwd_as_charp();
    if(cwd == NULL) {
        return "";
    } else {
        string result = string(cwd);
        free(cwd);
        return result;
    }
}

string get_listfile(void) {
    string user="~";
    string file=DefaultListfile;
    string effective_listfile = "";
    if(opt_user.size() > 0) {
        user = opt_user;
    }
    if(opt_listfile.size() > 0) {
        return canonify_filename(opt_listfile);
    }
    if(user[0] == '~') {
        effective_listfile = user + "/" + file;
    } else {
        effective_listfile = "/home/" + user + "/" + file;
    }
    return canonify_filename(effective_listfile);
}

// return the last dir in path with a capital initial letter
string capitalized_last_dirname(string path) {
    string dirname = path.substr(path.find_last_of('/') + 1);
    dirname[0] = toupper(dirname[0]);
    return dirname;
}
// same but not capitalized
string last_dirname(string path) {
    // trailing slash
    if(path.at(path.size()-1) == '/') {
        path = path.substr(0,path.size()-1);
    }
    string dirname = path.substr(path.find_last_of('/') + 1);
    return dirname;
}

string canonify_filename(string filename) {

    size_t pos=0;
    // remove double slashes
    if((pos = filename.find("//",0)) < filename.size()) {
        filename.replace(pos,2,"/");
    }
   
    if (filename[0] != '~') {
        return filename;
    }
    // resolve home directory
    return string(getenv("HOME")) + filename.substr(1);
}

void version(void) {
    printf("cdargs - version %s\n", VERSION);
}

void usage(void) {
    printf("expanding the shell built-in cd with bookmarks and browser\n\n");
    printf("  Usually you don't call this programm directly.\n");
    printf("  Copy it somewhere into your path and create\n");
    printf("  a shell alias (which takes arguments) as described\n");
    printf("  in the file INSTALL which came with this program.\n");
    printf("\n  For general usage press 'H' while running.\n\n\n");
    printf("Usage:\n");
    printf("  cdargs              select from bookmarks\n");
    printf("  cdargs [--noresolve] <Needle>\n");
    printf("                      Needle is a filter for the list: each\n");
    printf("                      entry is compared to the Needle and if it\n");
    printf("                      doesn't match it won't show up.\n");
    printf("                      The Needle performs some magic here. See\n");
    printf("                      manpage for details.\n");
    printf("                      If --noresolve is given the list will be shown\n");
    printf("                      even if Needle matches a description exactly.\n");
    printf("  cdargs <digit>      Needle is a digit: make digit the current entry\n\n");
    printf("  cdargs [-a|--add]=[:desc:]path \n");
    printf("                              add PATH to the list file using the\n");
    printf("                              optional description between the colons\n");
    printf("                              and exit\n");
    printf("\n");
    printf("Other Options\n");
    printf("  -f, --file=PATH    take PATH as bookmark file\n"); 
    printf("  -u, --user=USER    read (default) bookmark file of USER\n"); 
    printf("  -o,- -output=FILE  use FILE as result file\n"); 
    printf("  -r, --nowrap       change the scrolling behaviour for long lists\n");
    printf("  -c, --cwd          make current directory the current entry if on the list\n");
    printf("  -b, --browse       start in BROWSE mode with the current dir\n"); 
    printf("  -h, --help         print this help message and exit\n"); 
    printf("  -v, --version      print version info and exit\n");
    printf("\n");
}

/***********************************************************/
/*                      CURSES STUFF                       */
/***********************************************************/

/* stuff to initialise the ncurses lib */
void init_curses(void) {
    initscr();               // init curses screen
    //savetty();             // ??
    nonl();                  // no newline
    cbreak();                // not in cooked mode: char immediately available
    noecho();                // chars are not echoed
    keypad(stdscr, true);    // Arrow keys etc
    leaveok(stdscr, TRUE);   // Don't show the...
    curs_set(0);             // ...cursor
    set_areas();
    curses_running = true;
}

void message(const char* msg) {
    if (curses_running) {
        move(msg_area_y, 0);
        clrtoeol();
        printw("%s [any key to continue]", msg);
        refresh();
        getch();
        move(msg_area_y, 0);
        clrtoeol();
    } else {
        printf("%s\n", msg);
    }
}

void display_list(void) {

    char description_format[40];

    vector<pair<string, string> > list;
    if (mode == LIST) {
        // perform some magic here: if the list contains just one
        // entry (probably due to filtering by giving a Needle)
        // we are done. 
        if(default_list.size() == 1 && !opt_no_resolve) {
            finish(current_entry(), true);
        } else {
            list = default_list;
        }
    } else {
        list = cur_list;
    }
    clear();
    update_modeline();
    int pos = display_area_ymin;
    if(CurrPosition > static_cast<int>(list.size()) - 1) {
        CurrPosition = list.size()-1;
    }

    // Calculate actual maxlength of descriptions so we can eliminate
    // trailing blanks. We have to iterate thru the list to get the
    // longest description
    unsigned int actual_maxlength = 0;
    if(mode == LIST) {
        for (listit li = list.begin() + yoffset;li != list.end(); ++li) {
            if ( strlen(li->first.c_str() ) > actual_maxlength ) {
                actual_maxlength = strlen(li->first.c_str() );
            }
        }
        // Don't let actual_maxlength > DESCRIPTION_MAXLENGTH
        if ( actual_maxlength > DESCRIPTION_MAXLENGTH) {
            actual_maxlength = DESCRIPTION_MAXLENGTH;
        }
    } else {
        actual_maxlength = DESCRIPTION_BROWSELENGTH;
    }
   
    string cwd  = get_cwd_as_string();
    for (listit li = list.begin() + yoffset;li != list.end(); ++li) {
        //string desc = li->first.substr(0, (DESCRIPTION_MAXLENGTH+1));
        string desc = li->first.substr(0, actual_maxlength);
        string path = li->second.substr(0, xmax - 16);
        string fullpath = li->second;
        char validmarker = ' ';
        if(!valid(fullpath,PATH_IS_DIR)) {
            validmarker = '!';
        }
        if (pos > display_area_ymax) {
            break;
        }
        move(pos, 0);
        if (pos == CurrPosition - yoffset) {
            attron(A_STANDOUT);
        }
        if (fullpath == cwd) {
            attron(A_BOLD);
        }
        if (pos >= shorties_offset && pos < 10+shorties_offset) {
            printw("%2d", pos-shorties_offset);
        } else {
            printw("  ");
        }
      
        // Compose format string for printw. Notice %% to represent literal %
        sprintf(description_format, " [%%-%ds] %%c%%s", actual_maxlength );
      
        printw(description_format, desc.c_str(), validmarker,path.c_str());
        if (pos == CurrPosition - yoffset) {
            attroff(A_STANDOUT);
        }
        if (fullpath == cwd) {
            attroff(A_BOLD);
        }
        pos++;
    }      
}

void update_modeline(void) {
    move(modeliney, 0);
    clrtoeol();
    move(modeliney, 0);
    attron(A_REVERSE);
    string curmode;
    char modechar;
    if (mode == LIST) {
        curmode = "List";
        modechar = 'L';
    } else {
        curmode = "Browse";
        modechar = 'B';
    }
    string cwd = get_cwd_as_string();
    attron(A_BOLD);
    printw("%c: %s ", modechar, cwd.c_str());
//   printw(" yoff: %d shoff: %d currpos: %d ", yoffset,shorties_offset,CurrPosition);
    attroff(A_BOLD);
    hline(' ', xmax);
    attroff(A_REVERSE);
}

void set_areas(void) {
    getmaxyx(stdscr, terminal_height, terminal_width); // get win-dims
    xmax = terminal_width - 1;
    display_area_ymin = 0;
    display_area_ymax = terminal_height - 3;
    modeliney = terminal_height - 2;
    msg_area_y = terminal_height - 1;
}

void resizeevent(int sig) {
    // shut up, compiler
    (void)sig;  
    // re-connect
    signal(SIGWINCH, resizeevent);
    // FIXME: is this the correct way??
    endwin();
    refresh();
    init_curses();
    display_list();
}

bool visible(int pos) {
    if(pos < 0) {
        return false;
    }
    if(pos-yoffset<0) {
        return false;
    }
    if(pos+yoffset > display_area_ymax) {
        return false;
    }
    return true;
}

int max_yoffset(void) {
    int len, ret;
    if (mode == BROWSE) {
        len = cur_list.size() - 1;
    } else {
        len = default_list.size() - 1;
    }
    ret = len - display_area_ymax;
    return ret<0 ? 0:ret;
}


void helpscreen(void) {
    char *pager = getenv("PAGER");
    if (!pager) {
        // FIXME:  how to detect debian and the /usr/bin/pager rule?
        struct stat buf;
        if(stat("/usr/bin/pager",&buf) == 0) {
            pager = "/usr/bin/pager";
        } else {
            if(stat("/usr/bin/less",&buf) == 0) {
                pager = "/usr/bin/less";
            } else {
                pager = "more";
            }
        }
    }
    endwin();
    FILE* help = popen(pager,"w");
// FIXME: error checking this doesn't work, er?
    if(help == NULL) {
        init_curses();
        message("could not open pager");
        return;
    }
   
    int l = 0;
    char* help_lines[] = {
        "cdargs (c) 2001-2003 S. Kamphausen <http://www.skamphausen.de>",
        "<UP>/<DOWN>  move selection up/down and scroll",
        "             please see manpage for alternative bindings!",
        "<ENTER>      select current entry",
        "<TAB>        toggle modes: LIST or BROWSE",
        "<HOME>/<END> goto first/last entry in list if available in terminal",
        "c            add current directory to list",
        "C            same as 'c' but ask for a description",
        "<PgUP/Dn>    scroll the list 10 lines up or down",
        "e,v          edit the list in $EDITOR",
        "H,?          show this help",
        "~,/          browse home/root directory",
        "q            quit - saving the list",
        "C-c,C-g,C-[  abort - don't save the list",
        "             ",
        "Commands in BROWSE-mode:",
        "<LEFT>       descent into current directory",
        "<RIGHT>      up one directory",
        "[num]        make [num] the highlighted entry",
        "a            add current entry to list",
        "A            same as 'a' but ask for a description",
        ".            toggle display of hidden files",
        "             ",
        "Commands in LIST-mode:",
        "[num]        select and resolve entry [num] if displayed",
        "<LEFT>       descent into the current entry",
        "<RIGHT>      up one directory from current dir",
        "d            delete current entry from list",
        "a            add current directory to list",
        "s,t          swap(transpose) two lines in the list",
        "M/m          move an entry up down in the list"
    };
    int help_lines_size = 31;
    while (l < help_lines_size) {
        fprintf(help,"%s\n",help_lines[l]);
        l++;
    }
    fprintf(help,"\n");
    pclose(help);
    // convenience for more which exits at the end:
    if(strstr(pager,"more")) {
        sleep(1);
    }
    refresh();
    init_curses();
}   

/************************************************/
/*                     EXITs                    */
/************************************************/

void fatal_exit(char* msg) {
    endwin();
    fprintf(stderr, msg);
    exit(1);
}

void terminate(int sig) {
    endwin();
    if(sig == 11) {
        fprintf(stderr,"programm received signal %d\n",sig);
        fprintf(stderr,"This should never happen.\n");
        fprintf(stderr,"Maybe you want to send an email to the author and report a bug?\n");
        fprintf(stderr,"Author: Stefan Kamphausen <http://www.skamphausen.de>\n"); 
    }
    fprintf(stderr,"abort.\n");
    exit(1);
}

void finish(string result, bool retval) {

    curs_set(1); // re-show cursor (FIXME: necessary?)
    clear();     // clear screen for stupid terminals like aterm
    refresh();   // ..and make sure we really see the clean screen
    //resetty();   // ??
    endwin();    // finish the curses at all
   
    // only save if list was not filtered!
    if (!NeedleGiven){
        list_to_file();
    }

    // is the result really a dir?
    if(!valid(result,PATH_IS_DIR)) {
        fprintf(stderr,"This is not a valid directory:\n%s\n",result.c_str());
        exit(-3);
    }
//    string resfile = canonify_filename(Resultfile);
    string resfile = canonify_filename(get_resultfile());
    ofstream out(resfile.c_str());
    if (out) {
        out << result << endl;
        out.close();
    }
    if (!retval) {
        exit(1);
    }
    exit(0);
}
void abort_cdargs(void) {
    curs_set(1); // re-show cursor (FIXME: necessary?)
    clear();     // clear screen for stupid terminals like aterm
    refresh();   // ..and make sure we really see the clean screen
    //resetty();   // ??
    endwin();    // finish the curses at all
    exit(-1);
}

bool valid(string path, pathtype mode) {
    struct stat buf;
    string canon_path = canonify_filename(path);
   
    if(mode == PATH_IS_FILE) {
        stat(canon_path.c_str(), &buf);
        if (S_ISREG(buf.st_mode)) return true;
    } else if (mode == PATH_IS_DIR) {
        stat(canon_path.c_str(), &buf);
        if (S_ISDIR(buf.st_mode)) return true;
    }
    return false;
}


syntax highlighted by Code2HTML, v. 0.9.1