1. J.A. Roberts Tunney
  2. fabulous

Commits

Alan Trick  committed 0f17394

Added python code, docs, and tests from terminate. A bunch of it will need refactoring.

  • Participants
  • Parent commits 93dd1d9
  • Branches default

Comments (0)

Files changed (22)

File docs/support.rst

View file
+==================
+ Terminal Support
+==================
+
+Supported Terminals
+===================
+
+===============  =======  =======  ===  =========  =======  =======  ======
+Terminal         default  bright   dim  underline  blink    reverse  hidden
+===============  =======  =======  ===  =========  =======  =======  ======
+xterm            yes      yes      yes  yes        yes      yes      yes
+linux            yes      yes      yes  bright     yes      yes      no
+rxvt             yes      yes      no   yes        bright   yes      no
+Windows [0]_     yes      yes      yes  no         no       yes      yes
+PuTTY [1]_       yes      yes      no   yes        [2]_     yes      no
+Cygwin SSH [3]_  yes      yes      no   [4]_       [4]_     [2]_     yes
+===============  =======  =======  ===  =========  =======  =======  ======
+
+Currently unsupported, but should support
+=========================================
+
+===============  =======  =======  ===  =========  =======  =======  ======
+Terminal         default  bright   dim  underline  blink    reverse  hidden
+===============  =======  =======  ===  =========  =======  =======  ======
+dtterm           yes      yes      yes  yes        reverse  yes      yes
+teraterm         yes      reverse  no   yes        rev/red  yes      no
+aixterm          kinda    normal   no   yes        no       yes      yes
+Mac Terminal     yes      yes      no   yes        yes      yes      yes
+===============  =======  =======  ===  =========  =======  =======  ======
+
+Unsupported and will not support
+================================
+
+Windows Telnet
+	It thinks it supports ANSI control, but it's so horribly 
+	buggy its best to ignore it all together. (``TERM = ansi``)
+
+
+.. [0] The default windows terminal, ``cmd.exe`` does not set the ``TERM`` variable, so
+	detection is done by checking if the string ``'win32'`` is in ``sys.platform``. This
+	This method has some limitations, particularly with remote terminal. But if you're
+	allowing remote access to a Windows computer you probably have bigger problems.
+	
+.. [1] Putty has the ``TERM`` variable set to ``xterm`` by default
+
+.. [2] Makes background bright
+
+.. [3] Cygwin's SSH support's ANSI, but the regular terminal does not, check for
+	win32 first, then check for cygwin. That should give us the cases when
+	cygwin is used through SSH or telnet or something. (``TERM = cygwin``)
+
+.. [4] Sets foreground color to cyan

File fabulous/casts.py

View file
+"""A series of functions that provide useful casts from strings into other common objects.
+
+Mainly for usage with input_object in the above package.
+"""
+
+from os import path
+
+def yes_no(value):
+    """For a yes or no question, returns a boolean.
+    """
+    if value.lower() in ('yes','y'):
+        return True
+    if value.lower() in ('no','n'):
+        return False
+    raise ValueError, "value should be 'yes' or 'no'"
+
+
+def file(value, **kwarg):
+    """value should be a path to file in the filesystem.
+    
+    returns a file object
+    """
+    #a bit weird, but I don't want to hard code default values
+    try:
+        f = open(value, **kwarg)
+    except IOError, e:
+        raise ValueError, "unable to open %s : %s" % (path.abspath(value), e)
+    return f

File fabulous/prompt.py

View file
+"""Input and output functions
+"""
+import sys
+import os
+import os.path
+import term
+from term import stdout, stderr, display
+
+__all__ = ["input_object","query","file_chooser"]
+
+# this constant is here because float() and int() give error messages
+# that would confuse most sane users.
+
+ERROR_MESSAGE = ( display('bright','red') + 'Error: ' + display('default') +
+                 '%s' + '\a' + os.linesep )
+NICE_INPUT_ERRORS = {
+    float: "The input ('%s') must be a number",
+    int: "The input ('%s') must be an integer (-1, 0, 1, 2, etc.)"
+}
+
+DEFAULT_INPUT_ERRORS = "Bad input (%s)"
+
+def input_object(prompt_text, cast = None, default = None,
+                 prompt_ext = ': ', castarg = [], castkwarg = {}):
+    """Gets input from the command line and validates it.
+    
+    prompt_text
+        A string. Used to prompt the user. Do not include a trailing
+        space.
+        
+    prompt_ext
+        Added on to the prompt at the end. At the moment this must not
+        include any control stuff because it is send directly to
+        raw_input
+        
+    cast
+        This can be any callable object (class, function, type, etc). It
+        simply calls the cast with the given arguements and returns the 
+        result. If a ValueError is raised, it
+        will output an error message and prompt the user again.
+
+        Because some builtin python objects don't do casting in the way
+        that we might like you can easily write a wrapper function that
+        looks and the input and returns the appropriate object or exception.
+        Look in the cast submodule for examples.
+        
+        If cast is None, then it will do nothing (and you will have a string)
+        
+    default
+        function returns this value if the user types nothing in. This is
+        can be used to cancel the input so-to-speek
+        
+    castarg, castkwarg
+        list and dictionary. Extra arguments passed on to the cast.
+    """
+    while True:
+        stdout.write(prompt_text)
+        value = stdout.raw_input(prompt_ext)
+        if value == '': return default
+        try:
+            if cast != None: value = cast(value, *castarg, **castkwarg)
+        except ValueError, details:
+            if cast in NICE_INPUT_ERRORS: # see comment above this constant
+                stderr.write(ERROR_MESSAGE % (NICE_INPUT_ERRORS[cast] % details))
+            else: stderr.write(ERROR_MESSAGE % (DEFAULT_INPUT_ERRORS % str(details)))
+            continue
+        return value
+
+def query(question, values, default=None, list_values = False, ignorecase = True ):
+    """Preset a few options
+    
+    The question argument is a string, nothing magical.
+    
+    The values argument accepts input in two different forms. The simpler form
+    (a tuple with strings) looks like:
+    
+        .. code-block:: Python
+        
+            ('Male','Female')
+    
+    And it will pop up a question asking the user for a gender and requiring
+    the user to enter either 'male' or 'female' (case doesn't matter unless
+    you set the third arguement to false).
+    The other form is something like:
+    
+        .. code-block:: Python
+        
+            ({'values':('Male','M'),'fg':'cyan'},
+            {'values':('Female','F'),'fg':'magenta'})
+    
+    This will pop up a question with Male/Female (each with appropriate
+    colouring). Additionally, if the user types in just 'M', it will be
+    treated as if 'Male' was typed in. The first item in the 'values' tuple
+    is treated as default and is the one that is returned by the function
+    if the user chooses one in that group.
+    In addition the function can handle non-string objects quite fine. It
+    simple displays the output object.__str__() and compares the user's input
+    against that. So the the code
+    
+        .. code-block:: Python
+        
+            query("Python rocks? ",(True, False))
+    
+    will return a bool (True) when the user types in the string 'True' (Of
+    course there isn't any other reasonable answer than True anyways :P)
+    
+    ``default`` is the value function returns if the user types nothing in. This is
+    can be used to cancel the input so-to-speek
+    
+    Using list_values = False will display a list, with descriptions printed out
+    from the 'desc' keyword
+    """
+    values = list(values)
+    for i in range(len(values)):
+        if not isinstance(values[i], dict):
+            values[i] = {'values': [values[i]]}
+    try:
+        import readline, rlcomplete
+        wordlist = [ str(v) for value in values
+                    for v in value['values']]
+        completer = rlcomplete.ListCompleter(wordlist, ignorecase)
+        readline.parse_and_bind("tab: complete")
+        readline.set_completer(completer.complete)
+    except ImportError:
+        pass
+    valuelist = []
+    for item in values:
+        entry = ( display('bright', item.get('fg'), item.get('bg')) +
+            str(item['values'][0]) + display(['default']) )
+        if str(item['values'][0]) == str(default): entry = '['+entry+']'
+        if list_values: entry += ' : ' + item['desc']
+        valuelist.append(entry)
+    if list_values: question += os.linesep + os.linesep.join(valuelist) + os.linesep
+    else: question += ' (' + '/'.join(valuelist) + ')'
+    return input_object(question, cast = query_cast, default=default,
+                 castarg=[values,ignorecase])
+
+def query_cast(value, answers, ignorecase = False):
+    """A cast function for query
+    
+    Answers should look something like it does in query
+    """
+    if ignorecase: value = value.lower()
+    for item in answers:
+        for a in item['values']:
+            if ignorecase and (value == str(a).lower()):
+                return item['values'][0]
+            elif value == a:
+                return item['values'][0]
+    raise ValueError, "Response '%s' not understood, please try again." % value
+
+def file_chooser(prompt_text = "Enter File: ", default=None, filearg=[], filekwarg={}):
+    """A simple tool to get a file from the user. Takes keyworded arguemnts
+    and passes them to open().
+    
+    If the user enters nothing the function will return the ``default`` value.
+    Otherwise it continues to prompt the user until it get's a decent response.
+    
+    filekwarg may contain arguements passed on to ``open()``.
+    """
+    try:
+        import readline, rlcomplete
+        completer = rlcomplete.PathCompleter()
+        readline.set_completer_delims(completer.delims)
+        readline.parse_and_bind("tab: complete")
+        readline.set_completer(completer.complete)
+    except ImportError:
+        pass
+    while True:
+        f = raw_input(prompt_text)
+        if f == '': return default
+        f = os.path.expanduser(f)
+        if len(f) != 0 and f[0] == os.path.sep:
+            f = os.path.abspath(f)
+        try:
+            return open(f, *filearg, **filekwarg)
+        except IOError, e:
+            stderr.write(ERROR_MESSAGE % ("unable to open %s : %s" % (f, e)))
+
+if __name__ == '__main__':
+    import doctest
+    doctest.testmod()

File fabulous/queries.py

View file
+"""Example values for the answers argument in prompt.query()
+"""
+
+true_false = ({'values':(True, 'T', 'Yes','Y',),'fg':'green'},
+            {'values':(False, 'F', 'No', 'N'),'fg':'red'})
+
+sex = ({'values':('Male','M'),'fg':'cyan'},
+            {'values':('Female','F'),'fg':'magenta'})

File fabulous/rlcomplete.py

View file
+"""Readline related stuff
+"""
+
+import os
+
+class Completer(object):
+    """A base class for completers.
+    
+    Child classes should implement the completelist method.
+    """
+    text = None
+    
+    delims = '\t\n'
+    
+    def __init__(self):
+        pass
+    
+    def complete(self, text, state):
+        """The actual completion method
+        
+        This method is not meant to be overridden. Override the
+        completelist method instead. It will make your life much easier.
+        
+        For more detail see documentation for readline.set_completer
+        """
+        if text != self.text:
+            self.matches = self.completelist(text)
+            self.text = text
+        try:
+            return self.matches[state]
+        except IndexError:
+            return None
+
+    def completelist(self, text):
+        """Returns a list.
+        
+        The list contains a series of strings which are the suggestions
+        for the given string ``text``. It is valid to have no suggestions
+        (empty list returned).
+        """
+        return []
+
+class ListCompleter(Completer):
+    """A class that does completion based on a predefined list.
+    """
+    
+    def __init__(self, words, ignorecase):
+        self.words = words
+        self.ignorecase = ignorecase
+    
+    def completelist(self,text):
+        if self.ignorecase:
+            return [w for w in self.words if w.lower().startswith(text.lower())]
+        else:
+            return [w for w in self.words if w.startswith(text)]
+
+
+class PathCompleter(Completer):
+    """Does completion based on file paths. """
+    
+    def buildpath(self, base, *paths):
+        path = os.path.join(base,*paths)
+        if os.path.isdir(os.path.expanduser(path)) and path[-1] != os.path.sep:
+            path += os.path.sep
+        return path
+    
+    @staticmethod
+    def matchuserhome(prefix):
+        """To find matches that start with prefix.
+        
+        For example, if prefix = '~user' this
+        returns list of possible matches in form of ['~userspam','~usereggs'] etc.
+        
+        matchuserdir('~') returns all users
+        """
+        if not prefix.startswith('~'):
+            raise ValueError, "prefix must start with ~"
+        try: import pwd
+        except ImportError:
+            try: import winpwd as pwd
+            except ImportError: return []
+        return ['~' + u[0] for u in pwd.getpwall() if u[0].startswith(prefix[1:])]
+        
+    
+    def completelist(self, text):
+        """Return a list of potential matches for completion
+        
+        n.b. you want to complete to a file in the current working directory
+        that starts with a ~, use ./~ when typing in. Paths that start with
+        ~ are magical and specify users' home paths
+        """
+        path = os.path.expanduser(text)
+        if len(path) == 0 or path[0] != os.path.sep:
+            path = os.path.join(os.getcwd(), path)
+        if text == '~':
+            dpath = dtext = ''
+            bpath = '~'
+            files = ['~/']
+        elif text.startswith('~') and text.find('/', 1) < 0:
+            return self.matchuserhome(text)
+        else:
+            dtext = os.path.dirname(text)
+            dpath = os.path.dirname(path)
+            bpath = os.path.basename(path)
+            files = os.listdir(dpath)
+        if bpath =='':
+            matches = [self.buildpath(text, f) for f in files if not f.startswith('.')]
+        else:
+            matches = [self.buildpath(dtext, f) for f in files if f.startswith(bpath)]
+        if len(matches) == 0 and os.path.basename(path)=='..':
+            files = os.listdir(path)
+            matches = [os.path.join(text, f) for f in files]
+        return matches

File fabulous/term.py

View file
+"""Terminal abstraction layer
+
+Provides standard capabilites to a variety of terminals. Support information
+is being worked on.
+
+.. code-block:: Python
+
+    import os
+    stdout.write('spam' + 
+        display('bright','yellow','white') + 
+        'eggs' + 
+        display('default') + os.linesep
+    )
+
+Warning: on IPython setting sys.stdout to stdout will break readline
+
+Caveats
+-------
+
+0. failure to flush after ouput can cause weird ordering behaviour when
+writting to stdout and stderr simutaniously. This should fix the worst of 
+it, but application developers should be warned not to rely on the state 
+of things between call between one method call and another
+
+"""
+
+__all__ = ['display', 'stdin', 'stdout', 'stderr',
+           'Term', 'UnixTerm', 'CursesTerm',
+           'WinTerm', 'Win32Term', 'WinCTypesTerm']
+
+import sys
+import os
+import re
+
+# pylint: disable-msg=W0613
+# pylint: disable-msg=W0102
+# pylint: disable-msg=C0103
+# pylint: disable-msg=W0142
+# pylint: disable-msg=R0201
+# pylint: disable-msg=W0511
+
+class Term(object):
+    """A file-like object which also supports terminal features.
+    
+    This is a base class for dumb terminals. It supports almost nothing.
+    """
+    
+    def __init__(self, stream):
+        """Under Construction (:P)
+        
+        When overriding this in subclasses, please call
+        Term.__init__(self, stream) first.
+        
+        Class specific imports are being done in the contructors and
+        references to the modules are stored as instance variables. This
+        method is cleaner and works nicely with inheritance.
+        """
+        self.stream = stream
+    
+    def bell(self):
+        """Causes the computer to beep
+        
+        Use sparingly, it is mainly
+        to alert the user if something potentialy bad may be happening.
+        """
+        pass
+    
+    def display(self, codes=[], fg=None, bg=None):
+        """Not for public consumption (yet)
+        
+        Just use display() and stdout.write() for now.
+        
+        run this at the beginning::
+        
+            (codes, fg, bg) = Magic.displayformat(codes, fg, bg)
+        """
+        pass
+    
+    def move(self, place, distance = 1):
+        """Move cursor position
+        
+        The valid values for place are:
+        
+        up
+            Move up a line.
+        down
+            Move to the next line. This also puts you at the beginning of
+            the line.
+        left
+            Move one place to the left.
+        right
+            Move one place to the right.
+        beginning of line
+            Move to the beginning of the current line.
+        beginning of screen
+            Move to the beginning of the screen.
+        """
+        pass
+    
+    def clear(self, scope = 'screen'):
+        """clears part of the screen
+        
+        The valid values for scope are:
+        
+        right
+            Clears a single space directly to the right.
+        left
+            Clears a single space directly to the left.
+        line
+            Clears the current line.
+        screen
+            Clears the whole screen.
+        beginning of line
+            Clears from the current position to the beginning of the line
+        end of line
+            Clears from the current position to the end of the line
+        end of screen
+            Clears from the current position to the end of the screen
+        
+        N.b. this is not the same as deleting. After a place is cleared
+        it should still be there, but with nothing in it. Also, this
+        should not change the position of the cursor.
+        """
+        pass
+    
+    def get_size(self):
+        """Get the width and height of the terminal.
+        
+        Returns either a tuple of two integers or None. If two integers are
+        returned, the first one is the number of columns (or width) and the
+        second value is the number of lines (or height). If None is returned,
+        then the terminal does not support this feature. If you still need to
+        have a value to fall back on (75, 25) is a fairly descent fallback.
+        """
+        return None
+    
+    def set_title(self, name):
+        """Sets the title of the terminal
+        """
+        pass
+    
+    # methods below here are also methods of the file object
+    
+    def isatty(self):
+        """Returns True if the terminal is a terminal
+        
+        This should always be True. If it's not somebody is being rather
+        nauty.
+        """
+        return self.stream.isatty()
+    
+    def fileno(self):
+        """Returns the stream's file descriptor as an integer"""
+        return self.stream.fileno()
+    
+    # write-specific methods
+    
+    def write(self, text):
+        """Parses text and prints proper output to the terminal
+        
+        This method will extract escape codes from the text and
+        handle them as well as possible for whichever platform
+        is being used. At the moment only the display escape codes
+        are supported.
+        """
+        escape_parts = re.compile('\x01?\x1b\\[([0-9;]*)m\x02?')
+        chunks = escape_parts.split(text)
+        i = 0
+        for chunk in chunks:
+            if chunk != '':
+                if i % 2 == 0:
+                    self.stream.write(chunk)
+                else:
+                    c = chunk.split(';')
+                    r = Magic.rdisplay(c)
+                    self.display(**r) #see caveat 0
+                self.flush()
+            i += 1
+    
+    def writelines(self, sequence_of_strings):
+        """Write out a sequence of strings
+        
+        Note that newlines are not added.  The sequence may be any iterable object
+        producing strings. This is equivalent to calling write() for each string.
+        """
+        map(self.write, sequence_of_strings)
+    
+    def flush(self):
+        """Ensure the text is ouput to the screen.
+        
+        The write() method will do this automatically, so only use this when
+        using self.stream.write().
+        """
+        return self.stream.flush()
+    
+    # read-specific methods, they are in need of help
+    
+    def getch(self):
+        """Don't use this yet
+        
+        It doesn't belong here but I haven't yet thought about a proper
+        way to implement this feature and the features that will depend on
+        it.
+        """
+        pass
+    
+    def raw_input(self, prompt):
+        """Don't use this yet
+        
+        It doesn't belong here but I haven't yet thought about a proper
+        way to implement this feature and the features that will depend on
+        it.
+        """
+        return raw_input(prompt)
+    
+    def next(self):
+        return self.stream.next()
+    
+    def readline(self, *args, **kwargs):
+        return self.stream.readline(*args, **kwargs)
+    def readlines(self, *args, **kwargs):
+        return self.stream.readlines(*args, **kwargs)
+    def read(self, *args, **kwargs):
+        return self.stream.read(*args, **kwargs)
+    
+    # read-only properties
+    @property
+    def mode(self):
+        return self.stream.mode
+    @property
+    def newlines(self):
+        return self.stream.newlines
+    @property
+    def encoding(self):
+        return self.stream.encoding
+    @property
+    def softspace(self):
+        return self.stream.softspace
+    @property
+    def name(self):
+        return self.stream.name
+
+class UnixTerm(Term):
+    
+    def __init__(self, stream):
+        import termios
+        import tty
+        self.termios = termios
+        self.tty = tty
+        Term.__init__(self, stream)
+    
+    def getch(self):
+        """Don't use this yet
+        
+        It doesn't belong here but I haven't yet thought about a proper
+        way to implement this feature and the features that will depend on
+        it.
+        """
+        return NotImplemented
+        fno = stdout.fileno()
+        mode = self.termios.tcgetattr(fno)
+        try:
+            self.tty.setraw(fno, self.termios.TCSANOW)
+            ch = self.read(1)
+        finally:
+            self.termios.tcsetattr(fno, self.termios.TCSANOW, mode)
+        return ch
+
+class CursesTerm(UnixTerm):
+    
+    def __init__(self, stream):
+        import curses
+        self.curses = curses
+        UnixTerm.__init__(self, stream)
+        if not sys.stdout.isatty(): return
+        self.curses.setupterm()
+    
+    def bell(self):
+        self.stream.write(self._get_cap('bel'))
+    
+    def display(self, codes=[], fg=None, bg=None):
+        """Displays the codes using ANSI escapes
+        """
+        codes, fg, bg = Magic.displayformat(codes, fg, bg)
+        self.stream.write(Magic.display(codes, fg, bg))
+        self.flush()
+        
+    def move(self, place, distance = 1):
+        """see doc in Term class"""
+        for d in range(distance):
+            self.stream.write(self._get_cap('move '+place))
+        self.flush()
+    
+    def clear(self, scope = 'screen'):
+        """see doc in Term class"""
+        if scope == 'line':
+            self.clear('beginning of line')
+            self.clear('end of line')
+        else: self.stream.write(self._get_cap('clear '+scope))
+        self.flush()
+    
+    def get_size(self):
+        """see doc in Term class"""
+        self.curses.setupterm()
+        return self.curses.tigetnum('cols'), self.curses.tigetnum('lines')
+    
+    def set_title(self, name):
+        self.write(Magic.OSC + '0;'+str(name) + "\x07") 
+    
+    def _get_cap(self, cap):
+        strcaps = {
+       'move up':'cuu1', 'move down':'cud1', 
+           'move left':'cub1', 'move right':'cuf1',
+           'move beginning of line':'cr', 'move beginning of screen':'home',
+       'clear beginning of line':'el1','clear end of line':'el',
+           'clear screen':'clear', 'clear end of screen':'ed',
+           'clear left':'kbs','clear right':'dch1',
+       'delete line':'dl1',
+       'bell':'bel'}
+        if cap in ('cols','lines'):
+            self.curses.setupterm()
+            c = self.curses.tigetnum(cap)
+            if c > 0: return c
+        elif strcaps.has_key(cap):
+            c = self.curses.tigetstr(strcaps[cap])
+            if c != '': return c
+        raise ValueError, "capability '%s' not supported" % cap
+    
+
+class WinTerm(Term):
+    """Windows version of terminal control
+    
+    This class should not be used by itself, use either Win32Terminal or 
+    WinCTypesTerminal classes that subclasses of this class.
+    
+    This class makes extensive use of the Windows API
+    
+    The official documentation for the API is on MSDN (look for 'console
+    functions')
+    """
+    
+    # TODO: is there a better way to get this?
+    
+    STD_INPUT_HANDLE = -10
+    STD_OUTPUT_HANDLE = -11
+    STD_ERROR_HANDLE = -12
+    
+    # These contants are defined in PyWin32
+    # You can combine the values by doing a bitwise or (|)
+    # for example FG_BLUE | FG_RED would give magenta (0x05)
+    #
+    # these contants are just numbers, It's most useful to think of
+    # them in binary
+    FG_BLUE = 1 << 0
+    FG_GREEN = 1 << 1
+    FG_RED = 1 << 2
+    FG_INTENSITY = 1 << 3
+    BG_BLUE = 1 << 4
+    BG_GREEN = 1 << 5
+    BG_RED = 1 << 6
+    BG_INTENSITY = 1 << 7
+    FG_ALL = FG_BLUE | FG_GREEN | FG_RED
+    BG_ALL = BG_BLUE | BG_GREEN | BG_RED
+    
+    # there are also these codes, but according to tcsh's win32/console.c:
+    # COMMON_LVB_REVERSE_VIDEO is buggy, so I'm staying away from it. Future
+    # versions should implement COMMON_LVB_UNDERSCORE.
+    # COMMON_LVB_REVERSE_VIDEO = 0x4000
+    # COMMON_LVB_UNDERSCORE      0x8000
+    
+    FG = {
+    'black': 0,
+    'red': FG_RED,
+    'green': FG_GREEN,
+    'yellow': FG_GREEN | FG_RED,
+    'blue': FG_BLUE,
+    'magenta': FG_BLUE | FG_RED,
+    'cyan': FG_BLUE | FG_GREEN,
+    'white': FG_BLUE | FG_GREEN | FG_RED,
+    }
+    BG = {
+    'black':0,
+    'red':BG_RED,
+    'green':BG_GREEN,
+    'yellow':BG_GREEN | BG_RED,
+    'blue':BG_BLUE,
+    'magenta':BG_BLUE | BG_RED,
+    'cyan':BG_BLUE | BG_GREEN,
+    'white':BG_BLUE | BG_GREEN | BG_RED,
+    }
+    
+    default_attributes = None
+    hidden_output = False
+    reverse_output = False
+    reverse_input = False
+    dim_output = False
+    real_fg = None
+    
+    def __init__(self, stream):
+        import msvcrt
+        self.msvcrt = msvcrt
+        Term.__init__(self, stream)
+        self._stdout_handle = self._get_std_handle(self.STD_OUTPUT_HANDLE)
+        self._stderr_handle = self._get_std_handle(self.STD_ERROR_HANDLE)
+        self.default_attributes = self._get_console_info()['attributes']
+        self.real_fg = self.default_attributes & 0x7
+    
+    def display(self, codes=[], fg=None, bg=None):
+        """Displays codes using Windows kernel calls
+        """
+        codes, fg, bg = Magic.displayformat(codes, fg, bg)
+        color = 0
+        for c in codes:
+            try:
+                f = getattr(self, '_display_' + c)
+                out = f()
+                if out: color |= out
+            except AttributeError:
+                pass
+        cfg, cfgi, cbg, cbgi = self._split_attributes(
+                          self._get_console_info()['attributes'])
+        if self.reverse_input:
+            cfg, cbg = (cbg // 0x10), (cfg * 0x10)
+            cfgi, cbgi = (cbgi // 0x10), (cfgi * 0x10)
+        if fg != None:
+            color |= self.FG[fg]
+            self.real_fg = self.FG[fg]
+        else: color |= cfg
+        if bg != None:
+            color |= self.BG[bg]
+        else: color |= cbg
+        color |= (cfgi | cbgi)
+        fg, fgi, bg, bgi = self._split_attributes(color)
+        if self.dim_output:
+            # intense black
+            fg = 0
+            fgi = self.FG_INTENSITY
+        if self.reverse_output:
+            fg, bg = (bg // 0x10), (fg * 0x10)
+            fgi, bgi = (bgi // 0x10), (fgi * 0x10)
+            self.reverse_input = True
+        if self.hidden_output:
+            fg = (bg // 0x10)
+            fgi = (bgi // 0x10)
+        self._set_attributes(fg | fgi | bg | bgi)
+    
+    def get_size(self):
+        """see doc in Term class"""
+        attr = self._get_console_info()
+        cols = attr['window']['right'] - attr['window']['left'] + 1
+        lines = attr['window']['bottom'] - attr['window']['top'] + 1
+        return cols, lines
+    
+    def _get_std_handle(self, handleno):
+        """Returns a handle from GetStdHandle
+        
+        handleno is one of:
+        * self.STD_OUTPUT_HANDLE for stdout
+        * self.STD_ERROR_HANDLE for stderr
+        """
+        #TODO: is NotImplemented the proper way to do this?
+        return NotImplemented
+    
+    def _get_console_info(self):
+        """Get information from GetConsoleScreenBufferInfo
+        
+        Returns a dictionary with the following keys::
+        
+            max size
+            position
+            window
+            attributes
+            size
+        
+        Note: the y part of size is misleading
+        """
+        return NotImplemented
+    
+    def _clear_console(self, length, start):
+        """Clears a part of the console
+        
+        Has a similar effect as writing out spaces.
+        
+        length: int length of cleared section
+        start : tuple of x and y coords to start at
+        """
+        return NotImplemented
+    
+    def _set_attributes(self, code):
+        """ Set console attributes with `code`
+        
+        Not implemented here. To be implemented by subclasses.
+        """
+        return NotImplemented
+    
+    def _split_attributes(self, attrs):
+        """Spilt attribute code
+        
+        Takes an attribute code and returns a tuple containing
+        foreground (fg), foreground intensity (fgi), background (bg), and
+        background intensity (bgi)
+        
+        Attributes can be joined using ``fg | fgi | bg | bgi``
+        """
+        fg = attrs & self.FG_ALL
+        fgi = attrs & self.FG_INTENSITY
+        bg = attrs & self.BG_ALL
+        bgi = attrs & self.BG_INTENSITY
+        return fg, fgi, bg, bgi
+    
+    def _undim(self):
+        self.dim_output = False
+        if self.reverse_input:
+            a = self._get_console_info()['attributes'] & 0x8f
+            self._set_attributes( (self.real_fg * 0x10) | a)
+        else:
+            a = self._get_console_info()['attributes'] & 0xf8
+            self._set_attributes(self.real_fg | a)
+    
+    def _display_default(self):
+        self.hidden_output = False
+        self.reverse_output = False
+        self.reverse_input = False
+        self.dim_output = False
+        self.real_fg = self.default_attributes & 0x7
+        self._set_attributes(self.default_attributes)
+
+    def _display_bright(self):
+        self._undim()
+        return self.FG_INTENSITY
+    
+    def _display_dim(self):
+        self.dim_output = True
+    
+    def _display_reverse(self):
+        self.reverse_output = True
+    
+    def _display_hidden(self):
+        self.hidden_output = True
+    
+    def _get_position(self):
+        """Set the cursor's current position
+        
+        Returns a tuple in the form (x, y)
+        """
+        pos = self._get_console_info()['position']
+        return pos['x'], pos['y']
+    
+    
+    def _set_position(self, coord):
+        """Set the cursor's position
+        
+        coord is a tuple in the form (x, y)
+        """
+        return NotImplemented
+    
+    def move(self, place, distance = 1):
+        """see doc in Term class"""
+        x, y = self._get_position()
+        if place == 'up':
+            y -= distance
+        elif place == 'down':
+            for i in range(distance): print
+            nx, ny = self._get_position()
+            y = ny
+            self.move('beginning of line')
+        elif place == 'left':
+            x -= distance
+        elif place == 'right':
+            x += distance
+        elif place == 'beginning of line':
+            x = 0
+        elif place == 'beginning of screen':
+            x = 0
+            y = self._get_console_info()['window']['top']
+        else: raise ValueError, "invalid place to move"
+        self._set_position((x, y))
+    
+    def clear(self, scope = 'screen'):
+        """see doc in Term class
+        
+        According to http://support.microsoft.com/kb/99261 the best way
+        to clear the console is to write out empty spaces
+        """
+        #TODO: clear attributes too
+        if scope == 'screen':
+            bos = (0, self._get_console_info()['window']['top'])
+            cols, lines = self.get_size()
+            length = cols * lines
+            self._clear_console(length, bos)
+            self.move('beginning of screen')
+        elif scope == ' beginning of line':
+            pass
+        elif scope == 'end of line':
+            curx, cury = self._get_position()
+            cols, lines = self.get_size()
+            coord = (curx, cury)
+            length = cols - curx
+            self._clear_console(length, coord)
+        elif scope == 'end of screen':
+            curx, cury = self._get_position()
+            coord = (curx, cury)
+            cols, lines = self.get_size()
+            length = (lines - cury) * cols - curx
+            self._clear_console(length, coord)
+        elif scope == 'line':
+            curx, cury = self._get_position()
+            coord = (0, cury)
+            cols, lines = self.get_size()
+            self._clear_console(cols, coord)
+            self._set_position((curx, cury))
+        elif scope == 'left':
+            self.move('left')
+            self.write(' ')
+        elif scope == 'right':
+            self.write(' ')
+            self.move('left')
+        else: raise ValueError, "invalid scope to clear"
+        
+    def getch(self):
+        """Don't use this yet
+        
+        It doesn't belong here but I haven't yet thought about a proper
+        way to implement this feature and the features that will depend on
+        it.
+        """
+        return NotImplemented
+        return self.msvcrt.getch()
+    
+    def bell(self):
+        self.stream.write('\x07')
+
+class Win32Term(WinTerm):
+    """PyWin32 version of Windows terminal control.
+    
+    Uses the PyWin32 Libraries <http://sourceforge.net/projects/pywin32/>.
+    
+    ActiveState has good documentation for them:
+    
+    Main page:
+    http://aspn.activestate.com/ASPN/docs/ActivePython/2.4/pywin32/PyWin32.html
+    Console related objects and methods:
+    http://aspn.activestate.com/ASPN/docs/ActivePython/2.4/pywin32/PyConsoleScreenBuffer.html
+    """
+    
+    def __init__(self, stream):
+        import win32console
+        self.win32console = win32console
+        WinTerm.__init__(self, stream)
+    
+    def set_title(self, name):
+        return self.win32console.SetConsoleTitle(name)
+    
+    def _get_console_info(self):
+        # example output from GetConsoleScreenBufferInfo
+        # {'MaximumWindowSize': PyCOORDType(X=80,Y=82),
+        # 'CursorPosition': PyCOORDType(X=0,Y=6),
+        # 'Window': PySMALL_RECTType(Left=0,Top=0,Right=79,Bottom=24),
+        # 'Attributes': 7,
+        # 'Size': PyCOORDType(X=80,Y=300)}
+        attrs = self._stdout_handle.GetConsoleScreenBufferInfo()
+        return {'max size': self._pyCoord_dict(attrs['MaximumWindowSize']),
+                'position': self._pyCoord_dict(attrs['CursorPosition']),
+                'window': self._pySMALL_RECTType_dict(attrs['Window']),
+                'attributes': attrs['Attributes'],
+                # y part of size value is misleading
+                'size': self._pyCoord_dict(attrs['Size']) }
+    
+    def _get_std_handle(self, handle):
+        return self.win32console.GetStdHandle(handle)
+    
+    def _get_title(self):
+        return self.win32console.GetConsoleTitle()
+    
+    def _set_attributes(self, attr):
+        self._stdout_handle.SetConsoleTextAttribute(attr)
+    
+    def _set_position(self, coord):
+        coord = self.win32console.PyCOORDType(coord[0], coord[1])
+        self._stdout_handle.SetConsoleCursorPosition(coord)
+        
+    def _clear_console(self, length, start):
+        # length: int
+        # start : tuple of x and y coords
+        char = unicode(' ')
+        coord = self.win32console.PyCOORDType(start[0], start[1])
+        # char is unicode
+        self._stdout_handle.FillConsoleOutputCharacter(
+           char, length, coord)
+    
+    def _pyCoord_dict(self, coord):
+        return { 'x': coord.X, 'y': coord.Y}
+    
+    def _pySMALL_RECTType_dict(self, rect):
+        return { 'left': rect.Left, 'top': rect.Top,
+                'right': rect.Right, 'bottom': rect.Bottom}
+
+class WinCTypesTerm(WinTerm):
+    """CTypes version of Windows terminal control.
+    
+    It requires the CTypes libraries <http://sourceforge.net/projects/ctypes/>
+    
+    As of Python 2.5, CTypes is included in Python by default. User's of
+    previous version of Python will have to install it if they what to use
+    this.
+    """
+    
+    def __init__(self, stream):
+        import ctypes
+        self.ctypes = ctypes
+        WinTerm.__init__(self, stream)
+    
+    def set_title(self, name):
+        self.ctypes.windll.kernel32.SetConsoleTitleA(name)
+    
+    def _get_console_info(self):
+        # From IPython's winconsole.py, by Alexander Belchenko
+        import struct
+        csbi = self.ctypes.create_string_buffer(22)
+        res = self.ctypes.windll.kernel32.GetConsoleScreenBufferInfo(
+                                     self._stdout_handle, csbi)
+        (bufx, bufy, curx, cury, wattr, left, top, right, bottom, maxx,
+         maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw)
+        return {'max size': {'x':maxx, 'y':maxy },
+                'position': {'x':curx, 'y':cury },
+                'window': {'left': left, 'top': top,
+                           'right': right, 'bottom': bottom},
+                'attributes': wattr,
+                # y part of size value is misleading
+                'size': {'x':maxx, 'y':maxy } }
+    
+    def _get_std_handle(self, handle):
+        return self.ctypes.windll.kernel32.GetStdHandle(handle)
+    
+    def _get_title(self):
+        """According to http://support.microsoft.com/kb/124103 the buffer
+        size is 1024
+        
+        Does not support unicode, only ANSI"""
+        #TODO: unicode support
+        strbuffer = self.ctypes.create_string_buffer(1024)
+        size = self.ctypes.c_short(1024)
+        #unicode versions are (Get|Set)ConsolTitleW
+        self.ctypes.windll.kernel32.GetConsoleTitleA(strbuffer, size)
+        return strbuffer.value
+    
+    def _set_attributes(self, attr):
+        self.ctypes.windll.kernel32.SetConsoleTextAttribute(
+                        self._stdout_handle, attr)
+    
+    def _set_position(self, coord):
+        coord = self._get_coord(coord)
+        self.ctypes.windll.kernel32.SetConsoleCursorPosition(
+                        self._stdout_handle, coord)
+        
+    def _clear_console(self, length, start):
+        # length: int
+        # start : tuple of x and y coords
+        char = self.ctypes.c_char(' ')
+        coord = self._get_coord(start)
+        charswritten = self.ctypes.c_int()
+        clength = self.ctypes.c_int(length)
+        self.ctypes.windll.kernel32.FillConsoleOutputCharacterA(
+           self._stdout_handle, char, clength, coord, charswritten)
+    
+    def _get_coord(self, coord):
+        """ It's a hack, see fixcoord in pyreadline's console.py (revision 
+        1289)
+        """
+        x, y = coord
+        return self.ctypes.c_int(y << 16 | x)
+
+class Magic(object):
+    """Special codes and what not
+    
+    Don't use these alone
+    see http://vt100.net/docs/vt100-ug/chapter3.html
+    
+    Based on the ANSI X3.64 standard. See http://en.wikipedia.org/wiki/ANSI_X3.64
+    """
+    ESCAPE = '\x1b'
+    CSI = ESCAPE +'['
+    OSC = ESCAPE +']'
+    # see the reset method
+    RESET = ESCAPE + 'c'
+    
+    
+    # pylint: disable-msg=E0602
+    DISPLAY = {'default':0, 'bright':1, 'dim':2, 'underline':4, 'blink':5,
+             'reverse':7, 'hidden':8 }
+    rDISPLAY = dict( (v, k) for k, v in DISPLAY.iteritems())
+    # Yellow is a bit weird, xterm and rxvt display dark yellow, while linux
+    # and Windows display a more brown-ish color. Bright yellow is always
+    # yellow. Order is important here
+    COLORS = { 'black':0, 'red':1, 'green':2, 'yellow':3, 'blue':4, 'magenta':5,
+               'cyan':6, 'white':7 }
+    rCOLORS = dict( (v, k) for k, v in COLORS.iteritems())
+    # pylint: enable-msg=E0602
+    # TODO: setf from curses uses the colors in a different order for 
+    
+    @staticmethod
+    def displayformat(codes=[], fg=None, bg=None):
+        """Makes sure all arguments are valid"""
+        if isinstance(codes, basestring):
+            codes = [codes]
+        else:
+            codes = list(codes)
+        for code in codes:
+            if code not in Magic.DISPLAY.keys():
+                raise ValueError, ("'%s' not a valid display value" % code)
+        for color in (fg, bg):
+            if color != None:
+                if color not in Magic.COLORS.keys():
+                    raise ValueError, ("'%s' not a valid color" % color)
+        return [codes, fg, bg]
+    
+    @staticmethod
+    def rdisplay(codes):
+        """Reads a list of codes and generates dict
+        
+        >>> Magic.rdisplay([]) == {}
+        True
+        >>> format = {'codes': ['bold','dim'], 'bg':'blue', 'fg':'cyan'}
+        >>> Magic.rdisplay([1,2,34,46]) == format
+        True
+        """
+        dcodes = []
+        fg = bg = None
+        for code in codes:
+            code = int(code)
+            offset = code // 10
+            decimal = code % 10
+            if offset == 3 and decimal in Magic.COLORS.values(): fg = decimal
+            elif offset == 4 and decimal in Magic.COLORS.values(): bg = decimal
+            elif code in Magic.DISPLAY.values(): dcodes.append(code)
+            else: pass # drop unhandled values
+        r = {}
+        if len(codes): r['codes'] = [Magic.rDISPLAY[c] for c in dcodes]
+        if fg != None: r['fg'] = Magic.rCOLORS[fg]
+        if bg != None: r['bg'] = Magic.rCOLORS[bg]
+        return r
+
+    @staticmethod
+    def display(codes=[], fg=None, bg=None):
+        codes, fg, bg = Magic.displayformat(codes, fg, bg)
+        codes = [str(Magic.DISPLAY[code]) for code in codes]
+        if fg != None: codes.append(str(30 + Magic.COLORS[fg]))
+        if bg != None: codes.append(str(40 + Magic.COLORS[bg]))
+        return Magic.CSI + ";".join(codes) + 'm'
+
+def display(codes=[], fg=None, bg=None):
+    """Returns an ANSI display code. This is useful when writing to an Term
+        
+    codes
+        A list containing strings. The strings should one of the keys in
+        ``Magic.DISPLAY``. It can also be just a single string.
+    fg, bg
+        A string. Explicitly for setting the foreground or background. Use
+        one of the keys in ``Magic.COLORS``.
+        
+    .. code-block:: Python
+    
+        # give bright blue foreground and white background with underline
+        display(('bright','underline'),'blue','white')
+        # gives a blue foreground
+        display(fg='blue')
+        # resets the color to the default.
+        display('default')
+    
+    Avoid using black or white. Depending on the situation the default
+    background/foreground is normally black or white, but it's hard to
+    tell which. Bare terminals are normally white on black, but virtual
+    terminals run from X or another GUI system are often black on white.
+    This can lead to unpredicatble results. If you want reversed
+    colours, use the 'reverse' code, and if you want to set the
+    colors back to their original colors, use the 'default' code.
+    
+    Also, be prudent with your use of 'hidden' and 'blink'. Several terminals
+    do not support them (and for good reason too), they can be really
+    annoying and make reading difficult.
+    """
+    return Magic.display(codes, fg, bg)
+
+# try to use the Windows method first because their are some terminals on
+# MS Windows that support both the Windows and curses methods, but their
+# curses implementations are buggy.
+
+def _get_terms():
+    terms = None
+    if 'win32' in sys.platform or 'cygwin' == sys.platform:
+        terms = _get_term(WinCTypesTerm) or _get_term(Win32Term)
+    if not terms:
+        terms = (_get_term(CursesTerm) or 
+                 _get_term(UnixTerm) or 
+                 _get_term(Term))
+    return terms
+
+def _get_term(termclass):
+    try:
+        return (termclass(sys.stdin), termclass(sys.stdout),
+                termclass(sys.stderr))
+    except ImportError: return None
+
+stdin, stdout, stderr = _get_terms()

File fabulous/widget.py

View file
+"""Widget library using terminate
+
+"""
+
+import os
+import math
+from datetime import datetime
+# import textwrap
+from term import stdout, display
+
+class ProgressBar(object):
+    """A 3-line progress bar, which looks like::
+                                title
+        39% [================>----------------------------]
+                               message
+    
+        p = ProgressBar('spam') # create bar
+        p.update(0, 'starting spam') # start printing it out
+        p.update(50, 'spam almost ready') # progress
+        p.update(100, 'spam complete')
+    """
+    # content, length
+    TITLE_FORMAT = {'text':display('bright','cyan') + '%s' + display('default'),
+           'length':0,
+           'padding':0 }
+    BAR_FORMAT = {'text':' %3d%% ' + '[%s'+display('dim')+'%s'+display('default')+']',
+           'length':8,
+           'padding':2 }
+    MESSAGE_FORMAT = {'text': '%s',
+           'length': 0,
+           'padding': 0 }
+        
+    def __init__(self, title = None):
+        """
+        """
+        self.drawn = False
+        cols = stdout.get_size()[0]
+        self.width = cols -1 # TODO: make a better fix for systems that put \n on new line
+        self.title = []
+        self.barlines = 0
+        self.message = []
+        self.messageline = None
+        self.refresh = False
+        self.set_title(title)
+    
+    def set_title(self, title = None):
+        """
+        """
+        if title == None:
+            self.title = []
+        else:
+            length = self.width - self.TITLE_FORMAT['padding']*2 - self.TITLE_FORMAT['length']
+            text = title[:length].center(length) # we need to keep it on one line for now
+            padding = ' ' * self.TITLE_FORMAT['padding']
+            self.title = [padding + (self.TITLE_FORMAT['text'] % text) + padding]
+        self.refresh = self.drawn
+            #lines = [(padding + line + padding) for line in textwrap.wrap(
+            #          text, self.width - (self.TITLE_FORMAT['padding']*2),
+            #          replace_whitespace=False)]
+            #self.title = os.linesep.split(
+            #          self.TITLE_FORMAT['text'] % os.linesep.join(lines))
+    
+    def get_title(self):
+        """
+        """
+        return self.title
+    
+    def get_bar(self, percent):
+        """
+        """
+        barlength = self.width - self.BAR_FORMAT['padding']*2 - self.BAR_FORMAT['length']
+        full = int( math.ceil(barlength * (percent / 100.0)) )
+        empty = int(barlength - full)
+        if full == 0 or empty == 0: fullpiece = ('=' * full)
+        else: fullpiece = ('=' * (full-1)) + '>'
+        emptypiece = ('-' * empty)
+        return [(self.BAR_FORMAT['text'] % (percent, fullpiece, emptypiece))]
+    
+    def set_message(self, message = None):
+        """
+        """
+        """"""
+        if message == None:
+            self.message = []
+        else:
+            length = self.width - self.MESSAGE_FORMAT['padding']*2 - self.MESSAGE_FORMAT['length']
+            text = message[:length].center(length) # we need to keep it on one line for now
+            padding = ' ' * self.MESSAGE_FORMAT['padding']
+            self.message = [padding + (self.MESSAGE_FORMAT['text'] % text) + padding]
+    
+    def get_message(self):
+        """returns None or string"""
+        if self.message == []: return None
+        else: return os.linesep.join(self.message)
+    
+    def update(self, percent, message = None, test = False):
+        """
+        """
+        if self.refresh:
+            self.clear()
+        if self.drawn:
+            stdout.move('beginning of line')
+            stdout.move('up', len(self.message) + self.barlines)
+        else:
+            title = self.get_title()
+            if title != None:
+                for line in self.get_title():
+                    stdout.write(line + os.linesep)
+            self.drawn = True
+        bar = self.get_bar(percent)
+        refresh =  (len(bar) != self.barlines)
+        self.barlines = len(bar)
+        for line in bar:
+            stdout.clear('line')
+            stdout.write(line)
+            stdout.move('down')
+            stdout.move('beginning of line')
+        if (message != self.get_message()) or refresh:
+            stdout.clear('end of screen')
+            self.set_message(message)
+            for line in self.message:
+                stdout.write(line)
+                stdout.move('down')
+        else: stdout.move('down', len(self.message))
+    
+    def clear(self):
+        """
+        """
+        if self.drawn:
+            stdout.move('beginning of line')
+            stdout.move('up', len(self.message))
+            stdout.move('up', self.barlines)
+            stdout.move('up', len(self.get_title()))
+            stdout.clear('end of screen')
+            self.drawn = False
+        self.refresh = False
+
+class TimedProgressBar(ProgressBar):
+    """A 3-line progress bar, which looks like::
+                                      title
+        39% [================>----------------------------] ETA mm:ss
+                                     message
+    
+        p = ProgressBar('spam') # create bar
+        p.update(0, 'starting spam') # start printing it out
+        p.update(50, 'spam almost ready') # progress
+        p.update(100, 'spam complete')
+    """
+    
+    BAR_FORMAT = {'text':' %3d%% ' + '[%s'+display('dim')+'%s'+display('default')+']',
+           'length':13,
+           'padding':2 }
+    ' ETA 12:23'
+    
+    # what fraction of percent it acurate too
+    precision = 100
+    
+    def __init__(self, title = None):
+        ProgressBar.__init__(self, title)
+        self.start = datetime.today()
+    
+    def get_bar(self, percent):
+        now = datetime.today()
+        timed = now - self.start
+        etatext = ''
+        etadiv = int(percent*self.precision)
+        if timed.seconds >= 1:
+            etatext += ' '
+            if int(percent * self.precision) !=0:
+                eta = (timed * 100 * self.precision)/int(percent * self.precision)
+                days = eta.days
+                min, sec = divmod(eta.seconds, 60)
+                hours, min = divmod(min, 60)
+                if days == 1: etatext += '1 day, '
+                elif days: etatext += '%d days, ' % days
+                if hours: etatext += '%02d:' % hours
+                etatext += '%02d:%02d' % (min, sec)
+            else:
+                etatext += 'Never'
+        barlength = (self.width - self.BAR_FORMAT['padding']*2 
+                     - self.BAR_FORMAT['length'] - len(etatext))
+        full = int( math.ceil(barlength * (percent / 100.0)) )
+        empty = int(barlength - full)
+        if full == 0 or empty == 0: fullpiece = ('=' * full)
+        else: fullpiece = ('=' * (full-1)) + '>'
+        emptypiece = ('-' * empty)
+        return [(self.BAR_FORMAT['text'] % (percent, fullpiece, emptypiece))+etatext]
+
+class Spinner(object):
+    
+    spinners=['/','-','\\','|',]
+    
+    def __init__(self):
+        self.drawn = False
+        self.state = 0
+    
+    def spin(self):
+        if self.drawn == True: self.clear()
+        else: self.drawn = True
+        stdout.write(self.spinners[self.state])
+        self.state = (self.state + 1) % len(self.spinners)
+        
+    def clear(self):
+        stdout.clear('left')
+        stdout.move('left')

File tests/casts_test.py

View file
+#!/usr/bin/env python
+
+import unittest
+from terminate import casts
+
+class TestControl(unittest.TestCase):
+
+    def test_casts_yes_no(self):
+        self.failUnlessEqual(casts.yes_no('y'), True)
+        self.failUnlessEqual(casts.yes_no('n'), False)
+        self.failUnlessEqual(casts.yes_no('Yes'), True)
+        self.failUnlessEqual(casts.yes_no('nO'), False)
+        self.failUnlessRaises(ValueError, casts.yes_no, 'spam')
+        
+    def test_casts_file(self):
+        self.failUnlessEqual( type(casts.file(__file__)), type(open(__file__)) )
+        self.failUnlessRaises(ValueError, casts.file, '')
+        
+
+if __name__ == '__main__':
+    unittest.main()

File tests/doctests.txt

View file
+
+# note: this requires minimock <http://svn.colorstudy.com/home/ianb/recipes/minimock.py>
+>>> from minimock import Mock
+>>> terminal.raw_input = Mock('terminal.raw_input')
+>>> terminal.raw_input.mock_returns = 123
+>>> stdout.write = Mock('stdout.write')
+>>> input_object('Hello World', int)
+Called stdout.write('Hello World')
+Called terminal.raw_input(': ')
+123

File tests/manual/clear.py

View file
+#!/usr/bin/env python
+
+from terminate.term import stdout
+
+stdout.clear()

File tests/manual/colors.py

View file
+#!/usr/bin/env python
+
+from terminate.term import display, stdout, Magic
+
+stdout.write(display('default'))
+stdout.write("default text\n")
+
+stdout.write("regular foreground test:\n")
+for color in Magic.COLORS.iterkeys():
+    stdout.write(display(fg=color))
+    stdout.write("    " + color + '\n')
+
+stdout.write(display('default'))
+
+stdout.write("regular background test:\n")
+for color in Magic.COLORS.iterkeys():
+    stdout.write(display(bg=color))
+    stdout.write("    " + color + '\n')
+
+stdout.write(display('default'))
+
+stdout.write("bright foreground test:\n")
+stdout.write(display('bright'))
+for color in Magic.COLORS.iterkeys():
+    stdout.write(display(fg=color))
+    stdout.write("    " + color + '\n')
+
+stdout.write(display('default'))
+
+stdout.write("dim foreground test:\n")
+stdout.write(display('dim'))
+for color in Magic.COLORS.iterkeys():
+    stdout.write(display(fg=color))
+    stdout.write("    " + color + '\n')
+
+stdout.write(display('bright','red'))
+stdout.write("bright red\n")
+stdout.write(display("default"))

File tests/manual/convulsions.py

View file
+#!/usr/bin/env python
+
+"""I am *not* responsible if this causes severe physical or mental injury
+"""
+
+from terminate.term import display, stdout, Magic
+        
+
+try:
+    while True:
+        for c in Magic.COLORS.iterkeys():
+            stdout.write(display(bg=c))
+            stdout.move('down',4)
+except KeyboardInterrupt:
+    stdout.write(display('default'))
+    print
+    print "Interupt from keyboard. Exiting."

File tests/manual/filechooser.py

View file
+#!/usr/bin/env python
+
+
+from terminate.prompt import file_chooser
+
+f = file_chooser()
+print f

File tests/manual/info.py

View file
+#!/usr/bin/env python
+
+"""Prints out information that is useful for debugging
+"""
+
+from terminate.term import stdout, stderr
+
+print "term.stdout",stdout
+print "term.stderr",stderr

File tests/manual/longtimedprogressbar.py

View file
+#!/usr/bin/env python
+
+"""This is a long timed progress bar. Don't expect it to finish any 
+time soon. It updates every 0 to 10 seconds. Send it a keyboard 
+interupt (Ctrl-C) to stop."""
+
+from terminate.widget import TimedProgressBar
+import time
+import random
+
+print __doc__
+
+p = TimedProgressBar('bar')
+p.precision=10000
+n = 100000
+for i in range(n):
+    p.update(float(i)/n *100, 'update '+str(i))
+    time.sleep(random.random()* 10)

File tests/manual/move.py

View file
+#!/usr/bin/env python
+
+"""Outputs something like:
+spaand eggs
+george
+"""
+
+from terminate.term import stdout
+
+stdout.write('spam')
+stdout.move('left')
+stdout.write('and')
+stdout.move('right')
+stdout.write('eggs')
+stdout.move('left',3)
+stdout.move('down')
+stdout.write('george')
+print # trailing newline

File tests/manual/progressbar.py

View file
+#!/usr/bin/env python
+
+from terminate.widget import ProgressBar
+import time
+
+p = ProgressBar('bar')
+p.update(0)
+time.sleep(.2)
+p.update(20, 'safe epam and eggs')
+time.sleep(.2)
+p.update(50, None)
+time.sleep(.2)
+p.update(40)
+time.sleep(.2)
+p.set_title('baz')
+p.update(20, 'spam and eggs')
+time.sleep(.2)
+p.update(70)
+time.sleep(.2)
+p.update(80)
+time.sleep(.2)
+p.update(100)

File tests/manual/spinner.py

View file
+#!/usr/bin/env python
+
+from terminate.widget import Spinner
+import time
+
+s = Spinner()
+for i in range(100):
+    s.spin()
+    time.sleep(.01)

File tests/manual/timedprogressbar.py

View file
+#!/usr/bin/env python
+
+from terminate.widget import TimedProgressBar
+import time
+import random
+
+p = TimedProgressBar('bar')
+n = 100
+for i in range(n):
+    p.update(float(i)/n *100, 'update '+str(i))
+    time.sleep(random.random())

File tests/manual/title.py

View file
+#!/usr/bin/env python
+
+from terminate.term import stdout
+import time
+
+stdout.set_title("Terminal title has been changed")
+raw_input("Press Enter to exit")

File tests/rlcomplete_test.py

View file
+#!/usr/bin/env python
+
+import unittest
+from terminate import rlcomplete
+
+class TestControl(unittest.TestCase):
+
+    def test_ListCompleter(self):
+        c = rlcomplete.ListCompleter(['foo','bar','baz'], True)
+        self.failUnlessEqual(c.completelist('a'), [])
+        self.failUnlessEqual(c.completelist('B'), ['bar','baz'])
+        self.failUnlessEqual(c.completelist('f'), ['foo'])
+        c = rlcomplete.ListCompleter(['foo','bar','baz'], False)
+        self.failUnlessEqual(c.completelist('B'), [])
+        self.failUnlessEqual(c.completelist('b'), ['bar','baz'])
+
+    def test_PathCompleter(self):
+        #c = rlcomplete.PathCompleter()
+        #self.failUnlessEqual(c.completelist('m'), ['manual/'])
+        pass
+        
+if __name__ == '__main__':
+    unittest.main()
+
+#Desktop/    bin/  lib/    personal/  src/  workspace/
+#Templates/  doc/  media/  share/     tmp/  www/

File tests/term_test.py

View file
+#!/usr/bin/env python
+
+from terminate.term import display, Magic, Term
+import os
+import unittest
+
+devnull = open(os.devnull, 'w')
+stdout = Term(devnull)
+
+class TestTerm(unittest.TestCase):
+    
+    def set_display(self,codes=[], fg=None, bg=None):
+        stdout.write(display(codes, fg, bg))
+
+    def test_color(self):
+        self.set_display('default')
+        stdout.write("default text")
+        
+        stdout.write("regular foreground test:")
+        for color in Magic.COLORS.iterkeys():
+            self.set_display(fg=color)
+            stdout.write("    " + color)
+        
+        self.set_display('default')
+        
+        stdout.write("regular background test:")
+        for color in Magic.COLORS.iterkeys():
+            self.set_display(bg=color)
+            stdout.write("    " + color)
+        
+        self.set_display('default')
+        
+        stdout.write("bright foreground test:")
+        self.set_display('bright')
+        for color in Magic.COLORS.iterkeys():
+            self.set_display(fg=color)
+            stdout.write("    " + color)
+        
+        self.set_display('default')
+        
+        stdout.write("dim foreground test:")
+        self.set_display('dim')
+        for color in Magic.COLORS.iterkeys():
+            self.set_display(fg=color)
+            stdout.write("    " + color)
+        
+        self.set_display('bright','red')
+        stdout.write("bright red")
+        self.set_display("default")
+
+if __name__ == '__main__':
+    unittest.main()