Added tangle-file.sh + helper

This commit is contained in:
2026-06-13 15:37:55 +02:00
parent e75d42d4ac
commit 2c8fa86dd3
2 changed files with 365 additions and 0 deletions
+294
View File
@@ -0,0 +1,294 @@
#!/usr/bin/env bash
# tangle-file.sh
# Usage: tangle-file.sh <SOURCE_DIR> <DEST_DIR> <ORG_FILE>
# Example: tangle-file.sh ~/NixOS ~/Projects/DroidNix README.org
set -euo pipefail
# ---------------------------------------------------------------------------
# Arguments
# ---------------------------------------------------------------------------
if [[ $# -lt 3 ]]; then
echo "Usage: $0 <SOURCE_DIR> <DEST_DIR> <ORG_FILENAME>"
echo " SOURCE_DIR : Directory to scan recursively"
echo " DEST_DIR : Directory to write the .org file and assets"
echo " ORG_FILENAME : Name of the output org file (e.g. README.org)"
exit 1
fi
SOURCE_DIR="$(realpath "${1/#\~/$HOME}")"
DEST_DIR="$(realpath -m "${2/#\~/$HOME}")"
ORG_ARG="${3/#\~/$HOME}"
# Third arg may be a full path or just a filename.
# If it contains a slash, treat it as an absolute/relative path; otherwise place it in DEST_DIR.
if [[ "$ORG_ARG" == */* ]]; then
OUTPUT_FILE="$(realpath -m "$ORG_ARG")"
else
OUTPUT_FILE="$DEST_DIR/$ORG_ARG"
fi
ASSETS_DIR="$DEST_DIR/assets"
SKIPPED_LIST_FILE="$(mktemp)"
# ---------------------------------------------------------------------------
# Extension → language mapping
# ---------------------------------------------------------------------------
ALLOWED_EXTENSIONS=(
"sh" "bash" "zsh" "fish" "py" "rb" "js" "mjs" "cjs" "ts" "lua"
"conf" "cfg" "ini" "toml" "yaml" "yml" "json" "jsonc" "xml"
"css" "scss" "sass" "html" "htm" "md" "markdown" "nix" "vim"
"vimrc" "el" "rs" "go" "c" "cpp" "cc" "cxx" "h" "hpp" "java"
"kt" "sql" "r" "tex" "rasi" "qml" "hs" "ex" "exs" "clj" "cs"
"swift" "dart" "zig" "nu" "ps1" "bat" "cmd" "env" "lock"
"gitignore" "gitattributes" "editorconfig" "prettierrc" "eslintrc"
)
is_extension_allowed() {
local ext="${1,,}" # lowercase
for allowed in "${ALLOWED_EXTENSIONS[@]}"; do
[[ "$ext" == "$allowed" ]] && return 0
done
return 1
}
get_language_tag() {
local ext="${1,,}"
case "$ext" in
sh|bash) echo "bash" ;;
zsh) echo "zsh" ;;
fish) echo "fish" ;;
nu) echo "nu" ;;
py) echo "python" ;;
rb) echo "ruby" ;;
js|mjs|cjs) echo "javascript" ;;
ts) echo "typescript" ;;
lua) echo "lua" ;;
conf|cfg|ini) echo "conf" ;;
toml|lock) echo "toml" ;;
yaml|yml) echo "yaml" ;;
json|jsonc) echo "json" ;;
xml) echo "xml" ;;
css|rasi) echo "css" ;;
scss|sass) echo "scss" ;;
html|htm) echo "html" ;;
md|markdown) echo "markdown" ;;
nix) echo "nix" ;;
vim|vimrc) echo "vimscript" ;;
el) echo "emacs-lisp" ;;
rs) echo "rust" ;;
go) echo "go" ;;
c|h) echo "c" ;;
cpp|cc|cxx|hpp) echo "cpp" ;;
java) echo "java" ;;
kt) echo "kotlin" ;;
sql) echo "sql" ;;
r) echo "R" ;;
tex) echo "latex" ;;
qml) echo "qml" ;;
hs) echo "haskell" ;;
ex|exs) echo "elixir" ;;
clj) echo "clojure" ;;
cs) echo "csharp" ;;
swift) echo "swift" ;;
dart) echo "dart" ;;
zig) echo "zig" ;;
ps1) echo "powershell" ;;
bat|cmd) echo "bat" ;;
env|gitignore|gitattributes|editorconfig|prettierrc|eslintrc)
echo "text" ;;
*) echo "$ext" ;;
esac
}
# ---------------------------------------------------------------------------
# Helper: check if a file is text (by mime type or allowed extension)
# ---------------------------------------------------------------------------
is_text_file() {
local file="$1"
local filename ext mime_type
filename="$(basename "$file")"
ext="${filename##*.}"
# No extension (ext equals the whole filename)
[[ "$ext" == "$filename" ]] && ext=""
mime_type="$(file -b --mime-type "$file" 2>/dev/null || echo "application/octet-stream")"
# Always include if mime says text
if [[ "$mime_type" == text/* ]]; then
return 0
fi
# Explicitly skip binary/media mime types — regardless of extension
case "$mime_type" in
image/*|audio/*|video/*|\
application/octet-stream|\
application/zip|\
application/gzip|\
application/x-tar|\
application/x-bzip2|\
application/x-xz|\
application/x-zstd|\
application/x-7z-compressed|\
application/x-rar|\
application/pdf|\
application/vnd.*|\
font/*)
return 1 ;;
esac
# Some well-known text-ish mime types
case "$mime_type" in
application/json|\
application/x-shellscript|\
application/x-sh|\
application/x-nix|\
application/xml|\
application/javascript|\
application/typescript|\
inode/x-empty)
return 0 ;;
esac
# Fall back: if extension is in our allowed list → treat as text
if [[ -n "$ext" ]] && is_extension_allowed "$ext"; then
return 0
fi
# Last resort for extensionless files: ask `file` for a plain description
local description
description="$(file -b "$file" 2>/dev/null || echo "")"
if [[ "$description" == *"text"* || "$description" == *"ASCII"* || "$description" == *"UTF-8"* ]]; then
return 0
fi
return 1
}
# ---------------------------------------------------------------------------
# Prepare output
# ---------------------------------------------------------------------------
mkdir -p "$DEST_DIR"
mkdir -p "$ASSETS_DIR"
# Write org file header
# Use tangle-header.md if it exists in the same directory as the script, otherwise in DEST_DIR
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
HEADER_FILE=""
if [[ -f "$SCRIPT_DIR/tangle-header.md" ]]; then
HEADER_FILE="$SCRIPT_DIR/tangle-header.md"
elif [[ -f "$DEST_DIR/tangle-header.md" ]]; then
HEADER_FILE="$DEST_DIR/tangle-header.md"
else
echo "Error: tangle-header.md not found in script directory or destination directory"
exit 1
fi
cat "$HEADER_FILE" > "$OUTPUT_FILE"
echo "Scanning $SOURCE_DIR ..."
echo "Writing to $OUTPUT_FILE ..."
# ---------------------------------------------------------------------------
# Main loop: find all files, sorted, skipping .git
# ---------------------------------------------------------------------------
while IFS= read -r -d '' abs_file; do
filename="$(basename "$abs_file")"
ext="${filename##*.}"
[[ "$ext" == "$filename" ]] && ext="" # extensionless
# Relative path from SOURCE_DIR (e.g. modules/core/flatpak.nix)
rel_path="${abs_file#$SOURCE_DIR/}"
# Tangle path always under generated/
tangle_path="generated/$rel_path"
# Org display path (with ~ shorthand when under HOME, raw path otherwise)
if [[ "$SOURCE_DIR" == "$HOME"* ]]; then
org_path="~/${SOURCE_DIR#$HOME/}/$rel_path"
else
org_path="$SOURCE_DIR/$rel_path"
fi
if is_text_file "$abs_file"; then
# Determine language tag
if [[ -z "$ext" ]]; then
lang="text"
else
lang="$(get_language_tag "$ext")"
fi
{
echo "** =$tangle_path="
echo "Information read from $org_path"
echo "#+BEGIN_SRC $lang :tangle $tangle_path :noweb yes :mkdirp yes :eval never"
cat "$abs_file"
# Ensure there's a newline before #+END_SRC
echo ""
echo "#+END_SRC"
echo ""
} >> "$OUTPUT_FILE"
else
# Binary / skipped: record relative path, copy to assets
echo "$rel_path" >> "$SKIPPED_LIST_FILE"
asset_dest="$ASSETS_DIR/$rel_path"
mkdir -p "$(dirname "$asset_dest")"
cp "$abs_file" "$asset_dest"
fi
done < <(find "$SOURCE_DIR" -type f \
-not -path "*/.git/*" \
-not -name ".git" \
-not -path "$OUTPUT_FILE" \
-print0 | sort -z)
# ---------------------------------------------------------------------------
# Skipped-files section
# ---------------------------------------------------------------------------
if [[ -s "$SKIPPED_LIST_FILE" ]]; then
echo "" >> "$OUTPUT_FILE"
echo "* Skipped (non-text / binary) files" >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
echo "** =UntangledFiles.md=" >> "$OUTPUT_FILE"
echo "Tree of untangled files" >> "$OUTPUT_FILE"
echo "#+BEGIN_SRC text :tangle UntangledFiles.md :noweb yes :mkdirp yes :eval never" >> "$OUTPUT_FILE"
if command -v tree &>/dev/null; then
# Build a temporary shadow directory tree and run `tree` on it
TMP_DIR="$(mktemp -d)"
while IFS= read -r rel; do
mkdir -p "$TMP_DIR/$(dirname "$rel")"
touch "$TMP_DIR/$rel"
done < "$SKIPPED_LIST_FILE"
# Print tree output without the tmp path prefix
tree --noreport "$TMP_DIR" | tail -n +2 >> "$OUTPUT_FILE"
rm -rf "$TMP_DIR"
else
# Fallback: manual pseudo-tree using sorted paths
echo "/" >> "$OUTPUT_FILE"
sort "$SKIPPED_LIST_FILE" | while IFS= read -r rel; do
depth=$(echo "$rel" | tr -cd '/' | wc -c)
indent=""
for ((i=0; i<depth; i++)); do indent="$indent "; done
echo "${indent}└── $(basename "$rel")" >> "$OUTPUT_FILE"
done
fi
echo "#+END_SRC" >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
# Summary of where assets were copied
echo "Skipped files have been copied to: $ASSETS_DIR" >> "$OUTPUT_FILE"
fi
# ---------------------------------------------------------------------------
# Cleanup
# ---------------------------------------------------------------------------
rm -f "$SKIPPED_LIST_FILE"
echo ""
echo "Done!"
echo " Org file : $OUTPUT_FILE"
echo " Assets : $ASSETS_DIR"
+71
View File
@@ -0,0 +1,71 @@
#+title: Droidnix: A Dendritic NixOS + Home Manager Configuration
#+author: Henro Veijer
#+BEGIN_SRC emacs-lisp :results silent
(let ((dir (getenv "XDG_DROIDNIX_DIR")))
(when dir
(let ((langs '("nix" "sh" "bash" "conf" "lua" "qml" "jsonc" "el" "toml" "css")))
(dolist (lang langs)
(let* ((prop (concat "header-args:" lang))
(val (org-entry-get (point-min) prop t))
(expanded (when val (replace-regexp-in-string
(regexp-quote "$XDG_DROIDNIX_DIR")
dir val))))
(when expanded
(org-global-prop-set prop expanded)))))))
#+END_SRC
#+options: toc:t num:nil htmlize:nil
#+PROPERTY: header-args:nix :prologue "# --- This file has been auto-generated. For permanent changes alter the appropriate block in $XDG_DROIDNIX_DIR/README.org. ---"
#+PROPERTY: header-args:sh :prologue "# --- This file has been auto-generated. For permanent changes alter the appropriate block in $XDG_DROIDNIX_DIR/README.org. ---"
#+PROPERTY: header-args:bash :prologue "# --- This file has been auto-generated. For permanent changes alter the appropriate block in $XDG_DROIDNIX_DIR/README.org. ---"
#+PROPERTY: header-args:css :prologue "/* --- This file has been auto-generated. For permanent changes alter the appropriate block in $XDG_DROIDNIX_DIR/README.org. --- */"
#+PROPERTY: header-args:lua :prologue "--- This file has been auto-generated. For permanent changes alter the appropriate block in $XDG_DROIDNIX_DIR/README.org. ---"
#+PROPERTY: header-args:conf :prologue "# --- This file has been auto-generated. For permanent changes alter the appropriate block in $XDG_DROIDNIX_DIR/README.org. ---"
#+PROPERTY: header-args:qml :prologue "// --- This file has been auto-generated. For permanent changes alter the appropriate block in $XDG_DROIDNIX_DIR/README.org. ---"
#+PROPERTY: header-args:json :prologue ""
#+PROPERTY: header-args:jsonc :prologue "// --- This file has been auto-generated. For permanent changes alter the appropriate block in $XDG_DROIDNIX_DIR/README.org. ---"
#+PROPERTY: header-args:el :prologue ";; --- This file has been auto-generated. For permanent changes alter the appropriate block in $XDG_DROIDNIX_DIR/README.org. ---"
#+PROPERTY: header-args:toml :prologue "# --- This file has been auto-generated. For permanent changes alter the appropriate block in $XDG_DROIDNIX_DIR/README.org. ---"
#+PROPERTY: header-args:none :prologue ""
#+language: en
#+html_head: <style>pre.src { background-color: #1e1e2e; color: #cdd6f4; padding: 1em; border-radius: 4px; }</style>
#+HTML_HEAD: <script src="https://cdn.jsdelivr.net/npm/tree.js@1.0.0/dist/tree.min.js"></script>
#+HTML_HEAD: <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tree.js@1.0.0/dist/tree.min.css">
* Shortcuts
[#introduction][Introduction]
[#the-assets-folder][The Assets Folder]
[#the-nix-files][The .nix files]
---
* Introduction :intro:
:PROPERTIES:
:CUSTOM_ID: introduction
:END:
** What is Droidnix
Droidnix is a modular, declarative NixOS + Home Manager configuration with configurations managed via Emacs Org and Nix Flakes. The project is designed for reproducibility, maintainability, and cross-machine compatibility.
*** Installed components:
**** Core
**** Hyprland
*** Goals, project Structure, import hierarchy
This project uses a **modular NixOS configuration** with **Hyprland** support, designed for **literate programming** and **cross-device reusability**.
The Droidnix repository is organized into two main parts:
1. =.assets/=: Static, non-generated files (e.g., configs, scripts, themes).
2. Generated files and folders
#+title: NixOS Configuration Structure
* Root Level
- = is the entry point and imports:
- =generated/assets/=
- =generated/modules/=
- =generated/hosts/=
* Generated Structure
The =generated/= directory contains all generated configurations, divided into three main groups: =system=, =hyprland=, and =mangowc=.