#! /usr/bin/python3
r'''
a Python3 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(Mouse, or Arrow-keys and Return)", w) # multi-line q
x = choose("Which ?\n"+help_text(), w) # built-in help_text
confirm(text) and do_something()
answer = ask(question)
answer = ask(question, suggestion)
password = ask_password("Enter password : ")
filename = ask_filename("Which file ?")
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 either (since 1.50) the mouse; or 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. Also (since 1.50) the
SET_ANY_EVENT_MOUSE and kmous (terminfo) sequences, which are supported
by all xterm, rxvt, konsole, screen, linux, gnome and putty terminals.
Since version 1.60, a speaking interface is provided for the visually
impaired user; it employs eflite or espeak. Speech is turned on
if the CLUI_SPEAK environment variable is set to any non-empty string.
Since version 1.62, if speakup is running, it is silenced while
TermClui runs, and then restored. Because TermClui's metaphor for
the computer is a human-like conversation-partner, this works very
naturally. The application needs no modification.
Download TermClui.py from www.pjb.com.au/midi/free/TermClui.py or
from http://cpansearch.perl.org/src/PJB/Term-Clui-1.67/py/TermClui.py
and put it in your PYTHONPATH. TermClui.py depends on Python3.
TermClui.py is a translation into Python3 of the Perl CPAN Modules
Term::Clui and Term::Clui::FileSelect. This is version 1.67
'''
import re, sys, select, signal, subprocess, os, random
import termios, fcntl, struct, stat, time, dbm
VERSION = '1.67'
def _which(s):
for d in os.getenv('PATH').split(':'):
f = d+'/'+str(s)
if os.path.exists(f):
return f
return None
def _warn(string):
print(string, file=sys.stderr)
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
_Eflite = None
_Eflite_FH = None # open here at top-level so one sub can silence the previous
_Espeak = None
_Espeak_PID = 0 # defined at top-level so one espeak can kill the previous
_SpeakUpSilentFile = None # 1.62
if len(os.getenv('CLUI_SPEAK','')) > 0:
for d in ['/sys/accessibility', '/proc']:
if _is_writeable(d+"/speakup/silent"):
_SpeakUpSilentFile = d+"/speakup/silent"
break
_Eflite = _which('eflite')
_Espeak = _which('espeak')
if _Eflite:
_pipe = subprocess.Popen(_Eflite, shell=False, stdin=subprocess.PIPE)
if _pipe:
_Eflite_FH = _pipe.stdin
else:
_warn("can't run "+str(_Eflite)+": $!\n")
elif not _Espeak:
_warn("TermClui warning: CLUI_SPEAK set; but can't find eflite or espeak")
# ------------------------ 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_INSERT = 0o525
_KEY_DELETE = 0o524
_KEY_HOME = 0o523
_KEY_END = 0o522
_KEY_PPAGE = 0o521
_KEY_NPAGE = 0o520
_KEY_BTAB = 0o541
_getchar = lambda: sys.stdin.read(1)
_ttyin = 0
_ttyout = 0
_AbsCursX = 0
_AbsCursY = 0
_TopRow = 0
_CursorRow = 0
_LastEventWasPress = False
# _SpecialKey unneeded - we test for class int
_irow = 0 # maintained by _puts, _up, _down, _left and _right
_icol = 0
_irow_a = [] # maintined by _layout()
_icol_a = []
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)
_ttyout.flush()
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)
_ttyout.flush()
def _red():
global _ttyout
print("\033[31m", end='', file=_ttyout)
_ttyout.flush()
def _green():
global _ttyout
print("\033[32m", end='', file=_ttyout)
_ttyout.flush()
def _blue():
global _ttyout
print("\033[34m", end='', file=_ttyout)
_ttyout.flush()
def _violet():
global _ttyout
print("\033[35m", end='', file=_ttyout)
_ttyout.flush()
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 _dbc(c):
if ord(c) < 33:
_debug("ord(c)="+str(ord(c)))
else:
_debug("c="+str(c))
return
def _getch():
global _KEY_UP, _KEY_DOWN, _KEY_RIGHT, _KEY_LEFT
global _KEY_PPAGE, _KEY_NPAGE, _KEY_BTAB, _KEY_HOME, _KEY_END
global _AbsCursX, _AbsCursY
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 == '2'):
_getc_wrapper(0)
return _KEY_INSERT
if (c == '3'):
_getc_wrapper(0)
return _KEY_DELETE
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 == 'F'):
return _KEY_END # 1.67
if (c == 'H'):
return _KEY_HOME # 1.67
if (c == 'M'): # mouse report
# http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
event_type = ord(_getc_wrapper(0))-32;
x = ord(_getc_wrapper(0))-32;
y = ord(_getc_wrapper(0))-32;
#event_type = ord(_ttyin.buffer.read(1))-32;
#x = ord(_ttyin.buffer.read(1))-32;
#y = ord(_ttyin.buffer.read(1))-32;
# my $shift = $event_type & 0x04; # used by wm
# my $meta = $event_type & 0x08; # used by wm
# my $control = $event_type & 0x10; # used by xterm
button_drag = (event_type & 0x20) >> 5
low3bits = event_type & 0x03
if low3bits == 0x03:
button_pressed = 0
else: # button 4 means wheel-up, button 5 means wheel-down
if event_type & 0x40:
button_pressed = low3bits + 4
else:
button_pressed = low3bits + 1
t = _handle_mouse(x,y,button_pressed,button_drag)
if t != '':
return t
else:
return _getch()
if re.search('\d', c) != None:
c1 = _getc_wrapper(0)
if c1 == '~':
if c == '2':
return _KEY_INSERT
if c == '3':
return _KEY_DELETE
if c == '5':
return _KEY_PPAGE
if c == '6':
return _KEY_NPAGE
else: # cursor-position report, response to \033[6n
_AbsCursY = int(c)
while True:
if c1 == ';':
break
_AbsCursY = 10*_AbsCursY + int(c1)
c1 = _getc_wrapper(0)
_AbsCursX = 0
while True:
c1 = _getc_wrapper(0)
if c1 == 'R':
break
_AbsCursX = 10*_AbsCursX + int(c1)
return _getch()
if c == 'Z':
return _KEY_BTAB
return c
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; }
_InitscrAlreadyRun = 0 # its a counter
# tty = True
_ttyout_fnum = 0
_old_tcattr = 0
_IsMouseMode = False
_WasMouseMode = False
_IsSpeakUpSilent = False # 1.62
_WasSpeakUpSilent = False # 1.62
def _enter_mouse_mode (): # 1.50
global _ttyin, _IsMouseMode
if os.getenv('CLUI_MOUSE') == 'OFF':
return ''
if _IsMouseMode:
_warn("_enter_mouse_mode but already IsMouseMode\r\n")
return 1
#_ttyin.close()
#_ttyin = open("/dev/tty", mode="rb", buffering=0)
print("\033[?1003h", end='', file=_ttyout) # sets SET_ANY_EVENT_MOUSE mode
#_ttyout.flush()
_IsMouseMode = True
return 1
def _leave_mouse_mode (): # 1.50
global _ttyin, _IsMouseMode
if os.getenv('CLUI_MOUSE') == 'OFF':
return ''
if not _IsMouseMode:
_warn("_leave_mouse_mode but not IsMouseMode\r\n")
return 1
#_ttyin.close()
#_ttyin = open("/dev/tty", mode="r")
print("\033[?1003l", end='', file=_ttyout) # cancel SET_ANY_EVENT_MOUSE
_ttyout.flush()
_IsMouseMode = False
return 1
def _enter_speakup_silent (): # 1.62
global _ttyin, _IsSpeakUpSilent, _SpeakUpSilentFile
if not _SpeakUpSilentFile:
return False
if _IsSpeakUpSilent:
_warn("_enter_speakup_silent but already IsSpeakUpSilent\r\n")
return True
S = open(_SpeakUpSilentFile, 'w')
S.write("7\n")
# S.close()
_IsSpeakUpSilent = True
return True
def _leave_speakup_silent (): # 1.62
global _ttyin, _IsSpeakUpSilent, _SpeakUpSilentFile
if not _SpeakUpSilentFile:
return False
if not _IsSpeakUpSilent:
_warn("_leave_speakup_silent but not IsSpeakUpSilent\r\n")
return True
S = open(_SpeakUpSilentFile, 'w')
S.write("4\n")
# S.close()
_IsSpeakUpSilent = False
return True
def _initscr(mouse_mode=False, speakup_silent=False):
global _ttyout_fnum, _old_tcattr, _getchar, _ttyin, _ttyout
global _InitscrAlreadyRun, _icol,_irow
global _IsMouseMode, _WasMouseMode, _IsSpeakUpSilent, _WasSpeakUpSilent
if os.getenv('CLUI_MOUSE') == 'OFF':
mouse_mode = False
_icol = 0
_irow = 0
if _InitscrAlreadyRun > 0:
_InitscrAlreadyRun+=1
if not mouse_mode and _IsMouseMode:
if not _leave_mouse_mode():
return False
elif mouse_mode and not _IsMouseMode:
if not _enter_mouse_mode():
return False
_WasMouseMode = _IsMouseMode
if not speakup_silent and _IsSpeakUpSilent:
if not _leave_speakup_silent():
return False
elif speakup_silent and not _IsSpeakUpSilent:
if not _enter_speakup_silent():
return False
_WasSpeakUpSilent = _IsSpeakUpSilent
_icol = 0
_irow = 0
return
else:
_InitscrAlreadyRun = 1
_ttyout = open("/dev/tty", mode="w")
signal.signal(1, _cleanup)
signal.signal(3, _cleanup)
signal.signal(15, _cleanup)
if mouse_mode:
_ttyin = open("/dev/tty", mode="r")
_IsMouseMode = True
# encoding_string = ':bytes';
print("\033[?1003h", end='', file=_ttyout) # sets SET_ANY_EVENT_MOUSE
else:
_ttyin = open("/dev/tty", mode="r")
_IsMouseMode = False
if speakup_silent and not _IsSpeakUpSilent:
_enter_speakup_silent()
try:
import tty
_ttyout_fnum = _ttyout.fileno()
_old_tcattr = tty.tcgetattr(_ttyout_fnum)
tty.setcbreak(_ttyout_fnum)
mode = termios.tcgetattr(_ttyout_fnum)
OFLAG = 1
mode[OFLAG] = mode[OFLAG] & ~(termios.ONLCR | termios.ONLRET)
termios.tcsetattr(_ttyout_fnum, termios.TCSANOW, mode)
# _getchar = lambda: _ttyin.read(1) # but ttyin will be re-opened :-(
_getchar = lambda: _ttyin_read()
except (ImportError, AttributeError):
_ttyout_fnum = 0
# _getchar = lambda: _ttyin.readline()[:-1][:1]
_getchar = lambda: _ttyin.readline()
def _ttyin_read():
global _ttyin
return _ttyin.read(1)
def _ttyin_readline():
global _ttyin
return _ttyin.readline()[:-1][:1]
def _cleanup(num,frame):
import tty
global _ttyout, _ttyout_fnum, _old_tcattr
print("\033[0m", end='', file=_ttyout)
_leave_mouse_mode()
_ttyout_fnum = _ttyout.fileno()
tty.setcbreak(_ttyout_fnum)
termios.tcsetattr(_ttyout_fnum, termios.TCSANOW, _old_tcattr)
# raise KeyboardInterrupt
sys.exit()
def _endwin():
global _ttyout, _old_tcattr, _InitscrAlreadyRun
global _IsMouseMode, _WasMouseMode, _IsSpeakUpSilent, _WasSpeakUpSilent
print("\033[0m", end='', file=_ttyout)
if _InitscrAlreadyRun > 1:
if _IsMouseMode and not _WasMouseMode:
_leave_mouse_mode()
elif not _IsMouseMode and _WasMouseMode:
_enter_mouse_mode()
if _IsSpeakUpSilent and not _WasSpeakUpSilent:
_leave_speakup_silent()
elif not _IsSpeakUpSilent and _WasSpeakUpSilent:
_enter_speakup_silent()
_InitscrAlreadyRun -= 1
return
print("\033[?1003l", end='', file=_ttyout)
_ttyout.flush()
__IsMouseMode = False
if _IsSpeakUpSilent:
_leave_speakup_silent()
import tty
_ttyout_fnum = _ttyout.fileno()
tty.setcbreak(_ttyout_fnum)
termios.tcsetattr(_ttyout_fnum, termios.TCSANOW, _old_tcattr)
_InitscrAlreadyRun = 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:
_otherlines_a = _fmt(_otherlines)
_notherlines = len(_otherlines_a)
_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
return ask(question)
def ask_filename(question):
r'''Uses the readline module to provide filename-completion with the Tab
key, but also displays multi-line questions in the same way as ask()
and choose() do. This function was introduced in version 1.65.'''
import readline
_initscr(speakup_silent=True)
nol = _display_question(question)
_endwin()
# to what extent can this readline do anything but complete py builtins?
#$term = new Term::ReadLine 'ProgramName';
## print STDERR "$question ";
#my $filename = $term->readline('');
#print STDERR "\e[J";
#return $filename;
return ask(question)
def ask(question, default=''):
try:
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-A 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(speakup_silent=True)
nol = _display_question(question)
i = 0 # cursor position
n = 0 # string length
s_a = [] # list of letters in string
if default:
_speak(question+', default is '+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)
else:
_speak(question)
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:
if i > 0:
i-=1
_left(1)
elif c == _KEY_RIGHT and i < n:
_puts('x') if _silent else _puts(s_a[j])
i+=1
elif c == _KEY_DELETE and i < n:
n -= 1
s_a.pop(i) # splice(@s, $i, 1)
j = i
while j < n:
_puts('x') if _silent else _puts(s_a[j])
j += 1
_clrtoeol()
_left(n-i)
elif (c == "\b") or (c == "\177"):
if i > 0:
n -= 1
i -= 1
if not _silent: # 1.63
_speak(s_a[i])
s_a.pop(i) # splice(@s, $i, 1)
_left(1)
j = i
while j < n:
_puts('x') if _silent else _puts(s_a[j])
j += 1
_clrtoeol()
_left(n-i)
elif c == "\030" or c == "\004": # clear ...
_left(i)
i = 0
n = 0
_clrtoeol()
s_a = []
elif c == "\001" or c == _KEY_HOME: # 1.67
_left(i)
i = 0
elif c == "\005" or c == _KEY_END: # 1.67
_right(n-i)
i = n
elif c == "\014":
_speak("".join(s_a))
elif str(type(c)) == "<class 'int'>":
_beep()
elif ord(c) >= 32:
_beep()
# splice(@s, $i, 0, $c);
s_a.insert(i, c)
n+=1
i+=1
if _silent:
_puts('x')
else:
_puts(c)
_speak(c)
j = i
while j < n:
_puts(s_a[j])
j += 1
_clrtoeol()
_left(n-i)
else:
_beep()
_speak("".join(s_a), True)
_endwin()
_silent = False
return "".join(s_a)
except Exception as err:
# print("handling ask exception")
_endwin()
subprocess.call(['stty','sane'])
_warn(err)
sys.exit()
# ----------------------- 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):
try:
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 or ctrl-X 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 the environment variable CLUI_MOUSE is set to OFF
then choose() will not interpret mouse-clicks as making a choice.
The advantage of this is that the mouse can then be used
to highlight and paste text from this window as usual.``
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_a, _irow_a
global _irow, _otherlines, _notherlines, _otherlines_a
global _ttyout, _this_cell, _clue_has_been_given, _choice, _CursorRow
_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 = ''
_otherlines_a = []
lines = re.split('\r?\n', question, 1)
firstline = lines[0]
firstlinelength = len(firstline)
_choice = get_default(firstline)
chosen = []
_initscr(mouse_mode=True, speakup_silent=True)
_size_and_layout(0)
if (len(lines) > 1):
_otherlines = 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)
if _nrows >= _maxrows:
_speak(firstline+', ', wait=True)
else:
_speak(firstline+', multiple choice, '+_list[_this_cell])
else:
_puts(firstline)
if _nrows >= _maxrows:
_speak(firstline+', ', wait=True)
else:
_speak(firstline+', choose '+_list[_this_cell])
_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
_speak('choose '+_list[_this_cell])
_wr_screen()
print("\033[6n", end='', file=_ttyout) # u7 will set _AbsCursX, _AbsCur
_ttyout.flush()
_CursorRow = _irow_a[_this_cell] # global, needed by handle_mouse
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()
_speak('choose '+_list[_this_cell])
if (c == "q" or c == "\004" or c == "\030"):
_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()
_speak('choose '+_list[_this_cell])
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)
_speak(_list[_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)
_speak(_list[_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)
_speak(_list[_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)
_speak(_list[_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)
_speak(_list[_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)
_speak(_list[_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:
_speak(' and '.join(chosen), wait=True)
return chosen
else:
_speak(_list[_this_cell], wait=True)
return _list[_this_cell]
elif c == " ":
if multichoice:
_marked[_this_cell] = not _marked[_this_cell]
# if (_this_cell < (len(_list)-1)): # 1.50
# _this_cell+=1
# _wr_cell(_this_cell-1)
_wr_cell(_this_cell)
_speak('marked')
#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)
except KeyboardInterrupt:
# print("handling exception")
_leave_mouse_mode()
_endwin()
subprocess.call(['stty','sane'])
return ''
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_a) + "\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 + " "
_puts(no_tabs[:_maxcols]) # 1.42, 1.54
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
global _IsMouseMode
nchoices = len(a_list)
n = 0
i = 0
s_a = []
s = ''
my_list = a_list
_clue_has_been_given = True
if _IsMouseMode: # 1.55
_leave_mouse_mode()
_ask_for_clue(nchoices, i, s);
while True:
c = _getch()
if _size_changed:
_size_and_layout(0)
if _nrows < _maxrows:
_erase_lines(1)
_enter_mouse_mode()
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
_speak(s_a[i], wait=True) # 1.63
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)
_enter_mouse_mode()
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 str(type(c)) == "<class 'int'>":
_beep()
else:
if ord(c) >= 32: # nchoices and ?
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)
_speak(c, wait=True) # 1.63
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()
_enter_mouse_mode()
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)
_speak("still "+str(nchoices)+" choices, lengthen the clue")
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(31) # 1.62
_speak(str(nchoices)+" choices, give me a clue, or control-X to quit")
else:
_goto(0,1)
_puts("No choices fit this clue !")
_clrtoeol();
_goto(0,2)
_puts(" shorten the clue : ")
_right(i)
_speak("no choices fit, shorten the clue")
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(), 'c', 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"
def _handle_mouse(x, y, button_pressed, button_drag): # 1.50
global _TopRow, _AbsCursY, _CursorRow, _LastEventWasPress
global _this_cell, _irow_a, _icol_a, _list
_TopRow = _AbsCursY - _CursorRow
if _LastEventWasPress:
_LastEventWasPress = False
return ''
if y < _TopRow:
return ''
mouse_row = y - _TopRow
mouse_col = x - 1
found = False
for i in range(len(_irow_a)):
if _irow_a[i] == mouse_row:
if _icol_a[i] < mouse_col and (_icol_a[i]+len(_list[i])) >= mouse_col:
found = True
break
if _irow_a[i] > mouse_row:
break
i += 1
if not found:
return ''
# if xterm doesn't receive a button-up event it thinks it's dragging
return_char = ''
if button_pressed == 1 and not button_drag:
_LastEventWasPress = True
return_char = _KEY_ENTER
elif button_pressed == 3 and not button_drag:
_LastEventWasPress = True
return_char = ' '
if i != _this_cell:
t = _this_cell
_this_cell = i
_wr_cell(t)
_wr_cell(_this_cell)
return return_char
# ----------------------- 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(speakup_silent=True)
nol = _display_question(question)
_puts (" (y/n) ")
_speak(question + ', y or n')
while (True):
response=_getch()
if (re.match('[yYnN]', response)):
break
_beep()
_left(6)
_clrtoeol()
if (re.match('[yY]', response)):
_puts("Yes")
_speak('yess', wait=True)
else:
_puts("No")
_speak('know', wait=True)
_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_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)
_speak('Sorry, '+str(msg), wait=True)
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)
_speak(str(msg), wait=True)
# ----------------------- 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.
'''
# 1.65 if it's a .doc file, then wvText, antiword or catdoc should be used
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(title+"\r\n"+("\r\n".join(rows))+"\r\n")
_speak(title+', '+(' '.join(rows)), wait=True)
_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
_speak(title+', enter to continue,'+(' '.join(rows)))
_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
# ----------------------- help_text --------------------------
def help_text(mode=''):
'''
This returns a short help message for the user. If mode is "ask" then
the text describes the keys the user has available when responding to an
ask() question; If mode is "multi" then the text describes the keys and
mouse actions the user has available when responding to a multiple-choice
choose() question; otherwise, it describes the keys and mouse actions
the user has available when responding to a single-choice choose().
'''
if mode == 'ask':
return "\nLeft and Right arrowkeys, Backspace, Delete; control-B = beginning; control-E = end; control-X = clear; then Return."
if os.getenv('CLUI_MOUSE') == 'OFF':
text = "\nmove around with Arrowkeys (or hjkl);"
else:
text = "\nmove around with Mouse or Arrowkeys (or hjkl);"
if re.match('mult',mode):
text += " multiselect with Rightclick or Spacebar;"
text += " then either q or ctrl-X for quit,"
if os.getenv('CLUI_MOUSE') == 'OFF':
text += " or Return to choose."
else:
text += " or choose with Leftclick or Return."
return text
# -------------------------- infrastructure -------------------------
SpeakMode = set()
def _speak(text, wait=None): # 1.60
global _Eflite_FH, _Espeak, _Espeak_PID
if (not _Eflite_FH and not _Espeak) or not text or len(text) == 0:
return None
if 'dot' in SpeakMode:
text = re.sub('\s*\.\s*', ' dot ', text)
text = re.sub(r'\s*\.(\w)', r' dot \1', text)
if _Eflite_FH:
if len(text) == 1:
if text == '.':
_Eflite_FH.write(bytes("s\nq { dot }\nd\n",'ISO-8859-1'))
else:
_Eflite_FH.write(bytes("s\nl {"+text+"}\n",'ISO-8859-1'))
_Eflite_FH.flush()
if wait:
time.sleep(0.5)
else:
_Eflite_FH.write(bytes("s\nq {"+text+"}\nd\n",'ISO-8859-1'))
_Eflite_FH.flush()
# useless emacspeak op: tts_sy nc_state all 0 0 1 225\nq {[:np ]}
if wait:
time.sleep(0.3+0.07*len(text))
elif _Espeak:
if _Espeak_PID > 0.5:
os.kill(_Espeak_PID, signal.SIGHUP)
os.wait()
_Espeak_PID = None;
_Espeak_PID = os.fork()
if _Espeak_PID > 0.5:
if wait:
if len(text) == 1:
time.sleep(0.6)
else:
time.sleep(0.4+0.07*len(text))
else:
pipe = subprocess.Popen(_Espeak, stdin=subprocess.PIPE)
if not pipe:
sys.exit()
def _huphandler(signum,stackframe):
pipe.kill()
os.wait()
sys.exit()
signal.signal(signal.SIGHUP, _huphandler)
if text == '.':
text = 'dot'
pipe.stdin.write(text.encode('utf8')+b"\n")
pipe.stdin.flush()
pipe.stdin.close()
os.wait()
sys.exit()
_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)
_ttyout.flush()
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): # >= 1.54
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)
SpeakMode.add('dot')
if multichoice:
new = choose(title, allfiles, multichoice=True)
SpeakMode.remove('dot')
if not new:
return []
for i in range(len(new)):
new[i] = dir+new[i]
return new
new = choose (title, allfiles)
SpeakMode.remove('dot')
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