trmnlp
A basic self-hosted web server to ease the development and sharing of TRMNL plugins.
Liquid templates are rendered leveraging the TRMNL Design System. They may be generated as HTML (faster, and a good approximation of the final result) or as PNG images (slower, but more accurate).
The server watches the filesystem for changes to the Liquid templates, seamlessly updating the preview without the need to refresh.

Quick Start
gem install trmnl_preview # install
trmnlp init my_plugin # scaffold a project
cd my_plugin
trmnlp serve # preview at http://localhost:4567
No Ruby on hand? Run it through Docker instead — see Installing via Docker.
Project Structure
This is the structure of a plugin project:
.
├── .github
│ └── workflows
│ └── trmnl.yml
├── .gitignore
├── .trmnlp.yml
├── bin
│ └── trmnlp
└── src
├── full.liquid
├── half_horizontal.liquid
├── half_vertical.liquid
├── quadrant.liquid
├── shared.liquid
└── settings.yml
| File | Purpose |
|---|---|
.github/workflows/trmnl.yml |
GitHub Actions workflow — lints every PR, deploys to TRMNL on main |
.gitignore |
Keeps trmnlp build output out of version control |
.trmnlp.yml |
Local dev-server config — not uploaded to TRMNL |
src/full.liquid |
Markup for the full screen |
src/half_horizontal.liquid |
Top or bottom half of a stacked mashup |
src/half_vertical.liquid |
Left or right half of a side-by-side mashup |
src/quadrant.liquid |
One quarter of a 2x2 mashup |
src/shared.liquid |
Reusable markup included by the other templates |
src/settings.yml |
Plugin configuration — uploaded to TRMNL |
Creating a New Plugin
You can start building a plugin locally, then push it to the TRMNL server for display on your device.
trmnlp init [my_plugin] # generate
cd [my_plugin]
trmnlp serve # develop locally
trmnlp login # authenticate
trmnlp push # upload
Modifying an Existing Plugin
If you have built a plugin with the web-based editor, you can clone it, work on it locally, and push changes back to the server.
trmnlp login # authenticate
trmnlp clone [my_plugin] [id] # first download
cd [my_plugin]
trmnlp serve # develop locally
trmnlp push # upload
Commands
| Command | Description |
|---|---|
trmnlp init NAME |
Start a new plugin project |
trmnlp serve |
Start a local dev server |
trmnlp build |
Generate static HTML files, or PNGs with --png |
trmnlp lint |
Check plugin code against TRMNL best practices |
trmnlp login |
Authenticate with TRMNL server |
trmnlp list |
List private plugins from TRMNL server |
trmnlp clone NAME ID |
Copy a plugin project from TRMNL server |
trmnlp pull |
Download latest plugin settings from TRMNL server |
trmnlp push |
Upload latest plugin settings to TRMNL server |
trmnlp version |
Show version |
trmnlp lint exits non-zero when it finds issues, so you can gate CI on it. Run trmnlp help for all flags.
Building Static Files
trmnlp build renders every view to a static file under _build/ — handy for exporting a snapshot or feeding the output into another pipeline. Run it from inside a plugin project:
trmnlp build # writes _build/full.html, _build/half_horizontal.html, ...
trmnlp build --png # also writes a PNG for each view
--png renders each view through the same screenshot pipeline serve uses. By default a PNG is 800×480 at the bit depth declared by the markup's screen--Nbit class (1-bit if none). Override any of those:
trmnlp build --png --color-depth 2
| Flag | Purpose |
|---|---|
--png |
Render a PNG per view alongside the HTML |
--width |
PNG width in pixels (default 800) |
--height |
PNG height in pixels (default 480) |
--color-depth |
PNG bit depth — 1-8 — overriding the markup |
--width, --height, and --color-depth apply only with --png. PNG rendering needs Firefox and ImageMagick installed; plain trmnlp build needs neither.
Authentication
The trmnlp login command saves your API key to ~/.config/trmnlp/config.yml.
If an environment variable is more convenient (for example in a CI/CD pipeline), you can set $TRMNL_API_KEY instead.
Continuous Integration
trmnlp init and trmnlp clone scaffold a .github/workflows/trmnl.yml
workflow and initialize a Git repository, so a fresh project is ready to push
to GitHub. The workflow runs in GitHub Actions without trmnlp login — set the
TRMNL_API_KEY environment variable and it's used in place of the saved
config. Add it as a repository secret to activate the workflow; it looks like
this:
name: TRMNL
on:
pull_request:
push:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: ruby/setup-ruby@v1
with:
ruby-version: "4.0"
- run: gem install trmnl_preview
- run: trmnlp lint
push:
needs: lint
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: ruby/setup-ruby@v1
with:
ruby-version: "4.0"
- run: gem install trmnl_preview
- run: trmnlp push --force
env:
TRMNL_API_KEY: ${{ secrets.TRMNL_API_KEY }}
The lint job gates every pull request — trmnlp lint exits non-zero on
issues, so a failing check blocks the merge. The push job uploads to TRMNL
only on main.
Make sure
src/settings.ymlhas anid.trmnlp pushupdates the plugin with that id; without one it creates a new plugin on every run. Projects made withtrmnlp cloneortrmnlp pullalready have it.
Running trmnlp
The bin/trmnlp script is provided as a convenience. It will use the local Ruby gem if available, falling back to the trmnl/trmnlp Docker image.
You can modify the bin/trmnlp script to set up environment variables (plugin secrets, etc.) before running the server.
Gem or Docker? Install the gem if you already have Ruby >= 3.4 — it has the fastest startup. Use Docker for zero local setup.
Installing via RubyGems
Prerequisites:
- Ruby >= 3.4
- For PNG rendering (optional):
- Firefox
- ImageMagick
gem install trmnl_preview
trmnlp serve
Installing via Docker
docker run \
--pull always \
--publish 4567:4567 \
--volume "$(pwd):/plugin" \
trmnl/trmnlp serve
--pull always checks the registry on every run and pulls a newer image if one exists, so you don't have to remember to docker pull after each release.
Inside a container, serve binds to 0.0.0.0 automatically (it detects /.dockerenv) so the preview is reachable from your host browser. Outside Docker it binds to 127.0.0.1.
Swap serve for any other command (lint, login, clone, etc.) to run it in a one-off container.
Interactive Mode
For running multiple commands (login, clone, serve), you can start an interactive shell:
docker run -it \
--pull always \
--publish 4567:4567 \
--volume "$HOME/.config/trmnlp:/root/.config/trmnlp" \
--volume "$(pwd):/plugin" \
--entrypoint /bin/bash \
trmnl/trmnlp
Then run commands inside the container:
trmnlp login
trmnlp clone my_plugin 12345
cd my_plugin
trmnlp serve
The config volume ($HOME/.config/trmnlp) persists your API key between sessions.
Docker Compose
For a checked-in config — like examples/hn-stories/ uses — a minimal docker-compose.yml:
services:
trmnlp:
image: trmnl/trmnlp
pull_policy: always
command: ["serve"]
ports:
- "4567:4567"
volumes:
- .:/plugin
Then docker compose up.
Building Locally
To build the Docker image from source:
git clone https://github.com/usetrmnl/trmnlp.git
cd trmnlp
docker build -t trmnlp .
.trmnlp.yml Reference - Project Config
The .trmnlp.yml file lives in the root of the plugin project, and is for configuring the local dev server.
System environment variables are made available in the {{ env }} Liquid varible in this file only. This can be used to safely
supply plugin secrets, like API keys.
All fields are optional.
---
# auto-reload when files change (`watch: false` to disable)
watch:
- src
- .trmnlp.yml
# values of custom fields (defined in src/settings.yml)
custom_fields:
station: "{{ env.ICAO }}" # interpolate $IACO environment variable
# Time zone IANA identifier to inject into trmnl.user; see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
time_zone: America/New_York
# Serverless transforms run automatically when a src/transform.*
# file is present. Set to 'disabled' to turn off.
transform_runtime: enabled
# Optional remote transform daemon URL — when set, transforms POST
# here instead of running locally. Useful for production-fidelity
# testing against a real microVM daemon.
# serverless_daemon_url: https://transforms.your-team.example
# Optional explicit language for src/transform.* (otherwise inferred from extension)
# serverless_language: python
# override variables
variables:
trmnl:
user:
name: Peter Quill
plugin_settings:
instance_name: Kevin Bacon Facts
Serverless Transforms
trmnlp can run a transform script (python, ruby, php, or node) against the polled API response before handing data to your Liquid templates — matching the hosted plugin service's behavior.
Drop a file at src/transform.{py,rb,php,js} and define a run(input) function — transforms are enabled by default, so it runs automatically. To turn them off, set transform_runtime: disabled in .trmnlp.yml.
Heads up: because transforms run by default, a plugin you
cloneorpullfrom somewhere else will execute itssrc/transform.*code on your machine the first time you preview it — there is no opt-in prompt. Review a third-party plugin's transform script before serving it, or settransform_runtime: disabled.
The transform receives the polled response on stdin as JSON; whatever run(input) returns becomes the new merge data.
Example src/transform.py:
def run(input):
return {"items": [x["title"] for x in input["data"]]}
The trmnlp image bundles python3, node, php, and ruby — no sidecar daemon required.
Language detection
The transform language comes from the file extension:
| File | Language |
|---|---|
src/transform.py |
python |
src/transform.rb |
ruby |
src/transform.js |
node |
src/transform.php |
php |
trmnlp push uploads the file under its own name, and the hosted service records serverless_language from the extension automatically — you don't need to set it by hand. trmnlp pull / trmnlp clone bring the transform file back under the same name.
Pointing at a remote daemon
For production-fidelity testing against a real microVM daemon, set serverless_daemon_url: in .trmnlp.yml:
transform_runtime: enabled
serverless_daemon_url: https://transforms.your-team.example
Provide the daemon's bearer token via $TRMNL_SERVERLESS_DAEMON_API_KEY (env-first, mirroring how $TRMNL_API_KEY works for trmnl.com auth):
export TRMNL_SERVERLESS_DAEMON_API_KEY=...
trmnlp serve
Or commit a per-project value to .trmnlp.yml as serverless_daemon_api_key: — though the env var is preferred to keep the secret out of version control.
A complete worked example lives at examples/hn-stories/ — a polling plugin that fetches the Hacker News top-stories list, enriches each story via additional HTTPS calls from inside the transform, and renders the result with TRMNL design-system markup across all four sizes. cd examples/hn-stories && docker compose up and you're running it.
src/settings.yml Reference (Plugin Config)
The settings.yml file is part of the plugin definition, and is uploaded and downloaded by trmnlp push / pull.
framework_version: pins the TRMNL Design System version this plugin renders against — latest (the default) tracks the newest release, or set a specific version for reproducibility. It lives here rather than in .trmnlp.yml so the value round-trips with the hosted plugin service.
See TRMNL documentation for details on this file's contents.
Development
To run trmnlp from a checkout of this repo — handy for trying unreleased changes or contributing:
git clone https://github.com/usetrmnl/trmnlp.git
cd trmnlp
bundle install
bundle exec bin/trmnlp serve
The repo pins its Ruby version in .ruby-version — a version manager will pick it up when you cd in. This bin/trmnlp runs the CLI straight from lib/; it's a different script from the gem-or-Docker bin/trmnlp that trmnlp init scaffolds into a plugin project.
Tests
To test, run:
bin/rake
Specs run under SimpleCov; a coverage report is written to coverage/.
Releasing
Releases are automated. The Release workflow
fires whenever lib/trmnlp/version.rb changes on main, then tags the commit,
publishes the gem to RubyGems, and pushes the multi-arch Docker image. Each step
is idempotent, so the workflow is safe to re-run after a partial failure.
To cut a release:
- Bump the version in
lib/trmnlp/version.rb. - Run
bundle installsoGemfile.lockpicks up the new version. - Commit and merge to
main— the workflow does the rest.
By convention, add a matching CHANGELOG.md entry in the same change.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/usetrmnl/trmnlp.
License
The gem is available as open source under the terms of the MIT License.