diff --git a/.github/workflows/asciinema.yml b/.github/workflows/asciinema.yml
new file mode 100644
index 0000000..ac35dc5
--- /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 e598a8f..0000000
--- 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 f686650..0b7dacc 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 a7be73a..c7fb513 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 0528a1b..cea3fe2 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 9429656..747b7c2 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 9466287..6416191 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# asciinema
-[](https://travis-ci.org/asciinema/asciinema)
+[](https://github.com/asciinema/asciinema/actions/workflows/asciinema.yml)
[](https://pypi.org/project/asciinema/)
[](https://raw.githubusercontent.com/asciinema/asciinema/master/LICENSE)
@@ -71,34 +71,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:
@@ -117,6 +93,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
@@ -133,6 +135,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
+Ctrl+\. 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 Ctrl+\ 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 Ctrl+D or type
`exit`). If the recorded process is not a shell then recording finishes when
the process exits.
@@ -179,12 +188,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:
- Space - toggle pause,
- . - step through a recording a frame at a time (when paused),
- Ctrl+C - 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
@@ -254,7 +266,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.
@@ -282,7 +294,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
@@ -320,6 +332,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
@@ -327,6 +347,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:
@@ -357,6 +400,6 @@ source [contributors](https://github.com/asciinema/asciinema/contributors).
## License
-Copyright © 2011–2018 Marcin Kulik.
+Copyright © 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 142d49e..ee1eea4 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 3650881..73d715e 100644
--- a/asciinema/__main__.py
+++ b/asciinema/__main__.py
@@ -10,7 +10,6 @@ from asciinema.commands.record import RecordCommand
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 @@ For help on a specific command run:
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 e68db97..1d5dc86 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):
+ def write_stdout(self, ts, data):
+ self.file.write(data)
+
+ def write_stdin(self, ts, data):
pass
-
- def write_stdout(self, data):
- self.queue.put(data)
diff --git a/asciinema/asciicast/v2.py b/asciinema/asciicast/v2.py
index 34eb8cb..2c33cbf 100644
--- a/asciinema/asciicast/v2.py
+++ b/asciinema/asciicast/v2.py
@@ -3,17 +3,6 @@ import json.decoder
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 @@ class writer():
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 0000000..88ad995
--- /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 1fab7f6..cc435ce 100644
--- a/asciinema/commands/auth.py
+++ b/asciinema/commands/auth.py
@@ -3,9 +3,8 @@ from asciinema.commands.command import Command
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 2fe285a..fe12c90 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 bd4260e..3ba5ad0 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 4998d67..dd5adf3 100644
--- a/asciinema/commands/play.py
+++ b/asciinema/commands/play.py
@@ -5,17 +5,21 @@ import asciinema.asciicast as asciicast
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 6f61ffa..b8f9423 100644
--- a/asciinema/commands/record.py
+++ b/asciinema/commands/record.py
@@ -2,18 +2,19 @@ import os
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 @@ class RecordCommand(Command):
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 @@ class RecordCommand(Command):
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 @@ class RecordCommand(Command):
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 @@ class RecordCommand(Command):
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 7ca25dc..897ecb5 100644
--- a/asciinema/commands/upload.py
+++ b/asciinema/commands/upload.py
@@ -4,10 +4,9 @@ from asciinema.api import APIError
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 62f3e17..75703c0 100644
--- a/asciinema/config.py
+++ b/asciinema/config.py
@@ -109,6 +109,14 @@ class Config:
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 @@ class Config:
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 0000000..a0885aa
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 0000000..1240511
--- /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 e81b501..3670a8d 100644
--- a/asciinema/player.py
+++ b/asciinema/player.py
@@ -8,16 +8,18 @@ from asciinema.term import raw, read_blocking
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 @@ class Player:
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 @@ class Player:
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 9e731cc..e5d66a6 100644
--- a/asciinema/pty.py
+++ b/asciinema/pty.py
@@ -10,12 +10,22 @@ import signal
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 record(command, writer, env=os.environ, rec_stdin=False):
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 record(command, writer, env=os.environ, rec_stdin=False):
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 record(command, writer, env=os.environ, rec_stdin=False):
_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 0000000..1c02d49
--- /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 49858ce..f3b54fb 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 @@ class raw():
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 2d1a5c3..796a3cf 100644
--- a/asciinema/urllib_http_adapter.py
+++ b/asciinema/urllib_http_adapter.py
@@ -67,7 +67,7 @@ class URLLibHttpAdapter:
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 67d783a..21c46cb 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 9666d9d..2687c08 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
+Ctrl+\. 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 Ctrl+\ 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 Ctrl+D 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:
-:
+:
`--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=`
- : Specify command to record, defaults to **$SHELL**
+ : Specify command to record, defaults to **$SHELL**
`-e, --env=`
- : List of environment variables to capture, defaults to **SHELL,TERM**
+ : List of environment variables to capture, defaults to **SHELL,TERM**
`-t, --title=`
- : Specify the title of the asciicast
+ : Specify the title of the asciicast
`-i, --idle-time-limit=`
- : Limit recorded terminal inactivity to max `` seconds
+ : Limit recorded terminal inactivity to max `` 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:
-:
+:
`-i, --idle-time-limit=`
- : Limit replayed terminal inactivity to max `` seconds
+ : Limit replayed terminal inactivity to max `` seconds (can be fractional)
`-s, --speed=`
: Playback speed (can be fractional)
+While playing the following keyboard shortcuts are available:
+
+:
+
+ *`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
----
+
+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`*:
+
+:
+
+ cat /path/to/asciicast.json | asciinema play -
+
+Play file from remote host accessible with SSH:
+
+:
+
+ 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:
+
+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:
This Manual Page was written by Marcin Kulik with help from Kurt Pfeifle.
+
diff --git a/setup.cfg b/setup.cfg
index b88034e..bbd4d8d 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 950a04d..e7d3ef3 100644
--- a/setup.py
+++ b/setup.py
@@ -2,12 +2,11 @@ import asciinema
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 @@ setup(
'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 @@ setup(
'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',
diff --git a/tests/config_test.py b/tests/config_test.py
index a2ba67d..6bb1a08 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 2b1514f..8f42c14 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