Emulating Ubuntu Terminal with LaTeX
01 Sep 2019This 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}
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:
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.