HGRC Quarry Fossil Service
Simple tools for lasting software

Infrastructure as a Service / Nemoptera

A solid foundation to fork, tweak, and enhance.

Public | Owner: Infrastructure as a Service | Default branch: trunk | Last update: 2026-11-08 16:13

Artifact e785ab8deed55bb0249d3b4fbf6c14550748b55ae13e925d7fc93d3bdd1b883e:


(defmodule nemoptera_fossil_skin
  (export all))

;; Classic repository skin: serif title, pipe menu, status line,
;; and a two-pane file browser on the Files and file pages.
(defun apply (repo-path)
  (let* ((name (lists:flatten
                (io_lib:format "skin-~w.conf"
                               (list (erlang:unique_integer '(positive))))))
         (path (filename:join (skin-dir) name))
         (body (lists:flatten
                (list "# nemoptera repository skin\n"
                      (card "css" (css))
                      (card "header" (header))
                      (card "footer" (footer))
                      (card "details" (details))
                      (card "js" (js))))))
    (filelib:ensure_dir path)
    (case (file:write_file path body)
      ('ok
       (let ((result (nemoptera_fossil:run
                      (list "configuration" "import" path "-R" repo-path))))
         (catch (file:delete path))
         result))
      (error error))))

(defun skin-dir ()
  (filename:join (filename:dirname (nemoptera_config:db-path)) "skin"))

(defun card (name value)
  (let ((line (lists:flatten
               (io_lib:format "~p '~s' value '~s'"
                              (list (erlang:system_time 'second)
                                    name
                                    (fossil-quote value))))))
    (lists:flatten
     (list "config /config "
           (integer_to_list (byte_size (list_to_binary line)))
           "\n" line "\n"))))

(defun fossil-quote (value)
  (lists:flatten (string:replace (to-str value) "'" "''" 'all)))

(defun to-str (value)
  (cond
    ((is_binary value) (binary_to_list value))
    ((is_list value) value)
    ('true (lists:flatten (io_lib:format "~p" (list value))))))

(defun join (lines)
  (string:join lines "\n"))

(defun header ()
  (join
   (list
    "<div class=\"banner\">"
    "  <h1 class=\"title\">$<project_name></h1>"
    "  <p class=\"tagline\">$<project_description></p>"
    "  <p class=\"navlinks\">"
    "    <a href=\"$home/home\">Home</a> |"
    "    <a href=\"$home/timeline\">Timeline</a> |"
    "    <a href=\"$home/dir?ci=tip&amp;type=tree\">Files</a> |"
    "    <a href=\"$home/brlist\">Branches</a> |"
    "    <a href=\"$home/taglist\">Tags</a> |"
    "    <a href=\"$home/ticket\">Tickets</a> |"
    "    <a href=\"$home/wiki\">Wiki</a> |"
    "    <a href=\"$home/forum\">Forum</a> |"
    "    <a href=\"$home/search\">Search</a> |"
    "    <th1>if {[info exists login]} {"
    "      html \"<a href='$home/logout'>Logout</a>\""
    "    } else {"
    "      html \"<a href='$home/login'>Login</a>\""
    "    }</th1>"
    "  </p>"
    "  <p class=\"repobar\"><th1>"
    "set ncheckin 0"
    "set last none"
    "query {SELECT count(*) AS n FROM event WHERE type='ci'} {set ncheckin $n}"
    "query {SELECT cast(strftime('%m',mtime) as int) AS mo, cast(strftime('%d',mtime) as int) AS d, strftime('%Y',mtime) AS y, strftime('%H:%M',mtime) AS hm FROM event ORDER BY mtime DESC LIMIT 1} {"
    "  set names [list January February March April May June July August September October November December]"
    "  set mname [lindex $names [expr {$mo-1}]]"
    "  set last \"$mname $d, $y $hm UTC\""
    "}"
    "html \"Repository: <a href='$home/dir?ci=tip&amp;type=tree'>\""
    "html [htmlize $project_name]"
    "html \"</a> | Project: <a href='$home/home'>\""
    "html [htmlize $project_name]"
    "html \"</a> | Check-ins: \""
    "html [htmlize $ncheckin]"
    "html \" | Last change: \""
    "html [htmlize $last]"
    "</th1></p>"
    "</div>")))

(defun footer ()
  (join
   (list
    "<div class=\"footer\">"
    "This page was generated in about"
    "<th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s by"
    "Fossil $manifest_version $manifest_date"
    "</div>"
    "<script nonce=\"$nonce\">"
    "<th1>styleScript</th1>"
    "</script>")))

(defun details ()
  (join
   (list
    "timeline-arrowheads:        1"
    "timeline-circle-nodes:      1"
    "timeline-color-graph-lines: 0"
    "white-foreground:           0")))

(defun css ()
  (join
   (list
    "body{margin:0;background:#fff;color:#000;font:16px/1.45 Georgia,\"Times New Roman\",Times,serif}"
    "a:link,a:visited{color:#00e;text-decoration:underline}"
    "a:hover{color:#00e}"
    "div.banner{text-align:center;margin:22px 16px 0}"
    "div.banner h1.title{margin:0;font:700 46px/1.05 Georgia,\"Times New Roman\",Times,serif;color:#000}"
    "div.banner p.tagline{margin:8px auto 2px;font-size:16px;color:#000}"
    "div.banner p.navlinks{margin:10px 0 12px}"
    "div.banner p.navlinks a{display:inline;margin:0 2px;padding:0;background:transparent;border:0;font-weight:400;color:#00e;text-decoration:underline}"
    "div.banner p.repobar{border-top:1px solid #000;border-bottom:1px solid #000;margin:0;padding:7px 12px;font-size:15px}"
    "h1.page-title{max-width:1080px;margin:16px auto 0;padding:0 16px;text-align:left;font:700 22px Georgia,\"Times New Roman\",Times,serif}"
    "body.rpage-file h1.page-title,body.rpage-dir h1.page-title{display:none}"
    "div.content{max-width:1080px;margin:14px auto 28px;padding:0 16px}"
    "body.rpage-file div.content,body.rpage-dir div.content{max-width:none;margin:0;padding:0 0 28px}"
    "div.footer{text-align:center;color:#333;font-size:13px;margin:18px 12px 28px}"
    "div.submenu{text-align:center;margin:8px 0}"
    "div.submenu a.label{display:inline;background:transparent;border:0;border-radius:0;box-shadow:none;padding:0;margin:0 0 0 8px;color:#00e;font-weight:400;text-decoration:underline}"
    "div.filesplit{display:flex;align-items:stretch;min-height:70vh}"
    "aside.filepane-tree{flex:0 0 300px;width:300px;box-sizing:border-box;border-right:1px solid #222;padding:12px 16px 28px;overflow:auto}"
    "aside.filepane-tree h2.filestitle{margin:6px 0 14px;font:700 28px Georgia,\"Times New Roman\",Times,serif}"
    "div.filepane-view{flex:1;min-width:0;padding:14px 28px 32px}"
    "div.filebar{display:flex;justify-content:space-between;align-items:baseline;gap:18px;border-bottom:1px solid #000;margin:0 0 16px;padding:2px 0 8px}"
    "h2.filepath{margin:0;padding:0;border:0;font:18px/1.4 Georgia,\"Times New Roman\",Times,serif;font-weight:400}"
    "div.filebar div.submenu{margin:0;text-align:right;white-space:nowrap}"
    "div.filebar div.submenu a.label::before{content:\"[\"}"
    "div.filebar div.submenu a.label::after{content:\"]\"}"
    "p.filehint{margin:18px 0;color:#333;font-size:18px}"
    "body.rpage-dir div.filepane-view>h2,body.rpage-dir div.filepane-view>p:not(.filehint){display:none}"
    "div.filetree{font-size:15px;line-height:1.7}"
    "div.filetree>ul{display:block}"
    "div.filetree ul{display:block;list-style:none;margin:0;padding:0}"
    "div.filetree ul.collapsed{display:none}"
    "div.filetree ul ul{margin:0 0 0 18px}"
    "div.filetree li{display:block;position:static;clear:both;margin:0;padding:0}"
    "div.filetree li li:before,div.filetree li>ul:before{content:none;display:none;border:0}"
    "div.filetreeline{display:block;width:auto;white-space:nowrap;padding:1px 0;line-height:1.7}"
    "div.filetree div.filetreeage,div.filetree div.filetreesize{display:none}"
    "div.filetree a{display:inline;position:static;min-height:0;padding:0 2px;background-image:none;background-repeat:no-repeat;color:#00e;text-decoration:underline;z-index:auto;vertical-align:middle;line-height:1.7}"
    "div.filetree li.dir>div.filetreeline>a,div.filetree li.file>div.filetreeline>a{display:inline;background-image:none;background-repeat:no-repeat;background-position:0 0;padding:0 2px;min-height:0}"
    "div.filetree li.dir>div.filetreeline>a::before{content:\"▶\";display:inline-block;box-sizing:border-box;width:36px;height:16px;margin-right:4px;padding:0;border:0;color:#222;font-size:10px;line-height:16px;text-align:left;text-decoration:none;vertical-align:middle;background-image:url(\"data:image/gif;base64,R0lGODlhEAAQAJEAAP/WVCIiIv///wAAACH5BAEHAAIALAAAAAAQABAAAAInlI9pwa3XYniCgQtkrAFfLXkiFo1jaXpo+jUs6b5Z/K4siDu5RPUFADs=\");background-repeat:no-repeat;background-position:18px center}"
    "div.filetree li.dir:has(>ul:not(.collapsed))>div.filetreeline>a::before{content:\"▼\"}"
    "div.filetree li.dir>div.filetreeline>a::after{content:\"/\"}"
    "div.filetree li.file>div.filetreeline>a::before{content:\"\";display:inline-block;box-sizing:border-box;width:36px;height:16px;margin-right:4px;padding:0;border:0;vertical-align:middle;background:transparent url(\"data:image/gif;base64,R0lGODlhEAAQAJEAAP///yEhIf///wAAACH5BAEHAAIALAAAAAAQABAAAAIvlIKpxqcfmgOUvoaqDSCxrEEfF14GqFXImJZsu73wepJzVMNxrtNTj3NATMKhpwAAOw==\") no-repeat 18px center}"
    "div.filetree a.current{background-color:#e6e0f5;background-image:none}"
    "div.filetreeline:has(>a.current){background:#e6e0f5}"
    "div.filetreeline:hover{background:transparent}"
    "div.filetreeline:has(>a.current):hover{background:#e6e0f5}"
    "div.markdown h1,div.markdown h2,div.content h1,div.content h2{font-family:Georgia,\"Times New Roman\",Times,serif;color:#000}"
    "div.markdown h1{font-size:32px;font-weight:700;border-bottom:1px solid #000;padding-bottom:4px;margin:18px 0 12px}"
    "div.markdown h2{font-size:24px;font-weight:700;border-bottom:1px solid #000;padding-bottom:3px;margin:20px 0 10px}"
    "div.markdown p,div.markdown li{font-size:16px}"
    "div.content pre,div.markdown pre{background:#f2f2f2;border:0;border-radius:0;padding:12px 14px;overflow:auto;font:14px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}"
    "table.timelineTable,div.content table{border-collapse:collapse}"
    "input,select,textarea{font:inherit}"
    "@media(max-width:800px){div.filesplit{display:block}aside.filepane-tree{width:auto;flex:none;border-right:0;border-bottom:1px solid #222}div.filebar{display:block}div.filebar div.submenu{text-align:left;white-space:normal;margin-top:8px}}"
    )))

(defun js ()
  (join
   (list
    "(function () {"
    "  var home = \"$home\";"
    "  if (window.fossil && fossil.rootPath) home = String(fossil.rootPath);"
    "  if (home.length && home.charAt(home.length - 1) === \"/\") home = home.substring(0, home.length - 1);"
    "  var body = document.body;"
    "  if (!body) return;"
    "  var isFile = body.classList.contains(\"rpage-file\");"
    "  var isDir = body.classList.contains(\"rpage-dir\");"
    "  if (!isFile && !isDir) return;"
    "  var content = document.querySelector(\"div.content\");"
    "  if (!content) return;"
    "  var split = document.createElement(\"div\");"
    "  split.className = \"filesplit\";"
    "  var treePane = document.createElement(\"aside\");"
    "  treePane.className = \"filepane-tree\";"
    "  var heading = document.createElement(\"h2\");"
    "  heading.className = \"filestitle\";"
    "  heading.textContent = \"Files\";"
    "  treePane.appendChild(heading);"
    "  var view = document.createElement(\"div\");"
    "  view.className = \"filepane-view\";"
    "  var tree = content.querySelector(\"div.filetree\");"
    "  if (tree) treePane.appendChild(tree);"
    "  var submenu = document.querySelector(\"div.submenu\");"
    "  if (isFile && submenu) view.appendChild(submenu);"
    "  while (content.firstChild) view.appendChild(content.firstChild);"
    "  if (isDir) {"
    "    var hint = document.createElement(\"p\");"
    "    hint.className = \"filehint\";"
    "    hint.textContent = \"Select a file.\";"
    "    view.insertBefore(hint, view.firstChild);"
    "  }"
    "  split.appendChild(treePane);"
    "  split.appendChild(view);"
    "  content.appendChild(split);"
    "  if (isFile) {"
    "    rewritePath(view);"
    "    loadTree(treePane);"
    "  }"
    "  function currentName() {"
    "    var params = new URLSearchParams(window.location.search);"
    "    var name = params.get(\"name\") || \"\";"
    "    if (name) return name;"
    "    var path = window.location.pathname;"
    "    var marker = \"/file/\";"
    "    var at = path.indexOf(marker);"
    "    if (at < 0) return \"\";"
    "    return decodeURIComponent(path.substring(at + marker.length));"
    "  }"
    "  function currentCi() {"
    "    var params = new URLSearchParams(window.location.search);"
    "    return params.get(\"ci\") || \"tip\";"
    "  }"
    "  function addLink(parent, href, text) {"
    "    var anchor = document.createElement(\"a\");"
    "    anchor.href = href;"
    "    anchor.textContent = text;"
    "    parent.appendChild(anchor);"
    "  }"
    "  function rewritePath(pane) {"
    "    var h2 = pane.querySelector(\"h2\");"
    "    var name = currentName();"
    "    if (!h2 || !name) return;"
    "    var ci = currentCi();"
    "    var project = (window.fossil && fossil.config && fossil.config.projectName) || \"Repository\";"
    "    h2.className = \"filepath\";"
    "    h2.textContent = \"\";"
    "    addLink(h2, home + \"/dir?ci=\" + encodeURIComponent(ci) + \"&type=tree\", project);"
    "    var parts = name.split(\"/\");"
    "    var acc = \"\";"
    "    for (var i = 0; i < parts.length; i++) {"
    "      h2.appendChild(document.createTextNode(\" / \"));"
    "      acc = acc ? (acc + \"/\" + parts[i]) : parts[i];"
    "      if (i + 1 === parts.length) addLink(h2, home + \"/file?name=\" + encodeURIComponent(name) + \"&ci=\" + encodeURIComponent(ci), parts[i]);"
    "      else addLink(h2, home + \"/dir?ci=\" + encodeURIComponent(ci) + \"&type=tree&name=\" + encodeURIComponent(acc), parts[i]);"
    "    }"
    "    var bar = document.createElement(\"div\");"
    "    bar.className = \"filebar\";"
    "    h2.parentNode.insertBefore(bar, h2);"
    "    bar.appendChild(h2);"
    "    var menu = pane.querySelector(\"div.submenu\");"
    "    if (menu) bar.appendChild(menu);"
    "  }"
    "  function markCurrent(pane) {"
    "    var name = currentName();"
    "    if (!name) return;"
    "    var links = pane.querySelectorAll(\"a\");"
    "    for (var i = 0; i < links.length; i++) {"
    "      var href = links[i].getAttribute(\"href\") || \"\";"
    "      if (href.indexOf(\"/file\") < 0) continue;"
    "      var query = href.split(\"?\")[1] || \"\";"
    "      if ((new URLSearchParams(query)).get(\"name\") !== name) continue;"
    "      links[i].classList.add(\"current\");"
    "      var node = links[i];"
    "      while (node) {"
    "        if (node.classList && node.classList.contains(\"collapsed\")) node.classList.remove(\"collapsed\");"
    "        node = node.parentNode;"
    "      }"
    "    }"
    "  }"
    "  function enableToggle(tree) {"
    "    var outer = tree.querySelector(\"ul\");"
    "    if (!outer || outer.getAttribute(\"data-toggle\")) return;"
    "    outer.setAttribute(\"data-toggle\", \"1\");"
    "    outer.addEventListener(\"click\", function (event) {"
    "      var target = event.target;"
    "      if (!target || target.nodeName !== \"A\") return;"
    "      var line = target.parentNode;"
    "      var list = line ? line.nextSibling : null;"
    "      while (list && list.nodeType !== 1) list = list.nextSibling;"
    "      if (!list || list.nodeName !== \"UL\") return;"
    "      event.preventDefault();"
    "      if (list.classList.contains(\"collapsed\")) list.classList.remove(\"collapsed\");"
    "      else list.classList.add(\"collapsed\");"
    "    });"
    "  }"
    "  function loadTree(pane) {"
    "    var url = home + \"/dir?ci=\" + encodeURIComponent(currentCi()) + \"&type=tree\";"
    "    var xhr = new XMLHttpRequest();"
    "    xhr.open(\"GET\", url);"
    "    xhr.onload = function () {"
    "      if (xhr.status !== 200) return;"
    "      var doc = new DOMParser().parseFromString(xhr.responseText, \"text/html\");"
    "      var fetched = doc.querySelector(\"div.filetree\");"
    "      if (!fetched) return;"
    "      var node = document.importNode(fetched, true);"
    "      pane.appendChild(node);"
    "      markCurrent(pane);"
    "      enableToggle(node);"
    "    };"
    "    xhr.send();"
    "  }"
    "})();"
    )))

Clone

You can clone this repository with the following command:

fossil clone https://hgrc.dev/IaaS/Nemoptera Nemoptera.fossil

Contribute

The best way to contribute is through e-mail. See the project documentation for more details.