%<*driver> \def\nameofplainTeX{plain} \ifx\fmtname\nameofplainTeX \input l3docstrip.tex \keepsilent \askforoverwritefalse \preamble ======================================================================== Package ansiterm - ANSI-aware terminal transcripts for LuaLaTeX. (C) 2026 Matthias Werner E-mail: matthias.werner@informatik.tu-chemnitz.de Released under the LaTeX Project Public License v1.3c or later See http://www.latex-project.org/lppl.txt ======================================================================== \endpreamble \postamble This work is "maintained" (as per LPPL maintenance status) by Matthias Werner. \endpostamble \generate{\file{ansiterm.sty}{\from{ansiterm.dtx}{package}}} \expandafter\endbatchfile \fi \iffalse % %<*package> \NeedsTeXFormat{LaTeX2e}[2022-06-01] \ProvidesExplPackage{ansiterm}{2026-08-26}{v0.2.1} {ANSI-aware terminal windows and executable shell transcripts} \ExplSyntaxOff % %<*driver> \fi \documentclass[add-bib=false]{osgdoc} \OnlyDescription \usepackage{enumitem} \usepackage{microtype} \usepackage[ languages={de,en}, map={de=german,en=english}, targetlang={job=2}, load babel ]{langselect} \newpackagename{\ansitermpkg}{ansiterm} \makeindex \setcnltx{ package = ansiterm, version = \UseVersionOf{ansiterm.sty}, date = \UseDateOf{ansiterm.sty}, title = {\ldeen{Das \ansitermpkg\ Paket}{The \ansitermpkg\ Package}}, info = {\ldeen{Terminalfenster mit Aufzeichnung von Shellausführungen und ANSI-Interpretation}{ ANSI-aware terminal windows and executable shell transcripts}}, authors = {Matthias Werner[matthias.werner@informatik.tu-chemnitz.de]}, abstract={ \ldeen*{ Das Paket stellt zwei Umgebungen zur Verfügung, die Text in einer Box mit Terminal-Anmutung darstellen. Dabei ermöglicht \env{ansitermexec} (bei aktiviertem @1) die Ausführung und Protokollierung von Shellbefehlen und deren Ergebnissen, wobei ANSI-Steuerzeichen korrekt umgesetzt werden. Wenn @2 gesetzt ist, wird die Ausgabe getaggt. Das Paket ist Teil des \pkg*{osglecture}-Bundles, aber von den anderen Paketen weitgehend unabhängig. }{ The package provides two environments that display text in a box that resembles a terminal. In this context, \env{ansitermexec} allows (when @1 is enabled) the execution and logging of shell commands and their results, with ANSI control characters being correctly interpreted. If @2 is set, the output is tagged. This package is part of the \pkg*{osglecture} bundle, but is largely independent of the other packages. }{\texttt{--shell-escape}}{\cs{DocumentMetadata}\Marg{tagging=on}} }, url =https://github.com/werner-matthias/osglecture, build-title } \begin{document} % -------------------------------------------------------------------------- % Documentation % -------------------------------------------------------------------------- \section{\ldeen{Nutzung}{Usage}} \begin{warnbox}{\ldeen{Achtung, Sicherheitsrisiko!}{Attention, Security Risk!}} \ldeen*{Die Umgebung @1 benötigt zur Ausführung die Übersetzung mit @2. Dies stellt ein Sicherheitrisiko dar, da damit @3 beliebigen Code auf ihrem System ausführen kann. Stellen Sie sicher, dass Sie nur unkritischen Code in den Body der Umgebung schreiben. }{ Environment @1 requires the translation with @2 to run. This poses a security risk, as it allows @3 to execute arbitrary code on your system. Make sure to write only non-critical code in the body of the environment. }{\code{ansitermexec}}{\texttt{--shell-escape}}{Lua\LaTeX} \end{warnbox} \ldeen{Laden Sie das Paket auf dem üblichen Weg mit \cs{usepackage}\Marg{ansiterm}. Es gibt keine Paketoptionen. Das Paket stellt zwei Umgebungen zur Verfügung und benötigt Lua\LaTeX.}{Load the package in the usual way with \cs{usepackage}\Marg{ansiterm}. There are no package options. The package provides two environments and needs Lua\LaTeX.} \begin{environments} \environment{ansiterm}[\oarg{\ldeen{Optionen}{options}}]\ldeen{Gibt den Inhalt innerhalb eines Terminalfensters aus.}{Displays the content within a terminal window.} \environment{ansitermexec}[\oarg{\ldeen{Optionen}{options}}]\ldeen{Gibt den Inhalt zeilenweise an eine zur Ausführung Shell weiter und setzt gibt die Ein- und Ausgabe in einem Terminalfenster aus.}{Passes the content line by line to a shell for execution and displays the input and output in a terminal window. Benötigt zur Übersetzung \option{-{}-shell-escape}} \end{environments} \subsection{\ldeen{Beispiele}{Examples}} \begin{example}[compile,program=lualatex,exe-with=--shell-escape, graphics={trim=30mm 220mm 30mm 40mm,clip,scale=1.2}] \documentclass{article} \usepackage{ansiterm} \begin{document} \begin{ansiterm}[escape-char=§] §[1;31mred bold text§[0m \end{ansiterm} \end{document} \end{example} \begin{example}[compile,program=lualatex,exe-with=--shell-escape, graphics={trim=30mm 200mm 30mm 30mm,clip,scale=1.2}] \documentclass{article} \usepackage{ansiterm} \begin{document} \begin{ansitermexec}[ shell=/bin/sh, cache=true, sandbox=true, show-input=true, ] printf '\033[1;32mHello\033[0m\n' grep --color=always bar <<< 'foo bar baz' \end{ansitermexec} \end{document} \end{example} \subsection{\ldeen{Optionen}{Options}} \newnote{\xo}{\normalfont\env{ansitermexec}} \begin{options} \keyval{title}{\ldeen{Überschrift des Terminals}{Terminal title}}\Default*{terminal} \keyval{fontsize}{\ldeen{Schriftgrößenbefehl}{font size command}}\Default*{\cs{small}} \keyval{shell}{\ldeen{Shell}{shell}\xo}\Default{/bin/sh} \ldeen{Shell zur Ausführung}{shell for execution} \keyval{cwd}{\ldeen{Verzeichnis}{directory}\xo}\Default{.} \ldeen{Arbeitsverzeichnis für die Ausführung}{working directory for execution} \keychoice{cache}{true, false, refresh}\Default{\xo{}true} {\ldeen{Legt fest, ob Ergebnis bei erneuter Übersetzung wiederverwendet wird. \code{refresh} erneuert den Cache.}{Specifies whether the result is reused when the translation is performed again. \code{refresh} renews the cache.}} \keybool{sandbox}\Default{\xo{}true} {\ldeen{Wenn der Schlüssel gesetzt (true) ist, wird der in einem neu angelegten Verzeichnis ausgeführt, das anschließend wieder gelöscht wird. Die Ergebnisse werden trotzdem ggf.\ im Cache behalten.}{If the key is true, the command is executed in a newly created directory, which is then deleted. The results are still cached, if applicable.}} \keybool{show-input\xo}\Default{true} {\ldeen{Bei \code{false} wird nur die Ausgabe gezeigt.}{If \code{false}, only the output is displayed.}} \keyval{prompt}{\ldeen{Prompt}{prompt}\xo}\Default*{\code{\textdollar\textasciitilde}} \keyval{escape-char}{\ldeen{Zeichen für ESC}{escape character}}\Default*{\S} \keyval{lines}{\ldeen{Zeilenbereich}{line range}}\Default{-} {\ldeen{Auswahl der Zeilen, die dargestellt werden.}{Range of lines to display.}} \keybool{ellipsis}\Default{true} {\ldeen*{Zeigt Auslassungszeichen "`@1"' für übersprungene Zeilen}{Displays ellipsis “`@1”' for skipped lines}{⋮}} \keybool{fail-on-error\xo}\Default{true} {\ldeen{Erzeugt Fehlermeldung, wenn die die Ausführung einen Fehlerstatus zurück gibt.}{Generates an error message if the execution returns an error status.}} \end{options} \section{Implementation} \MaybeStop{\ldeen*{Um die Implementationsbeschreibung zu erhalten, kommentieren Sie bitte \cs{OnlyDescription} in @1 aus.}{To view the implementation description, please uncomment \cs{OnlyDescription} in @1.}{\File{ansiterm.dtx}}} \subsection{\emph{ansiterm.sty}} \AutoDocInput{ansiterm.dtx}{package} \subsection{ansiterm.lua} \AutoDocInput[lua]{ansiterm.lua}{lua} \end{document} % %<*package> \RequirePackage{xcolor} \RequirePackage{tcolorbox} \tcbuselibrary{skins,breakable} \RequirePackage{fvextra} \definecolor{AnsiTermRed}{RGB}{255,95,86} \definecolor{AnsiTermYellow}{RGB}{255,189,46} \definecolor{AnsiTermGreen}{RGB}{39,201,63} \definecolor{AnsiTermFrame}{RGB}{216,214,217} \ExplSyntaxOn \tl_new:N \l__ansiterm_title_tl \tl_new:N \l__ansiterm_fontsize_tl \tl_new:N \l__ansiterm_shell_tl \tl_new:N \l__ansiterm_cwd_tl \tl_new:N \l__ansiterm_cache_tl \tl_new:N \l__ansiterm_lines_tl \tl_new:N \l__ansiterm_prompt_tl \tl_new:N \l__ansiterm_escape_char_tl \bool_new:N \l__ansiterm_sandbox_bool \bool_new:N \l__ansiterm_show_input_bool \bool_new:N \l__ansiterm_ellipsis_bool \bool_new:N \l__ansiterm_fail_bool \int_new:N \g__ansiterm_exec_int \keys_define:nn { ansiterm } { title .tl_set:N = \l__ansiterm_title_tl, fontsize .tl_set:N = \l__ansiterm_fontsize_tl, shell .tl_set:N = \l__ansiterm_shell_tl, cwd .tl_set:N = \l__ansiterm_cwd_tl, cache .choices:nn = { true, false, refresh } { \tl_set:Nx \l__ansiterm_cache_tl { \l_keys_choice_tl } }, sandbox .bool_set:N = \l__ansiterm_sandbox_bool, show-input .bool_set:N = \l__ansiterm_show_input_bool, prompt .tl_set:N = \l__ansiterm_prompt_tl, escape-char .tl_set:N = \l__ansiterm_escape_char_tl, lines .tl_set:N = \l__ansiterm_lines_tl, ellipsis .bool_set:N = \l__ansiterm_ellipsis_bool, fail-on-error .bool_set:N = \l__ansiterm_fail_bool, } \keys_set:nn { ansiterm } { title=Terminal, fontsize=\small, shell=/bin/sh, cwd=., cache=true, sandbox=true, show-input=true, prompt={$~}, escape-char={§}, lines={1-}, ellipsis=true, fail-on-error=true } %\ldeen{Tagging darf nicht erfolgen, wenn das Dokument nicht mit @1 % geladen wurde}{Tagging must not happen when the document was not loaded with % @1}{\cs{DocumentMetadata}\Marg{tagging=on}} \cs_new_protected:Npn \__ansiterm_tag_struct_begin:n #1 { \cs_if_exist:NT \tagstructbegin { \tagstructbegin{ tag=#1 } } } \cs_new_protected:Npn \__ansiterm_tag_struct_end: { \cs_if_exist:NT \tagstructend { \tagstructend } } % \ldeen{Durch die Registrierung von Strukturbezeichnungen (anstatt \code{tag=Div} an der % Aufrufstelle fest zu codieren) kann nutzerseitig festgelegt werden, was % \code{input}/\code{output} zugeordnet wird.}{Registering structure names % (rather than hard-coding \code{tag=Div} at the call site) lets the user decide % what \code{input}/\code{output} map to} \cs_if_exist:NT \NewStructureName { \NewStructureName{ ansiterm/input } \AssignStructureRole{ ansiterm/input }{ Div } \NewStructureName{ ansiterm/output } \AssignStructureRole{ ansiterm/output }{ Div } } \cs_new_protected:Npn \__ansiterm_tagged_run:nn #1#2 { \__ansiterm_tag_struct_begin:n {#1} #2 \__ansiterm_tag_struct_end: } \cs_new_protected:Npn \ansitermtaginput #1 { \__ansiterm_tagged_run:nn { \UseStructureName{ ansiterm/input } } {#1} } \cs_new_protected:Npn \ansitermtagoutput #1 { \__ansiterm_tagged_run:nn { \UseStructureName{ ansiterm/output } } {#1} } \cs_new_protected:Npn \__ansiterm_box_begin: { \begin{tcolorbox}[ enhanced,breakable,title={\l__ansiterm_title_tl},coltitle=black!65, fonttitle=\sffamily\scriptsize,halign~title=center, colback=black!5!white,colframe=AnsiTermFrame,boxrule=.5mm, top=1pt,bottom=1pt,left=1em,right=1em,boxsep=.5ex, fontupper=\ttfamily\l__ansiterm_fontsize_tl, overlay~first={\node[anchor=north~west,inner~sep=0pt,xshift=5pt,yshift=-4pt] at(frame.north~west){\rlap{\textcolor{AnsiTermRed}{\Huge\textbullet}}\quad \rlap{\textcolor{AnsiTermYellow}{\Huge\textbullet}}\quad \textcolor{AnsiTermGreen}{\Huge\textbullet}};}, overlay~unbroken={\node[anchor=north~west,inner~sep=0pt,xshift=5pt,yshift=-4pt] at(frame.north~west){\rlap{\textcolor{AnsiTermRed}{\Huge\textbullet}}\quad \rlap{\textcolor{AnsiTermYellow}{\Huge\textbullet}}\quad \textcolor{AnsiTermGreen}{\Huge\textbullet}};} ] } \cs_new_protected:Npn \__ansiterm_box_end: { \end{tcolorbox} } \msg_new:nnn { ansiterm } { luatex-rendering-required } { ANSI~rendering~in~the~ansiterm~environment~requires~LuaLaTeX. } \cs_new_protected:Npn \__ansiterm_render:n #1 { \sys_if_engine_luatex:TF { \directlua{ ansiterm.render { source = "\luaescapestring{#1}", output = "\luaescapestring{#1.tex}", escape_char = "\luaescapestring{\l__ansiterm_escape_char_tl}" } } } { \msg_fatal:nn { ansiterm } { luatex-rendering-required } } } \NewDocumentEnvironment { ansiterm } { O{} } { \VerbatimEnvironment \group_begin: \keys_set:nn { ansiterm } {#1} \int_gincr:N \g__ansiterm_exec_int \tl_set:Nx \l_tmpa_tl { \c_sys_jobname_str.ansiterm-\int_use:N\g__ansiterm_exec_int.txt } \begin{VerbatimOut}{\l_tmpa_tl} } { \end{VerbatimOut} \__ansiterm_render:n { \l_tmpa_tl } \__ansiterm_box_begin: \__ansiterm_tag_struct_begin:n { Code } \input{\l_tmpa_tl.tex} \__ansiterm_tag_struct_end: \__ansiterm_box_end: \group_end: } \sys_if_engine_luatex:TF { \directlua{ansiterm = require("ansiterm")} } { \msg_new:nnn { ansiterm } { luatex-required } { The~ansitermexec~environment~requires~LuaLaTeX. } } \msg_new:nnn { ansiterm } { shell-escape-required } { ansitermexec~requires~compilation~with~--shell-escape. } \msg_new:nnn { ansiterm } { execution-failed } { Shell~execution~failed~with~exit~status~#1. } \cs_new_protected:Npn \__ansiterm_exec:n #1 { \sys_if_engine_luatex:TF { \sys_if_shell_unrestricted:TF { } { \msg_fatal:nn { ansiterm } { shell-escape-required } } \directlua{ ansiterm.run { source = "\luaescapestring{#1}", output = "\luaescapestring{#1.tex}", shell = "\luaescapestring{\l__ansiterm_shell_tl}", cwd = "\luaescapestring{\l__ansiterm_cwd_tl}", cache = "\luaescapestring{\l__ansiterm_cache_tl}", cache_dir = "\luaescapestring{\jobname.ansiterm-cache}", sandbox = \bool_if:NTF \l__ansiterm_sandbox_bool {true}{false}, show_input = \bool_if:NTF \l__ansiterm_show_input_bool {true}{false}, prompt = "\luaescapestring{\l__ansiterm_prompt_tl}", lines = "\luaescapestring{\l__ansiterm_lines_tl}", ellipsis = \bool_if:NTF \l__ansiterm_ellipsis_bool {true}{false} } } } { \msg_fatal:nn { ansiterm } { luatex-required } } } \NewDocumentEnvironment { ansitermexec } { O{} } { \VerbatimEnvironment \group_begin: \keys_set:nn { ansiterm } {#1} \int_gincr:N \g__ansiterm_exec_int \tl_set:Nx \l_tmpa_tl { \c_sys_jobname_str.ansiterm-\int_use:N\g__ansiterm_exec_int.sh } \begin{VerbatimOut}{\l_tmpa_tl} } { \end{VerbatimOut} \__ansiterm_exec:n { \l_tmpa_tl } \__ansiterm_box_begin: % \ldeen{@1 versieht die selbst geschriebenen Ein- und Ausgabezeilen mit Tags. % @2 erzeugt nur die umgebende @3-Struktur.}{@1 tags the % input/output lines it writes itself. % @2 supplies the surrounding @3 structure they nest in.} % {\File{ansiterm.lua}}{\cs{ansitermtaginput}/\cs{ansitermtagoutput}}{\code{Code}} \__ansiterm_tag_struct_begin:n { Code } \input{\l_tmpa_tl.tex} \__ansiterm_tag_struct_end: \__ansiterm_box_end: \bool_if:NT \l__ansiterm_fail_bool { \int_compare:nNnF { \csname ansitermstatus\endcsname } = { 0 } { \msg_error:nnx { ansiterm } { execution-failed } { \csname ansitermstatus\endcsname } } } \group_end: } \ExplSyntaxOff %