Last active
May 12, 2026 18:06
-
-
Save StachuDotNet/cfad0eae3e7d9467de3e09eecb4caf1d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # rm-md - Render markdown to a reMarkable 2-tuned PDF and upload to /printed. | |
| # | |
| # Pandoc → HTML → WeasyPrint PDF (157.96 x 210.61 mm, native rM2 size) | |
| # → rmapi put --content-only into /printed (overwrites same-name doc, preserves UUID). | |
| # | |
| # Usage: rm-md file.md [file2.md ...] | |
| # --no-upload Generate PDF only (prints path to stdout) | |
| # --force Use --force instead of --content-only (full replace, new UUID). | |
| # Use when --content-only fails because the doc was deleted or never existed. | |
| # --folder DIR Override target folder (default: /printed) | |
| # | |
| # First run: rmapi will prompt for an 8-char code from | |
| # https://my.remarkable.com/device/browser/connect — auth persists in ~/.config/rmapi/. | |
| # | |
| # Stylesheet is embedded at the bottom of this file, after the __STYLE__ marker. | |
| set -euo pipefail | |
| RMAPI="$HOME/bin/rmapi" | |
| FOLDER="/printed" | |
| NO_UPLOAD=false | |
| OVERWRITE_FLAG="--content-only" | |
| FILES=() | |
| while [ $# -gt 0 ]; do | |
| case "$1" in | |
| --no-upload) NO_UPLOAD=true; shift ;; | |
| --force) OVERWRITE_FLAG="--force"; shift ;; | |
| --folder) FOLDER="$2"; shift 2 ;; | |
| --) shift; while [ $# -gt 0 ]; do FILES+=("$1"); shift; done ;; | |
| -*) echo "Unknown option: $1" >&2; exit 2 ;; | |
| *) FILES+=("$1"); shift ;; | |
| esac | |
| done | |
| if [ ${#FILES[@]} -eq 0 ]; then | |
| sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//' >&2 | |
| exit 1 | |
| fi | |
| if ! $NO_UPLOAD && [ ! -x "$RMAPI" ]; then | |
| echo "rmapi not found at $RMAPI" >&2 | |
| exit 1 | |
| fi | |
| # Pull the embedded stylesheet out of this file. | |
| STYLE_CSS=$(awk '/^__STYLE__$/{p=1; next} p' "$0") | |
| # Ensure target folder exists (no-op if it already does). | |
| if ! $NO_UPLOAD; then | |
| echo "mkdir $FOLDER" | "$RMAPI" >/dev/null 2>&1 || true | |
| fi | |
| for input in "${FILES[@]}"; do | |
| if [ ! -f "$input" ]; then | |
| echo "File not found: $input" >&2 | |
| exit 1 | |
| fi | |
| base="$(basename "$input" .md)" | |
| title="$base" | |
| stamp="$(date '+%Y-%m-%d %H:%M')" | |
| pdf="/tmp/rm-md-${base}-$$.pdf" | |
| body=$(pandoc --from=markdown+lists_without_preceding_blankline --to=html5 --highlight-style=kate "$input") | |
| html="<!DOCTYPE html><html><head><meta charset=\"utf-8\"><style>${STYLE_CSS}</style></head><body>" | |
| html+="<span class=\"doc-title\">$title</span>" | |
| html+="<span class=\"doc-date\">$stamp</span>" | |
| html+="$body</body></html>" | |
| echo "$html" | weasyprint - "$pdf" 2>/dev/null | |
| if $NO_UPLOAD; then | |
| echo "$pdf" | |
| continue | |
| fi | |
| # rmapi names docs by basename(file, .pdf). We hand it the .pdf at /tmp/rm-md-NAME-$$.pdf | |
| # but want the rM doc to be titled $base — so rename the temp file first. | |
| final_pdf="/tmp/${base}.pdf" | |
| mv -f "$pdf" "$final_pdf" | |
| # rmapi shell always exits 0 — detect failure by grepping output for `Error:`. | |
| out=$(echo "put $OVERWRITE_FLAG $final_pdf $FOLDER" | "$RMAPI" 2>&1) | |
| if echo "$out" | grep -q "^Error:"; then | |
| # Likely first-time upload (no doc to overwrite). Retry without the overwrite flag. | |
| out=$(echo "put $final_pdf $FOLDER" | "$RMAPI" 2>&1) | |
| fi | |
| rm -f "$final_pdf" | |
| if echo "$out" | grep -q "^Error:"; then | |
| echo "$out" | grep "^Error:" >&2 | |
| echo "✗ $FOLDER/$base" >&2 | |
| exit 1 | |
| fi | |
| echo "→ $FOLDER/$base" | |
| done | |
| exit 0 | |
| __STYLE__ | |
| @page { | |
| size: 157.96mm 210.61mm; | |
| margin: 5mm 5mm 7mm 5mm; | |
| @bottom-left { | |
| content: string(doc-title); | |
| font-family: "Inter", "Helvetica Neue", sans-serif; | |
| font-size: 8pt; | |
| color: #000; | |
| } | |
| @bottom-center { | |
| content: string(doc-date); | |
| font-family: "Inter", "Helvetica Neue", sans-serif; | |
| font-size: 8pt; | |
| color: #000; | |
| } | |
| @bottom-right { | |
| content: counter(page) " / " counter(pages); | |
| font-family: "Inter", "Helvetica Neue", sans-serif; | |
| font-size: 8pt; | |
| color: #000; | |
| } | |
| } | |
| .doc-title { | |
| string-set: doc-title content(); | |
| height: 0; overflow: hidden; font-size: 0; line-height: 0; display: block; | |
| } | |
| .doc-date { | |
| string-set: doc-date content(); | |
| height: 0; overflow: hidden; font-size: 0; line-height: 0; display: block; | |
| } | |
| body { | |
| font-family: "Inter", "Helvetica Neue", "Segoe UI", sans-serif; | |
| font-size: 9.5pt; | |
| line-height: 1.3; | |
| color: #000; | |
| margin: 0; | |
| text-rendering: optimizeLegibility; | |
| } | |
| h1 { | |
| font-size: 18pt; | |
| font-weight: 800; | |
| margin-top: 0; | |
| margin-bottom: 0.4em; | |
| color: #000; | |
| border-bottom: 2pt solid #000; | |
| padding-bottom: 0.15em; | |
| letter-spacing: -0.01em; | |
| page-break-after: avoid; | |
| } | |
| h2 { | |
| font-size: 13.5pt; | |
| font-weight: 700; | |
| margin-top: 1.2em; | |
| margin-bottom: 0.3em; | |
| color: #000; | |
| border-bottom: 0.75pt solid #000; | |
| padding-bottom: 0.12em; | |
| page-break-after: avoid; | |
| } | |
| h3 { | |
| font-size: 10.5pt; | |
| font-weight: 700; | |
| margin-top: 1em; | |
| margin-bottom: 0.25em; | |
| color: #000; | |
| text-transform: uppercase; | |
| letter-spacing: 0.08em; | |
| page-break-after: avoid; | |
| } | |
| h4, h5, h6 { | |
| font-size: 10pt; | |
| font-weight: 700; | |
| margin-top: 0.7em; | |
| margin-bottom: 0.15em; | |
| color: #000; | |
| page-break-after: avoid; | |
| } | |
| p { | |
| margin: 0.3em 0; | |
| orphans: 2; | |
| widows: 2; | |
| } | |
| a { | |
| color: #000; | |
| text-decoration: underline; | |
| } | |
| ul, ol { | |
| margin: 0.3em 0; | |
| padding-left: 1.3em; | |
| } | |
| li { | |
| margin: 0.05em 0; | |
| } | |
| li > p { margin: 0.05em 0; } | |
| blockquote { | |
| border-left: 2pt solid #000; | |
| padding: 0.1em 0 0.1em 0.7em; | |
| margin: 0.5em 0; | |
| color: #000; | |
| font-style: italic; | |
| } | |
| code { | |
| font-family: "JetBrains Mono", "Source Code Pro", "Menlo", monospace; | |
| font-size: 9.5pt; | |
| background: #eaeaea; | |
| padding: 0.05em 0.25em; | |
| border-radius: 2pt; | |
| } | |
| pre { | |
| background: #f0f0f0; | |
| border: 0.5pt solid #000; | |
| padding: 0.5em 0.6em; | |
| border-radius: 3pt; | |
| font-size: 9pt; | |
| line-height: 1.35; | |
| overflow-wrap: break-word; | |
| white-space: pre-wrap; | |
| page-break-inside: avoid; | |
| } | |
| pre code { | |
| background: none; | |
| padding: 0; | |
| border-radius: 0; | |
| font-size: inherit; | |
| } | |
| table { | |
| border-collapse: collapse; | |
| width: 100%; | |
| margin: 0.6em 0; | |
| font-size: 10pt; | |
| page-break-inside: avoid; | |
| } | |
| th, td { | |
| border: 0.5pt solid #000; | |
| padding: 0.25em 0.45em; | |
| text-align: left; | |
| vertical-align: top; | |
| } | |
| th { | |
| background: #e5e5e5; | |
| font-weight: 700; | |
| } | |
| hr { | |
| border: none; | |
| border-top: 0.75pt solid #000; | |
| margin: 1em 0; | |
| } | |
| img { | |
| max-width: 100%; | |
| height: auto; | |
| filter: grayscale(100%) contrast(1.1); | |
| } | |
| .sourceCode { background: #f0f0f0; } | |
| .sourceCode .kw { color: #000; font-weight: 700; } | |
| .sourceCode .dt { color: #000; font-style: italic; } | |
| .sourceCode .co { color: #555; font-style: italic; } | |
| .sourceCode .st, .sourceCode .vs { color: #000; } | |
| .sourceCode .dv, .sourceCode .fl { color: #000; } | |
| .sourceCode .fu { color: #000; font-weight: 700; } | |
| .sourceCode .er { color: #000; text-decoration: underline; } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment