From 0e4503fac43ad8e14303f5a7dd309922a12dfed4 Mon Sep 17 00:00:00 2001
From: Lars Bilke <lars.bilke@ufz.de>
Date: Tue, 13 Jul 2021 16:02:05 +0200
Subject: [PATCH] User recipes, fixed Dockerfile output.

---
 README.md        |   7 +-
 Test.ipynb       | 301 ++++++++++++++++++++++++++++++++++++++++++++---
 ogscm/cli.py     |  28 +++--
 ogscm/jupyter.py | 132 +++++++++++++++------
 4 files changed, 399 insertions(+), 69 deletions(-)

diff --git a/README.md b/README.md
index 7824cef..4174f91 100644
--- a/README.md
+++ b/README.md
@@ -248,14 +248,15 @@ def run_cmd(cmd):
     print(f"Executing: {cmd}")
     !poetry run {cmd}
 
-out = setup_ui(run_cmd)
+user_recipes = ["user_recipes/my_recipe.py"] # optional user recipes
+out = setup_ui(run_cmd, user_recipes)
 ```
 
 Options `--build` and `--convert` are enabeld per default, then click on button `CREATE CONTAINER`. Optionally run a second cell to get a download link to the image file:
 
 ```py
-from ogscm.jupyter import display_download_link
-display_download_link(out.outputs[0]['text'])
+from ogscm.jupyter import display_download_link, display_source
+display_source(display_download_link(out.outputs[0]['text']), "docker")
 ```
 
 Output is stored in `[notebook-location]/_out`. UI state is preserved when notebook was saved. Enable recipes from the tab widget by clicking on the `Disabled`-button (`compiley.py`-recipe is enabled by default).
diff --git a/Test.ipynb b/Test.ipynb
index e181fe5..7c4f2dd 100644
--- a/Test.ipynb
+++ b/Test.ipynb
@@ -16,7 +16,7 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "e601a6cea8254f17ad460fb58473e517",
+       "model_id": "b5a18f4a9edf47429f58796fa4dcf460",
        "version_major": 2,
        "version_minor": 0
       },
@@ -37,7 +37,7 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "a92cb290669d4b71a950accb6dcbdf0c",
+       "model_id": "897d99046565461d98d2d2074ab6431b",
        "version_major": 2,
        "version_minor": 0
       },
@@ -58,7 +58,7 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "eda21a831fec407fbd1a6ce66b663f10",
+       "model_id": "77c051ed9f7646ff900daa6a719635a8",
        "version_major": 2,
        "version_minor": 0
       },
@@ -72,7 +72,7 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "d6e9df62ad8d448ea627e76f349d1e8e",
+       "model_id": "44a8aaedf2f04721b00bc14174e26ebd",
        "version_major": 2,
        "version_minor": 0
       },
@@ -86,7 +86,7 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "e8c6b62d436c4e9c9acb12dcd5d09f50",
+       "model_id": "aaaab6232b574919a3b6e177904967c2",
        "version_major": 2,
        "version_minor": 0
       },
@@ -100,7 +100,7 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "2658d20285c9434dbbfdf6205b2d8a2b",
+       "model_id": "5a284ea048354fcba17f157b3c8fb4ba",
        "version_major": 2,
        "version_minor": 0
       },
@@ -119,27 +119,290 @@
     "    print(f\"Executing: {cmd}\")\n",
     "    !poetry run {cmd}\n",
     "\n",
-    "out = setup_ui(run_cmd)"
+    "user_recipes = [\"ogscm/user_recipes/foo.py\"]\n",
+    "out = setup_ui(run_cmd, user_recipes)"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 4,
    "id": "b9c0249b-f350-4a0f-b903-cc4354c4c89b",
    "metadata": {},
-   "outputs": [],
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Download container:\n"
+     ]
+    },
+    {
+     "data": {
+      "text/html": [
+       "<a href='_out/images/gcc-default-foo-default_value-7a6be99e36c6.sif' download=gcc-default-foo-default_value-7a6be99e36c6.sif>gcc-default-foo-default_value-7a6be99e36c6.sif</a><br>"
+      ],
+      "text/plain": [
+       "/home/bilke/code/container-maker/_out/images/gcc-default-foo-default_value-7a6be99e36c6.sif"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "<style>pre { line-height: 125%; }\n",
+       "td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }\n",
+       "span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }\n",
+       "td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }\n",
+       "span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }\n",
+       ".output_html .hll { background-color: #ffffcc }\n",
+       ".output_html { background: #f8f8f8; }\n",
+       ".output_html .c { color: #408080; font-style: italic } /* Comment */\n",
+       ".output_html .err { border: 1px solid #FF0000 } /* Error */\n",
+       ".output_html .k { color: #008000; font-weight: bold } /* Keyword */\n",
+       ".output_html .o { color: #666666 } /* Operator */\n",
+       ".output_html .ch { color: #408080; font-style: italic } /* Comment.Hashbang */\n",
+       ".output_html .cm { color: #408080; font-style: italic } /* Comment.Multiline */\n",
+       ".output_html .cp { color: #BC7A00 } /* Comment.Preproc */\n",
+       ".output_html .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */\n",
+       ".output_html .c1 { color: #408080; font-style: italic } /* Comment.Single */\n",
+       ".output_html .cs { color: #408080; font-style: italic } /* Comment.Special */\n",
+       ".output_html .gd { color: #A00000 } /* Generic.Deleted */\n",
+       ".output_html .ge { font-style: italic } /* Generic.Emph */\n",
+       ".output_html .gr { color: #FF0000 } /* Generic.Error */\n",
+       ".output_html .gh { color: #000080; font-weight: bold } /* Generic.Heading */\n",
+       ".output_html .gi { color: #00A000 } /* Generic.Inserted */\n",
+       ".output_html .go { color: #888888 } /* Generic.Output */\n",
+       ".output_html .gp { color: #000080; font-weight: bold } /* Generic.Prompt */\n",
+       ".output_html .gs { font-weight: bold } /* Generic.Strong */\n",
+       ".output_html .gu { color: #800080; font-weight: bold } /* Generic.Subheading */\n",
+       ".output_html .gt { color: #0044DD } /* Generic.Traceback */\n",
+       ".output_html .kc { color: #008000; font-weight: bold } /* Keyword.Constant */\n",
+       ".output_html .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */\n",
+       ".output_html .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */\n",
+       ".output_html .kp { color: #008000 } /* Keyword.Pseudo */\n",
+       ".output_html .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */\n",
+       ".output_html .kt { color: #B00040 } /* Keyword.Type */\n",
+       ".output_html .m { color: #666666 } /* Literal.Number */\n",
+       ".output_html .s { color: #BA2121 } /* Literal.String */\n",
+       ".output_html .na { color: #7D9029 } /* Name.Attribute */\n",
+       ".output_html .nb { color: #008000 } /* Name.Builtin */\n",
+       ".output_html .nc { color: #0000FF; font-weight: bold } /* Name.Class */\n",
+       ".output_html .no { color: #880000 } /* Name.Constant */\n",
+       ".output_html .nd { color: #AA22FF } /* Name.Decorator */\n",
+       ".output_html .ni { color: #999999; font-weight: bold } /* Name.Entity */\n",
+       ".output_html .ne { color: #D2413A; font-weight: bold } /* Name.Exception */\n",
+       ".output_html .nf { color: #0000FF } /* Name.Function */\n",
+       ".output_html .nl { color: #A0A000 } /* Name.Label */\n",
+       ".output_html .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */\n",
+       ".output_html .nt { color: #008000; font-weight: bold } /* Name.Tag */\n",
+       ".output_html .nv { color: #19177C } /* Name.Variable */\n",
+       ".output_html .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */\n",
+       ".output_html .w { color: #bbbbbb } /* Text.Whitespace */\n",
+       ".output_html .mb { color: #666666 } /* Literal.Number.Bin */\n",
+       ".output_html .mf { color: #666666 } /* Literal.Number.Float */\n",
+       ".output_html .mh { color: #666666 } /* Literal.Number.Hex */\n",
+       ".output_html .mi { color: #666666 } /* Literal.Number.Integer */\n",
+       ".output_html .mo { color: #666666 } /* Literal.Number.Oct */\n",
+       ".output_html .sa { color: #BA2121 } /* Literal.String.Affix */\n",
+       ".output_html .sb { color: #BA2121 } /* Literal.String.Backtick */\n",
+       ".output_html .sc { color: #BA2121 } /* Literal.String.Char */\n",
+       ".output_html .dl { color: #BA2121 } /* Literal.String.Delimiter */\n",
+       ".output_html .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */\n",
+       ".output_html .s2 { color: #BA2121 } /* Literal.String.Double */\n",
+       ".output_html .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */\n",
+       ".output_html .sh { color: #BA2121 } /* Literal.String.Heredoc */\n",
+       ".output_html .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */\n",
+       ".output_html .sx { color: #008000 } /* Literal.String.Other */\n",
+       ".output_html .sr { color: #BB6688 } /* Literal.String.Regex */\n",
+       ".output_html .s1 { color: #BA2121 } /* Literal.String.Single */\n",
+       ".output_html .ss { color: #19177C } /* Literal.String.Symbol */\n",
+       ".output_html .bp { color: #008000 } /* Name.Builtin.Pseudo */\n",
+       ".output_html .fm { color: #0000FF } /* Name.Function.Magic */\n",
+       ".output_html .vc { color: #19177C } /* Name.Variable.Class */\n",
+       ".output_html .vg { color: #19177C } /* Name.Variable.Global */\n",
+       ".output_html .vi { color: #19177C } /* Name.Variable.Instance */\n",
+       ".output_html .vm { color: #19177C } /* Name.Variable.Magic */\n",
+       ".output_html .il { color: #666666 } /* Literal.Number.Integer.Long */\n",
+       "pre { line-height: 125%; }\n",
+       "td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }\n",
+       "span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }\n",
+       "td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }\n",
+       "span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }\n",
+       ".jp-RenderedHTML .hll { background-color: #ffffcc }\n",
+       ".jp-RenderedHTML { background: #f8f8f8; }\n",
+       ".jp-RenderedHTML .c { color: #408080; font-style: italic } /* Comment */\n",
+       ".jp-RenderedHTML .err { border: 1px solid #FF0000 } /* Error */\n",
+       ".jp-RenderedHTML .k { color: #008000; font-weight: bold } /* Keyword */\n",
+       ".jp-RenderedHTML .o { color: #666666 } /* Operator */\n",
+       ".jp-RenderedHTML .ch { color: #408080; font-style: italic } /* Comment.Hashbang */\n",
+       ".jp-RenderedHTML .cm { color: #408080; font-style: italic } /* Comment.Multiline */\n",
+       ".jp-RenderedHTML .cp { color: #BC7A00 } /* Comment.Preproc */\n",
+       ".jp-RenderedHTML .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */\n",
+       ".jp-RenderedHTML .c1 { color: #408080; font-style: italic } /* Comment.Single */\n",
+       ".jp-RenderedHTML .cs { color: #408080; font-style: italic } /* Comment.Special */\n",
+       ".jp-RenderedHTML .gd { color: #A00000 } /* Generic.Deleted */\n",
+       ".jp-RenderedHTML .ge { font-style: italic } /* Generic.Emph */\n",
+       ".jp-RenderedHTML .gr { color: #FF0000 } /* Generic.Error */\n",
+       ".jp-RenderedHTML .gh { color: #000080; font-weight: bold } /* Generic.Heading */\n",
+       ".jp-RenderedHTML .gi { color: #00A000 } /* Generic.Inserted */\n",
+       ".jp-RenderedHTML .go { color: #888888 } /* Generic.Output */\n",
+       ".jp-RenderedHTML .gp { color: #000080; font-weight: bold } /* Generic.Prompt */\n",
+       ".jp-RenderedHTML .gs { font-weight: bold } /* Generic.Strong */\n",
+       ".jp-RenderedHTML .gu { color: #800080; font-weight: bold } /* Generic.Subheading */\n",
+       ".jp-RenderedHTML .gt { color: #0044DD } /* Generic.Traceback */\n",
+       ".jp-RenderedHTML .kc { color: #008000; font-weight: bold } /* Keyword.Constant */\n",
+       ".jp-RenderedHTML .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */\n",
+       ".jp-RenderedHTML .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */\n",
+       ".jp-RenderedHTML .kp { color: #008000 } /* Keyword.Pseudo */\n",
+       ".jp-RenderedHTML .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */\n",
+       ".jp-RenderedHTML .kt { color: #B00040 } /* Keyword.Type */\n",
+       ".jp-RenderedHTML .m { color: #666666 } /* Literal.Number */\n",
+       ".jp-RenderedHTML .s { color: #BA2121 } /* Literal.String */\n",
+       ".jp-RenderedHTML .na { color: #7D9029 } /* Name.Attribute */\n",
+       ".jp-RenderedHTML .nb { color: #008000 } /* Name.Builtin */\n",
+       ".jp-RenderedHTML .nc { color: #0000FF; font-weight: bold } /* Name.Class */\n",
+       ".jp-RenderedHTML .no { color: #880000 } /* Name.Constant */\n",
+       ".jp-RenderedHTML .nd { color: #AA22FF } /* Name.Decorator */\n",
+       ".jp-RenderedHTML .ni { color: #999999; font-weight: bold } /* Name.Entity */\n",
+       ".jp-RenderedHTML .ne { color: #D2413A; font-weight: bold } /* Name.Exception */\n",
+       ".jp-RenderedHTML .nf { color: #0000FF } /* Name.Function */\n",
+       ".jp-RenderedHTML .nl { color: #A0A000 } /* Name.Label */\n",
+       ".jp-RenderedHTML .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */\n",
+       ".jp-RenderedHTML .nt { color: #008000; font-weight: bold } /* Name.Tag */\n",
+       ".jp-RenderedHTML .nv { color: #19177C } /* Name.Variable */\n",
+       ".jp-RenderedHTML .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */\n",
+       ".jp-RenderedHTML .w { color: #bbbbbb } /* Text.Whitespace */\n",
+       ".jp-RenderedHTML .mb { color: #666666 } /* Literal.Number.Bin */\n",
+       ".jp-RenderedHTML .mf { color: #666666 } /* Literal.Number.Float */\n",
+       ".jp-RenderedHTML .mh { color: #666666 } /* Literal.Number.Hex */\n",
+       ".jp-RenderedHTML .mi { color: #666666 } /* Literal.Number.Integer */\n",
+       ".jp-RenderedHTML .mo { color: #666666 } /* Literal.Number.Oct */\n",
+       ".jp-RenderedHTML .sa { color: #BA2121 } /* Literal.String.Affix */\n",
+       ".jp-RenderedHTML .sb { color: #BA2121 } /* Literal.String.Backtick */\n",
+       ".jp-RenderedHTML .sc { color: #BA2121 } /* Literal.String.Char */\n",
+       ".jp-RenderedHTML .dl { color: #BA2121 } /* Literal.String.Delimiter */\n",
+       ".jp-RenderedHTML .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */\n",
+       ".jp-RenderedHTML .s2 { color: #BA2121 } /* Literal.String.Double */\n",
+       ".jp-RenderedHTML .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */\n",
+       ".jp-RenderedHTML .sh { color: #BA2121 } /* Literal.String.Heredoc */\n",
+       ".jp-RenderedHTML .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */\n",
+       ".jp-RenderedHTML .sx { color: #008000 } /* Literal.String.Other */\n",
+       ".jp-RenderedHTML .sr { color: #BB6688 } /* Literal.String.Regex */\n",
+       ".jp-RenderedHTML .s1 { color: #BA2121 } /* Literal.String.Single */\n",
+       ".jp-RenderedHTML .ss { color: #19177C } /* Literal.String.Symbol */\n",
+       ".jp-RenderedHTML .bp { color: #008000 } /* Name.Builtin.Pseudo */\n",
+       ".jp-RenderedHTML .fm { color: #0000FF } /* Name.Function.Magic */\n",
+       ".jp-RenderedHTML .vc { color: #19177C } /* Name.Variable.Class */\n",
+       ".jp-RenderedHTML .vg { color: #19177C } /* Name.Variable.Global */\n",
+       ".jp-RenderedHTML .vi { color: #19177C } /* Name.Variable.Instance */\n",
+       ".jp-RenderedHTML .vm { color: #19177C } /* Name.Variable.Magic */\n",
+       ".jp-RenderedHTML .il { color: #666666 } /* Literal.Number.Integer.Long */</style><div class=\"highlight\"><pre><span></span><span class=\"c\"># syntax=docker/dockerfile:experimental</span>\n",
+       "\n",
+       "<span class=\"k\">FROM</span> <span class=\"s\">ubuntu:20.04</span> <span class=\"k\">AS</span> <span class=\"s\">build</span>\n",
+       "\n",
+       "<span class=\"c\"># Generated with ogs-container-maker 2.0.0</span>\n",
+       "\n",
+       "<span class=\"k\">RUN</span> apt-get update -y <span class=\"o\">&amp;&amp;</span> <span class=\"se\">\\</span>\n",
+       "    <span class=\"nv\">DEBIAN_FRONTEND</span><span class=\"o\">=</span>noninteractive apt-get install -y --no-install-recommends <span class=\"se\">\\</span>\n",
+       "        curl <span class=\"se\">\\</span>\n",
+       "        make <span class=\"se\">\\</span>\n",
+       "        tar <span class=\"se\">\\</span>\n",
+       "        unzip <span class=\"se\">\\</span>\n",
+       "        wget <span class=\"o\">&amp;&amp;</span> <span class=\"se\">\\</span>\n",
+       "    rm -rf /var/lib/apt/lists/*\n",
+       "\n",
+       "<span class=\"c\"># --- Begin compiler.py ---</span>\n",
+       "\n",
+       "<span class=\"c\"># GNU compiler</span>\n",
+       "<span class=\"k\">RUN</span> apt-get update -y <span class=\"o\">&amp;&amp;</span> <span class=\"se\">\\</span>\n",
+       "    <span class=\"nv\">DEBIAN_FRONTEND</span><span class=\"o\">=</span>noninteractive apt-get install -y --no-install-recommends <span class=\"se\">\\</span>\n",
+       "        g++ <span class=\"se\">\\</span>\n",
+       "        gcc <span class=\"o\">&amp;&amp;</span> <span class=\"se\">\\</span>\n",
+       "    rm -rf /var/lib/apt/lists/*\n",
+       "\n",
+       "<span class=\"c\"># --- End compiler.py ---</span>\n",
+       "\n",
+       "<span class=\"c\"># Begin ogscm/user_recipes/foo.py</span>\n",
+       "\n",
+       "<span class=\"c\"># --- End ogscm/user_recipes/foo.py ---</span>\n",
+       "</pre></div>\n"
+      ],
+      "text/latex": [
+       "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n",
+       "\\PY{c}{\\PYZsh{} syntax=docker/dockerfile:experimental}\n",
+       "\n",
+       "\\PY{k}{FROM} \\PY{l+s}{ubuntu:20.04} \\PY{k}{AS} \\PY{l+s}{build}\n",
+       "\n",
+       "\\PY{c}{\\PYZsh{} Generated with ogs\\PYZhy{}container\\PYZhy{}maker 2.0.0}\n",
+       "\n",
+       "\\PY{k}{RUN} apt\\PYZhy{}get update \\PYZhy{}y \\PY{o}{\\PYZam{}\\PYZam{}} \\PY{l+s+se}{\\PYZbs{}}\n",
+       "    \\PY{n+nv}{DEBIAN\\PYZus{}FRONTEND}\\PY{o}{=}noninteractive apt\\PYZhy{}get install \\PYZhy{}y \\PYZhy{}\\PYZhy{}no\\PYZhy{}install\\PYZhy{}recommends \\PY{l+s+se}{\\PYZbs{}}\n",
+       "        curl \\PY{l+s+se}{\\PYZbs{}}\n",
+       "        make \\PY{l+s+se}{\\PYZbs{}}\n",
+       "        tar \\PY{l+s+se}{\\PYZbs{}}\n",
+       "        unzip \\PY{l+s+se}{\\PYZbs{}}\n",
+       "        wget \\PY{o}{\\PYZam{}\\PYZam{}} \\PY{l+s+se}{\\PYZbs{}}\n",
+       "    rm \\PYZhy{}rf /var/lib/apt/lists/*\n",
+       "\n",
+       "\\PY{c}{\\PYZsh{} \\PYZhy{}\\PYZhy{}\\PYZhy{} Begin compiler.py \\PYZhy{}\\PYZhy{}\\PYZhy{}}\n",
+       "\n",
+       "\\PY{c}{\\PYZsh{} GNU compiler}\n",
+       "\\PY{k}{RUN} apt\\PYZhy{}get update \\PYZhy{}y \\PY{o}{\\PYZam{}\\PYZam{}} \\PY{l+s+se}{\\PYZbs{}}\n",
+       "    \\PY{n+nv}{DEBIAN\\PYZus{}FRONTEND}\\PY{o}{=}noninteractive apt\\PYZhy{}get install \\PYZhy{}y \\PYZhy{}\\PYZhy{}no\\PYZhy{}install\\PYZhy{}recommends \\PY{l+s+se}{\\PYZbs{}}\n",
+       "        g++ \\PY{l+s+se}{\\PYZbs{}}\n",
+       "        gcc \\PY{o}{\\PYZam{}\\PYZam{}} \\PY{l+s+se}{\\PYZbs{}}\n",
+       "    rm \\PYZhy{}rf /var/lib/apt/lists/*\n",
+       "\n",
+       "\\PY{c}{\\PYZsh{} \\PYZhy{}\\PYZhy{}\\PYZhy{} End compiler.py \\PYZhy{}\\PYZhy{}\\PYZhy{}}\n",
+       "\n",
+       "\\PY{c}{\\PYZsh{} Begin ogscm/user\\PYZus{}recipes/foo.py}\n",
+       "\n",
+       "\\PY{c}{\\PYZsh{} \\PYZhy{}\\PYZhy{}\\PYZhy{} End ogscm/user\\PYZus{}recipes/foo.py \\PYZhy{}\\PYZhy{}\\PYZhy{}}\n",
+       "\\end{Verbatim}\n"
+      ],
+      "text/plain": [
+       "# syntax=docker/dockerfile:experimental\n",
+       "\n",
+       "FROM ubuntu:20.04 AS build\n",
+       "\n",
+       "# Generated with ogs-container-maker 2.0.0\n",
+       "\n",
+       "RUN apt-get update -y && \\\n",
+       "    DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \\\n",
+       "        curl \\\n",
+       "        make \\\n",
+       "        tar \\\n",
+       "        unzip \\\n",
+       "        wget && \\\n",
+       "    rm -rf /var/lib/apt/lists/*\n",
+       "\n",
+       "# --- Begin compiler.py ---\n",
+       "\n",
+       "# GNU compiler\n",
+       "RUN apt-get update -y && \\\n",
+       "    DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \\\n",
+       "        g++ \\\n",
+       "        gcc && \\\n",
+       "    rm -rf /var/lib/apt/lists/*\n",
+       "\n",
+       "# --- End compiler.py ---\n",
+       "\n",
+       "# Begin ogscm/user_recipes/foo.py\n",
+       "\n",
+       "# --- End ogscm/user_recipes/foo.py ---"
+      ]
+     },
+     "execution_count": 4,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
    "source": [
-    "from ogscm.jupyter import display_download_link\n",
-    "display_download_link(out.outputs[0]['text'])"
+    "from ogscm.jupyter import display_download_link, display_source\n",
+    "display_source(display_download_link(out.outputs[0]['text']), \"docker\")"
    ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "id": "a5189654-2d4d-4f6f-a779-0d9c3d4e1846",
-   "metadata": {},
-   "outputs": [],
-   "source": []
   }
  ],
  "metadata": {
diff --git a/ogscm/cli.py b/ogscm/cli.py
index 4391b89..2a5e08a 100644
--- a/ogscm/cli.py
+++ b/ogscm/cli.py
@@ -64,21 +64,23 @@ def main():  # pragma: no cover
 
         # https://stackoverflow.com/a/1463370/80480
         ldict = {"filename": recipe}
-        try:
-            recipe_builtin = pkg_resources.read_text(recipes, recipe)
-            exec(compile(recipe_builtin, recipe, "exec"), locals(), ldict)
-        except Exception as err:
-            error_class = err.__class__.__name__
-            detail = err.args[0]
-            cl, exc, tb = sys.exc_info()
-            line_number = traceback.extract_tb(tb)[-1][1]
-            print(f"{error_class} at line {line_number}: {detail}")
-            if not os.path.exists(recipe):
-                print(f"{recipe} does not exist!")
-                exit(1)
-
+        if os.path.exists(recipe):
             with open(recipe, "r") as reader:
                 exec(compile(reader.read(), recipe, "exec"), locals(), ldict)
+        else:
+            try:
+                recipe_builtin = pkg_resources.read_text(recipes, recipe)
+                exec(compile(recipe_builtin, recipe, "exec"), locals(), ldict)
+            except Exception as err:
+                error_class = err.__class__.__name__
+                detail = err.args[0]
+                cl, exc, tb = sys.exc_info()
+                line_number = traceback.extract_tb(tb)[-1][1]
+                print(f"{error_class} at line {line_number}: {detail}")
+                if not os.path.exists(recipe):
+                    print(f"{recipe} does not exist!")
+                    exit(1)
+
         if "out_dir" in ldict:
             out_dir = ldict["out_dir"]
         if "toolchain" in ldict:
diff --git a/ogscm/jupyter.py b/ogscm/jupyter.py
index 8cf5816..0e543a2 100644
--- a/ogscm/jupyter.py
+++ b/ogscm/jupyter.py
@@ -4,18 +4,20 @@ import os
 import pathlib
 import importlib.resources as pkg_resources
 
+
 def on_button_recipe_clicked(b):
     # print(b)
-    if b['new']:
+    if b["new"]:
         b.owner.icon = "check"
         b.owner.description = "Enabled"
-        b.owner.button_style='success'
+        b.owner.button_style = "success"
     else:
         b.owner.icon = ""
         b.owner.description = "Disabled"
-        b.owner.button_style=''
+        b.owner.button_style = ""
+
 
-def setup_ui(run_cmd):
+def setup_ui(run_cmd, user_recipes=[]):
     from ogscm.args import setup_args_parser
     from ogscm import recipes
     import ipywidgets as widgets
@@ -24,36 +26,50 @@ def setup_ui(run_cmd):
 
     args_dict = {}
     for group in parser._action_groups:
-        if group.title in ['positional arguments', 'optional arguments', 'Image deployment', 'Maintenance']:
+        if group.title in [
+            "positional arguments",
+            "optional arguments",
+            "Image deployment",
+            "Maintenance",
+        ]:
             continue
         print(group.title)
         display(setup_args(group._group_actions, args_dict))
 
     dirname = pathlib.Path(__file__).parent.parent / "ogscm" / "recipes"
+    recipes = sorted(os.listdir(dirname))
+    for user_recipe in user_recipes:
+        if os.path.exists(user_recipe):
+            recipes.append(user_recipe)
     tab = widgets.Tab()
     tabs = []
     tab_names = []
-    for filename in sorted(os.listdir(dirname)):
+    for filename in recipes:
         if filename.endswith(".py") and not filename.startswith("_"):
             parser = argparse.ArgumentParser(add_help=False)
             ldict = {"filename": filename}
             execute = False
-            recipe_builtin = pkg_resources.read_text(recipes, filename)
-            exec(compile(recipe_builtin, filename, "exec"), locals(), ldict)
+            try:
+                recipe_builtin = pkg_resources.read_text(recipes, filename)
+                exec(compile(recipe_builtin, filename, "exec"), locals(), ldict)
+            except Exception as err:
+                if os.path.exists(filename):
+                    with open(filename, "r") as reader:
+                        exec(compile(reader.read(), filename, "exec"), locals(), ldict)
 
             if filename == "compiler.py":
                 button = widgets.ToggleButton(
                     value=True,
-                    description='Enabled',
-                    button_style='success',
-                    icon = "check"
+                    description="Enabled",
+                    button_style="success",
+                    icon="check",
                 )
             else:
                 button = widgets.ToggleButton(
                     value=False,
-                    description='Disabled',
+                    description="Disabled",
                 )
-            button.observe(on_button_recipe_clicked, 'value')
+            button.observe(on_button_recipe_clicked, "value")
             args_dict[filename] = button
             grid = setup_args(parser._actions, args_dict)
             tabs.append(widgets.VBox(children=[button, grid]))
@@ -64,17 +80,27 @@ def setup_ui(run_cmd):
         tab.set_title(idx, val)
     display(tab)
 
-    button = widgets.Button(description="CREATE CONTAINER", button_style="primary", layout=widgets.Layout(width='100%', height='35px'))
+    button = widgets.Button(
+        description="CREATE CONTAINER",
+        button_style="primary",
+        layout=widgets.Layout(width="100%", height="35px"),
+    )
     global out
-    out = widgets.Output(layout={'border': '1px solid black'})
+    out = widgets.Output(layout={"border": "1px solid black"})
     display(button, out)
 
-    button.on_click(functools.partial(on_button_clicked, out=out, args_dict=args_dict, run_cmd=run_cmd))
+    button.on_click(
+        functools.partial(
+            on_button_clicked, out=out, args_dict=args_dict, run_cmd=run_cmd
+        )
+    )
 
     return out
 
+
 def setup_args(actions, args_dict):
     import ipywidgets as widgets
+
     items = []
     for arg in actions:
         name = arg.option_strings[0]
@@ -88,19 +114,27 @@ def setup_args(actions, args_dict):
             default_value = ""
             if name == "--build_args":
                 default_value = "\"'--progress=plain'\""
-            widget = widgets.Text(value=default_value , placeholder=f"{arg.default}", description="(?)", description_tooltip=help)
+            widget = widgets.Text(
+                value=default_value,
+                placeholder=f"{arg.default}",
+                description="(?)",
+                description_tooltip=help,
+            )
         items.append(widgets.Label(value=name))
         items.append(widget)
         args_dict[name] = widget
     gridbox = widgets.GridBox(
-        children = items,
-        layout = widgets.Layout(
-            grid_template_columns='150px auto 150px auto',
-            grid_template_rows='auto',
-            grid_gap='10px 10px'))
+        children=items,
+        layout=widgets.Layout(
+            grid_template_columns="150px auto 150px auto",
+            grid_template_rows="auto",
+            grid_gap="10px 10px",
+        ),
+    )
 
     return gridbox
 
+
 def create_cli(items):
     cli_string = ""
     recipes = ""
@@ -118,6 +152,7 @@ def create_cli(items):
 
     return f"{recipes}{cli_string}"
 
+
 def on_button_clicked(b, out, args_dict, run_cmd):
     with out:
         out.clear_output()
@@ -125,8 +160,11 @@ def on_button_clicked(b, out, args_dict, run_cmd):
         cmd = f"ogscm {cli_string}"
         run_cmd(cmd)
 
+
 from IPython.display import FileLink
 import os
+
+
 class DownloadFileLink(FileLink):
     html_link_str = "<a href='{link}' download={file_name}>{link_text}</a>"
 
@@ -138,10 +176,37 @@ class DownloadFileLink(FileLink):
 
     def _format_path(self):
         from html import escape
-        fp = ''.join([self.url_prefix, escape(self.path)])
-        return ''.join([self.result_html_prefix,
-                        self.html_link_str.format(link=fp, file_name=self.file_name, link_text=self.link_text),
-                        self.result_html_suffix])
+
+        fp = "".join([self.url_prefix, escape(self.path)])
+        return "".join(
+            [
+                self.result_html_prefix,
+                self.html_link_str.format(
+                    link=fp, file_name=self.file_name, link_text=self.link_text
+                ),
+                self.result_html_suffix,
+            ]
+        )
+
+
+def display_source(code, language):
+    import IPython
+
+    def _jupyterlab_repr_html_(self):
+        from pygments import highlight
+        from pygments.formatters import HtmlFormatter
+
+        fmt = HtmlFormatter()
+        style = "<style>{}\n{}</style>".format(
+            fmt.get_style_defs(".output_html"), fmt.get_style_defs(".jp-RenderedHTML")
+        )
+        return style + highlight(self.data, self._get_lexer(), fmt)
+
+    # Replace _repr_html_ with our own version that adds the 'jp-RenderedHTML' class
+    # in addition to 'output_html'.
+    IPython.display.Code._repr_html_ = _jupyterlab_repr_html_
+    return IPython.display.Code(data=code, language=language)
+
 
 def display_download_link(output):
     from pygments import highlight
@@ -151,24 +216,23 @@ def display_download_link(output):
     import re
 
     # Sif
-    p = re.compile('.*Build.*: (.*\.sif)')
+    p = re.compile(".*Build.*: (.*\.sif)")
     m = p.search(output)
-    print("Download container:")
     if m:
         sif_file = m.group(1)
         from ogscm.jupyter import DownloadFileLink
         import os
+
+        print("Download container:")
         display(DownloadFileLink(os.path.relpath(sif_file)))
 
     # Dockerfile
-    p = re.compile('Created definition (.*)')
+    p = re.compile("Created definition (.*)")
     m = p.search(output)
     dockerfile = m.group(1)
 
     with open(dockerfile) as f:
         code = f.read()
-
-    formatter = HtmlFormatter()
-    IPython.display.HTML('<style type="text/css">{}</style>{}'.format(
-        formatter.get_style_defs('.highlight'),
-        highlight(code, DockerLexer(), formatter)))
+        # Does not work?
+        # display_source(code, "python3")
+        return code
-- 
GitLab