Skip to content
This repository has been archived by the owner on Jun 16, 2020. It is now read-only.

Commit

Permalink
Add font option for the command line
Browse files Browse the repository at this point in the history
With a contribution by ylmrx (Pull request #3)
  • Loading branch information
nbedos committed Jul 1, 2018
1 parent 6940495 commit d930740
Show file tree
Hide file tree
Showing 10 changed files with 1,333 additions and 59 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ examples:
$(VENV_ACTIVATE) && \
for cast_file in $$(find $(CASTS_DIR) -name '*.cast'); do \
svg_file="$(EXAMPLES_DIR)/$$(basename --suffix=.cast $$cast_file).svg" && \
termtosvg render "$$cast_file" "$$svg_file"; \
termtosvg render "$$cast_file" "$$svg_file" --font 'DejaVu Sans Mono'; \
done

1,261 changes: 1,259 additions & 2 deletions examples/awesome.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 changes: 44 additions & 28 deletions termtosvg/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,23 @@
logger = logging.getLogger('termtosvg')
LOG_FILENAME = os.path.join(tempfile.gettempdir(), 'termtosvg.log')

USAGE = """termtosvg [output_file] [--theme THEME] [--help] [--verbose]
USAGE = """termtosvg [output_file] [--font FONT] [--theme THEME] [--help] [--verbose]
Record a terminal session and render an SVG animation on the fly
"""
EPILOG = "See also 'termtosvg record --help' and 'termtosvg render --help'"
RECORD_USAGE = """termtosvg record [output_file] [--verbose] [--help]"""
RENDER_USAGE = """termtosvg render input_file [output_file] [--theme THEME] [--verbose] [--help]"""
RENDER_USAGE = """termtosvg render input_file [output_file] [--font FONT] [--theme THEME] """ \
"""[--verbose] [--help]"""


def parse(args, themes):
# type: (List) -> Tuple[Union[None, str], argparse.Namespace]
# Usage: termtosvg [--theme THEME] [--verbose] [output_file]
verbose_parser = argparse.ArgumentParser(add_help=False)
verbose_parser.add_argument(
'-v',
'--verbose',
action='store_true',
help='increase log messages verbosity'
# Usage: termtosvg [output_file] [--font FONT] [--theme THEME] [--verbose]
font_parser = argparse.ArgumentParser(add_help=False)
font_parser.add_argument(
'--font',
help='CSS font to use during rendering',
metavar='FONT'
)
theme_parser = argparse.ArgumentParser(add_help=False)
theme_parser.add_argument(
Expand All @@ -40,9 +40,16 @@ def parse(args, themes):
choices=themes,
metavar='THEME'
)
verbose_parser = argparse.ArgumentParser(add_help=False)
verbose_parser.add_argument(
'-v',
'--verbose',
action='store_true',
help='increase log messages verbosity'
)
parser = argparse.ArgumentParser(
prog='termtosvg',
parents=[theme_parser, verbose_parser],
parents=[font_parser, theme_parser, verbose_parser],
usage=USAGE,
epilog=EPILOG
)
Expand All @@ -69,10 +76,10 @@ def parse(args, themes):
)
return 'record', parser.parse_args(args[1:])
elif args[0] == 'render':
# Usage: termtosvg render [--theme THEME] [--verbose] input_file [output_file]
# Usage: termtosvg render [--font FONT] [--theme THEME] [--verbose] input_file [output_file]
parser = argparse.ArgumentParser(
description='render an asciicast recording as an SVG animation',
parents=[theme_parser, verbose_parser],
parents=[font_parser, theme_parser, verbose_parser],
usage=RENDER_USAGE
)
parser.add_argument(
Expand Down Expand Up @@ -130,7 +137,7 @@ def main(args=None, input_fileno=None, output_fileno=None):

columns, lines = term.get_terminal_size(output_fileno)
with term.TerminalMode(input_fileno):
records = term.record(columns, lines, None, input_fileno, output_fileno)
records = term.record(columns, lines, input_fileno, output_fileno)
with open(cast_filename, 'w') as cast_file:
for record in records:
print(record.to_json_line(), file=cast_file)
Expand All @@ -148,16 +155,20 @@ def rec_gen():
else:
svg_filename = args.output_file

font = configuration['GLOBAL']['font']
if args.theme is None:
theme_name = configuration['GLOBAL']['theme']
theme = configuration[theme_name]
if args.font is None:
font = configuration['GLOBAL']['font']
else:
theme = configuration[args.theme]
font = args.font

fallback_theme_name = configuration['GLOBAL']['theme']
fallback_theme = configuration[fallback_theme_name]
cli_theme = configuration.get(args.theme)

replayed_records = term.replay(rec_gen(), anim.CharacterCell.from_pyte, theme)
anim.render_animation(replayed_records, svg_filename)
replayed_records = term.replay(records=rec_gen(),
from_pyte_char=anim.CharacterCell.from_pyte,
override_theme=cli_theme,
fallback_theme=fallback_theme)
anim.render_animation(replayed_records, svg_filename, font)

logger.info('Rendering ended, SVG animation is {}'.format(svg_filename))
else:
Expand All @@ -169,17 +180,22 @@ def rec_gen():
svg_filename = args.output_file

columns, lines = term.get_terminal_size(output_fileno)
font = configuration['GLOBAL']['font']
if args.theme is None:
theme_name = configuration['GLOBAL']['theme']
theme = configuration[theme_name]

if args.font is None:
font = configuration['GLOBAL']['font']
else:
theme = configuration[args.theme]
font = args.font

fallback_theme_name = configuration['GLOBAL']['theme']
fallback_theme = configuration[fallback_theme_name]
cli_theme = configuration.get(args.theme)
with term.TerminalMode(input_fileno):
records = term.record(columns, lines, theme, input_fileno, output_fileno)
replayed_records = term.replay(records, anim.CharacterCell.from_pyte, theme)
anim.render_animation(replayed_records, svg_filename)
records = term.record(columns, lines, input_fileno, output_fileno)
replayed_records = term.replay(records=records,
from_pyte_char=anim.CharacterCell.from_pyte,
override_theme=cli_theme,
fallback_theme=fallback_theme)
anim.render_animation(replayed_records, svg_filename, font)

logger.info('Recording ended, SVG animation is {}'.format(svg_filename))

Expand Down
11 changes: 5 additions & 6 deletions termtosvg/anim.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,22 +155,21 @@ def make_text(group: List[int]) -> svgwrite.text.Text:
return texts


def render_animation(records, filename, end_pause=1, cell_width=8, cell_height=17):
# type: (Iterable[CharacterCellRecord], str, int) -> None
if end_pause < 0:
raise ValueError('Invalid end_pause (must be >= 0): "{}"'.format(end_pause))
def render_animation(records, filename, font, font_size=14, cell_width=8, cell_height=17, end_pause=1):
# type: (Iterable[CharacterCellRecord], str, str, int, int, int, int) -> None
if end_pause <= 0:
raise ValueError('Invalid end_pause (must be > 0): "{}"'.format(end_pause))

if not isinstance(records, Iterator):
records = iter(records)

header = next(records)

font_size = 14
css = {
# Apply this style to each and every element since we are using coordinates that
# depend on the size of the font
'*': {
'font-family': '"DejaVu Sans Mono", monospace',
'font-family': '"{}", monospace'.format(font),
'font-style': 'normal',
'font-size': '{}px'.format(font_size),
},
Expand Down
2 changes: 1 addition & 1 deletion termtosvg/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,6 @@ def init_read_conf():
os.makedirs(config_dir, exist_ok=True)
with open(config_path, 'w') as config_file:
config_file.write(DEFAULT_CONFIG)
logger.info('Created default configuration file: {}'.format(config_path))
logger.info('Created user configuration file: {}'.format(config_path))

return get_configuration(user_config, DEFAULT_CONFIG)
4 changes: 2 additions & 2 deletions termtosvg/data/termtosvg.ini
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

[global]
; properties of this section:
; - FONT: CSS font family used in the SVG animation (Deja Vu Sans Mono,
; - FONT: CSS font family used in the SVG animation (DejaVu Sans Mono,
; Lucida Console, Monaco...)
; Beware that termtosvg does not check the validity of the font. Please
; also note that the font is not embedded in the SVG animation so you
Expand All @@ -21,7 +21,7 @@
; classic-dark, classic-light, dracula, isotope, marrakesh, material,
; monokai, solarized-dark, solarized-light, zenburn)

font = Deja Vu Sans Mono
font = DejaVu Sans Mono
theme = solarized-dark


Expand Down
21 changes: 12 additions & 9 deletions termtosvg/term.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,15 @@ def __exit__(self, exc_type, exc_val, exc_tb):
tty.tcsetattr(self.fileno, tty.TCSAFLUSH, self.mode)


def record(columns, lines, theme, input_fileno, output_fileno):
# type: (int, int, Union[AsciiCastTheme, None], int, int) -> Generator[Union[AsciiCastHeader, AsciiCastEvent], None, None]
def record(columns, lines, input_fileno, output_fileno):
# type: (int, int, int, int) -> Generator[Union[AsciiCastHeader, AsciiCastEvent], None, None]
"""Record a terminal session in asciicast v2 format
The records returned are of two types:
- a single header with configuration information
- multiple event records with data captured from the terminal and timing information
"""
yield AsciiCastHeader(version=2, width=columns, height=lines, theme=theme)
yield AsciiCastHeader(version=2, width=columns, height=lines, theme=None)

start = None
for data, time in _record(columns, lines, input_fileno, output_fileno):
Expand Down Expand Up @@ -187,8 +187,8 @@ def _group_by_time(event_records, min_rec_duration, last_rec_duration):
yield accumulator_event


def replay(records, from_pyte_char, theme, min_frame_duration=0.001, last_frame_duration=1):
# type: (Iterable[Union[AsciiCastHeader, AsciiCastEvent]], Callable[[pyte.screen.Char, Dict[Any, str]], Any], Union[None, AsciiCastTheme], float, float) -> Generator[CharacterCellRecord, None, None]
def replay(records, from_pyte_char, override_theme, fallback_theme, min_frame_duration=0.001, last_frame_duration=1):
# type: (Iterable[Union[AsciiCastHeader, AsciiCastEvent]], Callable[[pyte.screen.Char, Dict[Any, str]], Any], Union[None, AsciiCastTheme], AsciiCastTheme, float, float) -> Generator[CharacterCellRecord, None, None]
"""Read the records of a terminal sessions, render the corresponding screens and return lines
of the screen that need updating.
Expand All @@ -202,6 +202,9 @@ def replay(records, from_pyte_char, theme, min_frame_duration=0.001, last_frame_
:param records: Records of the terminal session in asciicast v2 format. The first record must
be a header, which must be followed by event records.
:param from_pyte_char: Conversion function from pyte.screen.Char to any other format
:param override_theme: Color theme. If present, overrides the theme include in asciicast header
:param fallback_theme: Color theme. Used as a fallback override_theme is missing and no
theme is included in asciicast header
:param min_frame_duration: Minimum frame duration in seconds. SVG animations break when an
animation is 0s so setting this to at least 1ms is recommended.
:param last_frame_duration: Last frame duration in seconds
Expand All @@ -220,12 +223,12 @@ def sort_by_time(d, row):
screen = pyte.Screen(header.width, header.height)
stream = pyte.ByteStream(screen)

if theme is not None:
pass
elif theme is None and header.theme is not None:
if override_theme is not None:
theme = override_theme
elif header.theme is not None:
theme = header.theme
else:
raise ValueError('No valid theme')
theme = fallback_theme

config = CharacterCellConfig(width=header.width,
height=header.height,
Expand Down
2 changes: 1 addition & 1 deletion tests/test_anim.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,5 +154,5 @@ def line(i):
]

_, filename = tempfile.mkstemp(prefix='termtosvg_')
anim.render_animation(records, filename)
anim.render_animation(records, filename, 'DejaVu Sans Mono')
os.remove(filename)
10 changes: 5 additions & 5 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

MINIMAL_CONFIG = """[GLOBAL]
theme=dark
font=Deja Vu Sans Mono
font=DejaVu Sans Mono
[dark]
foreground=#FFFFFF
background=#000000
Expand All @@ -27,7 +27,7 @@
WRONG_THEME_CONFIG = MINIMAL_CONFIG.replace('theme=dark', 'theme=white')
DUPLICATES_CONFIG = MINIMAL_CONFIG.replace('theme=dark',
'font=courrier\r\ntheme=dark\r\ntheme=white\r\n[dark]')
OVERRIDE_CONFIG = MINIMAL_CONFIG.replace('#000000', '#FFFFFF').replace('Deja Vu Sans Mono', 'mono')
OVERRIDE_CONFIG = MINIMAL_CONFIG.replace('#000000', '#FFFFFF').replace('DejaVu Sans Mono', 'mono')


class TestConf(unittest.TestCase):
Expand All @@ -39,13 +39,13 @@ def test_conf_to_dict(self):
for case, configuration in test_cases:
with self.subTest(case=case):
config_dict = config.conf_to_dict(configuration)
self.assertEqual(config_dict['GlOBal']['font'].lower(), 'deja vu sans mono')
self.assertEqual(config_dict['GlOBal']['font'].lower(), 'dejavu sans mono')
self.assertEqual(config_dict['Dark'].fg.lower(), '#ffffff')
self.assertEqual(config_dict['dark'].bg.lower(), '#000000')

with self.subTest(case='minimal config'):
config_dict = config.conf_to_dict(MINIMAL_CONFIG)
self.assertEqual(config_dict['GLOBAL']['font'], 'Deja Vu Sans Mono')
self.assertEqual(config_dict['GLOBAL']['font'], 'DejaVu Sans Mono')
self.assertEqual(config_dict['dark'].fg.lower(), '#ffffff')
self.assertEqual(config_dict['dark'].bg.lower(), '#000000')

Expand All @@ -62,7 +62,7 @@ def test_get_configuration(self):
for case, user_config, default_config in test_cases:
with self.subTest(case=case):
config_dict = config.get_configuration(user_config, default_config)
self.assertEqual(config_dict['GLOBAL']['font'], 'Deja Vu Sans Mono')
self.assertEqual(config_dict['GLOBAL']['font'], 'DejaVu Sans Mono')
self.assertEqual(config_dict['solarized-dark'].fg.lower(), '#93a1a1')
self.assertEqual(config_dict['solarized-dark'].bg.lower(), '#002b36')
palette = config_dict['solarized-dark'].palette.split(':')
Expand Down
7 changes: 3 additions & 4 deletions tests/test_term.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ def test_record(self):

lines = 24
columns = 80
theme = AsciiCastTheme('#000000', '#111111', ':'.join(['#123456']*8))

pid = os.fork()
if pid == 0:
Expand All @@ -65,7 +64,7 @@ def test_record(self):

# Parent process
with term.TerminalMode(fd_in_read):
for _ in term.record(columns, lines, theme, fd_in_read, fd_out_write):
for _ in term.record(columns, lines, fd_in_read, fd_out_write):
pass

os.waitpid(pid, 0)
Expand All @@ -89,7 +88,7 @@ def pyte_to_str(x, _):
duration=None)
for i in range(1, nbr_records)]

records = term.replay(records, pyte_to_str, fallback_theme, 50, 1000)
records = term.replay(records, pyte_to_str, None, fallback_theme, 50, 1000)
# Last blank line is the cursor
lines = [str(i) for i in range(nbr_records)] + [' ']
for i, record in enumerate(records):
Expand All @@ -108,7 +107,7 @@ def pyte_to_str(x, _):
for i, data in enumerate(commands)]

screen = {}
for record in term.replay(records, pyte_to_str, theme, 50, 1000):
for record in term.replay(records, pyte_to_str, None, theme, 50, 1000):
if hasattr(record, 'line'):
screen[record.row] = ''.join(record.line[i] for i in sorted(record.line))

Expand Down

0 comments on commit d930740

Please sign in to comment.