commit 3f631fe1ab6791663d8481563958ac396bba9f23 Author: haskal Date: Tue Jan 15 14:31:28 2019 -0500 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..83456b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.venv +*.pyc +*.pyo +*.pyd +__pycache__ +*.egg-info +.env +.ycm_extra_conf.py diff --git a/asciicast2latex/__init__.py b/asciicast2latex/__init__.py new file mode 100644 index 0000000..505d68f --- /dev/null +++ b/asciicast2latex/__init__.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 + +import ast +import json +from pathlib import Path +import pyte +import re +import shutil +import subprocess +import sys +import tempfile + +def dconf_ls(path): + output = subprocess.check_output(["dconf", "list", str(path) + "/"]) + return [path / line for line in output.decode().strip().split("\n")] + +def dconf_read(path): + output = subprocess.check_output(["dconf", "read", str(path)]) + return ast.literal_eval(output.decode()) + +def rgb_to_arr(rgb): + return "".join(["{0:02x}".format(int(x)) for x in re.findall("[0-9]+", rgb)]) + +def get_terminal_conf(): + profiles = dconf_ls(Path("/org/gnome/terminal/legacy/profiles:/")) + bgc = dconf_read(profiles[0] / "background-color") + fgc = dconf_read(profiles[0] / "foreground-color") + palette = dconf_read(profiles[0] / "palette") + return (rgb_to_arr(bgc), + rgb_to_arr(fgc), + [rgb_to_arr(c) for c in palette]) + +def get_screen(filename): + width = 0 + height = 0 + data = "" + with open(filename, "r") as file: + hdr = True + for strline in file: + line = json.loads(strline) + if hdr: + width = line['width'] + height = line['height'] + hdr = False + else: + data += line[2] + # snip out weird escape code (??? what is this, some sort of window title?) + data = re.sub(r'\x1b_.*?\x1b\\', "", data) + screen = pyte.screens.HistoryScreen(width, height, history=9999999) + stream = pyte.Stream(screen) + stream.feed(data) + lines = [l for l in screen.history.top] + for i in range(len(screen.buffer)): + lines.append(screen.buffer[i]) + return (width, height, lines) + +def convert(filename, output): + print("Generating latex") + (bgc, fgc, palette) = get_terminal_conf() + print(bgc, fgc, palette) + (w, h, scr)= get_screen(filename) + latexsrc = toTikz(scr, w, len(scr), bgc, fgc, palette) + print("Compiling") + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + with (tmpdir / "console.tex").open("w") as file: + file.write(latexsrc) + subprocess.check_call(["xelatex", "console.tex"], cwd=str(tmpdir)) + shutil.copyfile(tmpdir / "console.pdf", output) + +# adapted from https://github.com/misc0110/asciicast2vector (MIT License) + +def sanitizeLatexChar(c): + if c in "&%$#_{}": + return "\\" + c + elif c == "~": + return "\\textasciitilde" + elif c == "^": + return "\\textasciicircum" + elif c == "\\": + return "\\textbackslash" + else: + return c + + +def toTikz(lines, max_col, max_row, bgc, fgc, palette): + pline = "" + + pline += tikzHeader(lines, bgc, fgc, palette) + + pline += "\\begin{tikzpicture}[yscale=-1]\\ttfamily\n" + lspace = 1.8 + pline += "\\draw[fill=%s,draw=none] (-1em,-1em) rectangle +(%.4fem,%dem);\n" % ("defaultbg", (max_col + 4) / lspace, max_row + 2) + for y in range(len(lines)): + line = lines[y] + for x in range(len(line)): + char = line[x] + if char.data == ' ' and ((char.reverse == False and char.bg == "default") or (char.reverse and char.fg == "default")): + continue + + if char.reverse: + fg = char.bg + bg = char.fg + else: + fg = char.fg + bg = char.bg + + if fg == "default": + fg = "defaultfg" + if bg == "default": + bg = "defaultbg" + + content = sanitizeLatexChar(char.data) + if char.bold: + content = "\\textbf{" + content + "}" + if char.italics: + content = "\\textit{" + content + "}" + + pline += "\\draw[fill=%s,draw=none] (%.4fem,%dem) rectangle +(0.6em,1em) node[pos=.5, anchor=base, yshift=-0.5ex] {\\textcolor{%s}{%s}};\n" % (bg, x / lspace, y, fg, content) + + pline += "\\end{tikzpicture}" + + pline += tikzFooter() + + return pline + +def color2tex(color, name=None): + if name is None: + name = color + line = "" + line += "\\definecolor{" + name + "}{RGB}{" + line += str(int(color[:2], 16)) + "," + line += str(int(color[2:4], 16)) + "," + line += str(int(color[4:], 16)) + "}\n" + return line + +def tikzColors(lines, bgc, fgc, palette): + line = "" + line += color2tex(bgc, "defaultbg") + line += color2tex(fgc, "defaultfg") + for i,c in enumerate(["black", "red", "green", "brown", "blue", "magenta", "cyan", "white"]): + line += color2tex(palette[i], c) + colors = set(["default", "black", "red", "green", "brown", "blue", "magenta", "cyan", "white"]) + for y in range(len(lines)): + _line = lines[y] + for x in range(len(_line)): + char = _line[x] + if not char.bg in colors: + colors.add(char.bg) + line += color2tex(char.bg) + if not char.fg in colors: + colors.add(char.fg) + line += color2tex(char.fg) + + return line + +def tikzHeader(lines, bgc, fgc, palette): + line = "\\documentclass[class=minimal,border=0pt]{standalone}\n" + line += "\\usepackage[utf8]{inputenc}\n" + line += "\\usepackage{tikz}\n" + line += "\\usepackage[utf8]{inputenc}\n" + line += "\\usepackage{fontspec}\n" + line += "\\setmonofont[Ligatures=TeX]{Hack Regular}\n" + # line += "\\usetikzlibrary{external}\n" + # line += "\\tikzexternalize\n" + line += "\\usepackage{xcolor}\n" + line += "\\usepackage{amssymb}\n" + line += "\\usepackage{pmboxdraw}\n" + line += tikzColors(lines, bgc, fgc, palette) + line += "\\begin{document}\n" + return line + +def tikzFooter(): + return "\\end{document}\n" + +def main(): + if len(sys.argv) < 3: + print("Usage: asciicast2latex ") + sys.exit(-1) + convert(sys.argv[1], sys.argv[2]) + +if __name__ == "__main__": + main() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..077fd55 --- /dev/null +++ b/setup.py @@ -0,0 +1,20 @@ +from setuptools import setup + +setup(name='asciicast2latex', + version='0.1', + description='Converts asciinema casts to latex (and compiles to pdf)', + url='https://notabug.org/haskal/asciicast2latex', + author='haskal', + author_email='haskal@bepis.xyz', + license='GPLv3', + packages=['asciicast2latex'], + install_requires=[ + "pyte" + ], + include_package_data=True, + entry_points={ + 'console_scripts': [ + 'asciicast2latex=asciicast2latex:main' + ] + }, + zip_safe=False)