#! /usr/local/bin/python3.0
r'''
a Python module offering a Command-Line User Interface

 from TermClui import *
 chosen = choose("A Title", a_list);  # single choice
 chosen = choose("A Title", a_list, multichoice=True) # multiple choice
 x = choose("Which ?\n(Arrow-keys and Return)", w) # multi-line question
 confirm(text) and do_something()
 answer = ask(question)
 answer = ask(question, suggestion)
 password = ask_password("Enter password : ")
 newtext = edit(title, oldtext)
 edit(filename)
 view(title, text)  # if title is not a filename
 view(textfile)    # if textfile _is_ a filename
 edit(choose("Edit which file ?", list_of_files));
 file  = select_file(Readable=True, TopDir="/home", FPat="*.html")
 files = select_file(Chdir=False, multichoice=True, FPat="*.mp3")
 os.chdir(select_file(Directory=True, Path=os.getcwd()))

TermClui.py offers a high-level user interface to give the user
of command-line applications a consistent "look and feel".  Its
metaphor for the computer is as a human-like conversation-partner;
as each question/response is completed, it is summarised to one line
and remains on screen, so that the history of the session gradually
accumulates on the screen, available for review or for cut/paste.
This user-interface can be intermixed with standard applications
which write to STDOUT or STDERR, such as make, pgp, rcs etc.

For the user, choose() uses arrow keys (or hjkl) and Return or q;
also SpaceBar for multiple choices.  confirm() expects y, Y, n or N.
In general, ctrl-L redraws the (currently active bit of the) screen.
edit() and view() use the default EDITOR and PAGER if possible.  
Window-size-changes are handled, though the screen only gets
redrawn after the next keystroke (e.g. ctrl-L)

choose(), ask() and confirm() all accept multi-line questions:
the first line should be the core question (typically it will
end in a question-mark) and will remain on the screen together
with the user's answer.  The subsequent lines appear beneath the
dialogue, and will disappear when the user has given the answer.

TermClui.py does not use curses (a whole-of-screen interface),
it uses a small and portable subset of vt100 sequences.

TermClui.py is a translation into Python3 of the Perl CPAN Modules
Term::Clui and Term::Clui::FileSelect.  This is version 1.43
'''
import re, sys, select, signal, subprocess, os, random
import termios, fcntl, struct, stat, time, dbm

VERSION = '1.43'

# ------------------------ vt100 stuff -------------------------

_A_NORMAL    =  0
_A_BOLD      =  1
_A_UNDERLINE =  2
_A_REVERSE   =  4
_KEY_UP    = 0o403
_KEY_LEFT  = 0o404
_KEY_RIGHT = 0o405
_KEY_DOWN  = 0o402
_KEY_ENTER = "\r"
_KEY_PPAGE = 0o523
_KEY_NPAGE = 0o522
_KEY_BTAB  = 0o541
_getchar = lambda: sys.stdin.read(1)
_ttyin   = 0
_ttyout  = 0

# my $irow; my $icol;   # maintained by puts, up, down, left and right
_irow = 0
_icol = 0
def _puts(s):
    global _ttyout, _irow, _icol
    _irow += s.count("\n")
    if re.search('\r$', s):
        _icol = 0
    else:
        _icol += len(s)
    print(s, end='', file=_ttyout)
    _ttyout.flush()

# could terminfo sgr0, bold, rev, cub1, cuu1, cuf1, cud1 ...
def _attrset(attr):
    global _ttyout, _A_BOLD, _A_REVERSE, _A_UNDERLINE
    if not attr:
        print("\033[0m", end='', file=_ttyout)
    else:
        if attr & _A_BOLD:
             print("\033[1m", end='', file=_ttyout)
        if attr & _A_REVERSE:
             print("\033[7m", end='', file=_ttyout)
        if attr & _A_UNDERLINE:
             print("\033[4m", end='', file=_ttyout)

def _beep():
    global _ttyout
    print("\07", end='', file=_ttyout)
    _ttyout.flush()
def _clear():
    global _ttyout
    print("\033[H\033[J", end='', file=_ttyout)
    _ttyout.flush()
def _clrtoeol():
    global _ttyout
    print("\033[K", end='', file=_ttyout)
    _ttyout.flush()
def _black():
    global _ttyout
    print("\033[30m", end='', file=_ttyout)
def _red():
    global _ttyout
    print("\033[31m", end='', file=_ttyout)
def _green():
    global _ttyout
    print("\033[32m", end='', file=_ttyout)
def _blue():
    global _ttyout
    print("\033[34m", end='', file=_ttyout)
def _violet():
    global _ttyout
    print("\033[35m", end='', file=_ttyout)

def _getc_wrapper(timeout):
    # may not work on openbsd...
    # on Py, the select.select seems to flush the remaining [A chars :-(
    global _getchar, _ttyin
    if timeout > 0.00001:
        nfound = select.select([_ttyin], [], [], timeout)
        if not nfound[0]:
            return None
    while (True):
        try:
            return _getchar()
        except (IOError):
            continue

def _getch():
    global _KEY_UP, _KEY_DOWN, _KEY_RIGHT, _KEY_LEFT
    global _KEY_PPAGE, _KEY_NPAGE, _KEY_BTAB
    c = _getc_wrapper(0)
    if c == "\033":
        c = _getc_wrapper(0)
        if (c == None):
            return("\033")
        if (c == 'A'):
            return(_KEY_UP)
        if (c == 'B'):
            return(_KEY_DOWN)
        if (c == 'C'):
            return(_KEY_RIGHT)
        if (c == 'D'):
            return(_KEY_LEFT)
        if (c == '5'):
            _getc_wrapper(0)
            return(_KEY_PPAGE)
        if (c == '6'):
            _getc_wrapper(0)
            return(_KEY_NPAGE)
        if (c == 'Z'):
            return(_KEY_BTAB)
        if (c == '['):
            c = _getc_wrapper(0)
            if (c == 'A'):
                return(_KEY_UP)
            if (c == 'B'):
                return(_KEY_DOWN)
            if (c == 'C'):
                return(_KEY_RIGHT)
            if (c == 'D'):
                return(_KEY_LEFT)
            if (c == '5'):
                _getc_wrapper(0)
                return(_KEY_PPAGE)
            if (c == '6'):
                _getc_wrapper(0)
                return(_KEY_NPAGE)
            if (c == 'Z'):
                return(_KEY_BTAB)
            return(c)
        return(c)
    elif c == "\217":
        c = _getc_wrapper(0)
        if (c == 'A'):
            return(_KEY_UP)
        if (c == 'B'):
            return(_KEY_DOWN)
        if (c == 'C'):
            return(_KEY_RIGHT)
        if (c == 'D'):
            return(_KEY_LEFT)
        return(c)
    elif c == "\233":
        c = _getc_wrapper(0)
        if (c == 'A'):
            return(_KEY_UP)
        if (c == 'B'):
            return(_KEY_DOWN)
        if (c == 'C'):
            return(_KEY_RIGHT)
        if (c == 'D'):
            return(_KEY_LEFT)
        if (c == '5'):
            _getc_wrapper(0)
            return(_KEY_PPAGE)
        if (c == '6'):
            _getc_wrapper(0)
            return(_KEY_NPAGE)
        if (c == 'Z'):
            return(_KEY_BTAB)
        return(c)
    else:
        return(c)

def _up(n):
    global _irow, _ttyout
    # if (n < 0) { &down(n); return; }
    print("\033[A"*n, end='', file=_ttyout)
    _ttyout.flush()
    _irow -= n

def _down(n):
    global _irow, _ttyout
    #if (n < 0) { &up(n); return; }
    # \033[B doesn't scroll, but \n needs stty ONLRET
    print("\n"*n, end='', file=_ttyout)
    _ttyout.flush()
    _irow += n

def _right(n):
    global _icol, _ttyout
    # if (n < 0) { &up(n); return; }
    print("\033[C"*n, end='', file=_ttyout)
    _ttyout.flush()
    _icol += n

def _left(n):
    global _icol, _ttyout
    # if (n < 0) { &up(n); return; }
    print("\033[D"*n, end='', file=_ttyout)
    _ttyout.flush()
    _icol -= n

def _goto(newcol,newrow):
    global _icol, _irow
    if (newcol == 0):
        print("\r", end='', file=_ttyout)
        _ttyout.flush()
        _icol = 0
    elif (newcol > _icol):
        _right(newcol-_icol)
    elif (newcol < _icol):
        _left(_icol-newcol)
 
    if (newrow > _irow):
        _down(newrow-_irow)
    elif (newrow < _irow):
        _up(_irow-newrow)

# def move(ix,iy):   Unused...
#     printf TTY "\033[%d;%dH",$iy+1,$ix+1; }

_initscr_already_run = 0   # its a counter
# tty = True
_ttyout_fnum = 0
_old_tcattr = 0
def _initscr():
    global tty,_ttyout_fnum,_old_tcattr,_getchar, _ttyin,_ttyout, _initscr_already_run, _icol,_irow
    _icol = 0
    _irow = 0
    if _initscr_already_run > 0:
        _initscr_already_run+=1
        return

    _initscr_already_run = 1
    _ttyout = open("/dev/tty", mode="w")
    _ttyin  = open("/dev/tty", mode="r")

    try:
        import tty
        _ttyout_fnum = _ttyout.fileno()
        _old_tcattr = tty.tcgetattr(_ttyout_fnum)
        tty.setcbreak(_ttyout_fnum)
        mode = tty.tcgetattr(_ttyout_fnum)
        OFLAG = 1
        mode[OFLAG] = mode[OFLAG] & ~(termios.ONLCR | termios.ONLRET)
        tty.tcsetattr(_ttyout_fnum, tty.TCSAFLUSH, mode)
        _getchar = lambda: _ttyin.read(1)
    except (ImportError, AttributeError):
        _ttyout_fnum = 0
        _getchar = lambda: _ttyin.readline()[:-1][:1]

def _endwin():
    global _ttyout, tty,_ttyout_fnum,_old_tcattr, _initscr_already_run
    print("\033[0m", end='', file=_ttyout)
    if _initscr_already_run > 1:
        _initscr_already_run-=1
        return
    if _ttyout_fnum:
        tty.tcsetattr(_ttyout_fnum, tty.TCSAFLUSH, _old_tcattr)
    _initscr_already_run = 0

# ----------------------- size handling ----------------------

_maxcols      = 79
_maxrows      = 24
_size_changed = True
_otherlines   = ''
_notherlines  = 0

def _check_size():
    global _size_changed, _maxcols, _maxrows, _ttyout_fnum
    global _otherlines, _notherlines
    if not _size_changed:
        return
    # http://bytes.com/groups/python/607757-getting-terminal-display-size
    s = struct.pack("HHHH", 0, 0, 0, 0)
    x = fcntl.ioctl(_ttyout_fnum, termios.TIOCGWINSZ, s)
    [_maxrows, _maxcols, xpixels, ypixels] = struct.unpack("HHHH", x)
    _maxcols -= 1

    if _notherlines:
        _otherlinesarray = _fmt(_otherlines)
        _notherlines = len(_otherlinesarray)
    _size_changed = False;

# $SIG{'WINCH'} = sub { $size_changed = 1; };
def _set_size_changed(signum,stackframe):
    global _size_changed
    _size_changed=True

signal.signal(28, _set_size_changed)

# ------------------------ ask stuff -------------------------

# Options such as integer, real, positive, >x, >=x, <x <=x,
# non-null, max-length, min-length, silent  ...
# default could be just one more option, and backward compatibilty
# could be preserved by checking whether the 2nd arg is a hashref ...

_silent = False
def ask_password(question):
    r'''Like ask, but with no echo. Use it for passwords.'''
    global _silent
    _silent = True
    ask(question)

def ask(question, default=''):
    r'''Prints the question and, on the same line, expects the user
to input a string. Left- and Right-arrow and Backspace work
as usual, ctrl-B goes to the beginning and ctrl-E to the end.
If default is specified, it appears on the line initially.
ask() returns the string when the user presses Enter.
'''
    global _silent, _KEY_LEFT, _KEY_RIGHT
    if not question:
        return ''
    _initscr()
    nol = _display_question(question);

    i = 0    # cursor position
    n = 0    # string length
    s_a = []   # list of letters in string
    if default:
        default = re.sub('\t', '    ', default)
        s_a = [y for y in default]
        n = len(default)
        i = n
        #for j in range(len(s_a)):
        #    _puts(s_a[j])
        _puts(default)
        #_left(n)

    while True:
        c = _getch()
        if (c == "\r" or c == "\n"):
            _erase_lines(1)
            break
        if _size_changed:
            _erase_lines(0)
            nol = _display_question(question)
        if (c == _KEY_LEFT) and (i > 0):
            i-=1
            _left(1)
        elif (c == _KEY_RIGHT) and (i < n):
            _puts('x') if _silent else _puts(s_a[i])
            i+=1
        elif (c == "\b") or (c == "\177"):
            if i > 0:
                 n -= 1
                 i -= 1
                 s_a.pop(i)   # splice(@s, $i, 1)
                 _left(1)
                 j = i
                 while j < n:
                     _puts(s_a[j])
                     j += 1
                 _clrtoeol()
                 _left(n-i)
        elif (c == "\003" or c == "\030" or c == "\004"):
             # clear ...
            _left(i)
            i = 0
            n = 0
            _clrtoeol()
            s_a = []
        elif c == "\002":
            _left(i)
            i = 0
        elif c == "\005":
            _right(n-i)
            i = n
        elif c == "\014":
            x = i  # do nothing
        elif str(type(c)) == "<class 'int'>":
            _beep()
        elif ord(c) > 255:
            _beep()
        elif re.match('^[\040-\376]$', c):
            # splice(@s, $i, 0, $c);
            s_a.insert(i, c)
            n+=1
            i+=1
            _puts('x') if _silent else _puts(c)
            j = i
            while j < n:
                _puts(s_a[j])
                j += 1
            _clrtoeol()
            _left(n-i)
        else:
            _beep()
    _endwin()
    _silent = False
    return "".join(s_a)

# ----------------------- choose stuff -------------------------
def _debug(string):
    tmp = open("/tmp/clui_debug", mode="a")
    print(string, file=tmp)
    tmp.close()

# my (%irow, %icol, $nrows, $clue_has_been_given, $choice, $this_cell);
random.seed(None)
HOME = os.getenv('HOME') or os.getenv('LOGDIR') or os.path.expanduser('~')
_marked    = []
_clue_has_been_given = False
_this_cell = 0
_choice    = ''
_list      = []

def choose(question, a_list, multichoice=False):
    r'''
Prints the question, then a compact formatting of the list of strings
with one (the cursor) highlit. Initially, the cursor is on that string
which the user chose previously in response to this same question.
The user then uses arrow keys (or hjkl) and Return, or q to quit.
The Return key causes choose() to return the string under the cursor;
q for Quit causes choose() to return None.

If there are too many choices to fit on the screen, the user is
prompted for a (case-sensitive) clue, which is used to narrow down
the choices until they do fit.

If multichoice is set, the SpaceBar works to select (or deselect)
the various choices (the choice under the cursor when Return is
pressed is also selected), and choose() returns a list of strings.
'''
    # wantarray doesn't exist in Python because no $ or @
    global _maxcols, _marked, _list, _size_changed, _nrows, _icol, _irow
    global _this_cell, _clue_has_been_given, _choice
    _list = a_list
    for i in range(len(_list)):
        _list[i] = re.sub('[\r\n]+$', '', _list[i])   # chop final \n if any
    a_list = _list
    icell = 0
    _marked = [False for item in _list]
    question = re.sub('[\r\n]+$', '', question)
    question = re.sub('^[\r\n]+', '', question)

    otherlines_a = []
    lines = re.split('\r?\n', question, 1)
    firstline = lines[0]
    firstlinelength = len(firstline)
    _choice = get_default(firstline)
    chosen = []
    _initscr()
    _size_and_layout(0)
    if (len(lines) > 1):
       otherlines_a = _fmt(lines[1])
    #if len(otherlines_a):
    #    puts("\r\n" + "\r\n".join(otherlines_a) + "\r")
    #    goto(1+len(firstline), 0)
    _notherlines = len(otherlines_a)
    if multichoice:
        if (firstlinelength < _maxcols-30):
            _puts(firstline+" (multiple choice with spacebar)")
        elif (firstlinelength < _maxcols-16):
            _puts(firstline + "(multiple choice)")
        elif (firstlinelength < _maxcols-9):
            _puts(firstline + "(multiple)")
        else:
            _puts(firstline)

    else:
        _puts(firstline)
    _clrtoeol()

    if (_nrows >= _maxrows):
        _list = _narrow_the_search(_list)
        if not _list:
            _up(1)
            _clrtoeol()
            _endwin()
            _clue_has_been_given = False
            if multichoice:
                return []
            else:
                return None
    _wr_screen()

    while True:
        c = _getch()
        if _size_changed:
            _size_and_layout(_nrows)
            if _nrows >= _maxrows:
                _list = _narrow_the_search(_list)
                if not _list:
                    _up(1)
                    _clrtoeol()
                    _endwin()
                    _clue_has_been_given = False
                    if multichoice:
                        return []
                    else:
                        return None
            _wr_screen()
        if (c == "q" or c == "\004"):
            _erase_lines(1)
            if _clue_has_been_given:
                re_clue = confirm("Do you want to change your clue ?")
                _up(1)
                _clrtoeol()   # erase the confirm
                if re_clue:
                    _irow = 1
                    _list = _narrow_the_search(a_list)
                    _wr_screen()
                    continue
                else:
                    _up(1)
                    _clrtoeol()
                    _endwin()
                    _clue_has_been_given = False
                    if multichoice:
                        return []
                    else:
                        return None
            _goto(0,0)
            _clrtoeol()
            _endwin()
            _clue_has_been_given = False
            if multichoice:
                return []
            else:
                return None
        elif (c == "\t") and (_this_cell < (len(_list)-1)):
            _this_cell+=1
            _wr_cell(_this_cell-1)
            _wr_cell(_this_cell)
        elif (((c == "l") or (c == _KEY_RIGHT)) and (_this_cell < (len(_list)-1)) and (_irow_a[_this_cell] == _irow_a[_this_cell+1])):
            _this_cell+=1
            _wr_cell(_this_cell-1)
            _wr_cell(_this_cell)
        elif (((c == "\010") or (c == _KEY_BTAB)) and (_this_cell > 0)):
            _this_cell-=1
            _wr_cell(_this_cell+1)
            _wr_cell(_this_cell)
        elif (((c == "h") or (c == _KEY_LEFT)) and (_this_cell > 0) and (_irow_a[_this_cell] == _irow_a[_this_cell-1])):
            _this_cell-=1
            _wr_cell(_this_cell+1)
            _wr_cell(_this_cell)
        elif (((c == "j") or (c == _KEY_DOWN)) and (_irow < _nrows)):
            mid_col = _icol_a[_this_cell] + int(0.5*len(_list[_this_cell]))
            left_of_target = 1000
            inew=_this_cell+1
            while inew < len(_list):
                if _icol_a[inew] < mid_col:
                    break    # skip rest of row
                inew+=1
            while inew < len(_list):
                new_mid_col = _icol_a[inew] + int(0.5*len(_list[inew]))
                if new_mid_col >= mid_col:        # we've reached it
                    break
                if (inew == (len(_list)-1)) or (_icol_a[inew+1]<=_icol_a[inew]):
                    break    # we're at EOL
                left_of_target = mid_col - new_mid_col
                inew+=1
            if ((new_mid_col - mid_col) > left_of_target):
                inew-=1
            iold = _this_cell
            _this_cell = inew
            _wr_cell(iold)
            _wr_cell(_this_cell)
        elif (((c == "k") or (c == _KEY_UP)) and (_irow > 1)):
            mid_col = _icol_a[_this_cell] + int(0.5*len(_list[_this_cell]))
            right_of_target = 1000
            inew = _this_cell-1
            while inew > 0:
                if _irow_a[inew] < _irow_a[_this_cell]:    # skip rest of row
                    break
                inew-=1
            while (inew > 0):
                if not _icol_a[inew]:
                    break
                new_mid_col = _icol_a[inew] + int(0.5*len(_list[inew]))
                if new_mid_col < mid_col:         # we're past it
                    break
                right_of_target = new_mid_col - mid_col
                inew-=1
            if ((mid_col - new_mid_col) > right_of_target):
                inew+=1
            iold = _this_cell
            _this_cell = inew
            _wr_cell(iold)
            _wr_cell(_this_cell)
        elif c == "\014":
            if _size_changed:
                _size_and_layout(_nrows)
                if _nrows >= _maxrows:
                    _list = _narrow_the_search(_list);
                    if not _list:
                        _up(1)
                        _clrtoeol()
                        _endwin()
                        _clue_has_been_given = False
                        if multichoice:
                            return []
                        else:
                            return None
            _wr_screen()
        elif (c == "\r") or (c == "\n"):
            _erase_lines(1)
            _goto(firstlinelength+1, 0)
            if multichoice:
                i = 0
                while i < len(_list):
                    if _marked[i] or (i==_this_cell):
                        chosen.append(_list[i])
                    i+=1
                _clrtoeol()
                remaining = _maxcols-firstlinelength
                last = chosen.pop()
                dotsprinted = False
                for item in chosen:
                    if ((remaining - len(item)) < 4):
                        dotsprinted = True
                        _puts("...")
                        remaining -= 3
                        break
                    else:
                        _puts(item+", ")
                        remaining -= (2 + len(item))
                if not dotsprinted:
                    if (remaining - len(last)) > 0:
                        _puts(last)
                    elif remaining > 2:
                        _puts('...')
                _puts("\n\r");
                chosen.append(last)
            else:
                _puts(_list[_this_cell]+"\n\r")
            _endwin()
            set_default(firstline, _list[_this_cell]); # join ($,,@chosen) ?
            _clue_has_been_given = False
            if multichoice:
                return chosen
            else:
                return _list[_this_cell]
        elif c == " ":
            if multichoice:
                _marked[_this_cell] = not _marked[_this_cell]
                if (_this_cell < (len(_list)-1)):
                    _this_cell+=1
                    _wr_cell(_this_cell-1)
                    _wr_cell(_this_cell)
            elif (_this_cell < (len(_list)-1)):
                _this_cell+=1
                _wr_cell(_this_cell-1)
                _wr_cell(_this_cell)

    _endwin()
    print("choose: shouldn't reach here ...\n", file=sys.stderr)

_irow_a = []
_icol_a = []
def _layout(my_list):
    global _irow_a, _icol_a, _this_cell, _maxcols, _maxrows, _choice
    _irow_a = []
    _icol_a = []
    _this_cell = 0
    my_irow = 1
    my_icol = 0
    l = []
    i = 0
    while (i < len(my_list)):
        l.append(len(my_list[i]) + 2)
        if (l[i] > _maxcols-1):
            l[i] = _maxcols-1
        if ((my_icol + l[i]) >= _maxcols):
            my_irow += 1
            my_icol = 0

        if my_irow > _maxrows:
            return my_irow
        _irow_a.append(my_irow)
        _icol_a.append(my_icol)
        my_icol += l[i]
        if my_list[i] == _choice:
            _this_cell = i
        i += 1
    return my_irow

def _wr_screen():
    global _otherlines, _notherlines, _nrows, _maxrows, _list, _this_cell

    i = 0
    while (i < len(_list)):
        if not  i == _this_cell:
            _wr_cell(i)
        i += 1
    if (_notherlines and (_nrows+_notherlines) < _maxrows):
        _puts("\r\n" + "\r\n".join(_otherlines) + "\r")
    _wr_cell(_this_cell)

def _wr_cell(i):
    global _icol_a, _irow_a, _icol, _marked, _this_cell, _list
    global _A_BOLD, _A_REVERSE, _A_NORMAL, _A_UNDERLINE
    _goto(_icol_a[i], _irow_a[i]);
    if _marked[i]:
        _attrset(_A_BOLD | _A_UNDERLINE)
    if i == _this_cell:
        _attrset(_A_REVERSE)
    no_tabs = _list[i]
    no_tabs = re.sub("\t", " ", no_tabs)
    no_tabs = no_tabs[:_maxcols-1]  # 1.42
    _puts(" " + no_tabs + " ")
    if _marked[i] or (i == _this_cell):
        _attrset(_A_NORMAL)

def _size_and_layout(erase_rows):
    global _maxrows, _nrows, _list
    _check_size()
    if (erase_rows):
        if (erase_rows > _maxrows):
            erase_rows = _maxrows
        _erase_lines(1)
    _nrows = _layout(_list)

def _narrow_the_search(a_list):
    global _maxrows, _nrows, _KEY_LEFT, _KEY_RIGHT, _clue_has_been_given
    nchoices = len(a_list)
    n = 0
    i = 0
    s_a = []
    s = ''
    my_list = a_list
    _clue_has_been_given = True
    _ask_for_clue(nchoices, i, s);
    while True:
        c = _getch()
        if _size_changed:
            _size_and_layout(0)
            if _nrows < _maxrows:
                _erase_lines(1)
                return my_list
        if (c == _KEY_LEFT) and (i > 0):
             i-=1
             _left(1)
             continue
        elif c == _KEY_RIGHT:
            if i < n:
                _puts(s_a[i])
                i+=1
                continue
        elif (c == "\b") or (c == "\177"):
            if i > 0:
                n-=1
                i-=1
                s_a.pop(i)
                _left(1)
                j = i
                while j < n:
                    _puts(s_a[j])
                    j += 1
                _clrtoeol()
                _left(n-i)
        elif c == "\003" or c == "\030" or c == "\004":
            if not s_a:
                _clue_has_been_given = False
                _erase_lines(1)
                return []
            _left(i)
            i = 0
            n = 0
            s_a = []
            _clrtoeol()
        elif c == "\002":
            _left(i)
            i = 0
            continue
        elif c == "\005":
            _right(n-i)
            i = n
            continue
        elif c == "\014":
            x = i   # do nothing
        elif ord(c) > 255:
            _beep()
        elif nchoices and re.match('^[\040-\376]$', c):
            s_a.insert(i, c)
            n+=1
            i+=1
            _puts(c)
            j = i
            while j < n:
                _puts(s_a[j])
                j += 1
            _clrtoeol()
            _left(n-i)
        else:
            _beep()

        # grep, and if $nchoices=1 return
        s = "".join(s_a);
        # list = grep($[ <= index($_,$s), @biglist);
        if s:
            # a lambda function can't refer to s :-(
            # my_list = list(filter(lambda x: s.find(x)>=0, biglist))
            my_list = []
            for tmp_str in a_list:
                tmp_str.find(s)>=0 and my_list.append(tmp_str)
        else:
            my_list = a_list
        nchoices = len(my_list)
        _nrows = _layout(my_list)
        if (nchoices==1 or (nchoices and (_nrows<_maxrows))):
            _puts("\r")
            _clrtoeol()
            _up(1)
            _clrtoeol()
            return my_list
        _ask_for_clue(nchoices, i, s)

    print("_narrow_the_search: shouldn't reach here ...", file=sys.stderr)

def _ask_for_clue(nchoices, i, s):
    if nchoices:
        if s:
            headstr = "the choices won't fit; there are still";
            _goto(0,1)
            _puts(headstr+" "+str(nchoices)+" of them")
            _clrtoeol()
            _goto(0,2)
            _puts("lengthen the clue : ")
            _right(i)
        else:
            headstr = "the choices won't fit; there are"
            _goto(0,1)
            _puts(headstr+" "+str(nchoices)+" of them")
            _clrtoeol()
            _goto(0,2)
            _puts("   give me a clue :            (or ctrl-X to quit)")
            _left(30)
    else:
        _goto(0,1)
        _puts("No choices fit this clue !")
        _clrtoeol();
        _goto(0,2)
        _puts(" shorten the clue : ")
        _right(i)

def get_default(question):
    r'''Returns (what the dbm database remembers as) the choice the
user made the last time they were asked this question.
'''
    if os.getenv('CLUI_DIR') == 'OFF':
        return ''
    if not question:
        return ''
    n_tries = 5
    while n_tries > 0:
        try:
            CHOICES = dbm.open (_dbm_file(), 'r', 0o600)
            break
        except NameError:
            return ''
        except IOError:
            if n_tries < 2:
                return ''
            select.select([], [], [], random.uniform(0.0, 0.45))
        else:
            return ''
        n_tries -= 1
    my_choice = CHOICES.get(question)
    CHOICES.close()
    if my_choice:
        return my_choice.decode()
    else:
        return ''

def set_default(question, answer):
    r'''Overwrites the choice the user made the last time they
were confronted with this question. This can be useful in
an application where one task typically follows another,
to set the next default choice.
'''
    if os.getenv('CLUI_DIR') == 'OFF':
        return None
    if not question:
        return None
    n_tries = 5
    while n_tries > 0:
        try:
            CHOICES = dbm.open (_dbm_file(), 'c', 0o600)
            break
        except NameError:
            return None
        except IOError:
            if n_tries < 2:
                return ''
            select.select([], [], [], random.uniform(0.0, 0.45))
        else:
            return None
        n_tries -= 1
    CHOICES[question] = answer
    CHOICES.close()
    return answer

def _dbm_file():
    global HOME
    if (os.getenv('CLUI_DIR') == 'OFF'):
        return None
    if (os.getenv('CLUI_DIR')):
        db_dir = os.getenv('CLUI_DIR')
        db_dir = re.sub('^~', HOME, db_dir)
    else:
        db_dir = HOME+"/.clui_dir"

    os.path.exists(db_dir) or os.mkdir(db_dir, 0o750)
    return db_dir+"/choices"

# ----------------------- confirm stuff -------------------------

def confirm(question):
    '''Print the question, and the user replies Yes or No using
"y", "Y", "n" or "N".  confirm() returns True or False.
'''
    global _ttyin, _ttyout
    if not question:
        return(False)
    # return(0) unless -t STDERR
    if not os.isatty(sys.stdout.fileno()):
        return(None)
    _initscr()
    nol = _display_question(question)
    _puts (" (y/n) ")
    while (True):
        response=_getch()
        if (re.match('[yYnN]', response)):
            break
        _beep()

    _left(6)
    _clrtoeol()
    if (re.match('[yY]', response)):
        _puts("Yes")
    else:
        _puts("No")
    _erase_lines(1)
    _endwin()
    if (re.match('[yY]', response)):
        return True
    else:
        return False

# ----------------------- edit stuff -------------------------

def edit(title='', text=''):
    r'''If there's no text and the "title" is a filename that exists
and is writeable, then the user's default EDITOR is invoked on
that file.  If the file is only readable, the user's default
PAGER is used.  If there is text, the editor is invoked on that
text, and the title is displayed within the temporary file-name.
In either case, the resulting text is returned.
'''
    # my ($dirname, $basename, $rcsdir, $rcsfile, $rcs_ok);

    editor = os.getenv('EDITOR') or "vi"; # should also get_default()
    if not title:    # start editor session with no preloaded file
        subprocess.call([editor])
    elif text:
        # must create tmp file with title embedded in name
        tmpdir = '/tmp/';
        safename = re.sub('[\W_]+', '_', title)
        fname = tmpdir + safename + str(os.getpid())
        try:
            fh = open(fname, mode="w")
        except EnvironmentError as err:
            sorry("can't open "+fname+": "+str(err))
            return ''
        print(text, file=fh)
        fh.close()
        subprocess.call([editor, fname])
        try:
            fh = open(fname, mode="r")
        except EnvironmentError as err:
            sorry("can't read "+fname+": "+str(err))
            return ''
        text = fh.read()
        fh.close()
        try:
            os.unlink(fname)
        except EnvironmentError as err:
            sorry("couldn't unlink "+fname+": "+str(err))
        return text
    else:    # its a file, we will try RCS ...
        file = title

        # weed out no-go situations
        file_stat = os.stat(file)
        # if os.path.isdir(file):  # less yukky, but does an extra stat
        if stat.S_ISDIR(file_stat.st_mode):  # YUK
            sorry(file+" is already a directory")
            return ''
        #if (-B _ and -s _):
        #    sorry(file+" is not a text file")
        #    return ''
        #if (-T _ and !-w _):
        if not _is_writeable(file_stat):
            view(file)
            return True

        # it's a writeable text file, so work out the locations
        if file.find(os.path.sep) >= 0:
            rcsdir   = os.path.dirname(file)+'/RCS'
            basename = os.path.basename(file)
            rcsfile  = rcsdir+os.path.sep+basename+',v'
        else:
            basename = file
            rcsdir   = "RCS"
            rcsfile  = rcsdir+os.path.sep+os.path.basename(file)+',v'

        rcslog = rcsdir+'/log'

        # we no longer create the RCS directory if it doesn't exist,
        # so you have to `mkdir RCS' to enable rcs in a directory ...
        rcs_ok = True
        if not os.path.isdir(rcsdir):
            rcs_ok = False
        elif not _is_writeable(rcsdir):
            rcs_ok = False
            print("can't write in "+rcsdir, file=sys.stderr)

        # if the file doesn't exist, but the RCS does, then check it out
        if rcs_ok and os.path.isfile(rcsfile) and not os.path.isfile(file):
            subprocess.call(["co", "-l", file, rcsfile])

        starttime = time.time()
        subprocess.call([editor, file])
        elapsedtime = time.time() - starttime;
        # could be output or logged, for worktime accounting

        # if (rcs_ok and -T file):     # check it in
        if rcs_ok:
            if not os.path.isfile(rcsfile):
                msg = ask (file+' is new. Please describe it:');
                if msg:
                    quotedmsg = re.sub("'","'\"'\"'", msg)
                    # system "ci -q -l -t-'$quotedmsg' -i $file $rcsfile";
                    subprocess.call(["ci", "-q", "-l", "-t-'"+quotedmsg+"'", file, rcsfile])
                    _logit(rcslog, basename, msg)
            else:
                msg = ask('What changes have you made to '+file+' ?')
                quotedmsg = re.sub(r"'", "'\"'\"'", msg)
                if msg:
                    subprocess.call(["ci", "-q", "-l", "-m'"+quotedmsg+"'", file, rcsfile])
                    _logit(rcslog, basename, msg)

def _logit(rcslog, file, msg):
    logfile = open(rcslog, mode="a")
    print(_timestamp()+' '+file+' '+os.getlogin()+' '+msg, file=logfile)
    logfile.close()

def _timestamp():
    # returns current date and time in "199403011 113520" format
    x = time.localtime(time.time())
    return '{0:0=4}{1:0=2}{2:0=2} {3:0=2}{4:0=2}{5:0=2}'.format(x.tm_year, x.tm_mon, x.tm_mday, x.tm_hour, x.tm_min, x.tm_sec)

# -------------------------- filetests --------------------------

def _re_grep(regexp, a_list):
    '''greps a regexp in a list of strings'''
    l = []
    for tmpstr in a_list:
        if re.match(regexp, tmpstr):
            l.append(tmpstr)
    return l

def _is_writeable(arg):
    my_type = str(type(arg))
    if my_type == "<class 'str'>":
        if not os.path.exists(arg):
            return False
        my_stat_result = os.stat(arg)
    elif my_type == "<class 'posix.stat_result'>":
        my_stat_result = arg
    else:
        return False
    my_euid = os.geteuid()
    my_groups = os.getgroups()
    my_fuid = my_stat_result.st_uid
    my_fgid = my_stat_result.st_gid
    my_mode = my_stat_result.st_mode
    if (my_euid == my_fuid) and (my_mode & 0o200):
        return True
    if my_mode & 0o20:
        for gid in my_groups:
            if gid == my_fgid:
                return True
    if my_mode & 0o2:
        return True
    return False

def _is_readable(arg):
    my_type = str(type(arg))
    if my_type == "<class 'str'>":
        if not os.path.exists(arg):
            return False
        my_stat_result = os.stat(arg)
    elif my_type == "<class 'posix.stat_result'>":
        my_stat_result = arg
    else:
        return False
    my_euid = os.geteuid()
    my_groups = os.getgroups()
    my_fuid = my_stat_result.st_uid
    my_fgid = my_stat_result.st_gid
    my_mode = my_stat_result.st_mode
    if (my_euid == my_fuid) and (my_mode & 0o400):
        return True
    if my_mode & 0o40:
        for gid in my_groups:
            if gid == my_fgid:
                return True
    if my_mode & 0o4:
        return True
    return False

def _is_executable(arg):
    my_type = str(type(arg))
    if my_type == "<class 'str'>":
        if not os.path.exists(arg):
            return False
        my_stat_result = os.stat(arg)
    elif my_type == "<class 'posix.stat_result'>":
        my_stat_result = arg
    else:
        return False
    my_euid = os.geteuid()
    my_groups = os.getgroups()
    my_fuid = my_stat_result.st_uid
    my_fgid = my_stat_result.st_gid
    my_mode = my_stat_result.st_mode
    if (my_euid == my_fuid) and (my_mode & 0o400) and (my_mode & 0o100):
        return True
    if (my_mode & 0o40) and (my_mode & 0o10):
        for gid in my_groups:
            if gid == my_fgid:
                return True
    if (my_mode & 0o4) and (my_mode & 0o1):
        return True
    return False


def _is_textfile(arg):   # arg must be a str, the filename
    my_type = str(type(arg))
    if my_type == "<class 'str'>":
        if not os.path.exists(arg):
            return False
    else:
        return False
    try:
        f = open(arg, mode='br')
    except EnvironmentError as err:
        print("can't open "+arg+": "+err, file=sys.stderr)
    ascii = 0
    nonascii = 0
    for byte in f.read(2048):
        # if ord(c) > 127:
        if (byte > 127) or (byte<9) or ((byte>14) and (byte<32)):
            nonascii += 1
        else:
            ascii += 1
    f.close()
    if ascii == 0:
        return None
    elif (nonascii/ascii) > 0.10:
        return False
    else:
        return True

def _is_owned(arg):
    my_type = str(type(arg))
    if my_type == "<class 'str'>":
        if not os.path.exists(arg):
            return False
        my_stat_result = os.stat(arg)
    elif my_type == "<class 'posix.stat_result'>":
        my_stat_result = arg
    else:
        return False
    my_euid = os.geteuid()
    my_fuid = my_stat_result.st_uid
    if my_euid == my_fuid:
        return True
    return False


# ----------------------- sorry stuff -------------------------

def sorry(msg):   # warns user of an error condition
    r'''Prints the message to stderr preceded by the word "Sorry, "
'''
    print('Sorry, '+str(msg), file=sys.stderr)

def inform(msg):
    r'''Prints the message to /dev/tty or to stderr.
'''
    msg = re.sub('[\r\n]+$', '', msg)
    try:
        ttyout = open('/dev/tty', mode='w')
        print(msg, file=ttyout)
        ttyout.close()
    except:
       print(str(msg), file=sys.stderr)

# ----------------------- view stuff -------------------------

def view(title='', text=''):    # or ($filename) =
    r'''If there's no text and the "title" is a filename that exists
and is readable, then a pager is invoked on that file.  Else,
a pager is invoked on the text, and the title is displayed
somewhere as a title.  If the text covers 60% or more of the
screen, the user's default PAGER is used; if the text is two lines
or less, it is just printed; in between, a built-in tiny pager is
used which offers the user the choices "q" to clear the text and
continue, or Enter to leave the text on the screen and continue.
'''
    global _OpenFile
    pager = os.getenv('PAGER')
    if not pager:
        for f in ["/usr/bin/less", "/usr/bin/more"]:
            if os.path.exists(f):
                default_pager = f
                break
    if (not text) and os.path.exists(title) and _Open(title, mode='r'):
        nlines = 0
        for line in _OpenFile:
            nlines += 1
            if (nlines > _maxrows):
                break
        _OpenFile.close()
        if (nlines > int(0.6*_maxrows)):
            subprocess.call(pager, title)
        else:
            fh = open(title, mode='r')
            text = fh.read()
            fh.close()
            _tiview(title, text);
    else:
        lines = re.split('\r?\n', text, _maxrows-1)
        if len(lines) < 21:
            _tiview (title, text)
        else:
            tmpdir = '/tmp/'
            safename = re.sub('[\W_]+', '_', title)
            fname = tmpdir + safename + os.getpid()
            if not _Open(fname, mode="w"):
                return ''
            _OpenFile.print(text)
            _OpenFile.close()
            subprocess.call(pager, fname)
            _Unlink(tmp)

def _tiview(title='', text=''):
    global _icol, _irow
    if not text:
        return False
    title = re.sub('\t', ' ', title)
    titlelength = len(title)

    _check_size()
    rows = _fmt(text, nofill=True);
    _initscr();
    if 3 > len(rows):
        _puts("\r\n".join(rows) + "\r\n")
        _endwin()
        return True
    if titlelength > (_maxcols-35):
        _puts (title+"\r\n")
    else:
        _puts (title+"   (<enter> to continue, q to clear)\r\n")

    _puts("\r" + "\r\n".join(rows) + "\r")  # the perl version does clrtoeol
    _icol = 0
    _irow = len(rows)
    _goto(titlelength+1, 0)

    while (True):
        c = _getch()
        if (c == 'q' or c == "\030" or c == "\027" or c == "\030" or c == "\003" or c == "\c\\"):
            _erase_lines(0)
            _endwin()
            return True
        elif (c == "\r" or c == "\n"):    # <enter> retains text on screen
            _clrtoeol()
            _goto(0, len(rows)+1)
            _endwin()
            return True
        elif (c == "\014"):
            _puts("\r")
            _endwin()
            _tiview(title, text)
            return True

    print("_tiview: shouldn't reach here\n", file=sys.stderr)
    return False

# -------------------------- infrastructure -------------------------

_OpenFile = 0
def _Open(filename, mode="r"):
    global _OpenFile
    try:
        _OpenFile = open(filename, mode=mode)
        return True
    except EnvironmentError as err:
        print("\ncan't open "+filename+': '+str(err), file=sys.stderr)
        return False

def _Unlink(filename):
    try:
        os.unlink(filename)
        return True
    except EnvironmentError as err:
        print("\ncan't unlink "+filename+": "+str(err), file=sys.stderr)
        return False

def _display_question(question, nofirstline=False):
    '''used by ask() and confirm(), but not by choose() ...'''
    _check_size()
    otherlines_a = []
    # my ($firstline, @otherlines);
    if nofirstline:
        otherlines_a = _fmt(question)
    else:
        # [firstline,otherlines] = re.split('\r?\n', question, 2)
        lines = re.split('\r?\n', question, 1)
        if (lines[0]):
            _puts(lines[0] + " ")
        if (len(lines) > 1):
           otherlines_a = _fmt(lines[1])
    if len(otherlines_a):
        _puts("\r\n" + "\r\n".join(otherlines_a) + "\r")
        _goto(1+len(lines[0]), 0)
    return len(otherlines_a)

def _erase_lines(nline):
    '''leaves cursor at beginning of line nline and clears rest of screen'''
    global _ttyout
    _goto(0, nline)
    print("\033[J", end='', file=_ttyout)

def _fmt(text, nofill=False):
    '''Used by _tiview, ask and confirm; formats the text within maxcols cols'''
    # my (@i_words, $o_line, @o_lines, $o_length, $last_line_empty, $w_length);
    # my (@i_lines, $initial_space);
    global _maxcols
    o_line = ''
    o_lines = []
    o_length = 0
    last_line_empty = False
    i_lines = re.split('\r?\n',text)
    for i_line in i_lines:
        if (re.search('^\s*$', i_line)):
            if (o_line):
                o_lines.append(o_line)
                o_line=''
                o_length=0
            if (not last_line_empty):
               o_lines.append('')
               last_line_empty = True
            continue

        last_line_empty = False

        if nofill:
            o_lines.append(i_line[0:_maxcols])
            continue

        # if ($i_line =~ s/^(\s+)//) {   # line begins with space ?
        split_list = re.split(r'^(\s+)', i_line, 1)
        if (len(split_list) > 2):
            i_line = split_list[2]
            initial_space = re.sub(r'\t', '   ', split_list[1])
            if (o_line):
                o_lines.append(o_line)
            o_line = initial_space
            o_length = len(initial_space)
        else:
            initial_space = ''

        i_words = re.split(r'\s+', i_line)
        for i_word in i_words:
            w_length = len(i_word)
            if ((o_length + w_length) > _maxcols):
                o_lines.append(o_line)
                o_line = initial_space
                o_length = len(initial_space)

            if (w_length > _maxcols):   # chop it !
                o_lines,append(i_word[0:_maxcols])
                continue

            if (o_line):
                o_line += ' '
                o_length += 1
            o_line += i_word
            o_length += w_length

    if (o_line):
        o_lines.append(o_line)
    if (len(o_lines) < _maxrows-2):
        return (o_lines)
    else:
        return o_lines[0, _maxrows-2]

def back_up():
    r'''Moves the cursor up one line, to the beginning of the line,
and clears the line.  Useful if your application is validating
the results of an ask() and wishes to re-pose the question.
'''
    ttyout = open("/dev/tty", mode="w")
    print("\r\033[K\033[A\033[K", end='', file = ttyout)
    ttyout.close

def select_file(Chdir=True, Create=False, ShowAll=False,
    DisableShowAll=False, SelDir=False, FPat='*', File='',
    Path='', Title='', TopDir='/', TextFile=False, Readable=False,
    Writeable=False, Executable=False, Owned=False, Directory=False,
    multichoice=False):
    r'''
This function asks the user to select a file from the filesystem.
It offers Rescan and ShowAll buttons.  The options are modelled
on those of Tk::FileDialog but with various new options: TopDir,
TextFile, Readable, Writeable, Executable, Owned and Directory

Multiple choice is possible in a limited circumstance; when
select_file() is invoked with multichoice=True, with Chdir=False
and without Create.  It is not possible to select multiple files
lying in different directories.

Three problem filenames: 'Create New File', 'Show DotFiles' and
'Hide DotFiles' will, if present in your filesystem, cause confusion.

Chdir

Enable the user to change directories. The default is True.
If it is set to False, and multichoice to True, and Create is
not set, then the user can select multiple files.

Create

Enables the user to specify a file that does not exist.
The default is False.

ShowAll

Determines whether hidden files (.*) are displayed.
The default is False.

DisableShowAll

Disables the ability of the user to change the status of the
ShowAll flag. By default the user is allowed to change the status).

SelDir

If True, enables selection of a directory rather than a file.
The default is False.  To _enforce_ selection of a directory,
use the Directory option.

FPat

Sets the default file selection pattern, in glob format, e.g.
'*.html'.  Only files matching this pattern will be displayed.
If you want multiple patterns, you can use formats like
'*.[ch]' or see glob.glob for details.  The default is '*'.

File

The file selected, or the default file.  The default default
is whatever the user selected last time in this directory.

Path

The path of the selected file, or the initial path.
The default is $HOME.

Title

The Title of the dialog box.  If Title is specified, then
select_file() dynamically appends "in </where/ever>" to it.
The default title is "in directory /where/ever".

TopDir

Restricts the user to remain within a directory or its
subdirectories.  The default is "/".

TextFile

Only text files will be displayed. The default is False.

Readable

Only readable files will be displayed. The default is False.

Writeable

Only writeable files will be displayed. The default is False.

Executable

Only executable files will be displayed.  The default is False.

Owned

Only files owned by the current user will be displayed.  This is
useful if the user is being asked to choose a file for a os.chmod()
or chgrp operation, for example.  The default is False.

Directory

Only directories will be displayed.  The default is False.
    '''
    import glob
#    if (!defined $option{'-Path'}) { $option{'-Path'}=$option{'-initialdir'}; }
#    if (!defined $option{'-FPat'}) { $option{'-FPat'}=$option{'-filter'}; }
#    if (!defined $option{'-ShowAll'}) {$option{'-ShowAll'}=$option{'-dotfiles'};}
#    if ($option{'-Directory'}) { $option{'-Chdir'}=1; $option{'-SelDir'}=1; }
#    my $multichoice = 0;
#    if (wantarray && !$option{'-Chdir'} && !$option{'-Create'}) {
#        $option{'-DisableShowAll'} = 1;
#        $multichoice = 1;
    if multichoice and not Chdir and not Create:
        DisableShowAll = True
    else:
        multichoice = False
#    } elsif (!defined $option{'-Chdir'}) {
#        $option{'-Chdir'} = 1;
#    }

    if Path and os.path.isdir(Path):
        dir = re.sub('([^/])$', r'\1/', Path)
    else:
        dir = re.sub('([^/])$', r'\1/', HOME)
 
    if TopDir:
        if os.path.isdir(TopDir):
            TopDir = re.sub('([^/])$', r'\1/', TopDir)
        if TopDir.find(dir) >= 0:
            dir = TopDir

    #my ($new, $file, @allfiles, @files, @dirs, @pre, @post, %seen, $isnew);
    #my @dotfiles;

    while True:
        if SelDir:
            pre = ['./']
        else:
            pre = []
        post = []
        try:
            allfiles = sorted(os.listdir(dir))
        except EnvironmentError as err:
            sorry(str(err))
            return None
        dotfiles = _re_grep(r'^\.', allfiles)
        if ShowAll:
            if dotfiles and not DisableShowAll:
                post=['Hide DotFiles']
        else:
            allfiles = _re_grep(r'^[^.]', allfiles)
            if dotfiles and not DisableShowAll:
                post=['Show DotFiles']
 
        # split @allfiles into @files and @dirs for option processing ...
        # @dirs  = grep(-d "$dir/$_" and -r "$dir/$_", @allfiles);
        dirs = []
        for f in allfiles:
            ff= os.path.join(dir, f)
            if os.path.isdir(ff) and _is_readable(ff):
                dirs.append(f)
        files = []
        if Directory:
            pass
        elif FPat:
            baselength = len(dir) + len(os.path.sep) -1
            for ff in glob.glob(os.path.join(dir,FPat)):
                if not os.path.isdir(ff):
                    f = ff[baselength:]
                    files.append(f)
        else:
            for f in allfiles:
                ff= os.path.join(dir, f)
                if not os.path.isdir(ff) and _is_readable(ff):
                    files.append(f)
 
        if Chdir:
            for i in range(len(dirs)):
                dirs[i] += os.path.sep
            if TopDir:
                up = re.sub('[^/]+/?$', '', dir)  # find parent directory
                if up.find(TopDir) >= 0:
                    pre.insert(0, '../')
                # must check for symlinks to outside the TopDir ...
            else:
                pre.insert(0, '../')
 
        elif not SelDir:
            dirs = []
 
        if Create:
            post.insert(0, 'Create New File')
        if TextFile:
            #@files = grep(-T "$dir/$_", @files); }
            i = 0
            while i < len(files):
                ff= os.path.join(dir, files[i])
                if not _is_textfile(ff):
                    files.pop(i)
                else:
                    i += 1
        if Owned:
            #@files = grep(-o "$dir/$_", @files); }
            i = 0
            while i < len(files):
                ff= os.path.join(dir, files[i])
                if not _is_owned(ff):
                    files.pop(i)
                else:
                    i += 1
        if Executable:
            #@files = grep(-x "$dir/$_", @files); }
            i = 0
            while i < len(files):
                ff= os.path.join(dir, files[i])
                if not _is_executable(ff):
                    files.pop(i)
                else:
                    i += 1
        if Writeable:
            #@files = grep(-w "$dir/$_", @files); }
            i = 0
            while i < len(files):
                ff= os.path.join(dir, files[i])
                if not _is_writeable(ff):
                    files.pop(i)
                else:
                    i += 1
        if Readable:
            #@files = grep(-r "$dir/$_", @files); }
            i = 0
            while i < len(files):
                ff= os.path.join(dir, files[i])
                if not _is_readable(ff):
                    files.pop(i)
                else:
                    i += 1

        allfiles = pre + sorted(dirs+files) + post  # reconstitute allfiles

        if Title:
            title = Title+" in "+dir
        else:
            title = "in directory "+dir+" ?"

        if File:
             set_default(title, File)
 
        if multichoice:
            new = choose(title, allfiles, multichoice=True)
            if not new:
                return []
            for i in range(len(new)):
                new[i] = dir+new[i]
            return new

        new = choose (title, allfiles)

        if (ShowAll and new == 'Hide DotFiles'):
            ShowAll = False
            _up(1)
            continue  # ARGHHHhhh no redo :-(
        elif (not ShowAll and new == 'Show DotFiles'):
            ShowAll = True
            _up(1)
            continue  # ARGHHHhhh no redo :-(
 
        if new == "Create New File":
            new = ask("new file name ?")  # validating this is a chore :-(
            if not new:
                continue
            if re.match('^/', new):
                file = new; 
            else:
                file = dir+new
            file = re.sub('//+', '/', file)  # simplify //// down to /
            while re.match(r'./\.\./', file):
                file = re.sub(r'[^/]*/\.\./', '', file)  # zap /../
            file = re.sub(r'/[^/]*/\.\.$', '', file)  # and /.. at end
            if TopDir:  # check against escape from TopDir
                if file.find(TopDir) > -1:
                    dir = TopDir
                    continue

            if os.path.isdir(file):  # pre-existing directory ?
                if SelDir:
                    return file
                else:
                    dir=file
                    if re.match('[^/]$', dir):
                        dir += '/'
                        continue
 
            #$file =~ m#^(.*/)([^/]+)$#;
            dirname = os.path.dirname(file)
            basename = os.path.basename(file)
            if os.path.exists(file):
                continue
            # must check for createbility (e.g. dir exists and is writeable)
            if os.path.isdir(dirname) and _is_writeable(dirname):
                return file
            if not _is_writeable(dirname):
                sorry ("directory "+dirname+" does not exist.")
                continue
            sorry ("directory "+dirname+" is not writeable.")
            continue

        if not new:
            return None
        if (new == './') and SelDir:
            return dir

        if re.match('^/', new):
            file = new      # abs filename
        else:
            file = dir+new  # rel filename (slash always at end)

        if (new == '../'):
            dir = re.sub('[^/]+/?$', '', dir)
            back_up()
            continue
        elif new == './':
            if SelDir:
                return dir
            file = dir
        elif re.search('/$', file):
            dir = file
            back_up()
            continue
        elif os.path.isfile(file):
            return file