mirror of
https://github.com/asciinema/asciinema.git
synced 2025-12-16 03:38:03 +01:00
Merge branch 'release/2.1.0'
This commit is contained in:
44
.github/workflows/asciinema.yml
vendored
Normal file
44
.github/workflows/asciinema.yml
vendored
Normal file
@@ -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
|
||||||
17
.travis.yml
17
.travis.yml
@@ -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
|
|
||||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,5 +1,19 @@
|
|||||||
# asciinema changelog
|
# 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)
|
## 2.0.2 (2019-01-12)
|
||||||
|
|
||||||
* Official support for Python 3.7
|
* Official support for Python 3.7
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ If you want to propose code change, either introducing a new feature or improvin
|
|||||||
## Asking for help
|
## Asking for help
|
||||||
|
|
||||||
Github issue tracker is not a support forum.
|
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
|
## Reporting security issues
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM ubuntu:16.04
|
FROM ubuntu:20.04
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
ca-certificates \
|
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
|
RUN mkdir /usr/src/app
|
||||||
COPY setup.cfg /usr/src/app
|
COPY setup.cfg /usr/src/app
|
||||||
COPY setup.py /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
|
COPY asciinema /usr/src/app/asciinema
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
RUN python3 setup.py install
|
RUN python3 setup.py install
|
||||||
@@ -17,4 +19,4 @@ ENV LANG en_US.utf8
|
|||||||
ENV SHELL /bin/bash
|
ENV SHELL /bin/bash
|
||||||
ENV USER docker
|
ENV USER docker
|
||||||
WORKDIR /root
|
WORKDIR /root
|
||||||
CMD ["asciinema", "rec"]
|
ENTRYPOINT ["/usr/local/bin/asciinema"]
|
||||||
|
|||||||
12
Makefile
12
Makefile
@@ -16,14 +16,16 @@ release-test: test push-test
|
|||||||
tag:
|
tag:
|
||||||
git tag | grep "v$(VERSION)" && echo "Tag v$(VERSION) exists" && exit 1 || true
|
git tag | grep "v$(VERSION)" && echo "Tag v$(VERSION) exists" && exit 1 || true
|
||||||
git tag -s -m "Releasing $(VERSION)" v$(VERSION)
|
git tag -s -m "Releasing $(VERSION)" v$(VERSION)
|
||||||
git push --tags
|
git push origin v$(VERSION)
|
||||||
|
|
||||||
push:
|
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:
|
push-test:
|
||||||
python3 setup.py sdist upload -r pypitest
|
python3 -m pip install --user --upgrade --quiet twine
|
||||||
|
python3 setup.py sdist bdist_wheel
|
||||||
release: test tag push
|
python3 -m twine upload --repository testpypi dist/*
|
||||||
|
|
||||||
.PHONY: test test-unit test-integration release release-test tag push push-test
|
.PHONY: test test-unit test-integration release release-test tag push push-test
|
||||||
|
|||||||
105
README.md
105
README.md
@@ -1,6 +1,6 @@
|
|||||||
# asciinema
|
# asciinema
|
||||||
|
|
||||||
[](https://travis-ci.org/asciinema/asciinema)
|
[](https://github.com/asciinema/asciinema/actions/workflows/asciinema.yml)
|
||||||
[](https://pypi.org/project/asciinema/)
|
[](https://pypi.org/project/asciinema/)
|
||||||
[](https://raw.githubusercontent.com/asciinema/asciinema/master/LICENSE)
|
[](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
|
X, Linux and FreeBSD. Look for package named `asciinema`. See the
|
||||||
[list of available packages](https://asciinema.org/docs/installation).
|
[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
|
### Running latest version from source code checkout
|
||||||
|
|
||||||
If none of the above works for you just clone the repo and run asciinema
|
If you can't use Python package or native package for your OS is outdated you
|
||||||
straight from the checkout.
|
can clone the repo and run asciinema straight from the checkout.
|
||||||
|
|
||||||
Clone the repo:
|
Clone the repo:
|
||||||
|
|
||||||
@@ -117,6 +93,32 @@ Then run it with:
|
|||||||
|
|
||||||
python3 -m asciinema --version
|
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
|
## Usage
|
||||||
|
|
||||||
asciinema is composed of multiple commands, similar to `git`, `apt-get` or
|
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
|
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.
|
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
|
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
|
`exit`). If the recorded process is not a shell then recording finishes when
|
||||||
the process exits.
|
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
|
This command replays given asciicast (as recorded by `rec` command) directly in
|
||||||
your terminal.
|
your terminal.
|
||||||
|
|
||||||
Following keyboard shortcuts are available:
|
Following keyboard shortcuts are available by default:
|
||||||
|
|
||||||
- <kbd>Space</kbd> - toggle pause,
|
- <kbd>Space</kbd> - toggle pause,
|
||||||
- <kbd>.</kbd> - step through a recording a frame at a time (when paused),
|
- <kbd>.</kbd> - step through a recording a frame at a time (when paused),
|
||||||
- <kbd>Ctrl+C</kbd> - exit.
|
- <kbd>Ctrl+C</kbd> - exit.
|
||||||
|
|
||||||
|
See "Configuration file" section for information on how to customize the
|
||||||
|
keyboard shortcuts.
|
||||||
|
|
||||||
Playing from a local file:
|
Playing from a local file:
|
||||||
|
|
||||||
asciinema play /path/to/asciicast.cast
|
asciinema play /path/to/asciicast.cast
|
||||||
@@ -254,7 +266,7 @@ asked to log in first.
|
|||||||
Install ID is a random ID ([UUID
|
Install ID is a random ID ([UUID
|
||||||
v4](https://en.wikipedia.org/wiki/Universally_unique_identifier)) generated
|
v4](https://en.wikipedia.org/wiki/Universally_unique_identifier)) generated
|
||||||
locally when you run asciinema for the first time, and saved at
|
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
|
with uploaded recordings, so they can later be associated with asciinema.org
|
||||||
account. This way we decouple uploading from account creation, allowing them to
|
account. This way we decouple uploading from account creation, allowing them to
|
||||||
happen in any order.
|
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
|
- setting up your own
|
||||||
[asciinema-server](https://github.com/asciinema/asciinema-server) instance,
|
[asciinema-server](https://github.com/asciinema/asciinema-server) instance,
|
||||||
and [setting API URL
|
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
|
## Configuration file
|
||||||
|
|
||||||
@@ -320,6 +332,14 @@ yes = true
|
|||||||
; Be quiet, suppress all notices/warnings, default: no
|
; Be quiet, suppress all notices/warnings, default: no
|
||||||
quiet = true
|
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]
|
[play]
|
||||||
|
|
||||||
; Playback speed (can be fractional), default: 1
|
; Playback speed (can be fractional), default: 1
|
||||||
@@ -327,6 +347,29 @@ speed = 2
|
|||||||
|
|
||||||
; Limit replayed terminal inactivity to max n seconds, default: off
|
; Limit replayed terminal inactivity to max n seconds, default: off
|
||||||
idle_time_limit = 1
|
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:
|
A very minimal config file could look like that:
|
||||||
@@ -357,6 +400,6 @@ source [contributors](https://github.com/asciinema/asciinema/contributors).
|
|||||||
|
|
||||||
## License
|
## 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.
|
All code is licensed under the GPL, v3 or later. See LICENSE file for details.
|
||||||
|
|||||||
@@ -1,58 +1,25 @@
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
__author__ = 'Marcin Kulik'
|
__author__ = 'Marcin Kulik'
|
||||||
__version__ = '2.0.2'
|
__version__ = '2.1.0'
|
||||||
|
|
||||||
if sys.version_info[0] < 3:
|
if sys.version_info[0] < 3:
|
||||||
raise ImportError('Python < 3 is unsupported.')
|
raise ImportError('Python < 3 is unsupported.')
|
||||||
|
|
||||||
|
import asciinema.recorder
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import time
|
|
||||||
|
|
||||||
import asciinema.asciicast.v2 as v2
|
|
||||||
import asciinema.pty as pty
|
|
||||||
import asciinema.term as term
|
|
||||||
|
|
||||||
|
|
||||||
def record_asciicast(path, command=None, append=False, idle_time_limit=None,
|
def record_asciicast(path, command=None, append=False, idle_time_limit=None,
|
||||||
rec_stdin=False, title=None, metadata=None,
|
rec_stdin=False, title=None, metadata=None,
|
||||||
command_env=None, capture_env=None, writer=v2.async_writer,
|
command_env=None, capture_env=None):
|
||||||
record=pty.record):
|
asciinema.recorder.record(
|
||||||
if command is None:
|
path,
|
||||||
command = os.environ.get('SHELL') or 'sh'
|
command=command,
|
||||||
|
append=append,
|
||||||
if command_env is None:
|
idle_time_limit=idle_time_limit,
|
||||||
command_env = os.environ.copy()
|
rec_stdin=rec_stdin,
|
||||||
command_env['ASCIINEMA_REC'] = '1'
|
title=title,
|
||||||
|
metadata=metadata,
|
||||||
if capture_env is None:
|
command_env=command_env,
|
||||||
capture_env = ['SHELL', 'TERM']
|
capture_env=capture_env
|
||||||
|
)
|
||||||
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)
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ from asciinema.commands.record import RecordCommand
|
|||||||
from asciinema.commands.play import PlayCommand
|
from asciinema.commands.play import PlayCommand
|
||||||
from asciinema.commands.cat import CatCommand
|
from asciinema.commands.cat import CatCommand
|
||||||
from asciinema.commands.upload import UploadCommand
|
from asciinema.commands.upload import UploadCommand
|
||||||
from asciinema.api import Api
|
|
||||||
|
|
||||||
|
|
||||||
def positive_float(value):
|
def positive_float(value):
|
||||||
@@ -21,36 +20,13 @@ def positive_float(value):
|
|||||||
return 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):
|
def maybe_str(v):
|
||||||
if v is not None:
|
if v is not None:
|
||||||
return str(v)
|
return str(v)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
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.")
|
print("asciinema needs an ASCII or UTF-8 character encoding to run. Check the output of `locale` command.")
|
||||||
sys.exit(1)
|
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('-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('-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.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
|
# create the parser for the "play" command
|
||||||
parser_play = subparsers.add_parser('play', help='Replay terminal session')
|
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('-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('-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.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
|
# create the parser for the "cat" command
|
||||||
parser_cat = subparsers.add_parser('cat', help='Print full output of terminal session')
|
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.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
|
# create the parser for the "upload" command
|
||||||
parser_upload = subparsers.add_parser('upload', help='Upload locally saved terminal session to asciinema.org')
|
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.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
|
# create the parser for the "auth" command
|
||||||
parser_auth = subparsers.add_parser('auth', help='Manage recordings on asciinema.org account')
|
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
|
# parse the args and call whatever function was selected
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if hasattr(args, 'func'):
|
if hasattr(args, 'cmd'):
|
||||||
command = args.func(args, cfg)
|
command = args.cmd(args, cfg, os.environ)
|
||||||
code = command.execute()
|
code = command.execute()
|
||||||
sys.exit(code)
|
sys.exit(code)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,39 +1,25 @@
|
|||||||
import os
|
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():
|
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
|
if append and os.path.exists(path) and os.stat(path).st_size == 0: # true for pipes
|
||||||
append = False
|
append = False
|
||||||
|
|
||||||
self.path = path
|
self.path = path
|
||||||
self.mode = 'a' if append else 'w'
|
self.buffering = buffering
|
||||||
self.queue = Queue()
|
self.mode = 'ab' if append else 'wb'
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
self.process = Process(
|
self.file = open(self.path, mode=self.mode, buffering=self.buffering)
|
||||||
target=write_bytes_from_queue,
|
|
||||||
args=(self.path, self.mode, self.queue)
|
|
||||||
)
|
|
||||||
self.process.start()
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, exc_traceback):
|
def __exit__(self, exc_type, exc_value, exc_traceback):
|
||||||
self.queue.put(None)
|
self.file.close()
|
||||||
self.process.join()
|
|
||||||
|
|
||||||
def write_stdin(self, data):
|
def write_stdout(self, ts, data):
|
||||||
|
self.file.write(data)
|
||||||
|
|
||||||
|
def write_stdin(self, ts, data):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def write_stdout(self, data):
|
|
||||||
self.queue.put(data)
|
|
||||||
|
|||||||
@@ -3,17 +3,6 @@ import json.decoder
|
|||||||
import time
|
import time
|
||||||
import codecs
|
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:
|
try:
|
||||||
JSONDecodeError = json.decoder.JSONDecodeError
|
JSONDecodeError = json.decoder.JSONDecodeError
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@@ -76,10 +65,9 @@ def get_duration(path):
|
|||||||
return last_frame[0]
|
return last_frame[0]
|
||||||
|
|
||||||
|
|
||||||
def build_header(metadata):
|
def build_header(width, height, metadata):
|
||||||
header = {}
|
header = {'version': 2, 'width': width, 'height': height}
|
||||||
header.update(metadata)
|
header.update(metadata)
|
||||||
header['version'] = 2
|
|
||||||
|
|
||||||
assert 'width' in header, 'width missing in metadata'
|
assert 'width' in header, 'width missing in metadata'
|
||||||
assert 'height' in header, 'height missing in metadata'
|
assert 'height' in header, 'height missing in metadata'
|
||||||
@@ -94,20 +82,18 @@ def build_header(metadata):
|
|||||||
|
|
||||||
class writer():
|
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.path = path
|
||||||
self.mode = mode
|
|
||||||
self.buffering = buffering
|
self.buffering = buffering
|
||||||
self.stdin_decoder = codecs.getincrementaldecoder('UTF-8')('replace')
|
self.stdin_decoder = codecs.getincrementaldecoder('UTF-8')('replace')
|
||||||
self.stdout_decoder = codecs.getincrementaldecoder('UTF-8')('replace')
|
self.stdout_decoder = codecs.getincrementaldecoder('UTF-8')('replace')
|
||||||
|
|
||||||
if mode == 'w':
|
if append:
|
||||||
self.header = {'version': 2, 'width': width, 'height': height}
|
self.mode = 'a'
|
||||||
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:
|
|
||||||
self.header = None
|
self.header = None
|
||||||
|
else:
|
||||||
|
self.mode = 'w'
|
||||||
|
self.header = build_header(width, height, metadata or {})
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
self.file = open(self.path, mode=self.mode, buffering=self.buffering)
|
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):
|
def __exit__(self, exc_type, exc_value, exc_traceback):
|
||||||
self.file.close()
|
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):
|
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):
|
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):
|
def __write_line(self, obj):
|
||||||
line = json.dumps(obj, ensure_ascii=False, indent=None, separators=(', ', ': '))
|
line = json.dumps(obj, ensure_ascii=False, indent=None, separators=(', ', ': '))
|
||||||
self.file.write(line + '\n')
|
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])
|
|
||||||
|
|||||||
31
asciinema/async_worker.py
Normal file
31
asciinema/async_worker.py
Normal file
@@ -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)
|
||||||
@@ -3,9 +3,8 @@ from asciinema.commands.command import Command
|
|||||||
|
|
||||||
class AuthCommand(Command):
|
class AuthCommand(Command):
|
||||||
|
|
||||||
def __init__(self, api):
|
def __init__(self, args, config, env):
|
||||||
Command.__init__(self)
|
Command.__init__(self, args, config, env)
|
||||||
self.api = api
|
|
||||||
|
|
||||||
def execute(self):
|
def execute(self):
|
||||||
self.print('Open the following URL in a web browser to link your '
|
self.print('Open the following URL in a web browser to link your '
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from asciinema.commands.command import Command
|
from asciinema.commands.command import Command
|
||||||
|
from asciinema.term import raw
|
||||||
import asciinema.asciicast as asciicast
|
import asciinema.asciicast as asciicast
|
||||||
|
|
||||||
|
|
||||||
class CatCommand(Command):
|
class CatCommand(Command):
|
||||||
|
|
||||||
def __init__(self, filename):
|
def __init__(self, args, config, env):
|
||||||
Command.__init__(self)
|
Command.__init__(self, args, config, env)
|
||||||
self.filename = filename
|
self.filename = args.filename
|
||||||
|
|
||||||
def execute(self):
|
def execute(self):
|
||||||
try:
|
try:
|
||||||
with asciicast.open_from_url(self.filename) as a:
|
stdin = open('/dev/tty')
|
||||||
for t, _type, text in a.stdout_events():
|
with raw(stdin.fileno()):
|
||||||
sys.stdout.write(text)
|
with asciicast.open_from_url(self.filename) as a:
|
||||||
sys.stdout.flush()
|
for t, _type, text in a.stdout_events():
|
||||||
|
sys.stdout.write(text)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
except asciicast.LoadError as e:
|
except asciicast.LoadError as e:
|
||||||
self.print_error("printing failed: %s" % str(e))
|
self.print_error("printing failed: %s" % str(e))
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from asciinema.api import Api
|
||||||
|
|
||||||
|
|
||||||
class Command:
|
class Command:
|
||||||
|
|
||||||
def __init__(self, quiet=False):
|
def __init__(self, args, config, env):
|
||||||
self.quiet = quiet
|
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):
|
def print(self, text, file=sys.stdout, end="\n", force=False):
|
||||||
if not self.quiet or force:
|
if not self.quiet or force:
|
||||||
|
|||||||
@@ -5,17 +5,21 @@ import asciinema.asciicast as asciicast
|
|||||||
|
|
||||||
class PlayCommand(Command):
|
class PlayCommand(Command):
|
||||||
|
|
||||||
def __init__(self, filename, idle_time_limit, speed, player=None):
|
def __init__(self, args, config, env, player=None):
|
||||||
Command.__init__(self)
|
Command.__init__(self, args, config, env)
|
||||||
self.filename = filename
|
self.filename = args.filename
|
||||||
self.idle_time_limit = idle_time_limit
|
self.idle_time_limit = args.idle_time_limit
|
||||||
self.speed = speed
|
self.speed = args.speed
|
||||||
self.player = player if player is not None else Player()
|
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):
|
def execute(self):
|
||||||
try:
|
try:
|
||||||
with asciicast.open_from_url(self.filename) as a:
|
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:
|
except asciicast.LoadError as e:
|
||||||
self.print_error("playback failed: %s" % str(e))
|
self.print_error("playback failed: %s" % str(e))
|
||||||
|
|||||||
@@ -2,18 +2,19 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
import asciinema
|
import asciinema.recorder as recorder
|
||||||
import asciinema.asciicast.raw as raw
|
import asciinema.asciicast.raw as raw
|
||||||
import asciinema.asciicast.v2 as v2
|
import asciinema.asciicast.v2 as v2
|
||||||
|
import asciinema.notifier as notifier
|
||||||
from asciinema.api import APIError
|
from asciinema.api import APIError
|
||||||
from asciinema.commands.command import Command
|
from asciinema.commands.command import Command
|
||||||
|
|
||||||
|
|
||||||
class RecordCommand(Command):
|
class RecordCommand(Command):
|
||||||
|
|
||||||
def __init__(self, api, args, env=None):
|
def __init__(self, args, config, env):
|
||||||
Command.__init__(self, args.quiet)
|
Command.__init__(self, args, config, env)
|
||||||
self.api = api
|
self.quiet = args.quiet
|
||||||
self.filename = args.filename
|
self.filename = args.filename
|
||||||
self.rec_stdin = args.stdin
|
self.rec_stdin = args.stdin
|
||||||
self.command = args.command
|
self.command = args.command
|
||||||
@@ -24,8 +25,13 @@ class RecordCommand(Command):
|
|||||||
self.append = args.append
|
self.append = args.append
|
||||||
self.overwrite = args.overwrite
|
self.overwrite = args.overwrite
|
||||||
self.raw = args.raw
|
self.raw = args.raw
|
||||||
self.writer = raw.writer if args.raw else v2.async_writer
|
self.writer = raw.writer if args.raw else v2.writer
|
||||||
self.env = env if env is not None else os.environ
|
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):
|
def execute(self):
|
||||||
upload = False
|
upload = False
|
||||||
@@ -50,6 +56,7 @@ class RecordCommand(Command):
|
|||||||
|
|
||||||
elif os.stat(self.filename).st_size > 0 and not append:
|
elif os.stat(self.filename).st_size > 0 and not append:
|
||||||
self.print_error("%s already exists, aborting" % self.filename)
|
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")
|
self.print_error("use --append option if you want to append to existing recording")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
@@ -66,7 +73,7 @@ class RecordCommand(Command):
|
|||||||
vars = filter(None, map((lambda var: var.strip()), self.env_whitelist.split(',')))
|
vars = filter(None, map((lambda var: var.strip()), self.env_whitelist.split(',')))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
asciinema.record_asciicast(
|
recorder.record(
|
||||||
self.filename,
|
self.filename,
|
||||||
command=self.command,
|
command=self.command,
|
||||||
append=append,
|
append=append,
|
||||||
@@ -75,7 +82,9 @@ class RecordCommand(Command):
|
|||||||
command_env=self.env,
|
command_env=self.env,
|
||||||
capture_env=vars,
|
capture_env=vars,
|
||||||
rec_stdin=self.rec_stdin,
|
rec_stdin=self.rec_stdin,
|
||||||
writer=self.writer
|
writer=self.writer,
|
||||||
|
notifier=self.notifier,
|
||||||
|
key_bindings=self.key_bindings
|
||||||
)
|
)
|
||||||
except v2.LoadError:
|
except v2.LoadError:
|
||||||
self.print_error("can only append to asciicast v2 format recordings")
|
self.print_error("can only append to asciicast v2 format recordings")
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ from asciinema.api import APIError
|
|||||||
|
|
||||||
class UploadCommand(Command):
|
class UploadCommand(Command):
|
||||||
|
|
||||||
def __init__(self, api, filename):
|
def __init__(self, args, config, env):
|
||||||
Command.__init__(self)
|
Command.__init__(self, args, config, env)
|
||||||
self.api = api
|
self.filename = args.filename
|
||||||
self.filename = filename
|
|
||||||
|
|
||||||
def execute(self):
|
def execute(self):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -109,6 +109,14 @@ class Config:
|
|||||||
def record_quiet(self):
|
def record_quiet(self):
|
||||||
return self.config.getboolean('record', 'quiet', fallback=False)
|
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
|
@property
|
||||||
def play_idle_time_limit(self):
|
def play_idle_time_limit(self):
|
||||||
fallback = self.config.getfloat('play', 'maxwait', fallback=None) # pre 2.0
|
fallback = self.config.getfloat('play', 'maxwait', fallback=None) # pre 2.0
|
||||||
@@ -118,6 +126,36 @@ class Config:
|
|||||||
def play_speed(self):
|
def play_speed(self):
|
||||||
return self.config.getfloat('play', 'speed', fallback=1.0)
|
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):
|
def get_config_home(env=os.environ):
|
||||||
env_asciinema_config_home = env.get("ASCIINEMA_CONFIG_HOME")
|
env_asciinema_config_home = env.get("ASCIINEMA_CONFIG_HOME")
|
||||||
|
|||||||
BIN
asciinema/data/icon-256x256.png
Normal file
BIN
asciinema/data/icon-256x256.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
83
asciinema/notifier.py
Normal file
83
asciinema/notifier.py
Normal file
@@ -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()
|
||||||
@@ -8,16 +8,18 @@ from asciinema.term import raw, read_blocking
|
|||||||
|
|
||||||
class Player:
|
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:
|
try:
|
||||||
stdin = open('/dev/tty')
|
stdin = open('/dev/tty')
|
||||||
with raw(stdin.fileno()):
|
with raw(stdin.fileno()):
|
||||||
self._play(asciicast, idle_time_limit, speed, stdin)
|
self._play(asciicast, idle_time_limit, speed, stdin, key_bindings)
|
||||||
except Exception:
|
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
|
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 = asciicast.stdout_events()
|
||||||
stdout = ev.to_relative_time(stdout)
|
stdout = ev.to_relative_time(stdout)
|
||||||
@@ -42,12 +44,12 @@ class Player:
|
|||||||
ctrl_c = True
|
ctrl_c = True
|
||||||
break
|
break
|
||||||
|
|
||||||
if 0x20 in data: # space
|
if data == pause_key:
|
||||||
paused = False
|
paused = False
|
||||||
base_time = base_time + (time.time() - pause_time)
|
base_time = base_time + (time.time() - pause_time)
|
||||||
break
|
break
|
||||||
|
|
||||||
if 0x2e in data: # period (dot)
|
if data == step_key:
|
||||||
delay = 0
|
delay = 0
|
||||||
pause_time = time.time()
|
pause_time = time.time()
|
||||||
base_time = pause_time - t
|
base_time = pause_time - t
|
||||||
@@ -62,7 +64,7 @@ class Player:
|
|||||||
ctrl_c = True
|
ctrl_c = True
|
||||||
break
|
break
|
||||||
|
|
||||||
if 0x20 in data: # space
|
if data == pause_key:
|
||||||
paused = True
|
paused = True
|
||||||
pause_time = time.time()
|
pause_time = time.time()
|
||||||
slept = t - (pause_time - base_time)
|
slept = t - (pause_time - base_time)
|
||||||
|
|||||||
@@ -10,12 +10,22 @@ import signal
|
|||||||
import struct
|
import struct
|
||||||
import sys
|
import sys
|
||||||
import termios
|
import termios
|
||||||
|
import time
|
||||||
|
|
||||||
from asciinema.term import raw
|
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
|
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():
|
def _set_pty_size():
|
||||||
'''
|
'''
|
||||||
@@ -40,7 +50,9 @@ def record(command, writer, env=os.environ, rec_stdin=False):
|
|||||||
def _handle_master_read(data):
|
def _handle_master_read(data):
|
||||||
'''Handles new data on child process stdout.'''
|
'''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)
|
_write_stdout(data)
|
||||||
|
|
||||||
def _write_master(data):
|
def _write_master(data):
|
||||||
@@ -53,10 +65,32 @@ def record(command, writer, env=os.environ, rec_stdin=False):
|
|||||||
def _handle_stdin_read(data):
|
def _handle_stdin_read(data):
|
||||||
'''Handles new data on child process stdin.'''
|
'''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)
|
_write_master(data)
|
||||||
|
|
||||||
if rec_stdin:
|
if rec_stdin and not pause_time:
|
||||||
writer.write_stdin(data)
|
writer.write_stdin(time.time() - start_time, data)
|
||||||
|
|
||||||
def _signals(signal_list):
|
def _signals(signal_list):
|
||||||
old_handlers = []
|
old_handlers = []
|
||||||
@@ -129,6 +163,8 @@ def record(command, writer, env=os.environ, rec_stdin=False):
|
|||||||
|
|
||||||
_set_pty_size()
|
_set_pty_size()
|
||||||
|
|
||||||
|
start_time = time.time() - time_offset
|
||||||
|
|
||||||
with raw(pty.STDIN_FILENO):
|
with raw(pty.STDIN_FILENO):
|
||||||
try:
|
try:
|
||||||
_copy(pipe_r)
|
_copy(pipe_r)
|
||||||
|
|||||||
102
asciinema/recorder.py
Normal file
102
asciinema/recorder.py
Normal file
@@ -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
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import select
|
import select
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import time
|
||||||
import tty
|
import tty
|
||||||
|
|
||||||
|
|
||||||
@@ -19,6 +20,8 @@ class raw():
|
|||||||
|
|
||||||
def __exit__(self, type, value, traceback):
|
def __exit__(self, type, value, traceback):
|
||||||
if self.restore:
|
if self.restore:
|
||||||
|
# Give the terminal time to send answerbacks
|
||||||
|
time.sleep(0.01)
|
||||||
tty.tcsetattr(self.fd, tty.TCSAFLUSH, self.mode)
|
tty.tcsetattr(self.fd, tty.TCSAFLUSH, self.mode)
|
||||||
|
|
||||||
|
|
||||||
@@ -30,8 +33,10 @@ def read_blocking(fd, timeout):
|
|||||||
|
|
||||||
|
|
||||||
def get_size():
|
def get_size():
|
||||||
# TODO maybe use os.get_terminal_size ?
|
try:
|
||||||
return (
|
return os.get_terminal_size()
|
||||||
int(subprocess.check_output(['tput', 'cols'])),
|
except:
|
||||||
int(subprocess.check_output(['tput', 'lines']))
|
return (
|
||||||
)
|
int(subprocess.check_output(['tput', 'cols'])),
|
||||||
|
int(subprocess.check_output(['tput', 'lines']))
|
||||||
|
)
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class URLLibHttpAdapter:
|
|||||||
|
|
||||||
if password:
|
if password:
|
||||||
auth = "%s:%s" % (username, 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
|
headers["Authorization"] = b"Basic " + encoded_auth
|
||||||
|
|
||||||
request = Request(url, data=body, headers=headers, method="POST")
|
request = Request(url, data=body, headers=headers, method="POST")
|
||||||
|
|||||||
@@ -1,2 +1,5 @@
|
|||||||
|
VERSION=`python3 -c "import asciinema; print(asciinema.__version__)"`
|
||||||
|
|
||||||
asciinema.1: asciinema.1.md
|
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`"
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,27 @@
|
|||||||
% ASCIINEMA(1) Version 2.0.1 | asciinema
|
% ASCIINEMA(1) Version 2.0.1 | asciinema
|
||||||
|
|
||||||
|
|
||||||
NAME
|
NAME
|
||||||
====
|
====
|
||||||
|
|
||||||
**asciinema** - terminal session recorder
|
**asciinema** - terminal session recorder
|
||||||
|
|
||||||
|
|
||||||
SYNOPSIS
|
SYNOPSIS
|
||||||
========
|
========
|
||||||
|
|
||||||
| **asciinema \-\-version**
|
| **asciinema \-\-version**
|
||||||
| **asciinema** _command_ \[_options_] \[_args_]
|
| **asciinema** _command_ \[_options_] \[_args_]
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTION
|
DESCRIPTION
|
||||||
===========
|
===========
|
||||||
|
|
||||||
asciinema lets you easily record terminal sessions and replay
|
asciinema lets you easily record terminal sessions, replay
|
||||||
them in a terminal as well as in a web browser.
|
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
|
COMMANDS
|
||||||
========
|
========
|
||||||
@@ -23,9 +29,10 @@ COMMANDS
|
|||||||
asciinema is composed of multiple commands, similar to `git`, `apt-get` or
|
asciinema is composed of multiple commands, similar to `git`, `apt-get` or
|
||||||
`brew`.
|
`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.
|
all available commands with their options.
|
||||||
|
|
||||||
|
|
||||||
rec [_filename_]
|
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
|
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.
|
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
|
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
|
`exit`). If the recorded process is not a shell then recording finishes when
|
||||||
the process exits.
|
the process exits.
|
||||||
@@ -55,37 +69,37 @@ prompt or play a sound when the shell is being recorded.
|
|||||||
|
|
||||||
Available options:
|
Available options:
|
||||||
|
|
||||||
:
|
:
|
||||||
|
|
||||||
`--stdin`
|
`--stdin`
|
||||||
: Enable stdin (keyboard) recording (see below)
|
: Enable stdin (keyboard) recording (see below)
|
||||||
|
|
||||||
`--append`
|
`--append`
|
||||||
: Append to existing recording
|
: Append to existing recording
|
||||||
|
|
||||||
`--raw`
|
`--raw`
|
||||||
: Save raw STDOUT output, without timing information or other metadata
|
: Save raw STDOUT output, without timing information or other metadata
|
||||||
|
|
||||||
`--overwrite`
|
`--overwrite`
|
||||||
: Overwrite the recording if it already exists
|
: Overwrite the recording if it already exists
|
||||||
|
|
||||||
`-c, --command=<command>`
|
`-c, --command=<command>`
|
||||||
: Specify command to record, defaults to **$SHELL**
|
: Specify command to record, defaults to **$SHELL**
|
||||||
|
|
||||||
`-e, --env=<var-names>`
|
`-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>`
|
`-t, --title=<title>`
|
||||||
: Specify the title of the asciicast
|
: Specify the title of the asciicast
|
||||||
|
|
||||||
`-i, --idle-time-limit=<sec>`
|
`-i, --idle-time-limit=<sec>`
|
||||||
: Limit recorded terminal inactivity to max `<sec>` seconds
|
: Limit recorded terminal inactivity to max `<sec>` seconds
|
||||||
|
|
||||||
`-y, --yes`
|
`-y, --yes`
|
||||||
: Answer "yes" to all prompts (e.g. upload confirmation)
|
: Answer "yes" to all prompts (e.g. upload confirmation)
|
||||||
|
|
||||||
`-q, --quiet`
|
`-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
|
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.
|
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
|
instance), it's disabled by default, and has to be explicitly enabled via
|
||||||
**--stdin** option.
|
**--stdin** option.
|
||||||
|
|
||||||
|
|
||||||
play <_filename_>
|
play <_filename_>
|
||||||
---
|
---
|
||||||
|
|
||||||
Replay recorded asciicast in a terminal.
|
Replay recorded asciicast in a terminal.
|
||||||
|
|
||||||
This command replays given asciicast (as recorded by **rec** command) directly in
|
This command replays a given asciicast (as recorded by **rec** command) directly in
|
||||||
your terminal.
|
your terminal. The asciicast can be read from a file or from *`stdin`* ('-'):
|
||||||
|
|
||||||
Following keyboard shortcuts are available:
|
|
||||||
|
|
||||||
Space - toggle pause,
|
|
||||||
. - step through a recording a frame at a time (when paused),
|
|
||||||
Ctrl+C - exit.
|
|
||||||
|
|
||||||
Playing from a local file:
|
Playing from a local file:
|
||||||
|
|
||||||
@@ -134,14 +143,32 @@ Playing from IPFS:
|
|||||||
|
|
||||||
Available options:
|
Available options:
|
||||||
|
|
||||||
:
|
:
|
||||||
|
|
||||||
`-i, --idle-time-limit=<sec>`
|
`-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>`
|
`-s, --speed=<factor>`
|
||||||
: Playback speed (can be fractional)
|
: 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_>
|
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 cat existing.cast >output.txt** gives the same result as recording via
|
||||||
**asciinema rec \-\-raw output.txt**.
|
**asciinema rec \-\-raw output.txt**.
|
||||||
|
|
||||||
upload <filename>
|
|
||||||
---
|
upload <_filename_>
|
||||||
|
------
|
||||||
|
|
||||||
Upload recorded asciicast to asciinema.org site.
|
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.
|
publishing it on asciinema.org.
|
||||||
|
|
||||||
auth
|
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
|
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.
|
account.
|
||||||
|
|
||||||
This command displays the URL to open in a web browser to do that. You may be
|
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.
|
happen in any order.
|
||||||
|
|
||||||
Note: A new install ID is generated on each machine and system user account you use
|
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
|
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.
|
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
|
Note: asciinema versions prior to 2.0 confusingly referred to install ID as "API
|
||||||
token".
|
token".
|
||||||
|
|
||||||
|
|
||||||
EXAMPLES
|
EXAMPLES
|
||||||
========
|
========
|
||||||
|
|
||||||
@@ -200,6 +235,10 @@ Record your first session:
|
|||||||
|
|
||||||
asciinema rec first.cast
|
asciinema rec first.cast
|
||||||
|
|
||||||
|
End your session:
|
||||||
|
|
||||||
|
exit
|
||||||
|
|
||||||
Now replay it with double speed:
|
Now replay it with double speed:
|
||||||
|
|
||||||
asciinema play -s 2 first.cast
|
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
|
You'll be asked to confirm the upload when the recording is done, so nothing is
|
||||||
sent anywhere without your consent.
|
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
|
ENVIRONMENT
|
||||||
===========
|
===========
|
||||||
|
|
||||||
**ASCIINEMA_API_URL**
|
**ASCIINEMA_API_URL**
|
||||||
|
|
||||||
: This variable allows overriding asciinema-server URL (which defaults to
|
: This variable allows overriding asciinema-server URL (which defaults to
|
||||||
https://asciinema.org) in case you're running your own asciinema-server instance.
|
https://asciinema.org) in case you're running your own asciinema-server instance.
|
||||||
|
|
||||||
**ASCIINEMA_CONFIG_HOME**
|
**ASCIINEMA_CONFIG_HOME**
|
||||||
|
|
||||||
: This variable allows overriding config directory location. Default location
|
: This variable allows overriding config directory location. Default location
|
||||||
is $XDG\_CONFIG\_HOME/asciinema (when $XDG\_CONFIG\_HOME is set)
|
is $XDG\_CONFIG\_HOME/asciinema (when $XDG\_CONFIG\_HOME is set)
|
||||||
or $HOME/.config/asciinema.
|
or $HOME/.config/asciinema.
|
||||||
|
|
||||||
|
|
||||||
BUGS
|
BUGS
|
||||||
====
|
====
|
||||||
|
|
||||||
See GitHub Issues: <https://github.com/asciinema/asciinema/issues>
|
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
|
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>
|
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.
|
This Manual Page was written by Marcin Kulik with help from Kurt Pfeifle.
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,6 @@
|
|||||||
[metadata]
|
[metadata]
|
||||||
description-file = README.md
|
description-file = README.md
|
||||||
|
license_file = LICENSE
|
||||||
|
|
||||||
|
[pycodestyle]
|
||||||
|
ignore = E501,E402,E722
|
||||||
|
|||||||
5
setup.py
5
setup.py
@@ -2,12 +2,11 @@ import asciinema
|
|||||||
import sys
|
import sys
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
if sys.version_info[0] < 3:
|
if sys.version_info.major < 3:
|
||||||
sys.exit('Python < 3 is unsupported.')
|
sys.exit('Python < 3 is unsupported.')
|
||||||
|
|
||||||
url_template = 'https://github.com/asciinema/asciinema/archive/v%s.tar.gz'
|
url_template = 'https://github.com/asciinema/asciinema/archive/v%s.tar.gz'
|
||||||
requirements = []
|
requirements = []
|
||||||
test_requirements = ['nose']
|
|
||||||
|
|
||||||
with open('README.md', encoding='utf8') as file:
|
with open('README.md', encoding='utf8') as file:
|
||||||
long_description = file.read()
|
long_description = file.read()
|
||||||
@@ -29,6 +28,7 @@ setup(
|
|||||||
'asciinema = asciinema.__main__:main',
|
'asciinema = asciinema.__main__:main',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
package_data={'asciinema': ['data/*.png']},
|
||||||
data_files=[('share/doc/asciinema', ['CHANGELOG.md',
|
data_files=[('share/doc/asciinema', ['CHANGELOG.md',
|
||||||
'CODE_OF_CONDUCT.md',
|
'CODE_OF_CONDUCT.md',
|
||||||
'CONTRIBUTING.md',
|
'CONTRIBUTING.md',
|
||||||
@@ -37,7 +37,6 @@ setup(
|
|||||||
'doc/asciicast-v2.md']),
|
'doc/asciicast-v2.md']),
|
||||||
('share/man/man1', ['man/asciinema.1'])],
|
('share/man/man1', ['man/asciinema.1'])],
|
||||||
install_requires=requirements,
|
install_requires=requirements,
|
||||||
tests_require=test_requirements,
|
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 5 - Production/Stable',
|
'Development Status :: 5 - Production/Stable',
|
||||||
'Environment :: Console',
|
'Environment :: Console',
|
||||||
|
|||||||
@@ -187,3 +187,22 @@ def test_play_idle_time_limit():
|
|||||||
|
|
||||||
config = create_config("[play]\nmaxwait = 2.35")
|
config = create_config("[play]\nmaxwait = 2.35")
|
||||||
assert_equal(2.35, config.play_idle_time_limit)
|
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)
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ class FakeStdout:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.data = []
|
self.data = []
|
||||||
|
|
||||||
def write_stdout(self, data):
|
def write_stdout(self, ts, data):
|
||||||
self.data.append(data)
|
self.data.append(data)
|
||||||
|
|
||||||
def write_stdin(self, data):
|
def write_stdin(self, ts, data):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user