Skip to content

Commit

Permalink
Merge branch 'release/2.0.1'
Browse files Browse the repository at this point in the history
  • Loading branch information
ku1ik committed Apr 4, 2018
2 parents 0e6c310 + 48e5dbf commit f173abe
Show file tree
Hide file tree
Showing 15 changed files with 176 additions and 82 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,12 @@
# asciinema changelog

## 2.0.1 (2018-04-04)

* Fixed example in asciicast v2 format doc (thanks Josh "@anowlcalledjosh" Holland!)
* Replaced deprecated `encodestring` (since Python 3.1) with `encodebytes` (thanks @delirious-lettuce!)
* Fixed location of config dir (you can `mv ~/.asciinema ~/.config/asciinema`)
* Internal refactorings

## 2.0 (2018-02-10)

This major release brings many new features, improvements and bugfixes. The most
Expand Down
2 changes: 1 addition & 1 deletion asciinema/__init__.py
@@ -1,7 +1,7 @@
import sys

__author__ = 'Marcin Kulik'
__version__ = '2.0.0'
__version__ = '2.0.1'

if sys.version_info[0] < 3:
raise ImportError('Python < 3 is unsupported.')
28 changes: 28 additions & 0 deletions asciinema/asciicast/events.py
@@ -0,0 +1,28 @@
def to_relative_time(events):
prev_time = 0

for frame in events:
time, type, data = frame
delay = time - prev_time
prev_time = time
yield [delay, type, data]


def to_absolute_time(events):
time = 0

for frame in events:
delay, type, data = frame
time = time + delay
yield [time, type, data]


def cap_relative_time(events, time_limit):
if time_limit:
return ([min(delay, time_limit), type, data] for delay, type, data in events)
else:
return events


def adjust_speed(events, speed):
return ([delay / speed, type, data] for delay, type, data in events)
28 changes: 0 additions & 28 deletions asciinema/asciicast/frames.py

This file was deleted.

25 changes: 19 additions & 6 deletions asciinema/asciicast/v1.py
@@ -1,7 +1,7 @@
import json
import json.decoder

from asciinema.asciicast.frames import to_absolute_time
from asciinema.asciicast.events import to_absolute_time


try:
Expand All @@ -16,13 +16,26 @@ class LoadError(Exception):

class Asciicast:

def __init__(self, stdout):
def __init__(self, attrs):
self.version = 1
self.__stdout = stdout
self.__attrs = attrs
self.idle_time_limit = None # v1 doesn't store it

def stdout(self):
return to_absolute_time(self.__stdout)
@property
def v2_header(self):
keys = ['width', 'height', 'duration', 'command', 'title', 'env']
header = {k: v for k, v in self.__attrs.items() if k in keys and v is not None}
return header

def __stdout_events(self):
for time, data in self.__attrs['stdout']:
yield [time, 'o', data]

def events(self):
return self.stdout_events()

def stdout_events(self):
return to_absolute_time(self.__stdout_events())


class open_from_file():
Expand All @@ -37,7 +50,7 @@ def __enter__(self):
attrs = json.loads(self.first_line + self.file.read())

if attrs.get('version') == 1:
return Asciicast(attrs['stdout'])
return Asciicast(attrs)
else:
raise LoadError(self.FORMAT_ERROR)
except JSONDecodeError as e:
Expand Down
109 changes: 79 additions & 30 deletions asciinema/asciicast/v2.py
Expand Up @@ -21,22 +21,24 @@ class LoadError(Exception):

class Asciicast:

def __init__(self, f, idle_time_limit):
def __init__(self, f, header):
self.version = 2
self.__file = f
self.idle_time_limit = idle_time_limit
self.v2_header = header
self.idle_time_limit = header.get('idle_time_limit')

def stdout(self):
def events(self):
for line in self.__file:
time, type, data = json.loads(line)
yield json.loads(line)

def stdout_events(self):
for time, type, data in self.events():
if type == 'o':
yield [time, data]
yield [time, type, data]


def build_from_header_and_file(header, f):
idle_time_limit = header.get('idle_time_limit')
return Asciicast(f, idle_time_limit)
return Asciicast(f, header)


class open_from_file():
Expand Down Expand Up @@ -64,38 +66,91 @@ def get_duration(path):
with open(path, mode='rt', encoding='utf-8') as f:
first_line = f.readline()
with open_from_file(first_line, f) as a:
for last_frame in a.stdout():
for last_frame in a.stdout_events():
pass
return last_frame[0]


def write_json_lines_from_queue(path, mode, queue):
with open(path, mode=mode, buffering=1) as f:
for json_value in iter(queue.get, None):
line = json.dumps(json_value, ensure_ascii=False, indent=None, separators=(', ', ': '))
f.write(line + '\n')
class writer():

def __init__(self, path, width=None, height=None, header=None, mode='w', buffering=-1):
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:
self.header = None

class writer():
def __enter__(self):
self.file = open(self.path, mode=self.mode, buffering=self.buffering)

if self.header:
self.__write_line(self.header)

return self

def __exit__(self, exc_type, exc_value, exc_traceback):
self.file.close()

def write_event(self, ts, etype=None, data=None):
if etype is None:
ts, etype, data = ts

ts = round(ts, 6)

if etype == 'o':
if type(data) == str:
data = data.encode(encoding='utf-8', errors='strict')
text = self.stdout_decoder.decode(data)
self.__write_line([ts, etype, text])
elif etype == 'i':
if type(data) == str:
data = data.encode(encoding='utf-8', errors='strict')
text = self.stdin_decoder.decode(data)
self.__write_line([ts, etype, text])
else:
self.__write_line([ts, etype, data])

def write_stdout(self, ts, data):
self.write_event(ts, 'o', data)

def write_stdin(self, ts, data):
self.write_event(ts, 'i', 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, header, rec_stdin, start_time_offset=0):
self.path = path
self.header = header
self.rec_stdin = rec_stdin
self.start_time_offset = start_time_offset
self.queue = Queue()
self.stdin_decoder = codecs.getincrementaldecoder('UTF-8')('replace')
self.stdout_decoder = codecs.getincrementaldecoder('UTF-8')('replace')

def __enter__(self):
mode = 'a' if self.start_time_offset > 0 else 'w'
self.process = Process(
target=write_json_lines_from_queue,
args=(self.path, mode, self.queue)
args=(self.path, self.header, mode, self.queue)
)
self.process.start()
if self.start_time_offset == 0:
self.queue.put(self.header)
self.start_time = time.time() - self.start_time_offset
return self

Expand All @@ -105,18 +160,12 @@ def __exit__(self, exc_type, exc_value, exc_traceback):

def write_stdin(self, data):
if self.rec_stdin:
text = self.stdin_decoder.decode(data)

if text:
ts = round(time.time() - self.start_time, 6)
self.queue.put([ts, 'i', text])
ts = time.time() - self.start_time
self.queue.put([ts, 'i', data])

def write_stdout(self, data):
text = self.stdout_decoder.decode(data)

if text:
ts = round(time.time() - self.start_time, 6)
self.queue.put([ts, 'o', text])
ts = time.time() - self.start_time
self.queue.put([ts, 'o', data])


class Recorder:
Expand Down Expand Up @@ -149,5 +198,5 @@ def record(self, path, append, command, command_env, captured_env, rec_stdin, ti
if title:
header['title'] = title

with writer(path, header, rec_stdin, start_time_offset) as w:
with async_writer(path, header, rec_stdin, start_time_offset) as w:
self.pty_recorder.record_command(['sh', '-c', command], w, command_env)
2 changes: 1 addition & 1 deletion asciinema/commands/cat.py
Expand Up @@ -13,7 +13,7 @@ def __init__(self, filename):
def execute(self):
try:
with asciicast.open_from_url(self.filename) as a:
for t, text in a.stdout():
for t, _type, text in a.stdout_events():
sys.stdout.write(text)
sys.stdout.flush()

Expand Down
7 changes: 4 additions & 3 deletions asciinema/config.py
Expand Up @@ -131,10 +131,11 @@ def get_config_home(env=os.environ):
elif env_xdg_config_home:
config_home = path.join(env_xdg_config_home, "asciinema")
elif env_home:
if path.isfile(path.join(env_home, ".config", "asciinema", "config")):
config_home = path.join(env_home, ".config", "asciinema")
if path.isfile(path.join(env_home, ".asciinema", "config")):
# location for versions < 1.1
config_home = path.join(env_home, ".asciinema")
else:
config_home = path.join(env_home, ".asciinema") # location for versions < 1.1
config_home = path.join(env_home, ".config", "asciinema")
else:
raise Exception("need $HOME or $XDG_CONFIG_HOME or $ASCIINEMA_CONFIG_HOME")

Expand Down
14 changes: 7 additions & 7 deletions asciinema/player.py
Expand Up @@ -2,7 +2,7 @@
import sys
import time

import asciinema.asciicast.frames as frames
import asciinema.asciicast.events as ev
from asciinema.term import raw, read_blocking


Expand All @@ -19,18 +19,18 @@ def play(self, asciicast, idle_time_limit=None, speed=1.0):
def _play(self, asciicast, idle_time_limit, speed, stdin):
idle_time_limit = idle_time_limit or asciicast.idle_time_limit

stdout = asciicast.stdout()
stdout = frames.to_relative_time(stdout)
stdout = frames.cap_relative_time(stdout, idle_time_limit)
stdout = frames.to_absolute_time(stdout)
stdout = frames.adjust_speed(stdout, speed)
stdout = asciicast.stdout_events()
stdout = ev.to_relative_time(stdout)
stdout = ev.cap_relative_time(stdout, idle_time_limit)
stdout = ev.to_absolute_time(stdout)
stdout = ev.adjust_speed(stdout, speed)

base_time = time.time()
ctrl_c = False
paused = False
pause_time = None

for t, text in stdout:
for t, _type, text in stdout:
delay = t - (time.time() - base_time)

while stdin and not ctrl_c and delay > 0:
Expand Down
2 changes: 1 addition & 1 deletion asciinema/urllib_http_adapter.py
Expand Up @@ -67,7 +67,7 @@ def post(self, url, fields={}, files={}, headers={}, username=None, password=Non

if password:
auth = "%s:%s" % (username, password)
encoded_auth = base64.encodestring(auth.encode('utf-8'))[:-1]
encoded_auth = base64.encodebytes(auth.encode('utf-8'))[:-1]
headers["Authorization"] = b"Basic " + encoded_auth

request = Request(url, data=body, headers=headers, method="POST")
Expand Down
4 changes: 2 additions & 2 deletions doc/asciicast-v2.md
Expand Up @@ -71,8 +71,8 @@ Example env:

```json
"env": {
"SHELL": "xterm-256color",
"TERM": "/bin/bash"
"SHELL": "/bin/bash",
"TERM": "xterm-256color"
}
```

Expand Down
4 changes: 2 additions & 2 deletions man/asciinema.1
@@ -1,6 +1,6 @@
.\" Automatically generated by Pandoc 2.1.1
.\" Automatically generated by Pandoc 2.1.3
.\"
.TH "ASCIINEMA" "1" "" "Version 2.0" "asciinema"
.TH "ASCIINEMA" "1" "" "Version 2.0.1" "asciinema"
.hy
.SH NAME
.PP
Expand Down
2 changes: 1 addition & 1 deletion man/asciinema.1.md
@@ -1,4 +1,4 @@
% ASCIINEMA(1) Version 2.0 | asciinema
% ASCIINEMA(1) Version 2.0.1 | asciinema

NAME
====
Expand Down
Empty file added tests/asciicast/__init__.py
Empty file.

0 comments on commit f173abe

Please sign in to comment.