Browse Source

drop launching functionality

Fabian Peter Hammerle 4 years ago
parent
commit
2acae9f56e
10 changed files with 36 additions and 566 deletions
  1. 5 4
      .gitignore
  2. 1 1
      Pipfile
  3. 1 20
      Pipfile.lock
  4. 26 13
      scripts/tooncher-extend-controls
  5. 2 3
      setup.py
  6. 0 74
      tests/test_api.py
  7. 0 155
      tests/test_cli.py
  8. 0 150
      tooncher/__init__.py
  9. 0 146
      tooncher/_cli.py
  10. 1 0
      tooncher/controls.py

+ 5 - 4
.gitignore

@@ -1,7 +1,8 @@
-build/
-.eggs/
-.pytest_cache/
+*.egg-info/
 *.pyc
 .coverage
+.eggs/
+.pytest_cache/
+build/
 dist/
-*.egg-info/
+tags

+ 1 - 1
Pipfile

@@ -4,7 +4,7 @@ verify_ssl = true
 name = "pypi"
 
 [packages]
-rescriptoon = {editable = true, extras = ["extended-controls"], path = "."}
+rescriptoon = {editable = true, path = "."}
 
 [dev-packages]
 black = "==19.10b0"

+ 1 - 20
Pipfile.lock

@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "bef0f4ce4f5c6eb429bc9a8d6a8bbe03b38141b6fff453aa772a82e779dc09c7"
+            "sha256": "6c12b2e0d167fe71ca27e3a6c4c7888291a12670f17e7048d35a5835db0a85ca"
         },
         "pipfile-spec": 6,
         "requires": {
@@ -32,27 +32,8 @@
             ],
             "version": "==5.6.7"
         },
-        "pyyaml": {
-            "hashes": [
-                "sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc",
-                "sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803",
-                "sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc",
-                "sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15",
-                "sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075",
-                "sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd",
-                "sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31",
-                "sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f",
-                "sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c",
-                "sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04",
-                "sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"
-            ],
-            "version": "==5.2"
-        },
         "rescriptoon": {
             "editable": true,
-            "extras": [
-                "extended-controls"
-            ],
             "path": "."
         },
         "six": {

+ 26 - 13
scripts/tooncher-extend-controls

@@ -1,16 +1,20 @@
 #!/usr/bin/env python3
 # PYTHON_ARGCOMPLETE_OK
 
-import os
 import sys
+
 import tooncher.controls
 
 
-def run(engine_pid,
-        toggle_keysym_name=tooncher.controls.EXTENDED_CONTROLS_DEFAULT_TOGGLE_KEYSYM_NAME):
+def run(
+    engine_pid: int,
+    engine_window_name: str,
+    toggle_keysym_name=tooncher.controls.EXTENDED_CONTROLS_DEFAULT_TOGGLE_KEYSYM_NAME,
+):
     ec = tooncher.controls.ExtendedControls(
         primary_engine_pid=engine_pid,
         toggle_keysym_name=toggle_keysym_name,
+        primary_engine_window_name=engine_window_name,
     )
     ec.run()
 
@@ -18,23 +22,30 @@ def run(engine_pid,
 def _init_argparser():
 
     import argparse
+
     argparser = argparse.ArgumentParser(
         description="Attach Extended Controls to an already running Toontown engine.",
     )
     argparser.add_argument(
-        'engine_pid',
-        type=int,
-        help="process id of engine to attach to",
+        "engine_pid", type=int, help="process id of engine to attach to",
     )
     argparser.add_argument(
-        '--toggle', '-t',
-        metavar='KEYSYM_NAME',
-        dest='toggle_keysym_name',
+        "--toggle",
+        "-t",
+        metavar="KEYSYM_NAME",
+        dest="toggle_keysym_name",
         default=tooncher.controls.EXTENDED_CONTROLS_DEFAULT_TOGGLE_KEYSYM_NAME,
-        help='key to turn extended keyboard controls on / off.'
-            + ' any keysym name may be used'
-            + ' (see XStringToKeysym & X11/keysymdef.h, '
-            + ' default: %(default)s)',
+        help="key to turn extended keyboard controls on / off."
+        + " any keysym name may be used"
+        + " (see XStringToKeysym & X11/keysymdef.h, "
+        + " default: %(default)s)",
+    )
+    argparser.add_argument(
+        "--change-window-name",
+        metavar="ENGINE_WINDOW_NAME",
+        dest="engine_window_name",
+        default=None,
+        help="change window name of engine after launch",
     )
     return argparser
 
@@ -44,6 +55,7 @@ def main(argv):
     argparser = _init_argparser()
     try:
         import argcomplete
+
         argcomplete.autocomplete(argparser)
     except ImportError:
         pass
@@ -53,5 +65,6 @@ def main(argv):
 
     return 0
 
+
 if __name__ == "__main__":
     sys.exit(main(sys.argv[1:]))

+ 2 - 3
setup.py

@@ -22,9 +22,8 @@ setuptools.setup(
         "Topic :: Games/Entertainment",
         "Topic :: Utilities",
     ],
-    entry_points={"console_scripts": ["tooncher = tooncher._cli:main"]},
-    install_requires=["pyyaml"],
-    extras_require={"extended-controls": ["xlib", "psutil"],},
+    scripts=["scripts/tooncher-extend-controls"],
+    install_requires=["xlib", "psutil"],
     setup_requires=["setuptools_scm"],
     tests_require=["pytest"],
 )

+ 0 - 74
tests/test_api.py

@@ -1,74 +0,0 @@
-import pathlib
-import shutil
-import subprocess
-import unittest.mock
-
-import tooncher
-
-
-def test_start_engine():
-    process = tooncher.start_engine(
-        engine_path=pathlib.Path(shutil.which("printenv")),
-        gameserver="gameserver",
-        playcookie="cookie",
-        stdout=subprocess.PIPE,
-        stderr=subprocess.PIPE,
-    )
-    assert isinstance(process, subprocess.Popen)
-    stdout, stderr = process.communicate()
-    assert not stderr
-    env = stdout.strip().split(b"\n")
-    assert b"TTR_GAMESERVER=gameserver" in env
-    assert b"TTR_PLAYCOOKIE=cookie" in env
-
-
-def test_start_engine_mac():
-    app_support_path = "/Users/me/Library/Application Support"
-    with unittest.mock.patch("subprocess.Popen") as popen_mock:
-        with unittest.mock.patch("sys.platform", "darwin"):
-            tooncher.start_engine(
-                engine_path=pathlib.PosixPath(
-                    app_support_path + "/Toontown Rewritten/Toontown Rewritten"
-                ),
-                gameserver="gameserver",
-                playcookie="cookie",
-                check=True,
-            )
-    popen_mock.assert_called_once_with(
-        args=[
-            "/Users/me/Library/Application Support/Toontown Rewritten/Toontown Rewritten"
-        ],
-        check=True,
-        cwd=pathlib.PosixPath(
-            "/Users/me/Library/Application Support/Toontown Rewritten"
-        ),
-        env={
-            "TTR_GAMESERVER": "gameserver",
-            "TTR_PLAYCOOKIE": "cookie",
-            "DYLD_LIBRARY_PATH": app_support_path
-            + "/Toontown Rewritten/Libraries.bundle",
-            "DYLD_FRAMEWORK_PATH": app_support_path + "/Toontown Rewritten/Frameworks",
-        },
-    )
-
-
-def test_start_engine_xorg():
-    with unittest.mock.patch("subprocess.Popen") as popen_mock:
-        with unittest.mock.patch("os.environ", {"XAUTHORITY": "/home/me/.Xauthority"}):
-            with unittest.mock.patch("sys.platform", "linux"):
-                tooncher.start_engine(
-                    engine_path=pathlib.PosixPath("/opt/toontown-rewritter/TTREngine"),
-                    gameserver="gameserver.tld",
-                    playcookie="cookie123",
-                    check=False,
-                )
-    popen_mock.assert_called_once_with(
-        args=["/opt/toontown-rewritter/TTREngine"],
-        check=False,
-        cwd=pathlib.PosixPath("/opt/toontown-rewritter"),
-        env={
-            "TTR_GAMESERVER": "gameserver.tld",
-            "TTR_PLAYCOOKIE": "cookie123",
-            "XAUTHORITY": "/home/me/.Xauthority",
-        },
-    )

+ 0 - 155
tests/test_cli.py

@@ -1,155 +0,0 @@
-import pathlib
-import subprocess
-from unittest.mock import patch
-
-import pytest
-import yaml
-
-# pylint: disable=protected-access
-import tooncher._cli
-
-
-def test_cli_help():
-    proc_info = subprocess.run(
-        ["tooncher", "--help"],
-        check=True,
-        stdout=subprocess.PIPE,
-        stderr=subprocess.PIPE,
-    )
-    assert b"optional arguments:" in proc_info.stdout
-    assert not proc_info.stderr
-
-
-@patch("tooncher._cli.run")
-@patch("os.environ", {})
-def test_engine_path_arg(run_mock):
-    with patch("sys.argv", ["", "--engine-path", "/opt/ttr/TTREngine", "username"]):
-        tooncher._cli.main()
-    run_mock.assert_called_once()
-    args, kwargs = run_mock.call_args
-    assert not args
-    assert kwargs["engine_path"] == "/opt/ttr/TTREngine"
-
-
-@patch("tooncher._cli.run")
-@patch("os.environ", {"TOONCHER_ENGINE_PATH": "/opt/ttr/TTREnvine"})
-def test_engine_path_env(run_mock):
-    with patch("sys.argv", ["", "username"]):
-        tooncher._cli.main()
-    run_mock.assert_called_once()
-    args, kwargs = run_mock.call_args
-    assert not args
-    assert kwargs["engine_path"] == "/opt/ttr/TTREnvine"
-
-
-@patch("tooncher._cli.run")
-@patch("os.environ", {"TOONCHER_ENGINE_PATH": "/opt/ttr/TTREnvine"})
-def test_engine_path_arg_env(run_mock):
-    with patch("sys.argv", ["", "--engine-path", "/opt/ttr/TTREngine", "username"]):
-        tooncher._cli.main()
-    run_mock.assert_called_once()
-    args, kwargs = run_mock.call_args
-    assert not args
-    assert kwargs["engine_path"] == "/opt/ttr/TTREngine"
-
-
-@patch("tooncher.launch")
-@patch("os.environ", {})
-def test_engine_path_config(launch_mock, tmpdir):
-    config_file = tmpdir.join("config")
-    config_file.write(
-        yaml.safe_dump(
-            {
-                "engine_path": "/opt/conf/TTR",
-                "accounts": [{"username": "someone", "password": "secret"}],
-            }
-        )
-    )
-    with patch("sys.argv", ["", "--config", config_file.strpath, "someone"]):
-        tooncher._cli.main()
-    launch_mock.assert_called_once()
-    args, kwargs = launch_mock.call_args
-    assert not args
-    assert kwargs["engine_path"] == pathlib.Path("/opt/conf/TTR")
-
-
-@patch("tooncher.launch")
-@patch("os.environ", {"TOONCHER_ENGINE_PATH": "/opt/ttr/TTREnvine"})
-def test_engine_path_env_config(launch_mock, tmpdir):
-    config_file = tmpdir.join("config")
-    config_file.write(
-        yaml.safe_dump(
-            {
-                "engine_path": "/opt/conf/TTR",
-                "accounts": [{"username": "someone", "password": "secret"}],
-            }
-        )
-    )
-    with patch("sys.argv", ["", "--config", config_file.strpath, "someone"]):
-        tooncher._cli.main()
-    launch_mock.assert_called_once()
-    args, kwargs = launch_mock.call_args
-    assert not args
-    assert kwargs["engine_path"] == pathlib.Path("/opt/ttr/TTREnvine")
-
-
-@patch("tooncher.launch")
-def test_account(launch_mock, tmpdir):
-    config_file = tmpdir.join("config")
-    config_file.write(
-        yaml.safe_dump(
-            {
-                "engine_path": "/opt/conf/TTR",
-                "accounts": [
-                    {"username": "someone", "password": "secret"},
-                    {"username": "toon", "password": "town"},
-                ],
-            }
-        )
-    )
-    with patch("sys.argv", ["", "--config", config_file.strpath, "toon"]):
-        tooncher._cli.main()
-    launch_mock.assert_called_once_with(
-        engine_path=pathlib.Path("/opt/conf/TTR"),
-        username="toon",
-        password="town",
-        validate_ssl_certs=True,
-        cpu_limit_percent=None,
-    )
-
-
-def test_account_duplicate_username(tmpdir):
-    config_file = tmpdir.join("config")
-    config_file.write(
-        yaml.safe_dump(
-            {
-                "engine_path": "/opt/conf/TTR",
-                "accounts": [
-                    {"username": "someone", "password": "secret"},
-                    {"username": "toon", "password": "town"},
-                    {"username": "toon", "password": "town2"},
-                ],
-            }
-        )
-    )
-    with patch("sys.argv", ["", "--config", config_file.strpath, "toon"]):
-        with pytest.raises(ValueError, match=r"multiple .* username"):
-            tooncher._cli.main()
-
-
-def test_account_unknown_username(tmpdir):
-    config_file = tmpdir.join("config")
-    config_file.write(
-        yaml.safe_dump(
-            {
-                "engine_path": "/opt/conf/TTR",
-                "accounts": [
-                    {"username": "someone", "password": "secret"},
-                    {"username": "toon", "password": "town"},
-                ],
-            }
-        )
-    )
-    with patch("sys.argv", ["", "--config", config_file.strpath, "player"]):
-        with pytest.raises(ValueError, match=r"not found"):
-            tooncher._cli.main()

+ 0 - 150
tooncher/__init__.py

@@ -1,150 +0,0 @@
-import copy
-import datetime
-import json
-import os
-import pathlib
-import ssl
-import subprocess
-import sys
-import traceback
-import typing
-import urllib.parse
-import urllib.request
-
-import tooncher.controls
-
-# official api documentation:
-# https://github.com/ToontownRewritten/api-doc/blob/master/login.md
-# https://github.com/ToontownRewritten/api-doc/blob/master/invasions.md
-
-_LOGIN_API_URL = "https://www.toontownrewritten.com/api/login?format=json"
-
-
-def start_engine(
-    engine_path: pathlib.Path, gameserver: str, playcookie: str, **popen_kwargs
-) -> subprocess.Popen:
-    # without XAUTHORITY:
-    # > :display:x11display(error): Could not open display ":0.0".
-    # > :ToonBase: Default graphics pipe is glxGraphicsPipe (OpenGL).
-    # > :ToonBase(warning): Unable to open 'onscreen' window.
-    # > Traceback (most recent call last):
-    # >   File "<compiled '__voltorbmain__'>", line 0, in <module>
-    # >   [...]
-    # >   File "<compiled 'direct.vlt8f63e471.ShowBase'>", line 0, in vltf05fd21b
-    # > Exception: Could not open window.
-    # optirun sets plenty of env vars
-    env = copy.copy(os.environ)
-    env["TTR_GAMESERVER"] = gameserver
-    env["TTR_PLAYCOOKIE"] = playcookie
-    engine_path = engine_path.resolve()
-    if sys.platform == "darwin":
-        env["DYLD_LIBRARY_PATH"] = str(engine_path.parent.joinpath("Libraries.bundle"))
-        env["DYLD_FRAMEWORK_PATH"] = str(engine_path.parent.joinpath("Frameworks"))
-    return subprocess.Popen(
-        args=[str(engine_path)], cwd=engine_path.parent, env=env, **popen_kwargs,
-    )
-
-
-def _api_request(
-    url: str, params: typing.Optional[dict] = None, validate_ssl_cert: bool = True
-):
-    resp = urllib.request.urlopen(
-        url=url,
-        data=urllib.parse.urlencode(params).encode("ascii") if params else None,
-        context=None if validate_ssl_cert else ssl._create_unverified_context(),
-    )
-    return json.loads(resp.read().decode("ascii"))
-
-
-class _LoginSuccessful:
-    def __init__(self, playcookie: str, gameserver: str):
-        self.playcookie = playcookie
-        self.gameserver = gameserver
-
-
-class _LoginDelayed:
-    def __init__(self, queue_token: str):
-        self.queue_token = queue_token
-
-
-def _login(
-    username: typing.Optional[str] = None,
-    password: typing.Optional[str] = None,
-    queue_token: typing.Optional[str] = None,
-    validate_ssl_cert: bool = True,
-) -> typing.Union[_LoginSuccessful, _LoginDelayed]:
-    if username is not None and queue_token is None:
-        assert password is not None
-        req_params = {
-            "username": username,
-            "password": password,
-        }
-    elif username is None and queue_token is not None:
-        req_params = {
-            "queueToken": queue_token,
-        }
-    else:
-        raise Exception("either specify username or queue token")
-    resp_data = _api_request(
-        url=_LOGIN_API_URL, params=req_params, validate_ssl_cert=validate_ssl_cert,
-    )
-    if resp_data["success"] == "true":
-        return _LoginSuccessful(
-            playcookie=resp_data["cookie"], gameserver=resp_data["gameserver"],
-        )
-    if resp_data["success"] == "delayed":
-        return _LoginDelayed(queue_token=resp_data["queueToken"],)
-    raise Exception(repr(resp_data))
-
-
-def launch(
-    engine_path: pathlib.Path,
-    username: str,
-    password: str,
-    validate_ssl_certs: bool = True,
-    cpu_limit_percent: typing.Optional[int] = None,
-    enable_extended_keyboard_controls=False,
-    extended_keyboard_control_toggle_keysym_name=None,
-    engine_window_name=None,
-) -> None:
-    if engine_window_name and not enable_extended_keyboard_controls:
-        raise Exception("Enable Extended Controls to change engine's window name",)
-    result = _login(
-        username=username, password=password, validate_ssl_cert=validate_ssl_certs,
-    )
-    if isinstance(result, _LoginDelayed):
-        result = _login(
-            queue_token=result.queue_token, validate_ssl_cert=validate_ssl_certs,
-        )
-    if not isinstance(result, _LoginSuccessful):
-        raise Exception("unexpected response: {!r}".format(result))
-    process = start_engine(
-        engine_path=engine_path,
-        gameserver=result.gameserver,
-        playcookie=result.playcookie,
-    )
-    if cpu_limit_percent is not None:
-        subprocess.Popen(
-            args=[
-                "cpulimit",
-                "--pid",
-                str(process.pid),
-                "--limit",
-                str(cpu_limit_percent),
-                # '--verbose',
-            ]
-        )
-    if enable_extended_keyboard_controls:
-        try:
-            tooncher.controls.ExtendedControls(
-                primary_engine_pid=process.pid,
-                primary_engine_window_name=engine_window_name,
-                toggle_keysym_name=extended_keyboard_control_toggle_keysym_name,
-            ).run()
-        except Exception as e:
-            if isinstance(e, KeyboardInterrupt):
-                raise e
-            else:
-                traceback.print_exc()
-    if process.poll() is None:
-        process.wait()

+ 0 - 146
tooncher/_cli.py

@@ -1,146 +0,0 @@
-import argparse
-import os
-import pathlib
-import sys
-
-import yaml
-
-import tooncher
-import tooncher.controls
-
-if sys.platform == "darwin":
-    _TOONTOWN_ENGINE_DEFAULT_PATH = os.path.join(
-        os.path.expanduser("~"),
-        "Library",
-        "Application Support",
-        "Toontown Rewritten",
-        "Toontown Rewritten",
-    )
-else:
-    _TOONTOWN_ENGINE_DEFAULT_PATH = None
-
-
-def run(
-    username,
-    config_path,
-    engine_path=None,
-    validate_ssl_certs=True,
-    cpu_limit_percent=None,
-    enable_extended_keyboard_controls=False,
-    extended_keyboard_control_toggle_keysym_name=None,
-    engine_window_name=None,
-):
-    if os.path.exists(config_path):
-        with open(config_path) as config_file:
-            config = yaml.safe_load(config_file.read())
-    else:
-        config = {}
-    if engine_path is None:
-        if "engine_path" in config:
-            engine_path = config["engine_path"]
-        else:
-            engine_path = _TOONTOWN_ENGINE_DEFAULT_PATH
-    if engine_path is None:
-        raise ValueError(
-            "missing path to toontown engine\n"
-            + "pass --engine-path, set $TOONCHER_ENGINE_PATH, or add to config file"
-        )
-    accounts = [a for a in config.get("accounts", []) if a["username"] == username]
-    if not accounts:
-        raise ValueError("username {!r} was not found in config file".format(username))
-    if len(accounts) > 1:
-        raise ValueError(
-            "multiple entries for username {!r} in config file".format(username)
-        )
-    tooncher.launch(
-        engine_path=pathlib.Path(engine_path),
-        username=accounts[0]["username"],
-        password=accounts[0]["password"],
-        validate_ssl_certs=validate_ssl_certs,
-        cpu_limit_percent=cpu_limit_percent,
-        enable_extended_keyboard_controls=enable_extended_keyboard_controls,
-        extended_keyboard_control_toggle_keysym_name=extended_keyboard_control_toggle_keysym_name,
-        engine_window_name=engine_window_name,
-    )
-
-
-class _EnvDefaultArgparser(argparse.ArgumentParser):
-    def add_argument(self, *args, envvar=None, **kwargs):
-        if envvar:
-            envvar_value = os.environ.get(envvar, None)
-            if envvar_value:
-                kwargs["required"] = False
-                kwargs["default"] = envvar_value
-        super().add_argument(*args, **kwargs)
-
-
-def _init_argparser():
-    argparser = _EnvDefaultArgparser(description=None)
-    argparser.add_argument("username")
-    argparser.add_argument(
-        "--config",
-        "-c",
-        metavar="path",
-        dest="config_path",
-        help="path to config file (default: %(default)s)",
-        default=os.path.join(os.path.expanduser("~"), ".tooncher"),
-    )
-    argparser.add_argument(
-        "--engine-path",
-        "-e",
-        metavar="path",
-        dest="engine_path",
-        envvar="TOONCHER_ENGINE_PATH",
-        default=None,
-        help="path to toontown engine (overrides path in config file, "
-        + "may also be set via env var $TOONCHER_ENGINE_PATH)",
-    )
-    argparser.add_argument(
-        "--no-ssl-cert-validation",
-        "-k",
-        dest="validate_ssl_certs",
-        help="do not validate ssl certificates",
-        action="store_false",
-    )
-    argparser.add_argument(
-        "--cpu-limit",
-        dest="cpu_limit_percent",
-        type=int,
-        default=None,
-        help="maximally allowed cpu usage in percent"
-        + " (requires cpulimit command, default: %(default)s)",
-    )
-    argparser.add_argument(
-        "--extended-controls",
-        dest="enable_extended_keyboard_controls",
-        action="store_true",
-        help="enable extended keyboard controls"
-        + ", e.g. walk with WASD"
-        + " (requires xlib for python, default: %(default)s)",
-    )
-    argparser.add_argument(
-        "--extended-controls-toggle",
-        metavar="KEYSYM_NAME",
-        dest="extended_keyboard_control_toggle_keysym_name",
-        default=tooncher.controls.EXTENDED_CONTROLS_DEFAULT_TOGGLE_KEYSYM_NAME,
-        help="key to turn extended keyboard controls on / off."
-        + " any keysym name may be used"
-        + " (see XStringToKeysym & X11/keysymdef.h, "
-        + " default: %(default)s)",
-    )
-    argparser.add_argument(
-        "--change-window-name",
-        metavar="ENGINE_WINDOW_NAME",
-        dest="engine_window_name",
-        default=None,
-        help="Change window name of engine after launch."
-        + " This requires Extended Controls to be enabled."
-        + " (default: no change)",
-    )
-    return argparser
-
-
-def main() -> None:
-    argparser = _init_argparser()
-    args = argparser.parse_args()
-    run(**vars(args))

+ 1 - 0
tooncher/controls.py

@@ -2,6 +2,7 @@ import copy
 import os
 import select
 import time
+
 from tooncher.actions import *
 
 try: