Added tangle-file.sh + helper
This commit is contained in:
Executable
+294
@@ -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"
|
||||||
@@ -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=.
|
||||||
Reference in New Issue
Block a user