[nylug-workshop] Input strategies

Dan Crosta dcrosta at sccs.swarthmore.edu
Sat Dec 9 11:29:54 EST 2006


Great! Thanks Peter and anyone else who helped out with this patch...  
I've imported it to r146, and it seems to be working well. There seem  
to still be a few keys which either curses doesn't properly wrangle  
or that the python curses module doesn't have a name for  
(curses.KEY_<foo>), surprisingly the Tab, Return and Escape keys. But  
the remainder of the keyboard seems to be well-supported in a fairly  
cross-platform way and well more readable than the old version, thanks!

Peter (and any other contributors): do I have your permission to  
include this code in DTK's trunk BSD-licensed branch? How would you  
like to be acknowledged (ie Name only, email, alias, etc) in the  
AUTHORS file I'm putting together?

- d



On Dec 8, 2006, at 1:19 AM, Not Peter wrote:

> (Mailman ate this, so I'm reposting it for Peter.  If you posted
> something that didn't come through, please repost now.
> - Ron)
>
> -------- Original Message --------
>
> OK, here's a rough explanation of what we spent some time looking at
> on tuesday:
>
> The input method is dodgy because of how curses works with chars. The
> summary of how curses (and ncurses when using the curses interface)
> deals with terminal input:
>
> a) If the terminal isn't capable of passing 8-bit characters cleanly*,
>    there is a mode enabled by running the screen's method
>    keypad(False). This tells curses that the terminal will send
>    special things like arrows, function keys, ctrl-<key> combinations,
>    etc. as sequences of two, three, four, or five 7-bit characters.
>
>    * It's worth noting that there's little or nothing that available
>      today that isn't 8-bit. 7-bit serial lines are rare artifacts of
>      the past - or toys bought by belligerant collectors who don't
>      have anything better to do than subject themselves to the misery
>      of debugging serial connections that people pretty much hated
>      when they were new.
>
> b) When using 7-bit input mode, there's a huge variability in what'll
>    come through. As far as the documentation I've seen goes, there is
>    no "end-of-character" signaling, so you essentially have to get a
>    bunch of characters and then look for a pause. This *may* happen by
>    some convention with slower serial terminals, but on something like
>    an xterm or a linux console, you can key-mash and the input
>    functions don't seem to offer a way, in this mode, of
>    distinguishing 5 events in a row from 5 keys being hit at the same
>    time. The attached updated cursesdemo.py shows you this behavior
>    when you use app_seq1(). Unfortunately it's display is confusing
>    because I haven't found a good way to clear the terminal so once
>    you have something that's got 5 characters, you'll display 5 lines.
>
> c) When using 8-bit mode, the python developers cheated in our
>    favor. They didn't bother limiting themselves to 8 bits, I think
>    they just figure out what's right and return the integer associated
>    with a character regardless of the size of the integer. This
>    doesn't need to rely on grabbing multiple chracters and it will
>    always return the correct character on modern, local devices
>    (xterms, linux consoles, vty's under screen, etc). Use app_seq2()
>    to see how this behaves.
>
> So I've hacked dtk on my system here to change the input method to use
> a more straightforward dictionary for this. The old way is still
> included and would probably be useful for use over an old serial link
> by a masochistic unix geezer (and I mean geezer independant of anyones
> age). It's probably something to be enabled via a keyword argument -
> perhaps in the future terminal capabilities would be detected and set
> the proper input mode if a 7-bit terminal came up and bit someone.
>
> Also, I've started adding logging. The kind of logging I'm doing
> should probably become more centralized, but it's something like this
> (from CursesEngine.py's __init__()):
>
>         if wantslog == True:
>             self.logger = logging.getLogger('dtk.CursesEngine')
>             self.hndlr  = logging.FileHandler('dtk.CursesEngine.log')
>             self.hndlr.setFormatter(logging.Formatter('%(asctime)s
> %(levelname)s %(message)s'))
>             self.logger.addHandler(self.hndlr)
>             try:
>                 if kwargs['loglevel']:
>                     self.logger.setLevel(kwargs['loglevel'])
>             except KeyError: # Not defined
>                 self.logger.setLevel(logging.WARNING)
>
> In principle this should create a logger local to the module that,
> when it's asked to, will create a file called "dtk.CursesEngine.log"
> that will contain log messages from the curses engine when it's
> initialized with a keyword argument of "wantslog" set to True. This is
> not necessarily the right way to do this because it means having to
> change the invocation of CursesEngine(), which is bad, but I think
> that a DtkLogging class to wrap the logging functionality that could
> be called on by all the dtk modules could be an easy way to globally
> define logging, and it could add granular configuration
> (eg. it could have declarations like
> DtkLogging.CursesEngineLogging = True
> DtkLogging.CursesEngineLoglevel = logging.DEBUG
> and when CursesEngine runs
>
> import DtkLogging
>
> it can check to see if its supposed to log, supposed to log to a
> separate file, what log level should be invoked, etc. I've attached a
> diff to CursesEngine.py to this message. Dan maybe you could let me
> know what you think. I know you said you were working on the logging,
> but I'm not brave enough to see how to keep account of rows etc. in
> the TextEditor yet (it doesn't seem to scroll when typing past the
> last line in the window), and to approach that I need more logging.
>
> Happy holidays! I think I may be out of town for the next meeting, but
> I'm hoping that other people can look at the attached and let me know
> if I missed anything regarding the differences in key input methods.
>
> -Peter
>
> -- 
> The 5 year plan: In five years we'll make up another plan.
> Or just re-use this one.
>
>
>
> Index: CursesEngine.py
> ===================================================================
> --- CursesEngine.py	(revision 144)
> +++ CursesEngine.py	(working copy)
> @@ -4,11 +4,13 @@
>  import types
>  import os
>  import time
> +import logging
>
>  from Engine import Engine
>
> +class NoInputCharException(Exception):
> +    pass
>
> -
>  class CursesEngine(Engine):
>      """
>      CursesEngine provides an abstraction of the screen
> @@ -18,6 +20,7 @@
>      else.
>      """
>
> +
>      # attributes for drawing
>      attrs = {
>          'bold':curses.A_BOLD,
> @@ -108,12 +111,51 @@
>                 339 : "page up",
>                 }
>
> +    # In keypad(True) mode, we're in better shape. We can get
> +    # any character the input layer wants to throw at us, one  
> character
> +    # at a time. The python layer seems to magically transform
> +    # characters that'd otherwise be >255 into a python int, which
> +    # is no one-byte bullshit.
> +    keymap = { 9   : "tab",
> +               10  : "enter",
> +               27  : "esc",
> +               259 : "up",
> +               258 : "down",
> +               260 : "left",
> +               261 : "right",
> +               262 : "home",
> +               265 : "F1",
> +               266 : "F2",
> +               267 : "F3",
> +               268 : "F4",
> +               269 : "F5",
> +               270 : "F6",
> +               271 : "F7",
> +               272 : "F8",
> +               273 : "F9",
> +               274 : "F10",
> +               275 : "F11",
> +               276 : "F12",
> +               330 : "delete",
> +               331 : "insert",
> +               338 : "page down",
> +               339 : "page up",
> +               360 : "end",
> +               263 : "backspace",
> +               }
>
> +
> +
>      def __init__(self, *args, **kwargs):
>          # some things can only be done once curses.init_scr has  
> been called
>          self.cursesInitialized = False
>          self.doWhenCursesInitialized = []
>
> +        try:
> +            wantslog = kwargs['wantslog']
> +        except KeyError:
> +            wantslog = False
> +
>          super(CursesEngine, self).__init__(*args, **kwargs)
>
>          # initially, we are tiny!
> @@ -127,7 +169,18 @@
>          # for input handling
>          self.curmap = self.keymap
>
> +        if wantslog == True:
> +            self.logger = logging.getLogger('dtk.CursesEngine')
> +            self.hndlr  = logging.FileHandler('dtk.CursesEngine.log')
> +            self.hndlr.setFormatter(logging.Formatter('%(asctime)s  
> %(levelname)s %(message)s'))
> +            self.logger.addHandler(self.hndlr)
> +            try:
> +                if kwargs['loglevel']:
> +                    self.logger.setLevel(kwargs['loglevel'])
> +            except KeyError: # Not defined
> +                self.logger.setLevel(logging.WARNING)
>
> +
>      def mainLoop(self):
>          """
>          initializes curses, and asks it to run the
> @@ -154,7 +207,7 @@
>
>          # this ensures that we always get things as multi-character
>          # input for special keys (arrows, etc)...
> -        self.scr.keypad(False)
> +        self.scr.keypad(True)
>
>          # this doesn't always work in all terms, but will never
>          # fail in such a way as to break anything. by default
> @@ -237,8 +290,11 @@
>              # input at a time, and must maintain its state
>              # (ie location in keymap) somehow
>              input = self.scr.getch()
> -            input = self.parseInput(input)
> -
> +            try:
> +                input = self.parseInput(input)
> +            except NoInputCharException:
> +                input = None
> +
>              # after this, input will be a convenient string
>              # such as 'a' or 'space', or None, which means
>              # we're waiting on further multi-byte input
> @@ -264,7 +320,7 @@
>                      self.handleInput(input)
>
>
> -    def parseInput(self, char):
> +    def parseInputOld(self, char):
>          """
>          if the char is a printable character, we just return it.
>          if it's one of the special curses characters (for things
> @@ -316,12 +372,56 @@
>          else:
>              self.log("couldn't parse char: %d" % char)
>              return None
> +
> +    def parseInput(self, char):
> +        """
> +        if the char is a printable character, we just return it.
> +        if it's one of the special curses characters (for things
> +        like arrow keys, combinations, etc) then we will try
> +        our best to figure out what it was and return that instead.
> +
> +        Ths method has been ruined by Peter Norton
> +
> +        Now with logging
> +        """
> +
> +        # In half-delay mode with 8-bit (and more! BONUS BITS!)   
> input
> +        # python curses' screen.getch() will return -1 to indicate
> +        # that no key was pressed. It's documented as raising an
> +        # exception, but I'm happy to get something consistant in  
> this
> +        # all-too-underdocumented module.
> +        #
> +        # XXX report the documentation inconsistancy to get it fixed?
> +        #
> +        # -PN
> +        if char == -1:
> +            # return # awwww... heck, raise an exception
> +            raise NoInputCharException
> +
> +        # If it's the decimal representation of
> +        # a printable character... HEY! THAT'S EASY!
> +        elif curses.ascii.isprint(char):
> +            self.logger("Returning char %s" % chr(char))
> +            return chr(char)
> +
> +        # If it's in keymap via a direct lookup, we're golden
> +        elif char in self.keymap.keys():
> +            self.logger("Returning char %d as %s" % (chr(char),  
> curses.keyname(char)))
> +            return(self.keymap[char])
> +
> +        # If we got here, bad user, bad user
> +        else:
> +            self.log("couldn't parse char: %d" % char)
> +            # return None
> +            raise NoInputCharException
> +
> +
>
>      def shellMode(self):
>          """
>          pauses the main loop and returns the terminal to the shell
>          mode it was in before starting the Engine. opposite of
> -        shellMode()
> +        dtkMode()
>          """
>
>          # save the program mode
> @@ -669,3 +769,4 @@
>              self.cursorpos = (-1, -1)
>          else:
>              self.cursorpos = (drawable.y + y, drawable.x + x)
> +
>
>
> #!/usr/bin/env python2.5
>
> import curses
> import curses.ascii
>
> def init_curses():
>     # start here
>     s = curses.initscr()
>
>     # save the (hopefully) sane state here
>     curses.def_shell_mode()
>
>     curses.noecho()
>     # curses.cbreak()
>     return (curses, s)
>
> def end_curses(curses, s):
>     curses.nocbreak()
>     s.keypad(False)
>     curses.echo()
>     # restore the (hopefully) sane state here
>     curses.reset_shell_mode()
>     # end
>
>
> def app_seq1(curses, scr):
>     ''' Squirriling away all of the work here in case this has to grow
>     to include other functions.
>
>     Demonstrate the values returned by the curses input loop.
>     When a key is hit, this displays:
>     1) the dimensions of the current display
>     2) The character rendered in a readable form
>     3) the decimal value of the input
>     '''
>
>     scr.keypad(False)
>
>     (y, x) = scr.getmaxyx()
>     curses.halfdelay(1)
>
>     while True:
>         ch_list = []
>
>         for c in range(10):
>             char = scr.getch()
>             if char == -1:
>                 break
>             else:
>                 ch_list.append(char)
>         try:
>             if ch_list[0] == -1:
>                 continue
>         except IndexError:
>             continue
>
>         if curses.ascii.unctrl(ch_list[0]) == 'q':
>             break
>
>         scr.addstr(0,0,"Screen is H: %s W: %s with %d chars in  
> ch_list\n" % (y, x, len(ch_list)))
>
>         for row in range(len(ch_list)):
>             scr.addstr(row + 1, 0, "%d: %d in ch_list\n" % (row,  
> ch_list[row]))
>
>         scr.refresh()
>
>
> def app_seq2(curses, scr):
>     """The second method is a bit easier. Since we're doing
>     keypad(True) we can avoid trying to buffer characters. We'll
>     always receive an 8-bit character (well... the documentation  
> *says*
>     8-bit, but as far as I can tell, it's more like a python it, so
>     I think the underlying library has managed to DTRT
>     """
>
>     # This sets up curses for multibyte input.
>     # http://www.amk.ca/python/howto/curses/ 
> curses.html#SECTION000300000000000000000
>     scr.keypad(True)
>
>     (y, x) = scr.getmaxyx()
>     curses.halfdelay(5)
>
>     while True:
>         char = scr.getch()
>         if char == -1:
>             continue
>
>         if curses.ascii.unctrl(char) == 'q':
>             break
>
>         scr.addstr(0,0,"Screen dimensions are Height: %d Width: %d 
> \n" % (y, x))
>         scr.addstr(1,0,"%s : %d \n" % (curses.ascii.unctrl(char),  
> char))
>         scr.refresh()
>
>
>
>
> def main():
>
>     (c, s) = init_curses()
>
>     try:
>         # app_seq1(c, s)
>         app_seq2(c, s)
>     except KeyboardInterrupt:
>         pass
>
>     end_curses(c, s)
>
> main()
>
>
> _______________________________________________
> nylug-workshop mailing list
> nylug-workshop at nylug.org
> http://nylug.org/mailman/listinfo/nylug-workshop
> Calendar with times and directions: http://tighturl.com/fp



More information about the nylug-workshop mailing list