"""
This module defines a built-in contrib module that enables terminal embedding
within a slide.
"""
import re
import shlex
from typing import Dict
import urwid
import yaml
from marshmallow import Schema, fields
import lookatme.config
import lookatme.render.markdown_block
from lookatme.exceptions import IgnoredByContrib
[docs]def user_warnings():
"""Provide warnings to the user that loading this extension may cause
shell commands specified in the markdown to be run.
"""
return [
"Code-blocks with a language starting with 'terminal' will cause shell",
" commands from the source markdown to be run",
"See https://lookatme.readthedocs.io/en/latest/builtin_extensions/terminal.html",
" for more details",
]
[docs]class YamlRender:
[docs] @staticmethod
def loads(data): return yaml.safe_load(data)
[docs] @staticmethod
def dumps(data): return yaml.safe_dump(data)
[docs]class TerminalExSchema(Schema):
"""The schema used for ``terminal-ex`` code blocks.
"""
command = fields.Str()
rows = fields.Int(dump_default=10, load_default=10)
init_text = fields.Str(dump_default=None, load_default=None)
init_wait = fields.Str(dump_default=None, load_default=None)
init_codeblock = fields.Bool(dump_default=True, load_default=True)
init_codeblock_lang = fields.Str(dump_default="text", load_default="text")
[docs] def loads(self, *args, **kwargs) -> Dict:
res = super(self.__class__, self).loads(*args, **kwargs)
if res is None:
raise ValueError("Could not loads")
return res
[docs] def load(self, *args, **kwargs) -> Dict:
res = super(self.__class__, self).load(*args, **kwargs)
if res is None:
raise ValueError("Could not load")
return res
CREATED_TERMS = []
[docs]def render_code(token, body, stack, loop):
lang = token["lang"] or ""
numbered_term_match = re.match(r'terminal(\d+)', lang)
if lang != "terminal-ex" and numbered_term_match is None:
raise IgnoredByContrib
if numbered_term_match is not None:
term_data = TerminalExSchema().load({
"command": token["text"].strip(),
"rows": int(numbered_term_match.group(1)),
"init_codeblock": False,
})
else:
term_data = TerminalExSchema().loads(token["text"])
if term_data["init_text"] is not None and term_data["init_wait"] is not None:
term_data["command"] = " ".join([shlex.quote(x) for x in [
"expect", "-c", ";".join([
'spawn -noecho {}'.format(term_data["command"]),
'expect {{{}}}'.format(term_data["init_wait"]),
'send {{{}}}'.format(term_data["init_text"]),
'interact',
'exit',
])
]])
term = urwid.Terminal(
shlex.split(term_data["command"].strip()),
main_loop=loop,
encoding="utf8",
)
CREATED_TERMS.append(term)
line_box = urwid.LineBox(urwid.BoxAdapter(term, height=term_data["rows"]))
line_box.no_cache = ["render"]
res = []
if term_data["init_codeblock"] is True:
fake_token = {
"text": term_data["init_text"],
"lang": term_data["init_codeblock_lang"],
}
res += lookatme.render.markdown_block.render_code(
fake_token, body, stack, loop
)
res += [
urwid.Divider(),
line_box,
urwid.Divider(),
]
return res
[docs]def shutdown():
for idx, term in enumerate(CREATED_TERMS):
lookatme.config.get_log().debug(
f"Terminating terminal {idx+1}/{len(CREATED_TERMS)}")
if term.pid is not None:
term.terminate()