commit 76a3330d51a2f999a98ee24a0227ca2ab9f924a1
Author: Fred Großkopf <fred@kuandu.systems>
Date: Tue, 14 Oct 2025 23:50:11 +0200
Init
Diffstat:
7 files changed, 747 insertions(+), 0 deletions(-)
diff --git a/README b/README
@@ -0,0 +1,59 @@
+# sent-tools
+
+A small toolkit of shell scripts for compiling suckless sent slideshows.
+
+## Scripts
+
+- `compile-sent-tables.sh` - Converts Markdown tables in tables/ to images/tables
+- `compile-sentspec.sh` - Compiles .sentspec to .sent (see -h for more info)
+- `sent-dev.sh` - Developer helper for automated workflow (requires `tmux`, `entr`).
+- `compile-sentspec-wrapper.sh` - a helper script which allows to use a `config`
+
+**Important:**
+
+Scripts have `.sh` in the repo but are intended to be called without `.sh` after installation.
+
+## Dependencies
+
+- Required:
+ `md-table-to-jpg`, `curl`, `convert`, `identify`, `mktemp`, `cut`, `grep`, `awk`, `sha256sum` (or `sha256`), `stat`
+
+- For developer workflows:
+ `tmux`, `entr`
+
+- Optional:
+ Config file providing a `compile_sentspec_args()` function
+
+## Installation
+
+1. Clone this repository:
+ ```
+ git clone https://some-url/sent-tools.git
+ cd sent-tools
+ ```
+
+2. Run the install script to symlink scripts (without `.sh`) to `~/bin`:
+ ```
+ ./install.sh
+ ```
+ If you want to symlink to a different directory in your `PATH`, provide it:
+ ```
+ ./install.sh /desired/path/bin
+ ```
+3. Make sure `~/bin` (or your chosen directory) is in your `PATH`:
+ ```
+ export PATH="$HOME/bin:$PATH"
+ ```
+ Add that line to your `.bashrc` or `.zshrc` for persistence.
+
+4. Optional: copy/edit `config.example` to `your-project/config`
+
+## Usage
+
+Run tools using their name (without `.sh`), for example:
+
+```
+compile-sentspec input.sentspec > output.sent
+compile-sent-tables
+sent-dev input.sentspec
+```
diff --git a/compile-sent-tables.sh b/compile-sent-tables.sh
@@ -0,0 +1,36 @@
+#!/bin/sh
+# convert-tables.sh — Convert tables/*.md to images/tables/*.jpg, with dependency checking
+
+MD_TABLE_TO_JPG="${MD_TABLE_TO_JPG:-md2jpg-table}"
+
+status() { printf "[*] %s\n" "$1" >&2; }
+
+# Dependency check
+for dep in basename "$MD_TABLE_TO_JPG"; do
+ if ! command -v "$dep" > /dev/null 2>&1; then
+ status "Dependency not found: $dep"
+ status "Please install the missing dependency and try again."
+ exit 1
+ fi
+done
+
+mkdir -p images/tables
+
+# Convert or update images from markdown files
+for f in tables/*.md; do
+ [ -f "$f" ] || continue
+ out="images/tables/$(basename "${f%.md}.jpg")"
+ if [ ! -e "$out" ] || [ -n "$(find "$f" -prune -newer "$out")" ]; then
+ "$MD_TABLE_TO_JPG" "$f" "$out"
+ fi
+done
+
+# Remove .jpg files that have no corresponding .md file
+for img in images/tables/*.jpg; do
+ [ -f "$img" ] || continue
+ base="$(basename "$img" .jpg)"
+ if [ ! -f "tables/${base}.md" ]; then
+ status "Removing orphan image $img"
+ rm "$img"
+ fi
+done
diff --git a/compile-sentspec-wrapper.sh b/compile-sentspec-wrapper.sh
@@ -0,0 +1,64 @@
+#!/bin/sh
+#
+# Usage: ./compile-sentspec-wrapper.sh file.sentspec
+#
+# This script:
+# - Checks that the input ends with .sentspec.
+# - Converts the input to an output filename ending with .sent.
+# - Optionally loads extra arguments from a 'config' file in the same directory.
+# - Runs compile-sentspec with these options and writes output to the .sent file.
+#
+# Config file format:
+# - Place a file named "config" in the script directory (optional).
+# - To add options, define a shell function named compile_sentspec_args that
+# prints each argument on its own line, for example:
+#
+# compile_sentspec_args() {
+# printf '%s\n' \
+# -b black \
+# -f '#00ff00' \
+# -F 'Comic Neue Regular'
+# }
+#
+# - If no config or function is given, only the input file is used.
+
+set -eu
+
+case $1 in
+ *.sentspec) ;;
+ *)
+ echo "Usage: $0 file.sentspec" >&2
+ exit 1
+ ;;
+esac
+
+status() {
+ printf '[*] %s\n' "$*" >&2
+}
+
+specfile="$1"
+outfile="${specfile%.sentspec}.sent"
+
+# Source "config" (if it exists) for optional argument customization
+specdir=$(dirname -- "$specfile")
+CONFIG="$specdir/config"
+if [ -f "$CONFIG" ]; then
+ status "Sourcing $CONFIG"
+ # shellcheck disable=SC1090
+ . "$CONFIG"
+
+ # If the user provided a compile_sentspec_args function in config, use its output for options
+ if command -v compile_sentspec_args > /dev/null 2>&1; then
+ # shellcheck disable=SC2046
+ set -- $(compile_sentspec_args)
+ # Add the .sentspec file passed as the first arg to this script
+ set -- "$@" "$specfile"
+ else
+ stauts "No compile_sentspec_args() found in config!"
+ fi
+else
+ status "No config file found"
+fi
+
+status "Calling: exec compile-sentspec $@ > $outfile"
+compile-sentspec "$@" > "$outfile" && status "Done"
diff --git a/compile-sentspec.sh b/compile-sentspec.sh
@@ -0,0 +1,367 @@
+#!/bin/sh
+#
+# compile-sent.sh - Compile a .sentspec meta-format file into a sent-compatible
+# slide deck, supporting download/local images and source bars.
+#
+
+set -eu
+
+VERBOSE=0
+readonly DOWNLOADED_DIR="./images/downloaded"
+readonly COMPILED_DIR="./images/compiled"
+readonly HASHES_FILE="./images.hashes"
+
+usage() {
+ cat << EOF
+Usage: $0 [-b bgcolor] [-f fontcolor] [-F fontname] [-W width] [-H height] [-v] [-h] <file.sentspec>
+
+Compile a .sentspec meta-format file into a sent-compatible slide deck.
+
+Options:
+ -b bgcolor Set background color (default: white)
+ -f fontcolor Set font color (default: black)
+ -F fontname Set font family (default: DejaVu-Sans-Mono)
+ -W width Set output width in pixels (default: 1920)
+ -H height Set output height in pixels (default: 1080)
+ -v Verbose output
+ -h Show this help and exit
+
+Lines starting with 'L|', 'R|', or 'C|' are interpreted as:
+
+ pos|mode|file|url|title|subtitle
+
+ pos L for image left, R for image right, C for image center
+ mode D for download, F for local file
+ file Local filename for the image (or to save download as)
+ url Image URL (optional, if present adds a source bar)
+ title Title text (L|R only)
+ subtitle Subtitle text (L|R only, optional)
+
+All other lines are passed through unchanged.
+EOF
+}
+
+error() {
+ printf "Error: %s\n" "$1" >&2
+ exit 1
+}
+
+status() {
+ if [ "$VERBOSE" = 1 ]; then
+ printf "[*] %s\n" "$1" >&2
+ fi
+}
+
+check_dependencies() {
+ for cmd in curl convert identify mktemp cut grep awk sha256 stat; do
+ command -v "$cmd" > /dev/null 2>&1 \
+ || error "Required command '$cmd' not found."
+ done
+}
+
+check_font() {
+ font="$1"
+ if ! convert -list font | grep -qi "^ *Font: *$font\$"; then
+ status "Font '$font' not found in ImageMagick. Using anyway, but rendering may fail."
+ fi
+}
+
+download_image() {
+ url="$1"
+ filename="$2"
+ status "Downloading $url"
+ curl -sSL "$url" -o "$filename" || error "Failed to download $url"
+}
+
+scale_image() {
+ input="$1"
+ output="$2"
+ max_width="$3"
+ max_height="$4"
+
+ img_w=$(identify -format "%w" "$input")
+ img_h=$(identify -format "%h" "$input")
+
+ scale_w=$(awk -v mw="$max_width" -v iw="$img_w" 'BEGIN {print mw/iw}')
+ scale_h=$(awk -v mh="$max_height" -v ih="$img_h" 'BEGIN {print mh/ih}')
+ scale=$(awk -v sw="$scale_w" -v sh="$scale_h" \
+ 'BEGIN {if (sw < sh) print sw; else print sh}')
+ new_w=$(awk -v iw="$img_w" -v s="$scale" 'BEGIN {printf "%d", iw*s}')
+ new_h=$(awk -v ih="$img_h" -v s="$scale" 'BEGIN {printf "%d", ih*s}')
+
+ convert "$input" -background white -flatten -resize "${new_w}x${new_h}" \
+ "$output"
+}
+
+combine_image_text() {
+ pos="$1"
+ imgfile="$2"
+ outfile="$3"
+ title="$4"
+ subtitle="$5"
+ screen_w="$6"
+ screen_h="$7"
+ bgcolor="$8"
+ fontcolor="$9"
+ font="${10}"
+ tmpdir="${11}"
+
+ half_w=$((screen_w / 2))
+ title_pt=68
+ subtitle_pt=52
+ title_y=$((-1 * (screen_h / 10)))
+ subtitle_y=$((title_y + 200))
+
+ text_img=$(mktemp "$tmpdir/textimg.XXXXXX.png")
+
+ if [ -n "$subtitle" ]; then
+ convert -size "${half_w}x${screen_h}" xc:"$bgcolor" \
+ -font "$font" -pointsize $title_pt -fill "$fontcolor" \
+ -gravity Center -annotate +0+$title_y "$title" \
+ -font "$font" -pointsize $subtitle_pt \
+ -gravity Center -annotate +0+$subtitle_y "$subtitle" \
+ "$text_img"
+ else
+ convert -size "${half_w}x${screen_h}" xc:"$bgcolor" \
+ -font "$font" -pointsize $title_pt -fill "$fontcolor" \
+ -gravity North -annotate +0+$title_y "$title" \
+ "$text_img"
+ fi
+ cp "$text_img" test.jpg
+
+ canvas=$(mktemp "$tmpdir/canvas.XXXXXX.png")
+ convert -size "${screen_w}x${screen_h}" xc:"$bgcolor" \
+ -colorspace sRGB "$canvas"
+ convert "$canvas" -fill "#00ff00" -draw "point 0,0" "$canvas"
+
+ img_w=$(identify -format "%w" "$imgfile")
+ half_w=$((screen_w / 2))
+
+ if [ "$img_w" -lt "$half_w" ]; then
+ offset=$((half_w - img_w))
+ else
+ offset=0
+ fi
+
+ if [ "$pos" = "L" ]; then
+ convert "$canvas" \
+ \( "$imgfile" -geometry +${offset}+0 -gravity west \) -composite \
+ \( "$text_img" -gravity east -geometry +0+0 \) -composite \
+ "$outfile"
+ else
+ convert "$canvas" \
+ \( "$text_img" -gravity west -geometry +0+0 \) -composite \
+ \( "$imgfile" -geometry +${offset}+0 -gravity east \) -composite \
+ "$outfile"
+ fi
+}
+
+add_source_bar() {
+ input_img="$1"
+ output_img="$2"
+ url="$3"
+ date="$4"
+ source_bar_font="$5"
+ source_bar_font_size="$6"
+ source_bar_height="$7"
+
+ img_w=$(identify -format "%w" "$input_img")
+ source_text="Source: $url, $date"
+ source_text_escaped=$(printf '%s' "$source_text" | sed 's/%/%%/g')
+
+ bar_img=$(mktemp "$TMPDIR/bar.XXXXXX.png")
+ convert -size "${img_w}x${source_bar_height}" xc:"rgba(255,255,255,0.8)" \
+ -font "$source_bar_font" -pointsize "$source_bar_font_size" -fill "black" \
+ -gravity northwest -annotate +10+0 "$source_text_escaped" \
+ "$bar_img"
+
+ convert "$input_img" "$bar_img" -gravity south -composite "$output_img"
+}
+
+compute_slide_hash() {
+ imgfile="${1:-}"
+ line="${2:-}"
+ font="${3:-}"
+ fontcolor="${4:-}"
+ bgcolor="${5:-}"
+ {
+ sha256 < "${imgfile:-/dev/null}"
+ printf "%s\n%s\n%s\n%s\n" "$line" "$font" "$fontcolor" "$bgcolor"
+ } | sha256 | awk '{print $1}'
+}
+
+get_stored_hash() {
+ [ -f "$HASHES_FILE" ] || return 1
+ grep -F -- "$1 " "$HASHES_FILE" 2> /dev/null | awk '{print $2}'
+}
+
+update_hash() {
+ tmpf=$(mktemp "$TMPDIR/hashupdate.XXXXXX")
+ if [ -f "$HASHES_FILE" ]; then
+ grep -v -F -- "$1 " "$HASHES_FILE" > "$tmpf" || true
+ fi
+ printf "%s %s\n" "$1" "$2" >> "$tmpf"
+ mv "$tmpf" "$HASHES_FILE"
+}
+
+cleanup() {
+ [ -n "${TMPDIR:-}" ] && [ -d "$TMPDIR" ] && rm -rf "$TMPDIR"
+}
+
+main() {
+ bgcolor="white"
+ fontcolor="black"
+ font="DejaVu-Sans-Mono"
+ screen_w=1920
+ screen_h=1080
+ source_bar_font="DejaVu-Sans-Condensed"
+ source_bar_font_size=12
+ source_bar_height=18
+
+ while getopts "b:f:F:W:H:vh" opt; do
+ case "$opt" in
+ b) bgcolor="$OPTARG" ;;
+ f) fontcolor="$OPTARG" ;;
+ F) font="$OPTARG" ;;
+ W) screen_w="$OPTARG" ;;
+ H) screen_h="$OPTARG" ;;
+ v) VERBOSE=1 ;;
+ h)
+ usage
+ exit 0
+ ;;
+ *)
+ usage
+ exit 1
+ ;;
+ esac
+ done
+ shift $((OPTIND - 1))
+
+ if [ $# -ne 1 ]; then
+ usage
+ exit 1
+ fi
+
+ input_file="$1"
+ [ -f "$input_file" ] || error "Input file '$input_file' not found"
+
+ check_dependencies
+ check_font "$font"
+
+
+ status "Using background color: $bgcolor"
+ status "Using font color: $fontcolor"
+ status "Using font: $font"
+ status "Output size: ${screen_w}x${screen_h}"
+
+ TMPDIR=$(mktemp -d "/tmp/compile-sent.XXXXXX")
+ readonly TMPDIR
+ trap cleanup EXIT INT TERM
+
+ for d in "$DOWNLOADED_DIR" "$COMPILED_DIR"; do
+ [ -d "$d" ] || mkdir -p "$d"
+ done
+
+ if [ ! -f "$HASHES_FILE" ]; then
+ : > "$HASHES_FILE"
+ fi
+
+ while IFS= read -r line || [ -n "$line" ]; do
+ case "$line" in
+ L\|* | R\|* | C\|*)
+ pos=$(printf '%s\n' "$line" | cut -d'|' -f1)
+ mode=$(printf '%s\n' "$line" | cut -d'|' -f2)
+ filename=$(printf '%s\n' "$line" | cut -d'|' -f3)
+ url=$(printf '%s\n' "$line" | cut -d'|' -f4)
+ title=""
+ subtitle=""
+ if [ "$pos" = "L" ] || [ "$pos" = "R" ]; then
+ title=$(printf '%s\n' "$line" | cut -d'|' -f5)
+ subtitle=$(printf '%s\n' "$line" | cut -d'|' -f6-)
+ fi
+
+ if [ -z "$filename" ]; then
+ status "Malformed meta line, skipping: $line"
+ continue
+ fi
+
+ if [ "$mode" = "F" ]; then
+ imgfile="$filename"
+ basefile=$(basename "$filename")
+ if [ ! -f "$imgfile" ]; then
+ status "Local image not found: $filename"
+ continue
+ fi
+ elif [ "$mode" = "D" ]; then
+ basefile=$(basename "$filename")
+ imgfile="$DOWNLOADED_DIR/$basefile"
+ if [ -f "$imgfile" ]; then
+ #status "Already downloaded: $imgfile"
+ :
+ else
+ if [ -z "$url" ]; then
+ status "No URL for download mode, skipping: $line"
+ continue
+ fi
+ download_image "$url" "$imgfile"
+
+ fi
+ else
+ status "Unknown mode: $mode"
+ continue
+ fi
+
+ outimg="$COMPILED_DIR/${basefile%.*}.jpg"
+
+ slide_hash=$(compute_slide_hash "$imgfile" "$line" "$font" "$fontcolor" "$bgcolor")
+ stored_hash=$(get_stored_hash "$outimg" || true)
+
+ if [ "$slide_hash" = "$stored_hash" ] && [ -f "$outimg" ]; then
+ status "Slide unchanged, skipping processing."
+ printf "@%s\n" "$outimg"
+ continue
+ fi
+
+ status "Compiling: $outimg"
+
+ scaled_img="$imgfile"
+ max_img_w="$screen_w"
+ max_img_h="$screen_h"
+ if [ "$pos" = "L" ] || [ "$pos" = "R" ]; then
+ max_img_w=$((screen_w / 2))
+ max_img_h=$screen_h
+ fi
+ scaled_img=$(mktemp "$TMPDIR/scaledimg.XXXXXX.jpg")
+ scale_image "$imgfile" "$scaled_img" "$max_img_w" "$max_img_h"
+
+ img_with_bar="$scaled_img"
+ if [ -n "$url" ]; then
+ if stat -f %B "$imgfile" 2> /dev/null | grep -qv '^0$'; then
+ date=$(stat -f "%SB" -t "%Y-%m-%d %H:%M:%S" "$imgfile")
+ else
+ date=$(stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$imgfile")
+ fi
+ img_with_bar=$(mktemp "$TMPDIR/withbar.XXXXXX.jpg")
+ add_source_bar "$scaled_img" "$img_with_bar" "$url" "$date" \
+ "$source_bar_font" "$source_bar_font_size" "$source_bar_height"
+ fi
+
+ if [ "$pos" = "L" ] || [ "$pos" = "R" ]; then
+ combine_image_text "$pos" "$img_with_bar" "$outimg" "$title" "$subtitle" \
+ "$screen_w" "$screen_h" "$bgcolor" "$fontcolor" "$font" "$TMPDIR"
+ else
+ cp "$img_with_bar" "$outimg"
+ fi
+
+ printf "@%s\n" "$outimg"
+ update_hash "$outimg" "$slide_hash"
+ ;;
+ *)
+ printf "%s\n" "$line"
+ ;;
+ esac
+ done < "$input_file"
+}
+
+main "$@"
diff --git a/config.example b/config.example
@@ -0,0 +1,8 @@
+compile_sentspec_args() {
+ # Each line becomes one argument, so spaces are preserved!
+ printf '%s\n' \
+ -b black \
+ -f '#00ff00' \
+ -F 'Comic-Neue-Regular' \
+ -v
+}
diff --git a/install.sh b/install.sh
@@ -0,0 +1,57 @@
+#!/bin/sh
+
+# Simple POSIX install script to symlink all *.sh scripts (except itself)
+# into a target directory (default: ~/bin), removing the .sh extension.
+# Warns if target dir is not in PATH.
+
+main() {
+ # Set install directory
+ if [ "$1" ]; then
+ INSTALL_DIR="$1"
+ else
+ INSTALL_DIR="$HOME/bin"
+ fi
+
+ # Create target directory if needed
+ if [ ! -d "$INSTALL_DIR" ]; then
+ mkdir -p "$INSTALL_DIR"
+ fi
+
+ printf 'Symlinking scripts to %s ...\n' "$INSTALL_DIR"
+
+ for script in *.sh; do
+ # Exclude this install script
+ if [ "$script" = "install.sh" ]; then
+ continue
+ fi
+
+ base=`basename "$script" .sh`
+ src="$PWD/$script"
+ dest="$INSTALL_DIR/$base"
+
+ chmod +x "$src"
+
+ if [ -e "$dest" ] || [ -L "$dest" ]; then
+ rm -f "$dest"
+ fi
+
+ ln -s "$src" "$dest"
+ printf ' -> %s\n' "$dest"
+ done
+
+ printf 'Done.\n'
+
+ # Quick check if INSTALL_DIR is in PATH
+ case ":$PATH:" in
+ *:"$INSTALL_DIR":*)
+ # Already in PATH
+ ;;
+ *)
+ printf '\nWARNING: %s is not in your PATH.\n' "$INSTALL_DIR"
+ printf 'Add the following to your shell'\''s rc file (e.g., ~/.profile):\n'
+ printf ' export PATH="%s:$PATH"\n' "$INSTALL_DIR"
+ ;;
+ esac
+}
+
+main "$@"
diff --git a/sent-dev.sh b/sent-dev.sh
@@ -0,0 +1,156 @@
+#!/bin/sh
+set -eu
+# sent-dev.sh — tmux-based development environment for sent slide decks
+#
+# This script:
+# - Opens your editor on a sent slide spec file, creating it if missing.
+# - Watches that file for changes and recompiles it to .sent.
+# - Provides a pane with a shell for interactive work.
+# - Watches the tables/ directory for Markdown tables + auto compiles.
+#
+# Each process runs in its own tmux pane/window.
+# Requires: tmux, entr, compile-sentspec-wrapper, compile-sentspec, compile-sent-tables
+#
+# Usage: sent-dev.sh <deck.sentspec>
+#
+
+# === Globals ===
+SPEC=""
+SPECDIR=""
+CONFIG_FILE=""
+SESSION=""
+COMPILE_SENTSPEC_ARGS=""
+EDITOR_CMD="${EDITOR:-vi}"
+
+# === Functions ===
+
+error() {
+ printf "Error: %s\n" "$1" >&2
+ exit 1
+}
+
+usage() {
+ printf "Usage: %s <deck.sentspec>\n" "$0" >&2
+ exit 1
+}
+
+check_deps() {
+ missing=0
+ for dep in tmux entr compile-sentspec-wrapper compile-sentspec compile-sent-tables; do
+ if ! command -v "$dep" > /dev/null 2>&1; then
+ printf "Dependency not found: %s\n" "$dep" >&2
+ missing=1
+ fi
+ done
+ if [ "$missing" -eq 1 ]; then
+ error "Please install the missing dependencies and try again."
+ fi
+}
+
+parse_args() {
+ if [ "$#" -ne 1 ]; then
+ usage
+ fi
+
+ case "$1" in
+ *.sentspec) ;;
+ *) error "Input file must have .sentspec extension." ;;
+ esac
+
+ # Make SPEC absolute
+ case "$1" in
+ /*) SPEC="$1" ;;
+ *)
+ SPEC=$(cd "$(dirname -- "$1")" && pwd)/$(basename -- "$1") \
+ || error "Invalid spec path: $1"
+ ;;
+ esac
+
+ [ -f "$SPEC" ] || : > "$SPEC"
+
+ SPECDIR=$(dirname -- "$SPEC")
+ CONFIG_FILE="$SPECDIR/config"
+
+ if [ -f "$CONFIG_FILE" ]; then
+ # shellcheck disable=SC1090
+ . "$CONFIG_FILE"
+ fi
+
+ COMPILE_SENTSPEC_ARGS=${COMPILE_SENTSPEC_ARGS:-}
+}
+
+ensure_workdirs() {
+ [ -d "$SPECDIR/tables" ] || mkdir -p "$SPECDIR/tables"
+}
+
+start_tmux() {
+ # Detect if inside tmux BEFORE unsetting TMUX
+ inside_tmux=0
+ if [ -n "${TMUX:-}" ]; then
+ inside_tmux=1
+ fi
+
+ if tmux has-session -t "$SESSION" 2> /dev/null; then
+ if [ "$inside_tmux" -eq 1 ]; then
+ tmux switch-client -t "$SESSION"
+ else
+ exec tmux attach-session -t "$SESSION"
+ fi
+ exit 0
+ fi
+
+ unset TMUX # Prevent nesting issues
+
+ # === Pane Layout ===
+
+ # 1. Editor pane
+ tmux new-session -d -s "$SESSION" -n edit -c "$SPECDIR"
+ tmux send-keys -t "$SESSION:0.0" "$EDITOR_CMD '$SPEC'" C-m
+
+ # 2. Compile watcher (top right)
+ tmux split-window -h -t "$SESSION:0" -c "$SPECDIR"
+ tmux send-keys -t "$SESSION:0.1" "ls '$SPEC' | entr -c compile-sentspec-wrapper '$SPEC'" C-m
+
+ # 3. Table watcher (below compile watcher)
+ if [ -d "$SPECDIR/tables" ]; then
+ if ! find "$SPECDIR/tables" -name '*.md' | grep -q .; then
+ : > "$SPECDIR/tables/.sent-empty.md"
+ fi
+ tmux split-window -v -t "$SESSION:0.1" -c "$SPECDIR"
+ tmux send-keys -t "$SESSION:0.2" \
+ "while true; do find \"$SPECDIR/tables\" -name '*.md' | entr -d compile-sent-tables; done" C-m
+ else
+ # Table dir doesn't exist – create pane as shell
+ tmux split-window -v -t "$SESSION:0.1" -c "$SPECDIR"
+ fi
+
+ # 4. Shell pane (always at bottom)
+ tmux split-window -v -t "$SESSION:0.2"
+ tmux send-keys -t "$SESSION:0.2" "sleep 0.1; cd '$SPECDIR'" C-m
+
+ # Cleanup: Focus back to editor
+ tmux select-window -t "$SESSION:0"
+ tmux select-pane -t "$SESSION:0.0"
+
+ # Attach or switch based on whether inside tmux *detected earlier*
+ if [ "$inside_tmux" -eq 1 ]; then
+ tmux switch-client -t "$SESSION"
+ else
+ exec tmux attach-session -t "$SESSION"
+ fi
+}
+
+main() {
+ check_deps
+ parse_args "$@"
+
+ # Unique session name based on SPEC path
+ basename=$(basename "$SPEC" | sed 's|\.sentspec$||')
+ cksum_val=$(printf "%s" "$SPEC" | cksum | awk '{print $1}')
+ SESSION="sentdev-${basename}-${cksum_val}"
+
+ ensure_workdirs
+ start_tmux
+}
+
+main "$@"