Emulating Ubuntu Terminal with LaTeX

This semester, one of the courses that I attend requires screenshot of terminal output as a proof of completion. Personally, I dislike including screenshots in my submissions as their nature is very different from the dominating component of the document (i.e. text). The reader (grader) is unable to interact with the image, and the difference in resolution and format will bring more trouble for him/her. Therefore, I would like to emulate Ubuntu terminal within \(\LaTeX\) documens, and the output would be text-based.

Basic settings

\(\LaTeX\)’s listings package already provides most of the functionalities we need. With the following \lstset parameters we can already create a listing block that is close to Ubuntu’s console. It is defined as command \lstconsolestyle so that one can switch between normal style and console style conveniently.

\definecolor{mygreen}{rgb}{0,0.6,0}
\definecolor{mygray}{rgb}{0.5,0.5,0.5}
\definecolor{mymauve}{rgb}{0.58,0,0.82}
\definecolor{terminalbgcolor}{HTML}{330033}
\definecolor{terminalrulecolor}{HTML}{000099}
\newcommand{\lstconsolestyle}{
\lstset{
	backgroundcolor=\color{terminalbgcolor},
	basicstyle=\color{white}\fontfamily{fvm}\footnotesize\selectfont,
	breakatwhitespace=false,  
	breaklines=true,
	captionpos=b,
	commentstyle=\color{mygreen},
	deletekeywords={...},
	escapeinside={\%*}{*)},
	extendedchars=true,
	frame=single,
	keepspaces=true,
	keywordstyle=\color{blue},
	%language=none,
	morekeywords={*,...},
	numbers=none,
	numbersep=5pt,
  framerule=2pt,
	numberstyle=\color{mygray}\tiny\selectfont,
	rulecolor=\color{terminalrulecolor},
	showspaces=false,
	showstringspaces=false,
	showtabs=false,
	stepnumber=2,
	stringstyle=\color{mymauve},
	tabsize=2
}
}

What these macro does is to set the correct background color and text format so that we can have a pure-text console that is close to Ubuntu’s style. For example, the following code can generate the listing block below.

\lstconsolestyle
\begin{lstlisting}
This is console style block!
\end{lstlisting}
Console style block

Supporting color with aha

Ubuntu terminals use ASCII escape codes to generate colorful output. With the help of aha, we can convert these escape sequences into HTML files. Then, we can write a Python program with the help of HTMLParser and pylatex to convert HTML files into \(\LaTeX\) files. Notice that the escapeinside property is important in this step. The Python program’s code is shown as below.



from html.parser import HTMLParser
from colour import Color
from pylatex.utils import escape_latex
from collections import deque
import re


def get_default_entity():
    return {
        'tag': None,
        'data': [],
        'attrs': None,
        'last_pointer': None
    }


class AhaHTMLParser(HTMLParser):
    def __init__(self):
        super().__init__()

        self.root = get_default_entity()
        self.root['tag'] = '@root'
        self.treeStorage = [self.root]

        self.curPointer = self.root

    def handle_starttag(self, tag, attrs):
        # create new structure in the tree
        entity = get_default_entity()
        entity['last_pointer'] = self.curPointer
        entity['tag'] = tag
        entity['attrs'] = attrs
        self.treeStorage.append(entity)
        self.curPointer = entity

    def handle_endtag(self, tag):
        # append this entity to last pointer
        self.curPointer['last_pointer']['data'].append(self.curPointer)
        self.curPointer = self.curPointer['last_pointer']

    def handle_data(self, data):
        # append data to current pointer
        dataEntity = get_default_entity()
        dataEntity['data'].append(data)
        self.curPointer['data'].append(dataEntity)


def get_html_tree(filename):
    with open(filename, 'r') as infile:
        htmlContent = infile.read()
    parser = AhaHTMLParser()
    parser.feed(htmlContent)
    return (parser.root, parser.treeStorage)


def find_pre_in_tree(node):
    if node['tag'] == 'pre':
        return node

    for data in node['data']:
        if isinstance(data, dict):
            result = find_pre_in_tree(data)
            if result is not None:
                return result

    return None


def parse_css_style(styleStr):
    styles = styleStr.split(';')
    result = dict()
    for style in styles:
        if len(style) == 0:
            continue

        key, val = style.split(':')
        key = key.strip()
        val = val.strip()
        assert len(key) > 0
        assert len(val) > 0
        result[key] = val

    return result


class HTMLTree2Latex:

    def __init__(self):
        self.colorConv = dict()
        self.result = []

    def to_latex(self, node):
        self.result.clear()
        self.colorConv.clear()

        self._to_latex(node)
        # generate color definition
        colorDefFormat = r'\definecolor{%s}{HTML}{%s}'

        colorDefs = []
        for key, val in self.colorConv.items():
            colorDef = colorDefFormat % (val['latex_name'], val['value'])
            colorDefs.append(colorDef)

        colorDefStr = '\n'.join(colorDefs)

        return colorDefStr, self.result

    def _get_color_item(self, colorStr):
        if colorStr not in self.colorConv:
            # create new color item
            colorItem = dict()
            colorItem['latex_name'] = self._get_color_latex_name(colorStr)
            colorItem['value'] = Color(colorStr).get_hex_l()[1:]
            self.colorConv[colorStr] = colorItem

        colorItem = self.colorConv[colorStr]
        return colorItem

    def _get_color_latex_name(self, color):
        return 'xxxhtmlcolor{}'.format(color)

    def _to_latex(self, node):
        result = self.result

        # process style
        hasStyle = False
        textEscape = False
        endCap = []

        if node['attrs'] is not None:
            for key, val in node['attrs']:
                if key == 'style':
                    # if there is style, then the entity has to be escaped
                    hasStyle = True
                    textEscape = True
                    cssStyle = parse_css_style(val)
                    result.append('%*')
                    endCap.insert(0, '*)')

                    if 'font-weight' in cssStyle:
                        if cssStyle['font-weight'] == 'bold':
                            result.append(r'{\bfseries')
                            endCap.insert(0, '}')

                    if 'color' in cssStyle:
                        colorStr = cssStyle['color']
                        colorItem = self._get_color_item(colorStr)
                        result.append(r'{\color{%s}' % colorItem['latex_name'])
                        endCap.insert(0, '}')

                    if 'background-color' in cssStyle:
                        self.addColorBoxDef = True
                        colorStr = cssStyle['background-color']
                        colorItem = self._get_color_item(colorStr)
                        result.append(r'\smash{\colorbox{%s}{'%colorItem['latex_name'])
                        endCap.insert(0, '}}')


        startResultSize = len(result)

        for data in node['data']:
            if isinstance(data, str):
                result.append(data)
            elif isinstance(data, dict):
                self._to_latex(data)

        if textEscape:
            for i in range(startResultSize, len(result)):
                result[i] = escape_latex(result[i])

        if hasStyle:
            result.extend(endCap)


def html_to_console_style(filename):
    root, tree = get_html_tree(filename)
    preEntity = find_pre_in_tree(root)
    assert preEntity is not None
    html2altex = HTMLTree2Latex()
    colorDef, content = html2altex.to_latex(preEntity)

    outputFmt = r'''{
\lstconsolestyle
%s
\begin{lstlisting}
%s
\end{lstlisting}
}
'''

    return outputFmt%(colorDef, ''.join(content))



if __name__ == '__main__':
    filename = input('please enter filename: ')
    print(html_to_console_style(filename))

For example, the result of ll --color=always in a folder converts into the following HTML code:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- This file was created with the aha Ansi HTML Adapter. http://ziz.delphigl.com/tool_aha.php -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="application/xml+xhtml; charset=UTF-8" />
<title>task5_ll.txt</title>
</head>
<body>
<pre>
total 84
-rw-rw-r-- 1 seed seed 3999 Aug 31 10:53 child.txt
-rw-rw-r-- 1 seed seed 1089 Aug 31 11:53 compare_env.py
-rw-rw-r-- 1 seed seed  776 Aug 31 18:35 ls.html
-rw-rw-r-- 1 seed seed 3999 Aug 31 10:54 parent.txt
drwxrwxr-x 2 seed seed 4096 Aug 31 11:44 <span style="color:blue;font-weight:bold;">__pycache__</span>
-rwxrwxr-x 1 seed seed 7496 Aug 31 10:53 <span style="color:green;font-weight:bold;">task2</span>
-rw-rw-r-- 1 seed seed  370 Aug 31 10:53 task2.c
-rwxrwxr-x 1 seed seed 7448 Aug 31 11:14 <span style="color:green;font-weight:bold;">task3</span>
-rw-rw-r-- 1 seed seed  188 Aug 31 11:14 task3.c
-rwxrwxr-x 1 seed seed 7348 Aug 31 11:27 <span style="color:green;font-weight:bold;">task4</span>
-rw-rw-r-- 1 seed seed   91 Aug 31 11:27 task4.c
-rw-rw-r-- 1 seed seed 3973 Aug 31 11:32 task4.txt
-rwsr-xr-x 1 root seed 7396 Aug 31 21:14 <span style="color:gray;background-color:red;">task5</span>
-rw-rw-r-- 1 seed seed 2119 Aug 31 21:17 task5_bash.html
-rw-rw-r-- 1 seed seed 1658 Aug 31 21:15 task5_bash.log
-rw-rw-r-- 1 seed seed  180 Aug 31 21:07 task5.c
-rw-rw-r-- 1 seed seed    0 Aug 31 22:00 task5_ll.txt
-rw-rw-r-- 1 seed seed 3983 Aug 31 11:32 terminal_env.txt
</pre>
</body>
</html>

This HTML files can be converted into the following \(\LaTeX\) code:

{
\lstconsolestyle
\definecolor{xxxhtmlcolorblue}{HTML}{0000ff}
\definecolor{xxxhtmlcolorgreen}{HTML}{008000}
\definecolor{xxxhtmlcolorgray}{HTML}{808080}
\definecolor{xxxhtmlcolorred}{HTML}{ff0000}
\begin{lstlisting}

total 84
-rw-rw-r-- 1 seed seed 3999 Aug 31 10:53 child.txt
-rw-rw-r-- 1 seed seed 1089 Aug 31 11:53 compare_env.py
-rw-rw-r-- 1 seed seed  776 Aug 31 18:35 ls.html
-rw-rw-r-- 1 seed seed 3999 Aug 31 10:54 parent.txt
drwxrwxr-x 2 seed seed 4096 Aug 31 11:44 %*{\bfseries{\color{xxxhtmlcolorblue}\_\_pycache\_\_}}*)
-rwxrwxr-x 1 seed seed 7496 Aug 31 10:53 %*{\bfseries{\color{xxxhtmlcolorgreen}task2}}*)
-rw-rw-r-- 1 seed seed  370 Aug 31 10:53 task2.c
-rwxrwxr-x 1 seed seed 7448 Aug 31 11:14 %*{\bfseries{\color{xxxhtmlcolorgreen}task3}}*)
-rw-rw-r-- 1 seed seed  188 Aug 31 11:14 task3.c
-rwxrwxr-x 1 seed seed 7348 Aug 31 11:27 %*{\bfseries{\color{xxxhtmlcolorgreen}task4}}*)
-rw-rw-r-- 1 seed seed   91 Aug 31 11:27 task4.c
-rw-rw-r-- 1 seed seed 3973 Aug 31 11:32 task4.txt
-rwsr-xr-x 1 root seed 7396 Aug 31 21:14 %*{\color{xxxhtmlcolorgray}\smash{\colorbox{xxxhtmlcolorred}{task5}}}*)
-rw-rw-r-- 1 seed seed 2119 Aug 31 21:17 task5_bash.html
-rw-rw-r-- 1 seed seed 1658 Aug 31 21:15 task5_bash.log
-rw-rw-r-- 1 seed seed  180 Aug 31 21:07 task5.c
-rw-rw-r-- 1 seed seed    0 Aug 31 22:00 task5_ll.txt
-rw-rw-r-- 1 seed seed 3983 Aug 31 11:32 terminal_env.txt

\end{lstlisting}
}

Note that the xcolor package is needed to support colored outputs. The code above generates the following output in pdf document:

Colored terminal output in LaTeX (slight differences in terms of content)

Supporting color with GNOME Terminal 3.28.2

In GONME Terminal 3.28.2 (bundled with Ubuntu 18.04), a “copy as HTML” feature is provided in the context menu. The output has a slightly different HTML structure, which calls for a modified version of our HTML to \(\LaTeX\) converter. The code of the new converter can be found here. It is compatible with the aha approach as well.