Repository: asciinema/asciinema (language: rust)

Issue:
ValueError: Invalid header value b'Basic <ConsumerKey>', error when uploading the local cast
I'm trying to upload the local cast, but below error is thrown instead.

Here's the error.
```
Traceback (most recent call last):
  File "/usr/local/Cellar/asciinema/2.0.2/libexec/bin/asciinema", line 11, in <module>
    load_entry_point('asciinema==2.0.2', 'console_scripts', 'asciinema')()
  File "/usr/local/Cellar/asciinema/2.0.2/libexec/lib/python3.7/site-packages/asciinema/__main__.py", line 131, in main
    code = command.execute()
  File "/usr/local/Cellar/asciinema/2.0.2/libexec/lib/python3.7/site-packages/asciinema/commands/upload.py", line 14, in execute
    result, warn = self.api.upload_asciicast(self.filename)
  File "/usr/local/Cellar/asciinema/2.0.2/libexec/lib/python3.7/site-packages/asciinema/api.py", line 40, in upload_asciicast
    password=self.install_id
  File "/usr/local/Cellar/asciinema/2.0.2/libexec/lib/python3.7/site-packages/asciinema/urllib_http_adapter.py", line 76, in post
    response = urlopen(request)
  File "/usr/local/Cellar/python/3.7.4/Frameworks/Python.framework/Versions/3.7/lib/python3.7/urllib/request.py", line 222, in urlopen
    return opener.open(url, data, timeout)
  File "/usr/local/Cellar/python/3.7.4/Frameworks/Python.framework/Versions/3.7/lib/python3.7/urllib/request.py", line 525, in open
    response = self._open(req, data)
  File "/usr/local/Cellar/python/3.7.4/Frameworks/Python.framework/Versions/3.7/lib/python3.7/urllib/request.py", line 543, in _open
    '_open', req)
  File "/usr/local/Cellar/python/3.7.4/Frameworks/Python.framework/Versions/3.7/lib/python3.7/urllib/request.py", line 503, in _call_chain
    result = func(*args)
  File "/usr/local/Cellar/python/3.7.4/Frameworks/Python.framework/Versions/3.7/lib/python3.7/urllib/request.py", line 1360, in https_open
    context=self._context, check_hostname=self._check_hostname)
  File "/usr/local/Cellar/python/3.7.4/Frameworks/Python.framework/Versions/3.7/lib/python3.7/urllib/request.py", line 1317, in do_open
    encode_chunked=req.has_header('Transfer-encoding'))
  File "/usr/local/Cellar/python/3.7.4/Frameworks/Python.framework/Versions/3.7/lib/python3.7/http/client.py", line 1244, in request
    self._send_request(method, url, body, headers, encode_chunked)
  File "/usr/local/Cellar/python/3.7.4/Frameworks/Python.framework/Versions/3.7/lib/python3.7/http/client.py", line 1285, in _send_request
    self.putheader(hdr, value)
  File "/usr/local/Cellar/python/3.7.4/Frameworks/Python.framework/Versions/3.7/lib/python3.7/http/client.py", line 1222, in putheader
    raise ValueError('Invalid header value %r' % (values[i],))
ValueError: Invalid header value b'Basic <ConsumerKey>'
```
Alpine Crash "FileNotFoundError: [Errno 2] No such file or directory: 'tput'"
$ cat /etc/os-release
```
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.11.3
PRETTY_NAME="Alpine Linux v3.11"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://bugs.alpinelinux.org/"
```

$ asciinema rec
```
asciinema: recording asciicast to /tmp/tmpoa7wg6xn-ascii.cast
asciinema: press <ctrl-d> or type "exit" when you're done
Traceback (most recent call last):
  File "/usr/bin/asciinema", line 11, in <module>
    load_entry_point('asciinema==2.0.2', 'console_scripts', 'asciinema')()
  File "/usr/lib/python3.8/site-packages/asciinema/__main__.py", line 131, in main
    code = command.execute()
  File "/usr/lib/python3.8/site-packages/asciinema/commands/record.py", line 69, in execute
    asciinema.record_asciicast(
  File "/usr/lib/python3.8/site-packages/asciinema/__init__.py", line 33, in record_asciicast
    w, h = term.get_size()
  File "/usr/lib/python3.8/site-packages/asciinema/term.py", line 35, in get_size
    int(subprocess.check_output(['tput', 'cols'])),
  File "/usr/lib/python3.8/subprocess.py", line 411, in check_output
    return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
  File "/usr/lib/python3.8/subprocess.py", line 489, in run
    with Popen(*popenargs, **kwargs) as process:
  File "/usr/lib/python3.8/subprocess.py", line 854, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "/usr/lib/python3.8/subprocess.py", line 1702, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'tput'
```


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/asciinema.yml b/.github/workflows/asciinema.yml
new file mode 100644
index 000000000..ac35dc578
--- /dev/null
+++ b/.github/workflows/asciinema.yml
@@ -0,0 +1,44 @@
+name: build
+
+on: [push, pull_request]
+
+jobs:
+  # Code style checks
+  health:
+    name: Code health check
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout Asciinema
+        uses: actions/checkout@v2
+      - name: Setup Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: 3.9
+      - name: Install dependencies
+        run: pip install pycodestyle twine setuptools>=38.6.0 cmarkgfm
+      - name: Run pycodestyle
+        run: find . -name \*.py -exec pycodestyle --ignore=E501,E402,E722 {} +
+      - name: Run twine
+        run: |
+          python setup.py --quiet sdist
+          twine check dist/*
+  # Asciinema checks
+  asciinema:
+    name: Asciinema - py${{ matrix.python }}
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python: [3.6, 3.7, 3.8, 3.9]
+    env:
+      TERM: dumb
+    steps:
+      - name: Checkout Asciinema
+        uses: actions/checkout@v2
+      - name: Setup Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python }}
+      - name: Install dependencies
+        run: pip install nose
+      - name: Run Asciinema tests
+        run: script -e -c make test
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index e598a8f6e..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-sudo: required
-dist: xenial
-language: python
-
-python:
-  - "3.4"
-  - "3.5"
-  - "3.6"
-  - "3.7"
-  - "3.8-dev"
-
-before_install:
-  - pip install pycodestyle
-
-script:
-  - find . -name \*.py -exec pycodestyle --ignore=E501,E402 {} +
-  - make test
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f686650aa..0b7dacc86 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
 # asciinema changelog
 
+## 2.1.0 (2021-10-02)
+
+* Ability to pause/resume terminal capture with `C-\` key shortcut
+* Desktop notifications - only for the above pause feature at the moment
+* Removed dependency on tput/ncurses (thanks @arp242 / Martin Tournoij!)
+* ASCIINEMA_REC env var is back (thanks @landonb / Landon Bouma!)
+* Terminal answerbacks (CSI 6 n) in `asciinema cat` are now hidden (thanks @djpohly / Devin J. Pohly!)
+* Codeset detection works on HP-UX now (thanks @michael-o / Michael Osipov!)
+* Attempt at recording to existing file suggests use of `--overwrite` option now
+* Upload for users with very long `$USER` is fixed
+* Added official support for Python 3.8 and 3.9
+* Dropped official support for EOL-ed Python 3.4 and 3.5
+* Internal refactorings
+
 ## 2.0.2 (2019-01-12)
 
 * Official support for Python 3.7
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a7be73a8a..c7fb513b1 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -32,7 +32,9 @@ If you want to propose code change, either introducing a new feature or improvin
 ## Asking for help
 
 Github issue tracker is not a support forum.
-If you need help then either join #asciinema IRC channel on freenode or drop us an email at support@asciinema.org.
+
+If you need help then either join #asciinema IRC channel on libera.chat or drop
+us an email at support@asciinema.org.
 
 ## Reporting security issues
 
diff --git a/Dockerfile b/Dockerfile
index 0528a1b2d..cea3fe237 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM ubuntu:16.04
+FROM ubuntu:20.04
 
 RUN apt-get update && apt-get install -y \
     ca-certificates \
@@ -9,7 +9,9 @@ RUN localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8
 RUN mkdir /usr/src/app
 COPY setup.cfg /usr/src/app
 COPY setup.py /usr/src/app
-COPY README.md /usr/src/app
+COPY *.md /usr/src/app/
+COPY doc/*.md /usr/src/app/doc/
+COPY man/asciinema.1 /usr/src/app/man/
 COPY asciinema /usr/src/app/asciinema
 WORKDIR /usr/src/app
 RUN python3 setup.py install
@@ -17,4 +19,4 @@ ENV LANG en_US.utf8
 ENV SHELL /bin/bash
 ENV USER docker
 WORKDIR /root
-CMD ["asciinema", "rec"]
+ENTRYPOINT ["/usr/local/bin/asciinema"]
diff --git a/Makefile b/Makefile
index 9429656f3..747b7c28d 100644
--- a/Makefile
+++ b/Makefile
@@ -16,14 +16,16 @@ release-test: test push-test
 tag:
 	git tag | grep "v$(VERSION)" && echo "Tag v$(VERSION) exists" && exit 1 || true
 	git tag -s -m "Releasing $(VERSION)" v$(VERSION)
-	git push --tags
+	git push origin v$(VERSION)
 
 push:
-	python3 setup.py sdist upload -r pypi
+	python3 -m pip install --user --upgrade --quiet twine
+	python3 setup.py sdist bdist_wheel
+	python3 -m twine upload dist/*
 
 push-test:
-	python3 setup.py sdist upload -r pypitest
-
-release: test tag push
+	python3 -m pip install --user --upgrade --quiet twine
+	python3 setup.py sdist bdist_wheel
+	python3 -m twine upload --repository testpypi dist/*
 
 .PHONY: test test-unit test-integration release release-test tag push push-test
diff --git a/README.md b/README.md
index 10e1c96c1..ff47317cd 100644
--- a/README.md
+++ b/README.md
@@ -2,14 +2,14 @@ _Note: This is README for `development` branch. [See the version for latest stab
 
 # asciinema
 
-[![Build Status](https://travis-ci.org/asciinema/asciinema.svg?branch=develop)](https://travis-ci.org/asciinema/asciinema)
+[![Build Status](https://github.com/asciinema/asciinema/actions/workflows/asciinema.yml/badge.svg)](https://github.com/asciinema/asciinema/actions/workflows/asciinema.yml)
 [![PyPI](https://img.shields.io/pypi/v/asciinema.svg)](https://pypi.org/project/asciinema/)
 [![license](http://img.shields.io/badge/license-GNU-blue.svg)](https://raw.githubusercontent.com/asciinema/asciinema/master/LICENSE)
 
 Terminal session recorder and the best companion of
 [asciinema.org](https://asciinema.org).
 
-[![demo](https://asciinema.org/a/113463.svg)](https://asciinema.org/a/113463?autoplay=1)
+[![demo](https://asciinema.org/a/335480.svg)](https://asciinema.org/a/335480?autoplay=1)
 
 ## Quick intro
 
@@ -73,34 +73,10 @@ asciinema is included in repositories of most popular package managers on Mac OS
 X, Linux and FreeBSD. Look for package named `asciinema`. See the
 [list of available packages](https://asciinema.org/docs/installation).
 
-### Docker image
-
-asciinema Docker image is based on Ubuntu 16.04 and has the latest version of
-asciinema recorder pre-installed.
-
-    docker pull asciinema/asciinema
-
-When running it don't forget to allocate a pseudo-TTY (`-t`), keep STDIN open
-(`-i`) and mount config directory volume (`-v`):
-
-    docker run --rm -ti -v "$HOME/.config/asciinema":/root/.config/asciinema asciinema/asciinema
-
-Default command run in a container is `asciinema rec`.
-
-There's not much software installed in this image though. In most cases you may
-want to install extra programs before recording. One option is to derive new
-image from this one (start your custom Dockerfile with `FROM
-asciinema/asciinema`). Another option is to start the container with `/bin/bash`
-as the command, install extra packages and manually start `asciinema rec`:
-
-    docker run --rm -ti -v "$HOME/.config/asciinema":/root/.config/asciinema asciinema/asciinema /bin/bash
-    root@6689517d99a1:~# apt-get install foobar
-    root@6689517d99a1:~# asciinema rec
-
 ### Running latest version from source code checkout
 
-If none of the above works for you just clone the repo and run asciinema
-straight from the checkout.
+If you can't use Python package or native package for your OS is outdated you
+can clone the repo and run asciinema straight from the checkout.
 
 Clone the repo:
 
@@ -119,6 +95,32 @@ Then run it with:
 
     python3 -m asciinema --version
 
+### Docker image
+
+asciinema Docker image is based on Ubuntu 18.04 and has the latest version of
+asciinema recorder pre-installed.
+
+    docker pull asciinema/asciinema
+
+When running it don't forget to allocate a pseudo-TTY (`-t`), keep STDIN open
+(`-i`) and mount config directory volume (`-v`):
+
+    docker run --rm -ti -v "$HOME/.config/asciinema":/root/.config/asciinema asciinema/asciinema rec
+
+Container's entrypoint is set to `/usr/local/bin/asciinema` so you can run the
+container with any arguments you would normally pass to `asciinema` binary (see
+Usage section for commands and options).
+
+There's not much software installed in this image though. In most cases you may
+want to install extra programs before recording. One option is to derive new
+image from this one (start your custom Dockerfile with `FROM
+asciinema/asciinema`). Another option is to start the container with `/bin/bash`
+as the entrypoint, install extra packages and manually start `asciinema rec`:
+
+    docker run --rm -ti -v "$HOME/.config/asciinema":/root/.config/asciinema --entrypoint=/bin/bash asciinema/asciinema
+    root@6689517d99a1:~# apt-get install foobar
+    root@6689517d99a1:~# asciinema rec
+
 ## Usage
 
 asciinema is composed of multiple commands, similar to `git`, `apt-get` or
@@ -135,6 +137,13 @@ By running `asciinema rec [filename]` you start a new recording session. The
 command (process) that is recorded can be specified with `-c` option (see
 below), and defaults to `$SHELL` which is what you want in most cases.
 
+You can temporarily pause the capture of your terminal by pressing
+<kbd>Ctrl+\</kbd>.  This is useful when you want to execute some commands during
+the recording session that should not be captured (e.g. pasting secrets). Resume
+by pressing <kbd>Ctrl+\</kbd> again. When pausing desktop notification is
+displayed so you're sure the sensitive output won't be captured in the
+recording.
+
 Recording finishes when you exit the shell (hit <kbd>Ctrl+D</kbd> or type
 `exit`). If the recorded process is not a shell then recording finishes when
 the process exits.
@@ -181,12 +190,15 @@ __Replay recorded asciicast in a terminal.__
 This command replays given asciicast (as recorded by `rec` command) directly in
 your terminal.
 
-Following keyboard shortcuts are available:
+Following keyboard shortcuts are available by default:
 
 - <kbd>Space</kbd> - toggle pause,
 - <kbd>.</kbd> - step through a recording a frame at a time (when paused),
 - <kbd>Ctrl+C</kbd> - exit.
 
+See "Configuration file" section for information on how to customize the
+keyboard shortcuts.
+
 Playing from a local file:
 
     asciinema play /path/to/asciicast.cast
@@ -256,7 +268,7 @@ asked to log in first.
 Install ID is a random ID ([UUID
 v4](https://en.wikipedia.org/wiki/Universally_unique_identifier)) generated
 locally when you run asciinema for the first time, and saved at
-`$HOME/.config/asciinema/install-id`. It's purpose is to connect local machine
+`$HOME/.config/asciinema/install-id`. Its purpose is to connect local machine
 with uploaded recordings, so they can later be associated with asciinema.org
 account. This way we decouple uploading from account creation, allowing them to
 happen in any order.
@@ -284,7 +296,7 @@ If you prefer to host the recordings yourself, you can do so by either:
 - setting up your own
   [asciinema-server](https://github.com/asciinema/asciinema-server) instance,
   and [setting API URL
-  accordingly](https://github.com/asciinema/asciinema-server/blob/master/docs/INSTALL.md#using-asciinema-recorder-with-your-instance).
+  accordingly](https://github.com/asciinema/asciinema-server/wiki/Installation-guide#using-asciinema-recorder-with-your-instance).
 
 ## Configuration file
 
@@ -322,6 +334,14 @@ yes = true
 ; Be quiet, suppress all notices/warnings, default: no
 quiet = true
 
+; Define hotkey for pausing recording (suspending capture of output),
+; default: C-\ (control + backslash)
+pause_key = C-p
+
+; Define hotkey prefix key - when defined other recording hotkeys must
+; be preceeded by it, default: no prefix
+prefix_key = C-a
+
 [play]
 
 ; Playback speed (can be fractional), default: 1
@@ -329,6 +349,29 @@ speed = 2
 
 ; Limit replayed terminal inactivity to max n seconds, default: off
 idle_time_limit = 1
+
+; Define hotkey for pausing/resuming playback,
+; default: space
+pause_key = p
+
+; Define hotkey for stepping through playback, a frame at a time,
+; default: .
+step_key = ]
+
+[notifications]
+; Desktop notifications are displayed on certain occasions, e.g. when
+; pausing/resuming the capture of terminal with C-\ keyboard shortcut.
+
+; Should desktop notifications be enabled, default: yes
+enabled = no
+
+; Custom notification command
+; asciinema automatically detects available desktop notification system
+; (notify-send on GNU/Linux, osacript/terminal-notifier on macOS). Custom
+; command can be used if needed.
+; When invoked, environment variable $TEXT contains notification text, while
+; $ICON_PATH contains path to the asciinema logo image.
+command = tmux display-message "$TEXT"
 ```
 
 A very minimal config file could look like that:
@@ -359,6 +402,6 @@ source [contributors](https://github.com/asciinema/asciinema/contributors).
 
 ## License
 
-Copyright &copy; 2011–2018 Marcin Kulik.
+Copyright &copy; 2011–2021 Marcin Kulik.
 
 All code is licensed under the GPL, v3 or later. See LICENSE file for details.
diff --git a/asciinema/__init__.py b/asciinema/__init__.py
index 142d49ec1..ee1eea4eb 100644
--- a/asciinema/__init__.py
+++ b/asciinema/__init__.py
@@ -1,58 +1,25 @@
 import sys
 
 __author__ = 'Marcin Kulik'
-__version__ = '2.0.2'
+__version__ = '2.1.0'
 
 if sys.version_info[0] < 3:
     raise ImportError('Python < 3 is unsupported.')
 
-
-import os
-import subprocess
-import time
-
-import asciinema.asciicast.v2 as v2
-import asciinema.pty as pty
-import asciinema.term as term
+import asciinema.recorder
 
 
 def record_asciicast(path, command=None, append=False, idle_time_limit=None,
                      rec_stdin=False, title=None, metadata=None,
-                     command_env=None, capture_env=None, writer=v2.async_writer,
-                     record=pty.record):
-    if command is None:
-        command = os.environ.get('SHELL') or 'sh'
-
-    if command_env is None:
-        command_env = os.environ.copy()
-        command_env['ASCIINEMA_REC'] = '1'
-
-    if capture_env is None:
-        capture_env = ['SHELL', 'TERM']
-
-    w, h = term.get_size()
-
-    full_metadata = {
-        'width': w,
-        'height': h,
-        'timestamp': int(time.time())
-    }
-
-    full_metadata.update(metadata or {})
-
-    if idle_time_limit is not None:
-        full_metadata['idle_time_limit'] = idle_time_limit
-
-    if capture_env:
-        full_metadata['env'] = {var: command_env.get(var) for var in capture_env}
-
-    if title:
-        full_metadata['title'] = title
-
-    time_offset = 0
-
-    if append and os.stat(path).st_size > 0:
-        time_offset = v2.get_duration(path)
-
-    with writer(path, full_metadata, append, time_offset) as w:
-        record(['sh', '-c', command], w, command_env, rec_stdin)
+                     command_env=None, capture_env=None):
+    asciinema.recorder.record(
+        path,
+        command=command,
+        append=append,
+        idle_time_limit=idle_time_limit,
+        rec_stdin=rec_stdin,
+        title=title,
+        metadata=metadata,
+        command_env=command_env,
+        capture_env=capture_env
+    )
diff --git a/asciinema/__main__.py b/asciinema/__main__.py
index 3650881a1..73d715ea5 100644
--- a/asciinema/__main__.py
+++ b/asciinema/__main__.py
@@ -10,7 +10,6 @@
 from asciinema.commands.play import PlayCommand
 from asciinema.commands.cat import CatCommand
 from asciinema.commands.upload import UploadCommand
-from asciinema.api import Api
 
 
 def positive_float(value):
@@ -21,36 +20,13 @@ def positive_float(value):
     return value
 
 
-def rec_command(args, config):
-    api = Api(config.api_url, os.environ.get("USER"), config.install_id)
-    return RecordCommand(api, args)
-
-
-def play_command(args, config):
-    return PlayCommand(args.filename, args.idle_time_limit, args.speed)
-
-
-def cat_command(args, config):
-    return CatCommand(args.filename)
-
-
-def upload_command(args, config):
-    api = Api(config.api_url, os.environ.get("USER"), config.install_id)
-    return UploadCommand(api, args.filename)
-
-
-def auth_command(args, config):
-    api = Api(config.api_url, os.environ.get("USER"), config.install_id)
-    return AuthCommand(api)
-
-
 def maybe_str(v):
     if v is not None:
         return str(v)
 
 
 def main():
-    if locale.nl_langinfo(locale.CODESET).upper() not in ['US-ASCII', 'UTF-8']:
+    if locale.nl_langinfo(locale.CODESET).upper() not in ['US-ASCII', 'UTF-8', 'UTF8']:
         print("asciinema needs an ASCII or UTF-8 character encoding to run. Check the output of `locale` command.")
         sys.exit(1)
 
@@ -100,34 +76,34 @@ def main():
     parser_rec.add_argument('-y', '--yes', help='answer "yes" to all prompts (e.g. upload confirmation)', action='store_true', default=cfg.record_yes)
     parser_rec.add_argument('-q', '--quiet', help='be quiet, suppress all notices/warnings (implies -y)', action='store_true', default=cfg.record_quiet)
     parser_rec.add_argument('filename', nargs='?', default='', help='filename/path to save the recording to')
-    parser_rec.set_defaults(func=rec_command)
+    parser_rec.set_defaults(cmd=RecordCommand)
 
     # create the parser for the "play" command
     parser_play = subparsers.add_parser('play', help='Replay terminal session')
     parser_play.add_argument('-i', '--idle-time-limit', help='limit idle time during playback to given number of seconds', type=positive_float, default=maybe_str(cfg.play_idle_time_limit))
     parser_play.add_argument('-s', '--speed', help='playback speedup (can be fractional)', type=positive_float, default=cfg.play_speed)
     parser_play.add_argument('filename', help='local path, http/ipfs URL or "-" (read from stdin)')
-    parser_play.set_defaults(func=play_command)
+    parser_play.set_defaults(cmd=PlayCommand)
 
     # create the parser for the "cat" command
     parser_cat = subparsers.add_parser('cat', help='Print full output of terminal session')
     parser_cat.add_argument('filename', help='local path, http/ipfs URL or "-" (read from stdin)')
-    parser_cat.set_defaults(func=cat_command)
+    parser_cat.set_defaults(cmd=CatCommand)
 
     # create the parser for the "upload" command
     parser_upload = subparsers.add_parser('upload', help='Upload locally saved terminal session to asciinema.org')
     parser_upload.add_argument('filename', help='filename or path of local recording')
-    parser_upload.set_defaults(func=upload_command)
+    parser_upload.set_defaults(cmd=UploadCommand)
 
     # create the parser for the "auth" command
     parser_auth = subparsers.add_parser('auth', help='Manage recordings on asciinema.org account')
-    parser_auth.set_defaults(func=auth_command)
+    parser_auth.set_defaults(cmd=AuthCommand)
 
     # parse the args and call whatever function was selected
     args = parser.parse_args()
 
-    if hasattr(args, 'func'):
-        command = args.func(args, cfg)
+    if hasattr(args, 'cmd'):
+        command = args.cmd(args, cfg, os.environ)
         code = command.execute()
         sys.exit(code)
     else:
diff --git a/asciinema/asciicast/raw.py b/asciinema/asciicast/raw.py
index e68db975a..1d5dc86cf 100644
--- a/asciinema/asciicast/raw.py
+++ b/asciinema/asciicast/raw.py
@@ -1,39 +1,25 @@
 import os
-from multiprocessing import Process, Queue
-
-
-def write_bytes_from_queue(path, mode, queue):
-    mode = mode + 'b'
-
-    with open(path, mode=mode, buffering=0) as f:
-        for data in iter(queue.get, None):
-            f.write(data)
 
 
 class writer():
 
-    def __init__(self, path, _metadata, append=False, _time_offset=0):
+    def __init__(self, path, metadata=None, append=False, buffering=0):
         if append and os.path.exists(path) and os.stat(path).st_size == 0:  # true for pipes
             append = False
 
         self.path = path
-        self.mode = 'a' if append else 'w'
-        self.queue = Queue()
+        self.buffering = buffering
+        self.mode = 'ab' if append else 'wb'
 
     def __enter__(self):
-        self.process = Process(
-            target=write_bytes_from_queue,
-            args=(self.path, self.mode, self.queue)
-        )
-        self.process.start()
+        self.file = open(self.path, mode=self.mode, buffering=self.buffering)
         return self
 
     def __exit__(self, exc_type, exc_value, exc_traceback):
-        self.queue.put(None)
-        self.process.join()
+        self.file.close()
 
-    def write_stdin(self, data):
-        pass
+    def write_stdout(self, ts, data):
+        self.file.write(data)
 
-    def write_stdout(self, data):
-        self.queue.put(data)
+    def write_stdin(self, ts, data):
+        pass
diff --git a/asciinema/asciicast/v2.py b/asciinema/asciicast/v2.py
index 34eb8cbbf..2c33cbf2d 100644
--- a/asciinema/asciicast/v2.py
+++ b/asciinema/asciicast/v2.py
@@ -3,17 +3,6 @@
 import time
 import codecs
 
-try:
-    # Importing synchronize is to detect platforms where
-    # multiprocessing does not work (python issue 3770)
-    # and cause an ImportError. Otherwise it will happen
-    # later when trying to use Queue().
-    from multiprocessing import synchronize, Process, Queue
-except ImportError:
-    from threading import Thread as Process
-    from queue import Queue
-
-
 try:
     JSONDecodeError = json.decoder.JSONDecodeError
 except AttributeError:
@@ -76,10 +65,9 @@ def get_duration(path):
             return last_frame[0]
 
 
-def build_header(metadata):
-    header = {}
+def build_header(width, height, metadata):
+    header = {'version': 2, 'width': width, 'height': height}
     header.update(metadata)
-    header['version'] = 2
 
     assert 'width' in header, 'width missing in metadata'
     assert 'height' in header, 'height missing in metadata'
@@ -94,20 +82,18 @@ def build_header(metadata):
 
 class writer():
 
-    def __init__(self, path, width=None, height=None, header=None, mode='w', buffering=-1):
+    def __init__(self, path, metadata=None, append=False, buffering=1, width=None, height=None):
         self.path = path
-        self.mode = mode
         self.buffering = buffering
         self.stdin_decoder = codecs.getincrementaldecoder('UTF-8')('replace')
         self.stdout_decoder = codecs.getincrementaldecoder('UTF-8')('replace')
 
-        if mode == 'w':
-            self.header = {'version': 2, 'width': width, 'height': height}
-            self.header.update(header or {})
-            assert type(self.header['width']) == int, 'width or header missing'
-            assert type(self.header['height']) == int, 'height or header missing'
-        else:
+        if append:
+            self.mode = 'a'
             self.header = None
+        else:
+            self.mode = 'w'
+            self.header = build_header(width, height, metadata or {})
 
     def __enter__(self):
         self.file = open(self.path, mode=self.mode, buffering=self.buffering)
@@ -120,73 +106,21 @@ def __enter__(self):
     def __exit__(self, exc_type, exc_value, exc_traceback):
         self.file.close()
 
-    def write_event(self, ts, etype=None, data=None):
-        if etype is None:
-            ts, etype, data = ts
-
-        ts = round(ts, 6)
-
-        if etype == 'o':
-            if type(data) == str:
-                data = data.encode(encoding='utf-8', errors='strict')
-            text = self.stdout_decoder.decode(data)
-            self.__write_line([ts, etype, text])
-        elif etype == 'i':
-            if type(data) == str:
-                data = data.encode(encoding='utf-8', errors='strict')
-            text = self.stdin_decoder.decode(data)
-            self.__write_line([ts, etype, text])
-        else:
-            self.__write_line([ts, etype, data])
-
     def write_stdout(self, ts, data):
-        self.write_event(ts, 'o', data)
+        if type(data) == str:
+            data = data.encode(encoding='utf-8', errors='strict')
+        data = self.stdout_decoder.decode(data)
+        self.__write_event(ts, 'o', data)
 
     def write_stdin(self, ts, data):
-        self.write_event(ts, 'i', data)
+        if type(data) == str:
+            data = data.encode(encoding='utf-8', errors='strict')
+        data = self.stdin_decoder.decode(data)
+        self.__write_event(ts, 'i', data)
+
+    def __write_event(self, ts, etype, data):
+        self.__write_line([round(ts, 6), etype, data])
 
     def __write_line(self, obj):
         line = json.dumps(obj, ensure_ascii=False, indent=None, separators=(', ', ': '))
         self.file.write(line + '\n')
-
-
-def write_json_lines_from_queue(path, header, mode, queue):
-    with writer(path, header=header, mode=mode, buffering=1) as w:
-        for event in iter(queue.get, None):
-            w.write_event(event)
-
-
-class async_writer():
-
-    def __init__(self, path, metadata, append=False, time_offset=0):
-        if append:
-            assert time_offset > 0
-
-        self.path = path
-        self.metadata = metadata
-        self.append = append
-        self.time_offset = time_offset
-        self.queue = Queue()
-
-    def __enter__(self):
-        header = build_header(self.metadata)
-        mode = 'a' if self.append else 'w'
-        self.process = Process(
-            target=write_json_lines_from_queue,
-            args=(self.path, header, mode, self.queue)
-        )
-        self.process.start()
-        self.start_time = time.time() - self.time_offset
-        return self
-
-    def __exit__(self, exc_type, exc_value, exc_traceback):
-        self.queue.put(None)
-        self.process.join()
-
-    def write_stdin(self, data):
-        ts = time.time() - self.start_time
-        self.queue.put([ts, 'i', data])
-
-    def write_stdout(self, data):
-        ts = time.time() - self.start_time
-        self.queue.put([ts, 'o', data])
diff --git a/asciinema/async_worker.py b/asciinema/async_worker.py
new file mode 100644
index 000000000..88ad995df
--- /dev/null
+++ b/asciinema/async_worker.py
@@ -0,0 +1,31 @@
+try:
+    # Importing synchronize is to detect platforms where
+    # multiprocessing does not work (python issue 3770)
+    # and cause an ImportError. Otherwise it will happen
+    # later when trying to use Queue().
+    from multiprocessing import synchronize, Process, Queue
+except ImportError:
+    from threading import Thread as Process
+    from queue import Queue
+
+
+class async_worker():
+
+    def __init__(self):
+        self.queue = Queue()
+
+    def __enter__(self):
+        self.process = Process(target=self.run)
+        self.process.start()
+        return self
+
+    def __exit__(self, exc_type, exc_value, exc_traceback):
+        self.queue.put(None)
+        self.process.join()
+
+    def enqueue(self, payload):
+        self.queue.put(payload)
+
+    def run(self):
+        for payload in iter(self.queue.get, None):
+            self.perform(payload)
diff --git a/asciinema/commands/auth.py b/asciinema/commands/auth.py
index 1fab7f6bf..cc435ce67 100644
--- a/asciinema/commands/auth.py
+++ b/asciinema/commands/auth.py
@@ -3,9 +3,8 @@
 
 class AuthCommand(Command):
 
-    def __init__(self, api):
-        Command.__init__(self)
-        self.api = api
+    def __init__(self, args, config, env):
+        Command.__init__(self, args, config, env)
 
     def execute(self):
         self.print('Open the following URL in a web browser to link your '
diff --git a/asciinema/commands/cat.py b/asciinema/commands/cat.py
index 2fe285af1..fe12c9020 100644
--- a/asciinema/commands/cat.py
+++ b/asciinema/commands/cat.py
@@ -1,21 +1,24 @@
 import sys
 
 from asciinema.commands.command import Command
+from asciinema.term import raw
 import asciinema.asciicast as asciicast
 
 
 class CatCommand(Command):
 
-    def __init__(self, filename):
-        Command.__init__(self)
-        self.filename = filename
+    def __init__(self, args, config, env):
+        Command.__init__(self, args, config, env)
+        self.filename = args.filename
 
     def execute(self):
         try:
-            with asciicast.open_from_url(self.filename) as a:
-                for t, _type, text in a.stdout_events():
-                    sys.stdout.write(text)
-                    sys.stdout.flush()
+            stdin = open('/dev/tty')
+            with raw(stdin.fileno()):
+                with asciicast.open_from_url(self.filename) as a:
+                    for t, _type, text in a.stdout_events():
+                        sys.stdout.write(text)
+                        sys.stdout.flush()
 
         except asciicast.LoadError as e:
             self.print_error("printing failed: %s" % str(e))
diff --git a/asciinema/commands/command.py b/asciinema/commands/command.py
index bd4260ebd..3ba5ad0e2 100644
--- a/asciinema/commands/command.py
+++ b/asciinema/commands/command.py
@@ -1,10 +1,13 @@
 import sys
 
+from asciinema.api import Api
+
 
 class Command:
 
-    def __init__(self, quiet=False):
-        self.quiet = quiet
+    def __init__(self, args, config, env):
+        self.quiet = False
+        self.api = Api(config.api_url, env.get("USER"), config.install_id)
 
     def print(self, text, file=sys.stdout, end="\n", force=False):
         if not self.quiet or force:
diff --git a/asciinema/commands/play.py b/asciinema/commands/play.py
index 4998d6711..dd5adf367 100644
--- a/asciinema/commands/play.py
+++ b/asciinema/commands/play.py
@@ -5,17 +5,21 @@
 
 class PlayCommand(Command):
 
-    def __init__(self, filename, idle_time_limit, speed, player=None):
-        Command.__init__(self)
-        self.filename = filename
-        self.idle_time_limit = idle_time_limit
-        self.speed = speed
+    def __init__(self, args, config, env, player=None):
+        Command.__init__(self, args, config, env)
+        self.filename = args.filename
+        self.idle_time_limit = args.idle_time_limit
+        self.speed = args.speed
         self.player = player if player is not None else Player()
+        self.key_bindings = {
+            'pause': config.play_pause_key,
+            'step': config.play_step_key
+        }
 
     def execute(self):
         try:
             with asciicast.open_from_url(self.filename) as a:
-                self.player.play(a, self.idle_time_limit, self.speed)
+                self.player.play(a, self.idle_time_limit, self.speed, self.key_bindings)
 
         except asciicast.LoadError as e:
             self.print_error("playback failed: %s" % str(e))
diff --git a/asciinema/commands/record.py b/asciinema/commands/record.py
index 6f61ffa7f..b8f9423e6 100644
--- a/asciinema/commands/record.py
+++ b/asciinema/commands/record.py
@@ -2,18 +2,19 @@
 import sys
 import tempfile
 
-import asciinema
+import asciinema.recorder as recorder
 import asciinema.asciicast.raw as raw
 import asciinema.asciicast.v2 as v2
+import asciinema.notifier as notifier
 from asciinema.api import APIError
 from asciinema.commands.command import Command
 
 
 class RecordCommand(Command):
 
-    def __init__(self, api, args, env=None):
-        Command.__init__(self, args.quiet)
-        self.api = api
+    def __init__(self, args, config, env):
+        Command.__init__(self, args, config, env)
+        self.quiet = args.quiet
         self.filename = args.filename
         self.rec_stdin = args.stdin
         self.command = args.command
@@ -24,8 +25,13 @@ def __init__(self, api, args, env=None):
         self.append = args.append
         self.overwrite = args.overwrite
         self.raw = args.raw
-        self.writer = raw.writer if args.raw else v2.async_writer
-        self.env = env if env is not None else os.environ
+        self.writer = raw.writer if args.raw else v2.writer
+        self.notifier = notifier.get_notifier(config.notifications_enabled, config.notifications_command)
+        self.env = env
+        self.key_bindings = {
+            'prefix': config.record_prefix_key,
+            'pause': config.record_pause_key
+        }
 
     def execute(self):
         upload = False
@@ -50,6 +56,7 @@ def execute(self):
 
             elif os.stat(self.filename).st_size > 0 and not append:
                 self.print_error("%s already exists, aborting" % self.filename)
+                self.print_error("use --overwrite option if you want to overwrite existing recording")
                 self.print_error("use --append option if you want to append to existing recording")
                 return 1
 
@@ -66,7 +73,7 @@ def execute(self):
         vars = filter(None, map((lambda var: var.strip()), self.env_whitelist.split(',')))
 
         try:
-            asciinema.record_asciicast(
+            recorder.record(
                 self.filename,
                 command=self.command,
                 append=append,
@@ -75,7 +82,9 @@ def execute(self):
                 command_env=self.env,
                 capture_env=vars,
                 rec_stdin=self.rec_stdin,
-                writer=self.writer
+                writer=self.writer,
+                notifier=self.notifier,
+                key_bindings=self.key_bindings
             )
         except v2.LoadError:
             self.print_error("can only append to asciicast v2 format recordings")
diff --git a/asciinema/commands/upload.py b/asciinema/commands/upload.py
index 7ca25dc03..897ecb532 100644
--- a/asciinema/commands/upload.py
+++ b/asciinema/commands/upload.py
@@ -4,10 +4,9 @@
 
 class UploadCommand(Command):
 
-    def __init__(self, api, filename):
-        Command.__init__(self)
-        self.api = api
-        self.filename = filename
+    def __init__(self, args, config, env):
+        Command.__init__(self, args, config, env)
+        self.filename = args.filename
 
     def execute(self):
         try:
diff --git a/asciinema/config.py b/asciinema/config.py
index 62f3e17b8..75703c016 100644
--- a/asciinema/config.py
+++ b/asciinema/config.py
@@ -109,6 +109,14 @@ def record_yes(self):
     def record_quiet(self):
         return self.config.getboolean('record', 'quiet', fallback=False)
 
+    @property
+    def record_prefix_key(self):
+        return self.__get_key('record', 'prefix')
+
+    @property
+    def record_pause_key(self):
+        return self.__get_key('record', 'pause', 'C-\\')
+
     @property
     def play_idle_time_limit(self):
         fallback = self.config.getfloat('play', 'maxwait', fallback=None)  # pre 2.0
@@ -118,6 +126,36 @@ def play_idle_time_limit(self):
     def play_speed(self):
         return self.config.getfloat('play', 'speed', fallback=1.0)
 
+    @property
+    def play_pause_key(self):
+        return self.__get_key('play', 'pause', ' ')
+
+    @property
+    def play_step_key(self):
+        return self.__get_key('play', 'step', '.')
+
+    @property
+    def notifications_enabled(self):
+        return self.config.getboolean('notifications', 'enabled', fallback=True)
+
+    @property
+    def notifications_command(self):
+        return self.config.get('notifications', 'command', fallback=None)
+
+    def __get_key(self, section, name, default=None):
+        key = self.config.get(section, name + '_key', fallback=default)
+
+        if key:
+            if len(key) == 3:
+                upper_key = key.upper()
+
+                if upper_key[0] == 'C' and upper_key[1] == '-':
+                    return bytes([ord(upper_key[2]) - 0x40])
+                else:
+                    raise ConfigError('invalid {name} key definition \'{key}\' - use: {name}_key = C-x (with control key modifier), or {name}_key = x (with no modifier)'.format(name=name, key=key))
+            else:
+                return key.encode('utf-8')
+
 
 def get_config_home(env=os.environ):
     env_asciinema_config_home = env.get("ASCIINEMA_CONFIG_HOME")
diff --git a/asciinema/data/icon-256x256.png b/asciinema/data/icon-256x256.png
new file mode 100644
index 000000000..a0885aa8b
Binary files /dev/null and b/asciinema/data/icon-256x256.png differ
diff --git a/asciinema/notifier.py b/asciinema/notifier.py
new file mode 100644
index 000000000..124051193
--- /dev/null
+++ b/asciinema/notifier.py
@@ -0,0 +1,83 @@
+import os.path
+import shutil
+import subprocess
+
+
+class Notifier():
+    def is_available(self):
+        return shutil.which(self.cmd) is not None
+
+    def notify(self, text):
+        subprocess.run(self.args(text), capture_output=True)
+        # we don't want to print *ANYTHING* to the terminal
+        # so we capture and ignore all output
+
+    def get_icon_path(self):
+        path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data/icon-256x256.png")
+
+        if os.path.exists(path):
+            return path
+
+
+class AppleScriptNotifier(Notifier):
+    cmd = "osascript"
+
+    def args(self, text):
+        text = text.replace('"', '\\"')
+        return ['osascript', '-e', 'display notification "{}" with title "asciinema"'.format(text)]
+
+
+class LibNotifyNotifier(Notifier):
+    cmd = "notify-send"
+
+    def args(self, text):
+        icon_path = self.get_icon_path()
+
+        if icon_path is not None:
+            return ['notify-send', '-i', icon_path, 'asciinema', text]
+        else:
+            return ['notify-send', 'asciinema', text]
+
+
+class TerminalNotifier(Notifier):
+    cmd = "terminal-notifier"
+
+    def args(self, text):
+        icon_path = self.get_icon_path()
+
+        if icon_path is not None:
+            return ['terminal-notifier', '-title', 'asciinema', '-message', text, '-appIcon', icon_path]
+        else:
+            return ['terminal-notifier', '-title', 'asciinema', '-message', text]
+
+
+class CustomCommandNotifier(Notifier):
+    def __init__(self, command):
+        Notifier.__init__(self)
+        self.command = command
+
+    def notify(self, text):
+        args = ['/bin/sh', '-c', self.command]
+        env = os.environ.copy()
+        env['TEXT'] = text
+        env['ICON_PATH'] = self.get_icon_path()
+        subprocess.run(args, env=env, capture_output=True)
+
+
+class NoopNotifier():
+    def notify(self, text):
+        pass
+
+
+def get_notifier(enabled=True, command=None):
+    if enabled:
+        if command:
+            return CustomCommandNotifier(command)
+        else:
+            for c in [TerminalNotifier, AppleScriptNotifier, LibNotifyNotifier]:
+                n = c()
+
+                if n.is_available():
+                    return n
+
+    return NoopNotifier()
diff --git a/asciinema/player.py b/asciinema/player.py
index e81b5019b..3670a8d7e 100644
--- a/asciinema/player.py
+++ b/asciinema/player.py
@@ -8,16 +8,18 @@
 
 class Player:
 
-    def play(self, asciicast, idle_time_limit=None, speed=1.0):
+    def play(self, asciicast, idle_time_limit=None, speed=1.0, key_bindings={}):
         try:
             stdin = open('/dev/tty')
             with raw(stdin.fileno()):
-                self._play(asciicast, idle_time_limit, speed, stdin)
+                self._play(asciicast, idle_time_limit, speed, stdin, key_bindings)
         except Exception:
-            self._play(asciicast, idle_time_limit, speed, None)
+            self._play(asciicast, idle_time_limit, speed, None, key_bindings)
 
-    def _play(self, asciicast, idle_time_limit, speed, stdin):
+    def _play(self, asciicast, idle_time_limit, speed, stdin, key_bindings):
         idle_time_limit = idle_time_limit or asciicast.idle_time_limit
+        pause_key = key_bindings.get('pause')
+        step_key = key_bindings.get('step')
 
         stdout = asciicast.stdout_events()
         stdout = ev.to_relative_time(stdout)
@@ -42,12 +44,12 @@ def _play(self, asciicast, idle_time_limit, speed, stdin):
                             ctrl_c = True
                             break
 
-                        if 0x20 in data:  # space
+                        if data == pause_key:
                             paused = False
                             base_time = base_time + (time.time() - pause_time)
                             break
 
-                        if 0x2e in data:  # period (dot)
+                        if data == step_key:
                             delay = 0
                             pause_time = time.time()
                             base_time = pause_time - t
@@ -62,7 +64,7 @@ def _play(self, asciicast, idle_time_limit, speed, stdin):
                         ctrl_c = True
                         break
 
-                    if 0x20 in data:  # space
+                    if data == pause_key:
                         paused = True
                         pause_time = time.time()
                         slept = t - (pause_time - base_time)
diff --git a/asciinema/pty.py b/asciinema/pty.py
index 9e731cca0..e5d66a632 100644
--- a/asciinema/pty.py
+++ b/asciinema/pty.py
@@ -10,12 +10,22 @@
 import struct
 import sys
 import termios
+import time
 
 from asciinema.term import raw
 
 
-def record(command, writer, env=os.environ, rec_stdin=False):
+def record(command, writer, env=os.environ, rec_stdin=False, time_offset=0, notifier=None, key_bindings={}):
     master_fd = None
+    start_time = None
+    pause_time = None
+    prefix_mode = False
+    prefix_key = key_bindings.get('prefix')
+    pause_key = key_bindings.get('pause')
+
+    def _notify(text):
+        if notifier:
+            notifier.notify(text)
 
     def _set_pty_size():
         '''
@@ -40,7 +50,9 @@ def _write_stdout(data):
     def _handle_master_read(data):
         '''Handles new data on child process stdout.'''
 
-        writer.write_stdout(data)
+        if not pause_time:
+            writer.write_stdout(time.time() - start_time, data)
+
         _write_stdout(data)
 
     def _write_master(data):
@@ -53,10 +65,32 @@ def _write_master(data):
     def _handle_stdin_read(data):
         '''Handles new data on child process stdin.'''
 
+        nonlocal pause_time
+        nonlocal start_time
+        nonlocal prefix_mode
+
+        if not prefix_mode and prefix_key and data == prefix_key:
+            prefix_mode = True
+            return
+
+        if prefix_mode or (not prefix_key and data in [pause_key]):
+            prefix_mode = False
+
+            if data == pause_key:
+                if pause_time:
+                    start_time = start_time + (time.time() - pause_time)
+                    pause_time = None
+                    _notify('Resumed recording')
+                else:
+                    pause_time = time.time()
+                    _notify('Paused recording')
+
+            return
+
         _write_master(data)
 
-        if rec_stdin:
-            writer.write_stdin(data)
+        if rec_stdin and not pause_time:
+            writer.write_stdin(time.time() - start_time, data)
 
     def _signals(signal_list):
         old_handlers = []
@@ -129,6 +163,8 @@ def _copy(signal_fd):
 
     _set_pty_size()
 
+    start_time = time.time() - time_offset
+
     with raw(pty.STDIN_FILENO):
         try:
             _copy(pipe_r)
diff --git a/asciinema/recorder.py b/asciinema/recorder.py
new file mode 100644
index 000000000..1c02d4971
--- /dev/null
+++ b/asciinema/recorder.py
@@ -0,0 +1,102 @@
+import os
+import time
+
+import asciinema.asciicast.v2 as v2
+import asciinema.pty as pty
+import asciinema.term as term
+from asciinema.async_worker import async_worker
+
+
+def record(path, command=None, append=False, idle_time_limit=None,
+           rec_stdin=False, title=None, metadata=None, command_env=None,
+           capture_env=None, writer=v2.writer, record=pty.record, notifier=None,
+           key_bindings={}):
+    if command is None:
+        command = os.environ.get('SHELL') or 'sh'
+
+    if command_env is None:
+        command_env = os.environ.copy()
+
+    command_env['ASCIINEMA_REC'] = '1'
+
+    if capture_env is None:
+        capture_env = ['SHELL', 'TERM']
+
+    w, h = term.get_size()
+
+    full_metadata = {
+        'width': w,
+        'height': h,
+        'timestamp': int(time.time())
+    }
+
+    full_metadata.update(metadata or {})
+
+    if idle_time_limit is not None:
+        full_metadata['idle_time_limit'] = idle_time_limit
+
+    if capture_env:
+        full_metadata['env'] = {var: command_env.get(var) for var in capture_env}
+
+    if title:
+        full_metadata['title'] = title
+
+    time_offset = 0
+
+    if append and os.stat(path).st_size > 0:
+        time_offset = v2.get_duration(path)
+
+    with async_writer(writer, path, full_metadata, append) as w:
+        with async_notifier(notifier) as n:
+            record(
+                ['sh', '-c', command],
+                w,
+                command_env,
+                rec_stdin,
+                time_offset,
+                n,
+                key_bindings
+            )
+
+
+class async_writer(async_worker):
+    def __init__(self, writer, path, metadata, append=False):
+        async_worker.__init__(self)
+        self.writer = writer
+        self.path = path
+        self.metadata = metadata
+        self.append = append
+
+    def write_stdin(self, ts, data):
+        self.enqueue([ts, 'i', data])
+
+    def write_stdout(self, ts, data):
+        self.enqueue([ts, 'o', data])
+
+    def run(self):
+        with self.writer(self.path, metadata=self.metadata, append=self.append) as w:
+            for event in iter(self.queue.get, None):
+                ts, etype, data = event
+
+                if etype == 'o':
+                    w.write_stdout(ts, data)
+                elif etype == 'i':
+                    w.write_stdin(ts, data)
+
+
+class async_notifier(async_worker):
+    def __init__(self, notifier):
+        async_worker.__init__(self)
+        self.notifier = notifier
+
+    def notify(self, text):
+        self.enqueue(text)
+
+    def perform(self, text):
+        try:
+            if self.notifier:
+                self.notifier.notify(text)
+        except:
+            # we catch *ALL* exceptions here because we don't want failed
+            # notification to crash the recording session
+            pass
diff --git a/asciinema/term.py b/asciinema/term.py
index 49858ce4c..f3b54fb9f 100644
--- a/asciinema/term.py
+++ b/asciinema/term.py
@@ -1,6 +1,7 @@
 import os
 import select
 import subprocess
+import time
 import tty
 
 
@@ -19,6 +20,8 @@ def __enter__(self):
 
     def __exit__(self, type, value, traceback):
         if self.restore:
+            # Give the terminal time to send answerbacks
+            time.sleep(0.01)
             tty.tcsetattr(self.fd, tty.TCSAFLUSH, self.mode)
 
 
@@ -30,8 +33,10 @@ def read_blocking(fd, timeout):
 
 
 def get_size():
-    # TODO maybe use os.get_terminal_size ?
-    return (
-        int(subprocess.check_output(['tput', 'cols'])),
-        int(subprocess.check_output(['tput', 'lines']))
-    )
+    try:
+        return os.get_terminal_size()
+    except:
+        return (
+            int(subprocess.check_output(['tput', 'cols'])),
+            int(subprocess.check_output(['tput', 'lines']))
+        )
diff --git a/asciinema/urllib_http_adapter.py b/asciinema/urllib_http_adapter.py
index 2d1a5c3ac..796a3cf09 100644
--- a/asciinema/urllib_http_adapter.py
+++ b/asciinema/urllib_http_adapter.py
@@ -67,7 +67,7 @@ def post(self, url, fields={}, files={}, headers={}, username=None, password=Non
 
         if password:
             auth = "%s:%s" % (username, password)
-            encoded_auth = base64.encodebytes(auth.encode('utf-8'))[:-1]
+            encoded_auth = base64.b64encode(bytes(auth, "utf-8"))
             headers["Authorization"] = b"Basic " + encoded_auth
 
         request = Request(url, data=body, headers=headers, method="POST")
diff --git a/man/Makefile b/man/Makefile
index 67d783a82..21c46cbbb 100644
--- a/man/Makefile
+++ b/man/Makefile
@@ -1,2 +1,5 @@
+VERSION=`python3 -c "import asciinema; print(asciinema.__version__)"`
+
 asciinema.1: asciinema.1.md
-	pandoc asciinema.1.md -s -t man -o asciinema.1
+	pandoc asciinema.1.md -s -t man -o asciinema.1 -V header:"Version $(VERSION), `date +%Y-%m-%d`"
+
diff --git a/man/asciinema.1.md b/man/asciinema.1.md
index 9666d9d93..2687c087f 100644
--- a/man/asciinema.1.md
+++ b/man/asciinema.1.md
@@ -1,21 +1,27 @@
 % ASCIINEMA(1) Version 2.0.1 | asciinema
 
+
 NAME
 ====
 
 **asciinema** - terminal session recorder
 
+
 SYNOPSIS
 ========
 
 | **asciinema \-\-version**
 | **asciinema** _command_ \[_options_] \[_args_]
 
+
 DESCRIPTION
 ===========
 
-asciinema lets you easily record terminal sessions and replay
-them in a terminal as well as in a web browser.
+asciinema lets you easily record terminal sessions, replay
+them in a terminal as well as in a web browser and share them on the web.
+asciinema is Free and Open Source Software licensed under
+the GNU General Public License v3.
+
 
 COMMANDS
 ========
@@ -23,9 +29,10 @@ COMMANDS
 asciinema is composed of multiple commands, similar to `git`, `apt-get` or
 `brew`.
 
-When you run **asciinema** with no arguments help message is displayed, listing
+When you run **asciinema** with no arguments a help message is displayed, listing
 all available commands with their options.
 
+
 rec [_filename_]
 ---
 
@@ -35,6 +42,13 @@ By running **asciinema rec [filename]** you start a new recording session. The
 command (process) that is recorded can be specified with **-c** option (see
 below), and defaults to **$SHELL** which is what you want in most cases.
 
+You can temporarily pause the capture of your terminal by pressing
+<kbd>Ctrl+\</kbd>.  This is useful when you want to execute some commands during
+the recording session that should not be captured (e.g. pasting secrets). Resume
+by pressing <kbd>Ctrl+\</kbd> again. When pausing desktop notification is
+displayed so you're sure the sensitive output won't be captured in the
+recording.
+
 Recording finishes when you exit the shell (hit <kbd>Ctrl+D</kbd> or type
 `exit`). If the recorded process is not a shell then recording finishes when
 the process exits.
@@ -55,37 +69,37 @@ prompt or play a sound when the shell is being recorded.
 
 Available options:
 
-: &nbsp;
+:   &nbsp;
 
     `--stdin`
-    : Enable stdin (keyboard) recording (see below)
+    :   Enable stdin (keyboard) recording (see below)
 
     `--append`
-    : Append to existing recording
+    :   Append to existing recording
 
     `--raw`
-    : Save raw STDOUT output, without timing information or other metadata
+    :   Save raw STDOUT output, without timing information or other metadata
 
     `--overwrite`
-    : Overwrite the recording if it already exists
+    :   Overwrite the recording if it already exists
 
     `-c, --command=<command>`
-    : Specify command to record, defaults to **$SHELL**
+    :   Specify command to record, defaults to **$SHELL**
 
     `-e, --env=<var-names>`
-    : List of environment variables to capture, defaults to **SHELL,TERM**
+    :   List of environment variables to capture, defaults to **SHELL,TERM**
 
     `-t, --title=<title>`
-    : Specify the title of the asciicast
+    :   Specify the title of the asciicast
 
     `-i, --idle-time-limit=<sec>`
-    : Limit recorded terminal inactivity to max `<sec>` seconds
+    :   Limit recorded terminal inactivity to max `<sec>` seconds
 
     `-y, --yes`
-    : Answer "yes" to all prompts (e.g. upload confirmation)
+    :   Answer "yes" to all prompts (e.g. upload confirmation)
 
     `-q, --quiet`
-    : Be quiet, suppress all notices/warnings (implies **-y**)
+    :   Be quiet, suppress all notices/warnings (implies **-y**)
 
 Stdin recording allows for capturing of all characters typed in by the user in
 the currently recorded shell. This may be used by a player (e.g.
@@ -94,19 +108,14 @@ pressed keys. Because it's basically a key-logging (scoped to a single shell
 instance), it's disabled by default, and has to be explicitly enabled via
 **--stdin** option.
 
+
 play <_filename_>
 ---
 
 Replay recorded asciicast in a terminal.
 
-This command replays given asciicast (as recorded by **rec** command) directly in
-your terminal.
-
-Following keyboard shortcuts are available:
-
-    Space - toggle pause,
-    . - step through a recording a frame at a time (when paused),
-    Ctrl+C - exit.
+This command replays a given asciicast (as recorded by **rec** command) directly in
+your terminal. The asciicast can be read from a file or from *`stdin`* ('-'):
 
 Playing from a local file:
 
@@ -134,14 +143,32 @@ Playing from IPFS:
 
 Available options:
 
-: &nbsp;
+:   &nbsp;
 
     `-i, --idle-time-limit=<sec>`
-    : Limit replayed terminal inactivity to max `<sec>` seconds
+    : Limit replayed terminal inactivity to max `<sec>` seconds (can be fractional)
 
     `-s, --speed=<factor>`
     : Playback speed (can be fractional)
 
+While playing the following keyboard shortcuts are available:
+
+:    &nbsp;
+
+    *`Space`*
+    :   Toggle pause
+
+    *`.`*
+    :   Step through a recording a frame at a time (when paused)
+
+    *`Ctrl+C`*
+    :   Exit
+
+Recommendation: run 'asciinema play' in a terminal of dimensions not smaller than the one
+used for recording as there's no "transcoding" of control sequences for the new terminal
+size.
+
+
 cat <_filename_>
 ---
 
@@ -154,8 +181,9 @@ output (including all escape sequences) to a terminal immediately.
 **asciinema cat existing.cast >output.txt** gives the same result as recording via
 **asciinema rec \-\-raw output.txt**.
 
-upload <filename>
----
+
+upload <_filename_>
+------
 
 Upload recorded asciicast to asciinema.org site.
 
@@ -167,12 +195,12 @@ demo.cast** is a nice combo if you want to review an asciicast before
 publishing it on asciinema.org.
 
 auth
----
+----
 
-Link your install ID with your asciinema.org user account.
+Link and manage your install ID with your asciinema.org user account.
 
 If you want to manage your recordings (change title/theme, delete) at
-asciinema.org you need to link your "install ID" with asciinema.org user
+asciinema.org you need to link your "install ID" with your asciinema.org user
 account.
 
 This command displays the URL to open in a web browser to do that. You may be
@@ -187,12 +215,19 @@ account. This way we decouple uploading from account creation, allowing them to
 happen in any order.
 
 Note: A new install ID is generated on each machine and system user account you use
-asciinema on, so in order to keep all recordings under a single asciinema.org
-account you need to run **asciinema auth** on all of those machines.
+asciinema on. So in order to keep all recordings under a single asciinema.org
+account you need to run **asciinema auth** on all of those machines. If you’re
+already logged in on asciinema.org website and you run 'asciinema auth' from a new
+computer then this new device will be linked to your account.
+
+While you CAN synchronize your config file (which keeps the API token) across
+all your machines so all use the same token, that’s not necessary. You can assign
+new tokens to your account from as many machines as you want.
 
 Note: asciinema versions prior to 2.0 confusingly referred to install ID as "API
 token".
 
+
 EXAMPLES
 ========
 
@@ -200,6 +235,10 @@ Record your first session:
 
     asciinema rec first.cast
 
+End your session:
+
+    exit
+
 Now replay it with double speed:
 
     asciinema play -s 2 first.cast
@@ -228,25 +267,67 @@ You can record and upload in one step by omitting the filename:
 You'll be asked to confirm the upload when the recording is done, so nothing is
 sent anywhere without your consent.
 
+
+Tricks
+------
+
+Record slowly, play faster:
+
+:   First record a session where you can take your time to type slowly what you want
+    to show in the recording:
+
+        asciinema rec initial.cast
+
+    Then record the replay of 'initial.cast' as 'final.cast', but with five times the
+    initially recorded speed, with all pauses capped to two seconds and with a title
+    set as "My fancy title"::
+
+        asciinema rec -c "asciinema play -s 5 -i 2 initial.cast" -t "My fancy title" final.cast
+
+Play from *`stdin`*:
+
+:   &nbsp;
+
+    cat /path/to/asciicast.json | asciinema play -
+
+Play file from remote host accessible with SSH:
+
+:   &nbsp;
+
+    ssh user@host cat /path/to/asciicat.json | asciinema play -
+
+
 ENVIRONMENT
 ===========
 
 **ASCIINEMA_API_URL**
 
-: This variable allows overriding asciinema-server URL (which defaults to
-  https://asciinema.org) in case you're running your own asciinema-server instance.
+:   This variable allows overriding asciinema-server URL (which defaults to
+    https://asciinema.org) in case you're running your own asciinema-server instance.
 
 **ASCIINEMA_CONFIG_HOME**
 
-: This variable allows overriding config directory location. Default location
-  is $XDG\_CONFIG\_HOME/asciinema (when $XDG\_CONFIG\_HOME is set)
-  or $HOME/.config/asciinema.
+:   This variable allows overriding config directory location. Default location
+    is $XDG\_CONFIG\_HOME/asciinema (when $XDG\_CONFIG\_HOME is set)
+    or $HOME/.config/asciinema.
+
 
 BUGS
 ====
 
 See GitHub Issues: <https://github.com/asciinema/asciinema/issues>
 
+
+MORE RESOURCES
+===============
+
+More documentation is available on the asciicast.org website and its GitHub wiki:
+
+* Web:  [asciinema.org/docs/](https://asciinema.org/docs/)
+* Wiki: [github.com/asciinema/asciinema/wiki](https://github.com/asciinema/asciinema/wiki)
+* Twitter: [@asciinema](https://twitter.com/asciinema)
+
+
 AUTHORS
 =======
 
@@ -255,3 +336,4 @@ asciinema's lead developer is Marcin Kulik.
 For a list of all contributors look here: <https://github.com/asciinema/asciinema/contributors>
 
 This Manual Page was written by Marcin Kulik with help from Kurt Pfeifle.
+
diff --git a/setup.cfg b/setup.cfg
index b88034e41..bbd4d8da7 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,2 +1,6 @@
 [metadata]
 description-file = README.md
+license_file = LICENSE
+
+[pycodestyle]
+ignore = E501,E402,E722
diff --git a/setup.py b/setup.py
index 950a04d5b..e7d3ef30c 100644
--- a/setup.py
+++ b/setup.py
@@ -2,12 +2,11 @@
 import sys
 from setuptools import setup
 
-if sys.version_info[0] < 3:
+if sys.version_info.major < 3:
     sys.exit('Python < 3 is unsupported.')
 
 url_template = 'https://github.com/asciinema/asciinema/archive/v%s.tar.gz'
 requirements = []
-test_requirements = ['nose']
 
 with open('README.md', encoding='utf8') as file:
     long_description = file.read()
@@ -29,6 +28,7 @@
             'asciinema = asciinema.__main__:main',
         ],
     },
+    package_data={'asciinema': ['data/*.png']},
     data_files=[('share/doc/asciinema', ['CHANGELOG.md',
                                          'CODE_OF_CONDUCT.md',
                                          'CONTRIBUTING.md',
@@ -37,7 +37,6 @@
                                          'doc/asciicast-v2.md']),
                 ('share/man/man1', ['man/asciinema.1'])],
     install_requires=requirements,
-    tests_require=test_requirements,
     classifiers=[
         'Development Status :: 5 - Production/Stable',
         'Environment :: Console',


Test patch:
diff --git a/tests/config_test.py b/tests/config_test.py
index a2ba67dff..6bb1a08fc 100644
--- a/tests/config_test.py
+++ b/tests/config_test.py
@@ -187,3 +187,22 @@ def test_play_idle_time_limit():
 
     config = create_config("[play]\nmaxwait = 2.35")
     assert_equal(2.35, config.play_idle_time_limit)
+
+
+def test_notifications_enabled():
+    config = create_config('')
+    assert_equal(True, config.notifications_enabled)
+
+    config = create_config("[notifications]\nenabled = yes")
+    assert_equal(True, config.notifications_enabled)
+
+    config = create_config("[notifications]\nenabled = no")
+    assert_equal(False, config.notifications_enabled)
+
+
+def test_notifications_command():
+    config = create_config('')
+    assert_equal(None, config.notifications_command)
+
+    config = create_config('[notifications]\ncommand = tmux display-message "$TEXT"')
+    assert_equal('tmux display-message "$TEXT"', config.notifications_command)
diff --git a/tests/pty_test.py b/tests/pty_test.py
index 2b1514f9b..8f42c141d 100644
--- a/tests/pty_test.py
+++ b/tests/pty_test.py
@@ -12,10 +12,10 @@ class FakeStdout:
     def __init__(self):
         self.data = []
 
-    def write_stdout(self, data):
+    def write_stdout(self, ts, data):
         self.data.append(data)
 
-    def write_stdin(self, data):
+    def write_stdin(self, ts, data):
         pass