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
 
 

implement tab key support
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()")

pip install -e .
parse_log_pytest
pytest
pytest pexpect pytest-cache
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
execnet==2.1.1
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
packaging @ file:///croot/packaging_1734472117206/work
pexpect==4.9.0
pluggy @ file:///croot/pluggy_1733169602837/work
ptyprocess==0.7.0
pytest @ file:///croot/pytest_1738938843180/work
pytest-cache==1.0
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
-e git+https://github.com/toejough/ugetch.git@2616fe2bbe55feb0048e3e973966f83d7f5f9710#egg=ugetch

name: ugetch
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - exceptiongroup=1.2.0=py39h06a4308_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - packaging=24.2=py39h06a4308_0
  - pip=25.0=py39h06a4308_0
  - pluggy=1.5.0=py39h06a4308_0
  - pytest=8.3.4=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py39h06a4308_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - execnet==2.1.1
      - pexpect==4.9.0
      - ptyprocess==0.7.0
      - pytest-cache==1.0
prefix: /opt/conda/envs/ugetch

test_ugetch.py::test_getch_tab
test_ugetch.py::test_getch_ascii test_ugetch.py::test_getch_utf8
README.rst ugetch/__init__.py .travis.yml
README.rst ugetch/__init__.py .travis.yml