commit 72d05bc6df007170349b8262cb5ca74cb811a97a Author: Manuel Cortéz Date: Wed Dec 14 17:07:37 2016 -0600 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e99e36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc \ No newline at end of file diff --git a/README b/README new file mode 100644 index 0000000..98fd077 --- /dev/null +++ b/README @@ -0,0 +1 @@ +A set of utilities to develop applications using ncurses and python. diff --git a/guicurses/__init__.py b/guicurses/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/guicurses/widgets.py b/guicurses/widgets.py new file mode 100644 index 0000000..bb0851b --- /dev/null +++ b/guicurses/widgets.py @@ -0,0 +1,709 @@ +#holds basic GUI structures for use in curses (treeview, listbox, checkbox, ETC) +# Taken and modified from http://bmcginty.us/clifox.git +import curses, time, os, os.path, string, sys +from curses import ascii + +class GuiObject(object): + done = 0 + + def beepIfNeeded(self): +# if self.base.config.beeps: + curses.beep() + + def setStatus(self,*a,**kw): + return self.base.setStatus(*a,**kw) + + def onFocus(self, *args, **kwargs): self.screen.move(self.y, self.x) + +class Dialog(GuiObject): + """control holder +down and up arrows move through controls +enter selects default button or displays error if not one +""" + @property + def controlIndex(self): + return self._controlIndex + + @controlIndex.setter + def controlIndex(self, i): + self._controlIndex = i + self.controls[self._controlIndex].onFocus() + return i + + def __init__(self, screen = None, base = None, y = 0, x = 0, controls = [], can_go_back=True): + self.base = base + self.screen = screen + self.y, self.x = y, x + self._controls = controls + self.controls = [] + self.can_go_back = can_go_back + self.initialDraw() + self.draw() + + def initialDraw(self): + for i in xrange(0, len(self._controls)): + co = self._controls[i] + c = co[0](screen=self.screen, base=self.base, y=i, can_go_back=self.can_go_back, **co[1]) + self.controls.append(c) + self.controlIndex = 0 + + def draw(self): + for i in self.controls: + i.draw() + + def handleKey(self, c): + ret = 1 + if c == curses.KEY_DOWN: + if self.controlIndex >= len(self.controls) -1: + self.beepIfNeeded() + self.setStatus("No more controls in this dialog. Please up arrow to the first control.") + self.controlIndex = len(self.controls) -1 + else: + self.controlIndex += 1 + elif c == curses.KEY_UP: + if self.controlIndex <= 0: + self.beepIfNeeded() + self.setStatus("This is the first control in this dialog.") + self.controlIndex = 0 + else: + self.controlIndex -= 1 + else: + ret = self.controls[self.controlIndex].handleKey(c) + return ret + +class Button(GuiObject): + def __init__(self, screen=None, base=None, y=1, x=0, can_go_back=True, prompt="Button", action="", help_string=""): + self.base = base + self.screen = screen + self.y, self.x = y, x + self.prompt = prompt + self.selected = 0 + self.draw() + self.action = action + self.help_string = help_string + self.can_go_back = can_go_back + + def draw(self): + s = self.prompt + self.screen.addstr(self.y, self.x, s.encode("utf-8")) + self.screen.refresh() + + def handleKey(self, k): + if k == 10 or k == curses.KEY_RIGHT: # Enter key or right for easier use + self.done = 1 + self.selected = 1 + return 1 + elif k == curses.KEY_F1: + if hasattr(self, "help_string"): + self.setStatus(self.help_string) + return 1 + elif k == curses.KEY_BACKSPACE or k == curses.KEY_LEFT and self.can_go_back: # Let's go back + self.base.go_back() + return 1 + return None + +class Editbox(object): + """Editing widget using the interior of a window object. + Supports the following Emacs-like key bindings: + Ctrl-A Go to left edge of window. + Ctrl-B Cursor left, wrapping to previous line if appropriate. + Ctrl-D Delete character under cursor. + Ctrl-E Go to right edge (stripspaces off) or end of line (stripspaces on). + Ctrl-F Cursor right, wrapping to next line when appropriate. + Ctrl-G Terminate, returning the window contents. + Ctrl-H Delete character backward. + Ctrl-J Terminate if the window is 1 line, otherwise insert newline. + Ctrl-K If line is blank, delete it, otherwise clear to end of line. + Ctrl-L Refresh screen. + Ctrl-N Cursor down; move down one line. + Ctrl-O Insert a blank line at cursor location. + Ctrl-P Cursor up; move up one line. + Move operations do nothing if the cursor is at an edge where the movement is not possible. +The following synonyms are supported where possible: + KEY_LEFT = Ctrl-B, KEY_RIGHT = Ctrl-F, KEY_UP = Ctrl-P, KEY_DOWN = Ctrl-N, KEY_BACKSPACE = Ctrl-h + """ + def __init__(self, screen=None, base=None, y=1, x=0, default="edit field"): + self.base = base + self.value = default + self.win = screen + self.loop = self.edit + (self.maxy, self.maxx) = self.win.getmaxyx() + self.maxy -= 2 + self.maxx -= 1 + self.stripspaces = 1 + self.lastcmd = None + self.text = [[] for y in xrange(self.maxy+1)] + self.win.keypad(1) + self.win.move(0,0) + + def text_insert(self, y, x, ch): + if len(self.text[y]) > x: + self.text[y].insert(x, ch) + else: # < = x +# self.text[y] + = [curses.ascii.SP] * (x - len(self.text[y])) + self.text[y].append(ch) + + def text_delete(self, y, x): + if y < 0 or x < 0 or y >= len(self.text) or x >= len(self.text[y]): return + del self.text[y][x] + + def _end_of_line(self, y): + """Go to the location of the first blank on the given line.""" + last = self.maxx + while 1: + if curses.ascii.ascii(self.win.inch(y, last)) != curses.ascii.SP: + last = min(self.maxx, last+1) + break + elif last == 0: + break + last = last - 1 + return last + + def do_command(self, ch): + "Process a single editing command." + (y, x) = self.win.getyx() + self.lastcmd = ch + if ch == curses.ascii.SOH: # ^a + x = 0 + self.win.move(y, x) + elif ch in (curses.ascii.STX,curses.KEY_LEFT, curses.ascii.BS, curses.KEY_BACKSPACE,127): + if x > 0: + x -= 1 + self.win.move(y, x) + elif y == 0: + pass + else: + y -= 1 + x = len(self.text[y])-1 #if len(self.text[y]) len(self.text[y]) else x + self.win.move(y, x) + else: + pass + elif ch == curses.ascii.SI: # ^o + self.win.insertln() + self.text.insert(y, []) + elif ch in (curses.ascii.DLE, curses.KEY_UP): # ^p + if y > 0: + y -= 1 + x = len(self.text[y]) if x > len(self.text[y]) else x + self.win.move(y, x) + else: + pass + elif ch == curses.KEY_HOME: + y = 0 + x = len(self.text[y]) if x > len(self.text[y]) else x + self.win.move(y, x) + elif ch == curses.KEY_END: + y = len(self.text) +# x = len(self.text[y]) if x > len(self.text[y]) else x + self.win.move(y,x) + elif ch == curses.KEY_F2: + if self.externalEdit() == None: + self.setStatus("No external editor found.") + else: + return True + elif ch>31 and ch<256: + ch = self.getunicode(ch) + if y < self.maxy or x < self.maxx: + self.text_insert(y, x, ch) + self.win.addstr(y, 0, ''.join(self.text[y])) + if x0: + ch = ord(text.pop(0)) + if len(text) == 0: + text = -1 + else: + ch = self.win.getch() + if ch == -1: + time.sleep(0.02) + continue + o_ch = ch + if self.do_command(ch): + break + if text == -1: + self.win.move(0,0) + self.win.refresh() + text = None + return self.gather() + +class Readline(GuiObject): + """ +prompt for user input, with bindings to that of the default readline implementation +prompt: prompt displayed before the users text +history: a list of strings which constitutes the previously entered set of strings given to the caller of this function during previous calls +text: the default text, entered as if the user had typed it directly +echo: acts as a mask for passwords (set to ' ' in order to not echo any visible character for passwords) +length: the maximum length for this text entry +delimiter: the delimiter between prompt and text +readonly: whether to accept new text +""" + def __init__(self, screen=None, base=None, y=0, x=0, history=[], prompt=u"input", default=u"", echo=None, maxLength=0, delimiter=u": ", readonly=0, action=""): + self.value = default + self.done = 0 + self.base = base + self.screen = screen + self.y, self.x = y, x + self.history = history + self.historyPos = len(self.history) if self.history else 0 + self.prompt = prompt + self.delimiter = delimiter + self.echo = echo + self.readonly = readonly + self.maxLength = maxLength +#prompt and delimiter + self.s = u"%s%s" % (self.prompt,self.delimiter,) if self.prompt else "" +#position in the currently-being-editted text + self.ptr = 0 +#start of text entry "on-screen", should be greater than self.ptr unless there is absolutely no prompt, (in other words, a completely blank line) +#if there's a prompt, startX should be right after the prompt and the delimiter +#if not, startX is going to be wherever self.x is, as that's where our text is going to appear + self.startX = len(self.s) if self.s else self.x +#put ptr at the end of the current bit of text + self.currentLine = self.value + self.ptr = len(self.currentLine) + self.insertMode = True + self.lastDraw = None + self.draw() + self.action = action + + def externalEdit(self): return None + + def getunicode(self, c): + tc = u' ' + buf = '' + done = False + nc = chr(c) + buf += nc + if ord(nc) in (194, 195): + nc = chr(self.screen.getch()) + buf += nc + try: + tc = buf.decode() + done = True + except: + pass + return tc + + def draw(self): + d = self.ptr, self.currentLine + if self.lastDraw and d == self.lastDraw: + return + loc = self.x + t = self.s + self.screen.move(self.y, self.x) + self.screen.clrtoeol() + if self.s: + self.screen.addstr(self.y, self.x, self.s) + t = self.currentLine + cnt = 0 + if self.echo: + t = str(self.echo)[:1]*len(t) + self.screen.addstr(self.y, self.startX, "".join(t)) + self.screen.move(self.y, self.startX+self.ptr) + self.screen.refresh() + self.lastDraw = self.ptr, self.currentLine + + def handleKey(self, c): + if c == -1: + return None + if c == 3: # ^C + self.setStatus("Input aborted!") + self.currentLine = u'' + elif c == 10: # ^J newline + if self.history != None and self.currentLine: + self.history.append(self.currentLine) + self.done = 1 + elif c in (1, 262): # ^A, Home key + self.ptr = 0 + elif c in (5, 360): # ^E, End key + self.ptr = len(self.currentLine) + elif c in (2, 260): # ^B, left arrow + if self.ptr>0: + self.ptr -= 1 + else: + self.beepIfNeeded() + elif c in (6, 261): # ^f, right arrow + if self.ptr0: + self.tempLine = self.currentLine + self.historyPos -= 1 + self.currentLine = self.history[self.historyPos] + self.ptr = len(self.currentLine) + else: + self.setStatus("Something odd occured, readLine, up arrow") + elif c == 258: # Down arrow +#if there is no history, or we're off the end of the history list (therefore using tempLine), show an error + if not self.history or self.historyPos>= len(self.history): + self.beepIfNeeded() + msg = "No history to move down through." if not self.history else "No more history to move down through." + self.setStatus(msg) +#otherwise, we've got more history, or tempLine left to view + elif self.history: +#go ahead and move down + self.historyPos += 1 +#if we're now off the end of the history, pull up tempLine +#maybe user thought they'd typed something and they hadn't, so they can get back to their pre-history command + if self.historyPos == len(self.history): + self.currentLine = self.tempLine +#normal history item + else: + self.currentLine = self.history[self.historyPos] +#move to the end of this line, history or tempLine + self.ptr = len(self.currentLine) + else: + self.setStatus("Something odd occured, readLine, down arrow") + elif c in (8, 263): # ^H, backSpace + if self.ptr>0: + self.currentLine = u"%s%s" % (self.currentLine[:self.ptr-1],self.currentLine[self.ptr:]) + self.ptr -= 1 + else: + self.beepIfNeeded() + elif c in (4, 330): # ^D, delete + if self.ptr0 and self.ptr >= self.maxLength: + if self.history != None and self.currentLine: + self.history.append(self.currentLine) + self.done = True + self.setStatus("Maximum field length reached.") + #handled keystroke + self.draw() + return 1 + +class Listbox(GuiObject): + """Listbox +render a listbox to the screen in `title \n separator \n items` format +y,x: y and x coordinates where to draw this window on the screen +height: maximum height of this window on the screen, including title and separator (defaults to the length of the list, or the height of the window) +base: base clifox object for accessing settings and other clifox state +default: the index of the currently selected item (or a list of selected items, if multiple is true) +title: the title of this select box (might be taken from the element on the webpage) +keysWaitTime: maximum amount of time the system will consider a consecutive set of key-presses as a single search +items: a list of options in string form, or a list of (id,option) tuples +multiple: whether to allow selecting multiple options +""" + def __init__(self, screen=None, base=None, y=0, x=0, height=None, title=None, items=[], keysWaitTime=0.4, default=0, multiple=False): + self.screen = screen + self.base = base + self.multiple = multiple + self.y, self.x = y, x + self.title = title +#if we've got a list of strings or a list of non-list objects, turn them into itemIndex,item +#so ["a","b","c"] would become [[0,"a"],[1,"b"],[2,"c"]] + if items and type(items[0]) not in (tuple, list): + items = zip(xrange(len(items)),items) + else: + items = items + items = [(i,str(j)) for i, j in items] + self.items = items + self.keys = [] + self.lastKeyTime = -1 + self.keysWaitTime = keysWaitTime + if height == None: + height = (len(self.items)+2) if (len(self.items)+2) 2: + controls.append((widgets.Button, dict(prompt=i[1], action=i[0], help_string=i[2]))) + else: + controls.append((widgets.Button, dict(prompt=i[1], action=i[0]))) + if is_submenu: + controls.append((widgets.Button, "Back")) + return controls \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..632739a --- /dev/null +++ b/setup.py @@ -0,0 +1,18 @@ +import os +from setuptools import setup + +def read(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() + +setup( + name = "guicurses", + version = "0.3", + author = "Manuel Cortez", + author_email = "manuel@manuelcortez.net", + description = "A set of utilities for building accessible applications with curses", + license = "GPL V2", + keywords = "Gui curses accessibility speakup", + url = "http://manuelcortez.net:1337/pi/guicurses", + long_description=read('README'), + packages = ["guicurses"], +) \ No newline at end of file