Skip to content

Instantly share code, notes, and snippets.

@StachuDotNet
Last active May 12, 2026 18:06
Show Gist options
  • Select an option

  • Save StachuDotNet/cfad0eae3e7d9467de3e09eecb4caf1d to your computer and use it in GitHub Desktop.

Select an option

Save StachuDotNet/cfad0eae3e7d9467de3e09eecb4caf1d to your computer and use it in GitHub Desktop.
#!/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