Repository: toejough/ugetch

Issue:
implement tab key support

Discussion:

Patch:
diff --git a/.travis.yml b/.travis.yml
index 0386a42..4d40821 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,5 +1,11 @@
+# basic config
 language: python
 python: 3.4
 install:
 - pip install tox
 script: tox
+
+# whitelist
+branches:
+    only:
+        - master
diff --git a/README.rst b/README.rst
index 91ee28f..ae7af94 100644
--- a/README.rst
+++ b/README.rst
@@ -24,17 +24,21 @@ API
 getkey
 ------
 
-Gets a "key" from the ``infile`` and returns it to the user, where "key" is a character or glyph.
-Currently supports parsing ASCII and UTF-8.
+Gets a "key" from the ``infile`` and returns it to the user, where "key" is a character, glyph, or string describing a control key.
+
+Currently supports parsing ASCII, UTF-8, and tab.
 Any other keys entered (such as arrow keys, UTF-16, etc) will result in returning individual bytes, one at a time.
 
+The tab key is represented as a string: 'TAB'.  This is not a "character", but it *is* a "key".  The implementation
+of keys may change in the future, but for now this seems to be simple and clear.
+
 Parameters:
 
 * ``infile`` - defaults to the ``sys.stdin`` file, as it is set at call time.
 
 Returns:
 
-* ``key`` - a string value corresponding to the key read in from the TTY ('a', 'B', '-', etc).
+* ``key`` - a string value corresponding to the key read in from the TTY ('a', 'B', '-', '☢', 'TAB', etc).
 
 contribution
 ============
diff --git a/ugetch/__init__.py b/ugetch/__init__.py
index 2c7bd98..b615ca3 100644
--- a/ugetch/__init__.py
+++ b/ugetch/__init__.py
@@ -14,40 +14,62 @@ import termios
 def ascii_parser(infile):
     '''Parse ascii bytes to ascii keys'''
     byte = _get_byte(infile)
+    key = None
     if byte < 128:
-        return chr(byte)
+        key = chr(byte)
     else:
         _put_byte(byte)
+    return key
 
 
 def utf8_parser(infile):
     '''Parse utf-8 bytes to a string'''
     first_byte = _get_byte(infile)
+    key = None
+    utf8_bytes = [first_byte]
     # get byte string in py 2 and 3
-    if sys.version_info.major == 3:
-        string = chr(first_byte)
+    try:
+        if sys.version_info.major == 3:
+            key = chr(first_byte)
+        else:
+            if 194 <= first_byte <= 223:
+                # 2 bytes
+                utf8_bytes.append(_get_byte(infile))
+            elif 224 <= first_byte <= 239:
+                # 3 bytes
+                utf8_bytes.append(_get_byte(infile))
+                utf8_bytes.append(_get_byte(infile))
+            elif 240 <= first_byte <= 244:
+                # 4 bytes
+                utf8_bytes.append(_get_byte(infile))
+                utf8_bytes.append(_get_byte(infile))
+                utf8_bytes.append(_get_byte(infile))
+            byte_string = ''.join([chr(b) for b in utf8_bytes])
+            key = byte_string.decode('utf-8')
+    except:
+        # couldn't parse utf-8 out.  not a failure, just not a success.
+        for b in utf8_bytes:
+            _put_byte(b)
+    return key
+
+
+def tab_parser(infile):
+    '''Parse a tab key out'''
+    first_byte = _get_byte(infile)
+    key = None
+    if first_byte == 9:
+        # Tab key
+        key = 'TAB'
     else:
-        utf8_bytes = [first_byte]
-        if 194 <= first_byte <= 223:
-            # 2 bytes
-            utf8_bytes.append(_get_byte(infile))
-        elif 224 <= first_byte <= 239:
-            # 3 bytes
-            utf8_bytes.append(_get_byte(infile))
-            utf8_bytes.append(_get_byte(infile))
-        elif 240 <= first_byte <= 244:
-            # 4 bytes
-            utf8_bytes.append(_get_byte(infile))
-            utf8_bytes.append(_get_byte(infile))
-            utf8_bytes.append(_get_byte(infile))
-        byte_string = ''.join([chr(b) for b in utf8_bytes])
-        string = byte_string.decode('utf-8')
-    return string
+        _put_byte(first_byte)
+    return key
 
 
 # [ Global ]
 _DEFAULT=object()  # enable dynamic defaults
-_KEY_PARSERS=[ascii_parser, utf8_parser]  # ways to parse keys from byte lists
+# Tab needs to go before the ASCII parser, because it is technically in the ASCII range,
+#  and would be sucked in raw by the ASCII parser.
+_KEY_PARSERS=[tab_parser, ascii_parser, utf8_parser]  # ways to parse keys from byte lists
 _BYTES=[]  # byte buffer from the input file
 
 


Test patch:
diff --git a/test_ugetch.py b/test_ugetch.py
index 52f60b7..072b7bf 100644
--- a/test_ugetch.py
+++ b/test_ugetch.py
@@ -19,8 +19,7 @@ import ugetch
 
 
 # [ Helpers ]
-def in_vs_out(input_values):
-    '''supply ascii chars, use getch, and validate ascii returned'''
+def start_cli_shim():
     # choose the correct spawn
     spawn = pexpect.spawn
     if sys.version_info.major == 3:
@@ -28,23 +27,35 @@ def in_vs_out(input_values):
     # spawn the subproc
     p = spawn("python ugetch-cli.py", timeout=0.5)
     p.expect_exact("hit ctrl-c to exit.")
+    return p
+
+
+def assert_expected(p, expected):
+    '''Assert that we get the expected value'''
+    index = p.expect_exact([expected, pexpect.EOF, pexpect.TIMEOUT])
+    assert index == 0, "did not find expected ({}) in output ({})".format(
+        value, p.before
+    )
+
+
+def in_vs_out(input_values):
+    '''supply ascii chars, use getch, and validate ascii returned'''
+    # get shim
+    p = start_cli_shim()
     # test each char
     for value in input_values:
         p.expect_exact("hit a key to print its representation: ")
         p.send(value)
-        index = p.expect_exact([value, pexpect.EOF, pexpect.TIMEOUT])
-        assert index == 0, "did not find expected ({}) in output ({})".format(
-            value, p.before
-        )
+        assert_expected(p, value)
 
 
 # [ Unit tests ]
 def test_getch_ascii():
     '''supply ascii chars, use getch, and validate ascii returned'''
-    in_vs_out(" \n\t")
     all_printable_acii = [chr(n) for n in range(32, 127)]  # not including 127
     in_vs_out(all_printable_acii)
 
+
 def test_getch_utf8():
     '''supply utf-8 glyphs, use getch, and validate the keys returned'''
     in_vs_out(['☢'])
@@ -52,6 +63,17 @@ def test_getch_utf8():
     in_vs_out(['༰','༱','༲','༳','༴',' ༵','༶','༸',' ༹','༺',' ','༻',' ','༼','༽',' ༾',' ༿',' ','ཀ','ཁ','ག','གྷ','ང','ཅ','ཆ'])  # tibetan
 
 
+def test_getch_tab():
+    # get shim
+    p = start_cli_shim()
+    # test each char
+    p.expect_exact("hit a key to print its representation: ")
+    p.send('\t')
+    assert_expected(p, 'TAB')
+    p.sendcontrol('i')
+    assert_expected(p, 'TAB')
+
+
 if __name__ == '__main__':
     import cProfile
     cProfile.run("test_getch_ascii()")