#!/usr/bin/env python3
# -*-mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
# https://github.com/reid-k/gridfire

from __future__ import unicode_literals, print_function

__program__ = "Gridfire"
__version__ = "v0.5+testforge"
__doc__ = """
Gridfire is inspired by gpggrid, a security tool included in
Tinfoil Hat Linux, intended to resist shoulder-surfing and keylogging.
For more information on the project, see the README file distributed with Gridfire.

Gridfire is named after a fictional superweapon in Iain M Banks' Culture novels.
See http://everything2.com/title/Gridfire for more.
RIP Iain M Banks (1954-02-16 to 2013-06-09)

usage: gridfire.py [-h] [-a TTYALERT] [-c] [-g] [-u] [-l] [-b] [-o] [-d] [-f]
                   [-p] [-w] [-m METHOD] [-n] [-v VERBOSITY] 
                   [-S SINGLE] [-D DOUBLE] [-R REPEAT ] 
                   [ -P POS] [-H HEADER] [-A ANSWER] [-O fd]
                   [STDIN COMMAND [STDIN ARGS ...]]

positional arguments:
  STDIN COMMAND         Arguments to run a subprocess wih the password in stdin.

optional arguments:
  -h, --help            show this help message and exit
  -a TTYALERT, --ttyalert TTYALERT
                        Set the alert mode (none, beep or flash)
  -c                    Force concealment of entered text.
  -g, --grid            Ignore other options and behave like gpggrid.
  -u                    Allow uppercase letters [A-Z].
  -l                    Allow lowercase letters [a-z].
  -b                    Allow binary numbers [0-1].
  -o                    Allow octal numbers [0-7].
  -d                    Allow decimal numbers [0-9].
  -f                    Allow hexadecimal numbers [0-9][a-f][A-F].
  -p                    Allow punctuation.
  -w                    Allow whitespace.
  -m METHOD, --method METHOD
                        Specify indexing method (0-3, higher is more secure,
                        default=3)
  -n                    Append N newlines on output.
  -v VERBOSITY, --verbosity VERBOSITY
                        Specify verbosity (0-5, higher is verbose, default=3)
  -S SINGLE, --single SINGLE
                        Arg to pass to a subprocess with -SINGLE=password.
  -D DOUBLE, --double DOUBLE
                        Arg to pass to a subprocess with --DOUBLE=password.
  -R, --repeat
                        Repeat the password with a newline to a subprocess.
  -P POS, --pos POS     Position to place the Arg to a subprocess with
                        --DOUBLE/SINGLE=password.
  -H HEADER, --header HEADER
                        Header line as a prompt (default "").
  -A ANSWER, --answer ANSWER
                        Skip the grid and give the answer (for testing only).

"""

#Learn more about Tinfoil Hat Linux:
#Homepage:  http://tinfoilhat.shmoo.com/
#Wikipedia: https://en.wikipedia.org/wiki/Tinfoil_Hat_Linux

#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU Affero General Public License as published by
#    the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
#    You should have received a copy of the GNU Affero General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.

#TODO: Decide whether or not implement method 4
#TODO: Fix row index for non-default charsets

import os
import sys
import signal
import curses
import locale
import string
import random
import argparse
import _curses
from io import StringIO
import subprocess
from time import sleep

PY3 = sys.version_info[0] == 3
VERBOSE = 3
# 
#    0: stdin
#    1: stdout
#    2: stderr
iOUT_FD = sys.stdout

try:
    from pinentry import PinEntry
except ImportError:
    PinEntry = None
oPinEntry = None

try:
    from dialog import Dialog
except ImportError:
    Dialog = None
oDIALOG = None
oLAST_DIR = '/tmp/'

# Allow stderr for the caller to use
def debug(message):
    if VERBOSE <= 4: return
    sys.stdout.write('DEBUG: ' + repr(message) +'\n')
    pass
def info(message):
    if VERBOSE <= 3: return
    sys.stdout.write('INFO: ' + repr(message) +'\n')
    pass
def warn(message):
    if VERBOSE <= 2: return
    sys.stdout.write('WARN: ' + repr(message) +'\n')
    pass
def error(message):
    if VERBOSE <= 1: return
    sys.stdout.write('ERROR: ' + message +'\n')
    pass

class DummyContextManager:
    def __enter__(self):
        return self

    def __exit__(self, *exc):
        return False
                            
#Helps curses play nice
locale.setlocale(locale.LC_ALL, '')
code = locale.getpreferredencoding()

##### Defaults & Initial variables #####
charsets = ''
min_x, min_y = 55, 17
upperchar = ''
lowerchar = ''
out = ''
hide_out = 0
newline = 1

def spaceout(s):
    i=len(s)-1 #number of spaces to be inserted
    while i >= 1:
        s = s[:-i]+" "+s[-i:]
        i -= 1
    return s

def make_parser():
    """Argument parsing"""
    global VERBOSE
    
    parser = argparse.ArgumentParser(add_help=True)
    parser.add_argument('-a', '--ttyalert', type=str,       help='Set the alert mode (none, beep or flash)')
    parser.add_argument('-c', help="Force concealment of entered text.", action="store_true")
    parser.add_argument('-g', '--grid', help="Ignore other options and behave like gpggrid.", action="store_true")
    #~ parser.add_argument('-u', dest='char_u', action='store_true')
    parser.add_argument('-u', help="Allow uppercase letters [A-Z].", action="store_true")
    parser.add_argument('-l', help="Allow lowercase letters [a-z].", action="store_true")
    parser.add_argument('-b', help="Allow binary numbers [0-1].", action="store_true")
    parser.add_argument('-o', help="Allow octal numbers [0-7].", action="store_true")
    parser.add_argument('-d', help="Allow decimal numbers [0-9].", action="store_true")
    parser.add_argument('-f', help="Allow hexadecimal numbers [0-9][a-f][A-F].", action="store_true")
    parser.add_argument('-p', help="Allow punctuation.", action="store_true")
    parser.add_argument('-w', help="Allow whitespace.", action="store_true")
    parser.add_argument('-m', '--method', help='Specify indexing method (0-3, higher is more secure, default=3)', dest='method', type=int, default=3)
    parser.add_argument('-n', help="Append newlines on output.", dest='newline', type=int, default=1)
    parser.add_argument('-v', '--verbosity', help='Specify verbosity (0-5, higher is verbose, default=3)', type=int, default=3)
    parser.add_argument('-S', '--single', help="Arg to pass to a subprocess with -SINGLE=password.", dest="single", default='')
    parser.add_argument('-D', '--double', help="Arg to pass to a subprocess with --DOUBLE=password.", dest="double", default='')
    parser.add_argument('-R', '--repeat', help="Repeat the password with a newline to a subprocess", action="store_true")
    parser.add_argument('-P', '--pos', help="Position to place the Arg to a subprocess with --DOUBLE/SINGLE=password.", dest="pos", type=int, default=1)
    parser.add_argument('-H', '--header', help="Header line as a prompt (default  \"\").", dest="header", default='')
    parser.add_argument('-A', '--answer', help="Skip the grid and give the answer (for testing only).", dest="answer", default='')
    parser.add_argument('-O', '--output_fd', help="Output file descriptor to print the answer (default 1).", dest="output_fd", type=int, default=1)
    parser.add_argument('sargs', help="Arguments to run a subprocess wih the password in stdin.", type=str, default='', nargs='*') # metavar='STDIN COMMAND', 
    return parser

def make_grid(args):
    """Define character sets"""
    punct      = spaceout(string.punctuation)
    punct2     = punct[52:]
    punct      = punct[:52]
    whitespace = spaceout(string.whitespace)

    #Select character sets
    #TODO: Can this be simplified? args['u'+'l'] or whatnot?
    #use_uppercases = (-1 != charsets.find("u"))
    use_uppercases = args['u']
    use_lowercases = args['l']
    use_bindigits  = args['b']
    use_octdigits  = args['o']
    use_decdigits  = args['d']
    use_hexdigits  = args['f']
    use_punct      = args['p']
    use_whitespace = args['w']
    _setcount = args['u']+args['l']+args['b']+args['o']+args['d']+args['f']+args['p']+args['w']

    #if use_uppercases+use_lowercases+use_bindigits+use_octdigits+use_decdigits+use_hexdigits+use_punct+use_whitespace == 0:
    if _setcount == 0:
        use_uppercases = 1
        use_lowercases = 1
        use_decdigits  = 1
        use_punct      = 1
        use_whitespace = 1

    use_numbers    = 0
    numbers = ''
    if use_bindigits+use_octdigits+use_decdigits+use_hexdigits: use_numbers = 1
    if use_bindigits: numbers = '0 1'
    if use_octdigits: numbers = spaceout(string.octdigits)
    if use_decdigits: numbers = spaceout(string.digits)
    if use_hexdigits: numbers = spaceout(string.hexdigits)

    ##### Build static grid #####
    grid = []
    if use_uppercases:
        if PY3:
            grid.append(spaceout(string.ascii_uppercase))
        else:
            grid.append(spaceout(string.uppercase))
    if use_lowercases:
        if PY3:
            grid.append(spaceout(string.ascii_lowercase))
        else:
            grid.append(spaceout(string.lowercase))
    if use_numbers: grid.append(numbers)
    if use_punct:
        grid.append(punct)
        grid.append(punct2)
    return (use_uppercases,
                use_lowercases,
                use_decdigits,
                use_punct,
                use_whitespace,
                punct,
                punct2,
                whitespace,
                numbers,
                grid, )

##### Build user interface #####
usagebanner = "Choose from grid by typing letter pairs like eF or Dg"

#? not hasattr(sys, 'frozen') and
if Dialog is not None and os.path.isfile('/usr/bin/dialog'):
    shortcuts = "4:Shell 5:FName 6:FContents 7:Delete 8:Show/Hide 9:Quit 0:Done"
else:
    shortcuts = "7:Delete 8:Show/Hide 9:Quit 0:Done"
    
def total_rows(use_lowercases, use_uppercases, use_decdigits, use_whitespace, use_punct):
    return use_lowercases+use_uppercases+use_decdigits+use_whitespace+use_punct+use_punct+1

def iMain(lSysArgv):
    global password, exit_code, VERBOSE, iOUT_FD, oDIALOG, oLAST_DIR
    global min_x, min_y, upperchar, lowerchar, out, hide_out
    global oLAST_DIR

    padpos = 0
    
    stdscr = None
    password = ''
    exit_code = -1
    out = ''
    upperchar = ''
    lowerchar = ''

    if PY3:
        uppercase = string.ascii_uppercase
        lowercase = string.ascii_lowercase
    else:
        uppercase = string.uppercase
        lowercase = string.lowercase

    # Check for required size before continuing
    def check_size(stdscr):
        (size_y,size_x) = stdscr.getmaxyx()
        if ((size_x < min_x)|(size_y < min_y)):
            error( __program__+" needs a "+str(min_x)+"x"+str(min_y)+" character area to function.\n")
            error( "please adjust your terminal size and restart "+__program__+".")
            sys.exit(1)

    def pad_draw(stdscr, pad, ly, lx, hy, hx, use_lowercases, use_uppercases, use_decdigits, use_whitespace, use_punct, header, title):
        rv = __program__ +" "+ __version__
        pad.addstr(0,0," "*(hx+1),curses.A_REVERSE)
        pad.addstr(0,2,rv,curses.A_REVERSE)
        pad.addstr(1,0," "*min_x)
        #~ pad.addstr(0,40,"    ")
        pad.addstr(0,40,lowerchar,curses.A_REVERSE)
        pad.addstr(0,41,upperchar,curses.A_REVERSE)
        pad.addstr(0,73,"SHOW")
        if hide_out:
            pad.addstr(0,73,"HIDE")
        pad.addstr(1,0,header)
        pad.addstr(2,0,">>")
        pad.addstr(2,4," "*(len(out)+1))
        disp_out = out
        if hide_out:
            disp_out = len(out)*"*"
        pad.addstr(2,4,disp_out)
        
        ##Draw the grid
        global gridrow
        gridrow = 6
        gridoffset = 4
        
        #Grid Indexes
        pad.addstr(gridrow-2,gridoffset,spaceout(col_index))
        for t in range(0, total_rows(use_lowercases, use_uppercases, use_decdigits, use_whitespace, use_punct)):
            pad.addstr(gridrow+t,1,row_index[t])
            t += 1
        
        #Index selection highlighting
        if len(upperchar):
            stdscr.addstr(gridrow-2,4+spaceout(col_index).find(upperchar),upperchar,curses.A_REVERSE)
        if len(lowerchar):
            stdscr.addstr(gridrow+row_index.find(lowerchar),1,lowerchar,curses.A_REVERSE)
        
        #Static grid elements
        ### New grid draw method ###
        r = 0
        for s in grid:
            pad.addstr(gridrow,gridoffset,grid[r])
            r += 1
            gridrow += 1
        
        if use_whitespace:
            #Draw the whitespace row
            pad.addstr(gridrow,gridoffset," :Space   :Tab     :Enter")
            pad.addstr(gridrow,gridoffset,col_index[0])
            pad.addstr(gridrow,9+gridoffset,col_index[1])
            pad.addstr(gridrow,18+gridoffset,col_index[2])
            #If the corresponding columns are selected, highlight them
            if len(upperchar)&(upperchar==col_index[0]):
                stdscr.addstr(gridrow,4+col_index.find(upperchar),upperchar,curses.A_REVERSE)
            if len(upperchar)&(upperchar==col_index[1]):
                stdscr.addstr(gridrow,11+col_index.find(upperchar),upperchar,curses.A_REVERSE)
            if len(upperchar)&(upperchar==col_index[2]):
                stdscr.addstr(gridrow,19+col_index.find(upperchar),upperchar,curses.A_REVERSE)
            gridrow += 1
    
        #Draw the 'done' line, and highlight column if selected
        pad.addstr(gridrow,gridoffset," :Ignore")
        pad.addstr(gridrow,gridoffset,col_index[0])
#        pad.addstr(gridrow,9+gridoffset,col_index[1])
#        pad.addstr(gridrow,18+gridoffset,col_index[2])
        if len(upperchar)&(upperchar==col_index[0]):
            stdscr.addstr(gridrow,4+spaceout(col_index).find(upperchar),upperchar,curses.A_REVERSE)
        if len(upperchar)&(upperchar==col_index[1]):
            stdscr.addstr(gridrow,10+spaceout(col_index).find(upperchar),upperchar,curses.A_REVERSE)
        if len(upperchar)&(upperchar==col_index[2]):
            stdscr.addstr(gridrow,17+spaceout(col_index).find(upperchar),upperchar,curses.A_REVERSE)
        
        #Help banners
        pad.addstr(14,0,title)
        pad.addstr(15,0,usagebanner)
        # (1+hx-len(shortcuts))*' '+
        pad.addstr(16,0,shortcuts,curses.A_REVERSE)
        
        pad.refresh(padpos, 0, ly, lx, hy, hx)

    def main_draw(stdscr, pad, use_lowercases, use_uppercases, use_decdigits, use_whitespace, use_punct, header, title):
        (max_y, max_x,) = stdscr.getmaxyx()
        pad_draw(stdscr, pad, 0, 0, (max_y-2), (max_x-1), use_lowercases, use_uppercases, use_decdigits, use_whitespace, use_punct, header, title)
        stdscr.move(max_y-1,0)
        try:
            curses.curs_set(0)
        except:
            pass
        stdscr.refresh()

    def cycle_index(cyclemethod=3): #Argument specifies method, default is 3
        global col_index, row_index
        if PY3:
            uppercase = string.ascii_uppercase
            lowercase = string.ascii_lowercase
        else:
            uppercase = string.uppercase
            lowercase = string.lowercase
        #Method 0: Ordered letters and numbers, no randomization
        #This method has 1 possible state and provides no security.
        #Included to allow gridfire's use to limit possible symbols entered.
        #For example, hex digits only when entering a WEP key.
        if cyclemethod==0:
            col_index = uppercase
            row_index = lowercase

        #Method 1: Ordered letters, random offset (as with gpggrid)
        #The math might differ from gpggrid
        #This method has 676 or 26^2 possible states
        if cyclemethod==1:
            offset = random.randint(0,25)
            col_index = uppercase[offset:]+uppercase[:offset]
            offset = random.randint(0,25)
            row_index = lowercase[offset:]+lowercase[:offset]

        #Method 2: use random.shuffle() to shuffle one index in place
        #This might not work very well, see module documentation on
        #issues with small len(x) exceeding period of RNGs
        # http://docs.python.org/2/library/random.html
        ##This method has:
        ##403,291,461,126,605,635,584,000,000
        ##or 26! possible states (403.3 septillion)
        if cyclemethod==2:
            col_index = uppercase
            colcycle = list(uppercase)
            random.shuffle(colcycle)
            col_index = ''.join(colcycle)
            row_index = col_index.lower()

        #Method 3: use random.shuffle() to shuffle indices in place
        #This might not work very well, see module documentation on
        #issues with small len(x) exceeding period of RNGs
        # http://docs.python.org/2/library/random.html
        #This method has:
        #162,644,002,617,632,464,507,038,883,409,628,607,021,056,000,000,000,000
        ##or 26!^2 possible states (162.6 sexdecillion).
        if cyclemethod==3:
            col_index = uppercase
            colcycle = list(uppercase)
            random.shuffle(colcycle)
            col_index = ''.join(colcycle)

            row_index = lowercase
            rowcycle = list(lowercase)
            random.shuffle(rowcycle)
            row_index = ''.join(rowcycle)

        #TODO: Implement method 4 (mixed cases in row/column indexes)
            # (This would require redoing input handling to recognize same-case pairs)
        #Method 4 would have:
        #80,658,175,170,943,878,571,660,636,856,403,766,975,289,505,440,883,277,824,000,000,000,000
        #or 52! possible states (80.6 Unvigintillion).

    def quit_scr(leave_message = "", exit_status= 0):
        global password, exit_code
        stdscr.keypad(0)
        curses.echo()
        curses.nocbreak()
        curses.endwin()
        # print leave_message
        password = leave_message
        exit_code = exit_status
    
    parser = make_parser()
    args = vars(parser.parse_args(lSysArgv))
    
    debug('args.keys() ' +repr(args.keys()))
    VERBOSE = args['verbosity']
    cyclemethod = args['method'] or 3
    header = args['header'] or ''
    answer = args['answer'] or ''
    output_fd = args['output_fd'] or 1
    sargs = args['sargs']

    if output_fd != 1:
        iOUT_FD = os.fdopen(int(output_fd), 'w')
        
    info('DEBUG: sargs=' + repr(sargs))

    (use_uppercases,
     use_lowercases,
     use_decdigits,
     use_punct,
     use_whitespace,
     punct,
     punct2,
     whitespace,
     numbers,
     grid ) = make_grid(args)

    if answer:
        # for testing - bypass the grid and accept the answer
        password = answer
        exit_code = 0
    else:
        if not stdscr:
            stdscr = curses.initscr()
            curses.noecho()
            curses.start_color()
            try:
                curses.use_default_colors()
            except:
                pass
            stdscr.keypad(1)

            for key, value in _curses.__dict__.items():
                if key[0:4] == 'ACS_' or key in ('LINES', 'COLS'):
                    if key == 'LINES':
                        value = value - 3
                    setattr(curses, key, value)

        curses.flushinp ()
        check_size(stdscr)
        pad = curses.newpad(100,100)
        if args['ttyalert'] == 'beep':
            curses.beep()
        elif args['ttyalert'] == 'flash':
            curses.flash()

        ##### MAIN LOOP: draw the interface and handle user input #####
        cycle_index(cyclemethod) #Don't start the program with unrandomized indexes
        # If this isn't done, the program initially shows nothing
        main_draw(stdscr, pad, use_lowercases, use_uppercases, use_decdigits, use_whitespace, use_punct, header,  '')

    _previous_sigcont_handler = None
    while exit_code == -1:
        if hasattr(sys.stderr, 'getvalue'):
            title = sys.stderr.getvalue().strip()
            sys.stderr.buf = ''
        else:
            title = ''
        if title:
            title = title.split('\n')[-1]
        main_draw(stdscr, pad, use_lowercases, use_uppercases, use_decdigits, use_whitespace, use_punct, header, title)
        curses.noecho()
        curses.cbreak()
        sys.stderr.flush()
        sys.stdout.flush()
        if not _previous_sigcont_handler:
            _previous_sigcont_handler = signal.getsignal(signal.SIGCONT)

        try:
            c = stdscr.getkey()
        except:
            quit_scr()
            break
            
        if c == '0':
            quit_scr(out) #on 'Done', write output and exit
            break
        if c in ['', '9', chr(27)]: # Quit
            quit_scr() #on 'Cancel', exit without writing output
            break
        if c == '8': # Hide
            hide_out -= hide_out+hide_out
            hide_out += 1
        elif ((c == '7')&(len(out)>=1)) or ((c == 'KEY_BACKSPACE')&(len(out)>=1)):
            out = out[:len(out)-1] # Delete
        elif Dialog is not None and c == '6': # File Contents
                        
# you will first need to save the tty modes with a call to def_prog_mode()
# and then call endwin() to end the curses mode. This will leave you in the
# original tty mode. To get back to curses once you are done, call
# reset_prog_mode() . This function returns the tty to the state stored by
# def_prog_mode(). Then do refresh(), and you are back to the curses mode.     
            _curses.def_prog_mode() ; _curses.endwin()
            sBut, sCode = lDialogFselect(oLAST_DIR, height=10, width=50, help_button=False)
            if sBut == 'ok':
                sOut = sCode
            else:
               return
            sBut = 'ok'; sCode = '='
            if sOut and out:
               sText = sOut # "New text at Beginning, Replace or End"
               sBut, sCode = lDialogCode(sText)                   
            if sBut != 'ok':
                pass
            elif sCode == '<':
                out = sOut + ' ' + out
            elif sCode == '>':
                out = out + ' ' + sOut
            elif sCode == '=':
                out = sOut
            elif sCode == '':
                out = ''

            # oDIALOG.msgbox("Bug Alert: If the screen goes black, suspend to shell with ^Z and then use fg to come back to Gridfire " +sCode, height=8, width=40)

            main_draw(stdscr, pad, use_lowercases, use_uppercases, use_decdigits, use_whitespace, use_punct, header,  '')
            _curses.reset_prog_mode()
            stdscr.refresh()
            
        elif Dialog is not None and c == '5': # File Name
                        
# you will first need to save the tty modes with a call to def_prog_mode()
# and then call endwin() to end the curses mode. This will leave you in the
# original tty mode. To get back to curses once you are done, call
# reset_prog_mode() . This function returns the tty to the state stored by
# def_prog_mode(). Then do refresh(), and you are back to the curses mode.     
            _curses.def_prog_mode() ; _curses.endwin()
            sOut = sDialogFget()
            sBut = 'ok'; sCode = '='
            if sOut and out:
               sText = sOut # "New text at Beginning, Replace or End"
               sBut, sCode = lDialogCode(sText)                   
            if sBut != 'ok':
                pass
            elif sCode == '<':
                out = sOut + ' ' + out
            elif sCode == '>':
                out = out + ' ' + sOut
            elif sCode == '=':
                out = sOut
            elif sCode == '':
                out = ''

            # oDIALOG.msgbox("Bug Alert: If the screen goes black, suspend to shell with ^Z and then use fg to come back to Gridfire " +sCode, height=8, width=40)

            main_draw(stdscr, pad, use_lowercases, use_uppercases, use_decdigits, use_whitespace, use_punct, header,  '')
            _curses.reset_prog_mode()
            stdscr.refresh()
            
        elif Dialog is not None and c == '4': # Shell
            if oDIALOG is None:
                oDIALOG = Dialog()
            sCode = '='
            sOut = ''
            yStdoutdata = yStderrdata = b''

# you will first need to save the tty modes with a call to def_prog_mode()
# and then call endwin() to end the curses mode. This will leave you in the
# original tty mode. To get back to curses once you are done, call
# reset_prog_mode() . This function returns the tty to the state stored by
# def_prog_mode(). Then do refresh(), and you are back to the curses mode.     
            _curses.def_prog_mode() ; _curses.endwin()
            sCode, sAnswer = oDIALOG.inputbox("Run command (erase to cancel)",
                                              init='')
            if sCode == 'ok' and sAnswer:

                try:
                    devnull = subprocess.DEVNULL
                except AttributeError: # Python < 3.3
                    devnull_context = devnull = open(os.devnull, "wb")
                else:
                    devnull_context = DummyContextManager()

                with devnull_context:
                    p = subprocess.Popen(sAnswer,
                                         stdout=subprocess.PIPE,
                                         stderr=devnull, close_fds=True)
                    if True:
                        ( yStdoutdata, yStderrdata ) = p.communicate()
                        retcode = p.returncode
                        oDIALOG.scrollbox(yStdoutdata.decode().strip(),
                                          15,50)
                    else:
                        retcode = p.wait()
                        ystdoutdata = p.stdout.read()
                        # One could use title=... instead of text=... to put the text
                        # in the title bar.
                        oDIALOG.programbox(fd=p.stdout.fileno(),
                                        text="Output of command: " +sAnswer)

#                oDIALOG.msgbox("Bug Alert: If the screen goes black, suspend to shell with ^Z and then use fg to come back to Gridfire " +str(retcode),  height=8, width=40)

                if retcode == 0:
                    sOut = yStdoutdata.decode().strip()
                sBut = 'ok'; sCode = '='
                if sOut and out:
                   sText = sOut # "New text at Beginning, Replace or End"
                   sBut, sCode = lDialogCode(sText)                   
                if sBut != 'ok':
                    pass
                elif sCode == '<':
                    out = sOut + ' ' + out
                elif sCode == '>':
                    out = out + ' ' + sOut
                elif sCode == '=':
                    out = sOut
                elif sCode == '':
                    out = ''
                _curses.reset_prog_mode()
                stdscr.refresh()
                
        elif c == '3': # was Ignore
            # now 'Ignore' is a keypair to fool keystroke counting
            pass
        elif PinEntry is not None and c == '2': # unused
            if oPinEntry is None:
                oPinEntry = PinEntry()
            oPinEntry.run()
            # unfinished
        #handle row/column selections
        elif (uppercase.find(c)+1):
            upperchar = c
        elif (row_index[:total_rows(use_lowercases, use_uppercases, use_decdigits, use_whitespace, use_punct)].find(c)+1):
            lowerchar = c

        #if selection completes a pair, find choice, add to output, reset selection
        if len(upperchar)&len(lowerchar):
            #if selected pair chooses 'Done' or 'Cancel'
            if((upperchar==col_index[0])&(lowerchar==row_index[len(grid)+1])):
                #on 'Ignore', do nothing
                pass
            #  :Space  :Tab  :Enter
            elif((upperchar==col_index[0])&(lowerchar==row_index[len(grid)])):
                out += ' ' #on 'Space'
            elif((upperchar==col_index[1])&(lowerchar==row_index[len(grid)])):
                out += '	' #on 'Tab'
            elif((upperchar==col_index[2])&(lowerchar==row_index[len(grid)])):
                out += '\n' #on 'Enter'
#            elif((upperchar==col_index[2])&(lowerchar==row_index[len(grid)])):
#                out = out[:len(out)-1] #on 'Delete'
            else:
                rowchoice = row_index.find(lowerchar)
                if rowchoice < len(grid):
                    if (grid[rowchoice].find("A") == 0):
                        out += uppercase[col_index.find(upperchar)]
                    if (grid[rowchoice].find("a") == 0):
                        out += lowercase[col_index.find(upperchar)]
                    if (grid[rowchoice].find("0") == 0)&(spaceout(col_index).find(upperchar) < len(numbers)):
                        out += numbers[spaceout(col_index).find(upperchar)]
                    if (grid[rowchoice].find("!") == 0):
                        out += punct[spaceout(col_index).find(upperchar)]
                    if (grid[rowchoice].find("_") == 0)&(spaceout(col_index).find(upperchar) < len(punct2)):
                        out += punct2[spaceout(col_index).find(upperchar)]

            upperchar = ''
            lowerchar = ''
            cycle_index(cyclemethod)
            
    single = args['single'] or ''
    double = args['double'] or ''
    repeat = args['repeat'] or ''
    if single:
        thing = ['-'+single, password]
        sStdIn = None
    elif double:
        thing = ['--'+double, password]
        sStdIn = None
    elif repeat:
        thing = []
        sStdIn = password +'\n' +password
    else:
        thing = []
        sStdIn = password
        
    if exit_code == 0 and sargs and password:
        # sys.stderr.write('EXIT: ' + repr(password) + '\n')
        if type(sargs) == str: 
            #? quote
            largs = sargs.split(' ')
        elif type(sargs) == list and len(sargs) == 1 and sargs[0].find(' ') > 0: 
            largs = sargs[0].strip().split()
        elif type(sargs) == list and len(sargs) > 1: 
            largs = sargs
        else:
            raise RuntimeError('ERROR: not list or str type(sargs) ' + str(type(sargs) ))

        if thing:
            pos = args['pos']
            if pos == -1:
                largs.append(thing)
            else:
                largs.insert(pos, thing)
        shell = False
        executable = '' # largs[0]        sargs = ' '.join(largs[1:])
        (exit_code, password,) = lSubprocess(largs, shell, executable, sStdIn=sStdIn)
    else:
        debug('len(password)=%d' %  len(password))
   
    # del password, exit_code
    stdscr = None
    cycle_index(cyclemethod) #Don't start the program with unrandomized indexes
    retval = [exit_code, password][:]
    out = ''
    exit_code = -1

    return retval

# should be getpass.getpass compatible
def getpass(prompt="", stream=None, tty=None, main_args=None):
    # what should stream be if curses should be a tty
    if tty is not None:
        pass
    elif stream is not None:
        tty = os.ttyname(stream.fileno())
    elif 'GPG_TTY' in os.environ:
        tty = os.environ['GPG_TTY']
    else:
        tty = os.ttyname(sys.stdin.fileno())
    with open(tty, 'r') as fd:
        assert fd.isatty(), "The input stream must be a tty: %r" % (tty,)

    if not hasattr(sys, '_stderr'):
        sys._stderr = sys.stderr
        sys.stderr = StringIO()
        
    password = ''
    exit_code = -1
    if main_args is None:
        main_args = ['-H', prompt]
    else:
        main_args += ['-H', prompt]
    try:
        (exit_code, password,) = main(main_args)
        info('EXIT: getpass ' + prompt +' ' + repr(password))
    except KeyboardInterrupt:
        info('EXIT: getpass KeyboardInterrupt')
        
    if hasattr(sys, '_stderr'):
        sys.stderr = sys._stderr
    
    return password
 
def sDialogFget():
    global oLAST_DIR
    sBut, sCode = lDialogFselect(oLAST_DIR, height=10, width=50, help_button=False)
    sOut = ''
    if sBut == 'ok':
        sFile = sCode
        try:
            with open( sFile, 'rt') as iFd:
                sOut = iFd.read().strip()
        except Exception as e:
            # dunno
            pass
        else:                    
            if os.path.isdir(sFile):
                oLAST_DIR = sFile
            else:
                oLAST_DIR = os.path.dirname(sFile)
            oLAST_DIR = oLAST_DIR + '/'
    return sOut
    
def lDialogFselect(sLastDir=None, height=10, width=50, help_button=False, prompt=None):
    global oDIALOG, oLAST_DIR
    if oDIALOG is None:
        oDIALOG = Dialog()
    if not sLastDir:
        sLastDir = oLAST_DIR
    sBut, sCode = oDIALOG.fselect(sLastDir, height, width, help_button=False)
    return (sBut, sCode,)

def lDialogCode(sText):
   lChoices = [("<", "Begin - Add the text at the beginning"),
               ("=", "All - Replace all with the text"),
               (">", "End - Add the text at the end")]
   sCode = '='
   sBut, sCode = oDIALOG.menu(sText, height=16, width=50,
                     choices=lChoices,
                     title="Replace the buffer with file contents",
                     help_button=False)
   return (sBut, sCode,)

def lSubprocess(largs, shell=False, executable='', sStdIn=''):

    env = os.environ.copy()
    if 'PYTHONPATH' in env:
        del env['PYTHONPATH']
    debug('executable=' + repr(executable) +' largs=' + repr(largs) ) # + ' len(password)=%d' %  len(password)

    yStdoutData = stderrdata = ''
    try:
        oProcess = subprocess.Popen(largs,
                               stdin=subprocess.PIPE,
                               stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE,
                               env=env,
                               shell=shell)
        sleep(3)
    except Exception as e:
        error('Popen executable=' + repr(executable) +' largs=' + repr(largs) + '\n' + str(e))
        exit_code = 1
        sStdOut = ''
    else:
        if sStdIn:
            (yStdoutData, stderrdata,) = oProcess.communicate(sStdIn.encode())
        else:
            #? wait
            (yStdoutData, stderrdata,) = oProcess.communicate()
        #? oProcess.close()
        if stderrdata:
            sys.stderr.write(stderrdata.decode() + '\n')
        exit_code = oProcess.returncode
        sStdOut = yStdoutData.decode()
    return (exit_code, sStdOut,)

def main():
    sargs = sys.argv[1:]
    if type(sargs) == list and len(sargs) == 1 and sargs[0].find(' ') > 0: 
        lSysArgv = sargs[0].strip().split()
    else:
        lSysArgv = sargs
    # print('DEBUG: ' +repr(sys.argv[1:]) +' ' +repr(type(sys.argv[1:])))
    if hasattr(sys, 'frozen') and hasattr(sys, '_MEIPASS'):
        print('DEBUG: running in a PyInstaller bundle')
    (exit_code, password,) = iMain(lSysArgv)
    iOUT_FD.write(password+('\n'*newline))
    password = ''
    sys.stderr.flush()
    return exit_code

if __name__ == '__main__':
    sys.exit(main())
