trmnl_preview 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf11a528033d7882be345a627e2a480f1cdf95a5f9cef22d01f51f5b46012635
4
- data.tar.gz: 973c3dc1b96f1d7a81ffa8e9fc82f403285d9449463d210684732a43028509d8
3
+ metadata.gz: 236a5502b02ab161fffc5330fc712c329810f877038c71baadc86b376e607157
4
+ data.tar.gz: 41400bbbe0b54639311ae928280277ee1717f58f1de74bd71ab92f213bbe92db
5
5
  SHA512:
6
- metadata.gz: 39963f376ff877a4170ce57564876e5cb4bd6eacc23de4b1792a6cba6cd01f4252579fe19c8d678f21144f37e9ba050ed03866b38a3f7aaa9a67521aa324fd04
7
- data.tar.gz: 835b494bb56d29e9e4b607a482ae05466b08aa527ff38c00fae2a98e87524bbcbb6db78c9abd5dc8288c3b36692008107a1c9bbbdd298c36ddf1d435cd0d2e65
6
+ metadata.gz: 604a7f74231c2e738dbf61995469a64e488cc1d69fa9eb84076fb9badfb2d83fd8c490fa261154471852e7c7effb6071e458d40e748bc70de7ea27455d5a8485
7
+ data.tar.gz: c4396159f68882180618578725f2e386e703a9d992a195a5ce2de463d8dab407d9b0a30655495963e0b1af35ee2ab0813fae75ed77633d616312fa64246ef753
data/CHANGELOG.md CHANGED
@@ -1,6 +1,29 @@
1
1
 
2
2
  # Changelog
3
3
 
4
+ ## 0.8.2
5
+
6
+ - Fixed `framework_version: latest` rendering against the auto-upgrading `/latest/` asset path instead of the current concrete release, matching the hosted service (#99)
7
+ - Cleanup and minor improvements
8
+
9
+ ## 0.8.1
10
+
11
+ ### Added
12
+
13
+ - `trmnlp build --png` renders a PNG for every view alongside the HTML, with `--width`, `--height`, and `--color-depth` flags to override the defaults (#92)
14
+ - Colour-coded the preview's payload-size badge — yellow from 75 KB, red from 100 KB — so an oversized merge-variable payload is visible at a glance (#67)
15
+ - Added colour to CLI output — `lint` results, warnings, and errors — suppressed automatically when output is piped or redirected (#33)
16
+
17
+ ### Fixed
18
+
19
+ - `trmnlp init` no longer produces read-only project files when trmnlp itself is installed read-only, such as on NixOS (#83)
20
+ - Non-JSON polling responses (`text/html`, `text/plain`) are exposed to templates as `{{ data }}`, matching the hosted service — previously `{{ text }}` (#81)
21
+
22
+ ### Housekeeping
23
+
24
+ - Added SimpleCov coverage tracking, gated in CI at a 90% floor, plus dedicated specs for every lint check
25
+ - Extracted the headless-Firefox driver into a shared `FirefoxDriver` module used by both `serve` and `build --png`
26
+
4
27
  ## 0.8.0
5
28
 
6
29
  ### Housekeeping
data/README.md CHANGED
@@ -80,7 +80,7 @@ trmnlp push # upload
80
80
  |---|---|
81
81
  | `trmnlp init NAME` | Start a new plugin project |
82
82
  | `trmnlp serve` | Start a local dev server |
83
- | `trmnlp build` | Generate static HTML files |
83
+ | `trmnlp build` | Generate static HTML files, or PNGs with `--png` |
84
84
  | `trmnlp lint` | Check plugin code against TRMNL best practices |
85
85
  | `trmnlp login` | Authenticate with TRMNL server |
86
86
  | `trmnlp list` | List private plugins from TRMNL server |
@@ -91,12 +91,83 @@ trmnlp push # upload
91
91
 
92
92
  `trmnlp lint` exits non-zero when it finds issues, so you can gate CI on it. Run `trmnlp help` for all flags.
93
93
 
94
+ ## Building Static Files
95
+
96
+ `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:
97
+
98
+ ```sh
99
+ trmnlp build # writes _build/full.html, _build/half_horizontal.html, ...
100
+ trmnlp build --png # also writes a PNG for each view
101
+ ```
102
+
103
+ `--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:
104
+
105
+ ```sh
106
+ trmnlp build --png --color-depth 2
107
+ ```
108
+
109
+ | Flag | Purpose |
110
+ |---|---|
111
+ | `--png` | Render a PNG per view alongside the HTML |
112
+ | `--width` | PNG width in pixels (default 800) |
113
+ | `--height` | PNG height in pixels (default 480) |
114
+ | `--color-depth` | PNG bit depth — 1-8 — overriding the markup |
115
+
116
+ `--width`, `--height`, and `--color-depth` apply only with `--png`. PNG rendering needs Firefox and ImageMagick installed; plain `trmnlp build` needs neither.
117
+
94
118
  ## Authentication
95
119
 
96
120
  The `trmnlp login` command saves your API key to `~/.config/trmnlp/config.yml`.
97
121
 
98
122
  If an environment variable is more convenient (for example in a CI/CD pipeline), you can set `$TRMNL_API_KEY` instead.
99
123
 
124
+ ## Continuous Integration
125
+
126
+ `trmnlp` runs in GitHub Actions without `trmnlp login` — set the `TRMNL_API_KEY`
127
+ environment variable and it's used in place of the saved config. Add it as a
128
+ repository secret, then drop this into `.github/workflows/trmnl.yml`:
129
+
130
+ ```yaml
131
+ name: TRMNL
132
+ on:
133
+ pull_request:
134
+ push:
135
+ branches: [main]
136
+
137
+ jobs:
138
+ lint:
139
+ runs-on: ubuntu-latest
140
+ steps:
141
+ - uses: actions/checkout@v6
142
+ - uses: ruby/setup-ruby@v1
143
+ with:
144
+ ruby-version: "4.0"
145
+ - run: gem install trmnl_preview
146
+ - run: trmnlp lint
147
+
148
+ push:
149
+ needs: lint
150
+ if: github.ref == 'refs/heads/main'
151
+ runs-on: ubuntu-latest
152
+ steps:
153
+ - uses: actions/checkout@v6
154
+ - uses: ruby/setup-ruby@v1
155
+ with:
156
+ ruby-version: "4.0"
157
+ - run: gem install trmnl_preview
158
+ - run: trmnlp push --force
159
+ env:
160
+ TRMNL_API_KEY: ${{ secrets.TRMNL_API_KEY }}
161
+ ```
162
+
163
+ The `lint` job gates every pull request — `trmnlp lint` exits non-zero on
164
+ issues, so a failing check blocks the merge. The `push` job uploads to TRMNL
165
+ only on `main`.
166
+
167
+ > **Make sure `src/settings.yml` has an `id`.** `trmnlp push` updates the
168
+ > plugin with that id; without one it creates a *new* plugin on every run.
169
+ > Projects made with `trmnlp clone` or `trmnlp pull` already have it.
170
+
100
171
  ## Running trmnlp
101
172
 
102
173
  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.
@@ -310,6 +381,23 @@ To test, run:
310
381
  bin/rake
311
382
  ```
312
383
 
384
+ Specs run under SimpleCov; a coverage report is written to `coverage/`.
385
+
386
+ ## Releasing
387
+
388
+ Releases are automated. The [`Release` workflow](.github/workflows/release.yaml)
389
+ fires whenever `lib/trmnlp/version.rb` changes on `main`, then tags the commit,
390
+ publishes the gem to RubyGems, and pushes the multi-arch Docker image. Each step
391
+ is idempotent, so the workflow is safe to re-run after a partial failure.
392
+
393
+ To cut a release:
394
+
395
+ 1. Bump the version in `lib/trmnlp/version.rb`.
396
+ 2. Run `bundle install` so `Gemfile.lock` picks up the new version.
397
+ 3. Commit and merge to `main` — the workflow does the rest.
398
+
399
+ By convention, add a matching `CHANGELOG.md` entry in the same change.
400
+
313
401
  ## Contributing
314
402
 
315
403
  Bug reports and pull requests are welcome on GitHub at https://github.com/usetrmnl/trmnlp.
data/bin/trmnlp CHANGED
@@ -11,7 +11,8 @@ ENV['TZ'] = 'UTC'
11
11
  begin
12
12
  TRMNLP::CLI.start
13
13
  rescue TRMNLP::Error => e
14
- puts "Error: #{e.message}"
14
+ reporter = TRMNLP::Reporter.new
15
+ reporter.info reporter.red("Error: #{e.message}")
15
16
  exit 1
16
17
  rescue Interrupt
17
18
  exit 1
data/lib/trmnlp/app.rb CHANGED
@@ -18,6 +18,16 @@ module TRMNLP
18
18
  bytes < 1024 ? "#{bytes} bytes" : format('%.1f KB', bytes / 1024.0)
19
19
  end
20
20
 
21
+ # Colour-codes the payload badge so an author notices when merge
22
+ # variables approach the size the hosted service starts rejecting.
23
+ # KB = 1024, matching format_bytes.
24
+ def payload_size_class(bytes)
25
+ return 'payload-size--over' if bytes >= 100 * 1024
26
+ return 'payload-size--warn' if bytes >= 75 * 1024
27
+
28
+ 'payload-size--ok'
29
+ end
30
+
21
31
  # NOTE: render_html.erb's layout yields raw plugin HTML through `<%= yield %>`,
22
32
  # so a global `escape_html` setting would corrupt the render. Escape per-value.
23
33
  def h(text)
data/lib/trmnlp/cli.rb CHANGED
@@ -19,6 +19,10 @@ module TRMNLP
19
19
  def self.default_bind = File.exist?('/.dockerenv') ? '0.0.0.0' : '127.0.0.1'
20
20
 
21
21
  desc 'build', 'Generate static HTML files'
22
+ method_option :png, type: :boolean, default: false, desc: 'Also render a PNG per view'
23
+ method_option :width, type: :numeric, desc: 'PNG width in pixels (with --png)'
24
+ method_option :height, type: :numeric, desc: 'PNG height in pixels (with --png)'
25
+ method_option :color_depth, type: :numeric, desc: 'PNG bit depth: 1-8 (with --png)'
22
26
  def build
23
27
  Commands::Build.run(options)
24
28
  end
@@ -50,7 +50,7 @@ module TRMNLP
50
50
  # plugin author notices before the field misbehaves in production.
51
51
  def report_form_field_warnings
52
52
  FormField.validate_all(config.plugin.custom_field_definitions).each do |warning|
53
- reporter.info("warning: settings.yml custom_fields — #{warning}")
53
+ reporter.info(reporter.yellow("warning: settings.yml custom_fields — #{warning}"))
54
54
  end
55
55
  end
56
56
 
@@ -1,11 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'fileutils'
4
+
3
5
  require_relative 'base'
6
+ require_relative '../browser_pool'
7
+ require_relative '../firefox_driver'
8
+ require_relative '../screen_generator'
9
+ require_relative '../screenshot'
4
10
 
5
11
  module TRMNLP
6
12
  module Commands
7
13
  class Build < Base
8
- Options = Data.define(:dir, :quiet)
14
+ Options = Data.define(:dir, :quiet, :png, :width, :height, :color_depth)
9
15
 
10
16
  def call
11
17
  context.validate!
@@ -13,13 +19,49 @@ module TRMNLP
13
19
  context.poller.poll_data
14
20
  context.paths.create_build_dir
15
21
 
16
- Screen.all.each do |screen|
17
- output_path = context.paths.build_dir.join("#{screen.name}.html")
18
- reporter.info "Writing #{output_path}..."
19
- output_path.write(context.renderer.render_full_page(screen.name))
20
- end
22
+ Screen.all.each { |screen| build_screen(screen) }
21
23
 
22
24
  reporter.info 'Done!'
25
+ ensure
26
+ @browser_pool&.shutdown
27
+ end
28
+
29
+ private
30
+
31
+ def build_screen(screen)
32
+ html = context.renderer.render_full_page(screen.name)
33
+ write_html(screen.name, html)
34
+ write_png(screen.name, html) if options.png
35
+ end
36
+
37
+ def write_html(view, html)
38
+ path = context.paths.build_dir.join("#{view}.html")
39
+ reporter.info "Writing #{path}..."
40
+ path.write(html)
41
+ end
42
+
43
+ # --png is additive: the HTML is rendered either way, so it stays on
44
+ # disk alongside the PNG rather than being replaced by it.
45
+ def write_png(view, html)
46
+ path = context.paths.build_dir.join("#{view}.png")
47
+ reporter.info "Writing #{path}..."
48
+ image = screen_generator(html).process
49
+ FileUtils.cp(image.path, path)
50
+ ensure
51
+ image&.close!
52
+ end
53
+
54
+ # --width/--height/--color-depth are optional; nil lets ScreenGenerator
55
+ # fall back to 800x480 and the screen--Nbit depth sniffed from the markup.
56
+ def screen_generator(html)
57
+ ScreenGenerator.new(html, screenshot:, width: options.width,
58
+ height: options.height, color_depth: options.color_depth)
59
+ end
60
+
61
+ def screenshot = @screenshot ||= Screenshot.new(pool: browser_pool)
62
+
63
+ def browser_pool
64
+ @browser_pool ||= BrowserPool.new(driver_factory: FirefoxDriver.method(:build))
23
65
  end
24
66
  end
25
67
  end
@@ -35,6 +35,10 @@ module TRMNLP
35
35
 
36
36
  reporter.info "Creating #{destination_pathname}"
37
37
  FileUtils.cp(source_pathname, destination_pathname)
38
+ # NOTE: cp preserves the source mode. Templates installed read-only
39
+ # (e.g. NixOS /nix/store is 0444) would leave the author unable to
40
+ # edit their own project. Add owner-write; keep any exec bit.
41
+ destination_pathname.chmod(destination_pathname.stat.mode | 0o200)
38
42
  end
39
43
 
40
44
  reporter.info <<~HEREDOC
@@ -1,10 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'selenium-webdriver'
4
-
5
3
  require_relative 'base'
6
4
  require_relative '../api_client'
7
5
  require_relative '../browser_pool'
6
+ require_relative '../firefox_driver'
8
7
 
9
8
  module TRMNLP
10
9
  module Commands
@@ -20,7 +19,7 @@ module TRMNLP
20
19
 
21
20
  # Now we can configure things
22
21
  App.set(:context, context)
23
- App.set(:browser_pool, BrowserPool.new(driver_factory: method(:build_firefox_driver)))
22
+ App.set(:browser_pool, BrowserPool.new(driver_factory: FirefoxDriver.method(:build)))
24
23
  App.set(:bind, options.bind)
25
24
  App.set(:port, options.port)
26
25
  permit_all_hosts if codespaces?
@@ -37,18 +36,6 @@ module TRMNLP
37
36
  def permit_all_hosts
38
37
  App.set(:host_authorization, { allow_if: ->(_env) { true } })
39
38
  end
40
-
41
- def build_firefox_driver
42
- options = Selenium::WebDriver::Firefox::Options.new
43
- options.add_argument('--headless')
44
- options.add_argument('--disable-web-security')
45
- # Disable subpixel antialiasing — its colour fringing quantizes badly on 1-bit e-ink.
46
- options.add_preference('gfx.text.disable-aa', true)
47
- options.add_preference('gfx.text.subpixel-position.force-disabled', true)
48
- Selenium::WebDriver.for(:firefox, options: options).tap do |driver|
49
- driver.manage.window.maximize
50
- end
51
- end
52
39
  end
53
40
  end
54
41
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'selenium-webdriver'
4
+
5
+ module TRMNLP
6
+ # Builds the headless Firefox driver that screenshots rendered plugins.
7
+ # Shared by `trmnlp serve` (the PNG preview route) and `trmnlp build --png`.
8
+ module FirefoxDriver
9
+ module_function
10
+
11
+ def build
12
+ Selenium::WebDriver.for(:firefox, options:).tap do |driver|
13
+ driver.manage.window.maximize
14
+ end
15
+ end
16
+
17
+ def options
18
+ Selenium::WebDriver::Firefox::Options.new.tap do |opts|
19
+ opts.add_argument('--headless')
20
+ opts.add_argument('--disable-web-security')
21
+ # Subpixel antialiasing colour-fringes badly when quantized to 1-bit e-ink.
22
+ opts.add_preference('gfx.text.disable-aa', true)
23
+ opts.add_preference('gfx.text.subpixel-position.force-disabled', true)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -44,9 +44,13 @@ module TRMNLP
44
44
 
45
45
  def pinned? = @pinned
46
46
 
47
- def css_url = "#{@asset_host}/css/#{path_segment}/plugins.css"
47
+ # Both a pinned and an unpinned ("latest") version resolve to a
48
+ # concrete release here — #number is never the literal "latest" — so a
49
+ # local preview renders the same bundle as the hosted service instead
50
+ # of drifting onto a new release the moment one ships.
51
+ def css_url = "#{@asset_host}/css/#{number}/plugins.css"
48
52
 
49
- def js_url = "#{@asset_host}/js/#{path_segment}/plugins.js"
53
+ def js_url = "#{@asset_host}/js/#{number}/plugins.js"
50
54
 
51
55
  def ==(other) = other.is_a?(self.class) && number == other.number
52
56
 
@@ -59,11 +63,5 @@ module TRMNLP
59
63
  def as_json(*) = number
60
64
 
61
65
  def to_s = number
62
-
63
- private
64
-
65
- # When pinned, requests assets at /css/<version>/plugins.css to lock
66
- # behavior; otherwise hit /css/latest/ for live updates.
67
- def path_segment = pinned? ? @number : 'latest'
68
66
  end
69
67
  end
@@ -24,8 +24,8 @@ module TRMNLP
24
24
  def pass? = static_image_urls.all? { |url| reachable?(url) }
25
25
 
26
26
  def static_image_urls
27
- source.view_markup.values
28
- .flat_map { |html| html.scan(/<img[^>]+src\s*=\s*["']([^"']+)["']/i).flatten }
27
+ source.all_markup
28
+ .scan(/<img[^>]+src\s*=\s*["']([^"']+)["']/i).flatten
29
29
  .map(&:strip)
30
30
  .reject { |src| src.empty? || src.include?('{{') || src.start_with?('data:') }
31
31
  end
@@ -6,7 +6,8 @@ module TRMNLP
6
6
  module Lint
7
7
  module Checks
8
8
  class WaitsForDomLoad < Check
9
- MESSAGE = 'JavaScript should listen for the DOMContentLoaded event, not window.onLoad()'
9
+ MESSAGE = 'JavaScript should listen for the DOMContentLoaded event, ' \
10
+ 'not window.onload or window.addEventListener("load")'
10
11
  LEARN_MORE = 'https://help.trmnl.com/en/articles/9510536-private-plugins#h_db7030f8b8'
11
12
  FORBIDDEN = ['window.onload', 'window.addeventlistener("load")',
12
13
  "window.addeventlistener('load')"].freeze
data/lib/trmnlp/poller.rb CHANGED
@@ -25,7 +25,7 @@ module TRMNLP
25
25
  # and keep the preview server alive, not crash the user's session. We
26
26
  # deliberately swallow here and return {} so the renderer keeps rendering.
27
27
  rescue StandardError => e
28
- reporter.info("warning: #{e.message}")
28
+ reporter.info(reporter.yellow("warning: #{e.message}"))
29
29
  {}
30
30
  end
31
31
 
@@ -34,7 +34,7 @@ module TRMNLP
34
34
  # NOTE: Same rationale as #poll_data — a bad webhook payload shouldn't take
35
35
  # down the dev server. Report a warning and keep serving.
36
36
  rescue StandardError => e
37
- reporter.info("webhook warning: #{e.message}")
37
+ reporter.info(reporter.yellow("webhook warning: #{e.message}"))
38
38
  end
39
39
 
40
40
  private
@@ -76,7 +76,7 @@ module TRMNLP
76
76
  case content_type
77
77
  when 'application/json', %r{^application/.+\+json} then wrap_array(JSON.parse(body))
78
78
  when 'text/xml', 'application/xml', %r{^application/.+\+xml} then wrap_array(Hash.from_xml(body))
79
- when 'text/html', 'text/plain' then sniff_json(body) || { 'text' => body }
79
+ when 'text/html', 'text/plain' then sniff_json(body) || { 'data' => body }
80
80
  else log_unknown_type(content_type_header)
81
81
  end
82
82
  end
@@ -72,7 +72,7 @@ module TRMNLP
72
72
  end
73
73
  end
74
74
 
75
- # bindings must match the `GET /render/{view}.html` route in app.rb
75
+ # ivars must match the @-references in web/views/render_html.erb
76
76
  class TemplateBinding
77
77
  def initialize(renderer, view, params)
78
78
  @view = view
@@ -11,6 +11,9 @@ module TRMNLP
11
11
  @quiet = quiet
12
12
  @stream = stream
13
13
  @messages = []
14
+ # Colour only when the stream is a real terminal, so ANSI codes
15
+ # never leak into piped or redirected output.
16
+ @tty = stream.tty?
14
17
  end
15
18
 
16
19
  def info(message)
@@ -20,9 +23,10 @@ module TRMNLP
20
23
 
21
24
  def green(text) = colorize(text, 32)
22
25
  def yellow(text) = colorize(text, 33)
26
+ def red(text) = colorize(text, 31)
23
27
 
24
28
  private
25
29
 
26
- def colorize(text, code) = "\e[#{code}m#{text}\e[0m"
30
+ def colorize(text, code) = @tty ? "\e[#{code}m#{text}\e[0m" : text
27
31
  end
28
32
  end
@@ -3,10 +3,13 @@
3
3
  require 'selenium-webdriver'
4
4
  require 'tempfile'
5
5
 
6
+ require_relative 'errors'
7
+
6
8
  module TRMNLP
7
9
  class Screenshot
8
- def initialize(pool:)
10
+ def initialize(pool:, viewport_timeout: 5)
9
11
  @pool = pool
12
+ @viewport_timeout = viewport_timeout
10
13
  end
11
14
 
12
15
  def call(html:, width:, height:)
@@ -50,15 +53,25 @@ module TRMNLP
50
53
  # NOTE: A cold Firefox — e.g. the first render after the container boots —
51
54
  # applies a window resize lazily. The old fixed sleep raced that reflow and
52
55
  # clipped the first screenshot short (800x433 instead of 800x480). Poll the
53
- # real viewport instead, re-applying the size until it lands; a resize that
54
- # never settles surfaces as a TimeoutError that #call already retries.
56
+ # real viewport instead, re-applying the size until it lands. A width the
57
+ # browser refuses to honour (below its ~500px window minimum) never settles;
58
+ # that surfaces as a clear RenderError rather than an opaque, retried timeout.
55
59
  def wait_for_viewport(driver, width, height)
56
- Selenium::WebDriver::Wait.new(timeout: 5, interval: 0.1).until do
60
+ Selenium::WebDriver::Wait.new(timeout: @viewport_timeout, interval: 0.1).until do
57
61
  next true if viewport(driver) == [width, height]
58
62
 
59
63
  apply_window_size(driver, width, height)
60
64
  false
61
65
  end
66
+ rescue Selenium::WebDriver::Error::TimeoutError
67
+ raise RenderError, viewport_clamp_message(driver, width, height)
68
+ end
69
+
70
+ def viewport_clamp_message(driver, width, height)
71
+ actual_width, actual_height = viewport(driver)
72
+ "Could not render at #{width}x#{height}: the browser clamped the viewport " \
73
+ "to #{actual_width}x#{actual_height}. PNG rendering needs a width of " \
74
+ 'roughly 500px or more — headless Firefox will not size its window narrower.'
62
75
  end
63
76
 
64
77
  def viewport(driver)
@@ -59,9 +59,7 @@ module TRMNLP
59
59
  TransformClient::Result.new(
60
60
  stdout: parsed['stdout'] || '',
61
61
  stderr: parsed['stderr'] || '',
62
- # Fall back to stdout for daemons that haven't been upgraded to
63
- # the separate `output` channel yet.
64
- output: (parsed['output'].to_s.empty? ? parsed['stdout'] : parsed['output']).to_s,
62
+ output: parsed['output'].to_s,
65
63
  exit_code: parsed['exit_code'] || 0,
66
64
  duration_ms: parsed['duration_ms'] || 0,
67
65
  error: parsed['error']
@@ -9,8 +9,10 @@ module TRMNLP
9
9
  # snippet supplied by the caller. Subprocess writes to a tempfile
10
10
  # path, Http writes to FD 3 for the production daemon to capture.
11
11
  #
12
- # Mirrors the hosted serverless runtime's code-wrapping behavior
13
- # verbatim except for the configurable sink.
12
+ # Mirrors the hosted serverless runtime's code-wrapping behavior,
13
+ # parameterized on the per-backend output sink. PHP additionally
14
+ # carries a leading `<?php` tag the file-based Subprocess path
15
+ # needs; the daemon tolerates it, so parity holds.
14
16
  #
15
17
  # NOTE: `output_sink` is spliced verbatim into the generated script as
16
18
  # executable code. It MUST be trmnlp-generated (see Subprocess#sink_for
@@ -17,9 +17,9 @@ module TRMNLP
17
17
 
18
18
  # Assembles the merged data hash. The trmnl namespace is built first,
19
19
  # layered with static_data / cached polled data / user_data_overrides,
20
- # then piped through the transform. The trmnl namespace is re-applied
21
- # after the transform so device, user, and plugin_settings survive
22
- # even when the transform doesn't pass them through.
20
+ # then piped through the transform. The whole trmnl namespace is
21
+ # re-applied after the transform so it survives even when the
22
+ # transform doesn't pass it through.
23
23
  def call(device: {})
24
24
  namespace = base_trmnl_data(device:)
25
25
  merged = assemble(namespace)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TRMNLP
4
- VERSION = '0.8.0'
4
+ VERSION = '0.8.2'
5
5
  end
data/web/public/index.css CHANGED
@@ -110,6 +110,11 @@ pre > code {
110
110
  margin: 0.5em 0 0.2em;
111
111
  }
112
112
 
113
+ /* A coloured badge is a warning — full opacity so the colour reads true. */
114
+ .payload-size--ok { color: #1a7f37; opacity: 1; }
115
+ .payload-size--warn { color: #bf8700; opacity: 1; }
116
+ .payload-size--over { color: #d1242f; opacity: 1; }
117
+
113
118
  @media (prefers-color-scheme: dark) {
114
119
  :root {
115
120
  --bg-color: #191b21;
data/web/views/index.erb CHANGED
@@ -66,7 +66,7 @@
66
66
 
67
67
  <iframe style="width: 800px; height: 480px; border: 1px solid black; background: white;">Rendering…</iframe>
68
68
 
69
- <div class="payload-size">Payload: <%= format_bytes(@payload_size) %></div>
69
+ <div class="payload-size <%= payload_size_class(@payload_size) %>">Payload: <%= format_bytes(@payload_size) %></div>
70
70
  <pre id="user-data"><code><%= h @user_data %></code></pre>
71
71
  </main>
72
72
  </body>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trmnl_preview
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rockwell Schrock
@@ -284,6 +284,7 @@ files:
284
284
  - lib/trmnlp/config/project.rb
285
285
  - lib/trmnlp/context.rb
286
286
  - lib/trmnlp/errors.rb
287
+ - lib/trmnlp/firefox_driver.rb
287
288
  - lib/trmnlp/form_field.rb
288
289
  - lib/trmnlp/framework_version.rb
289
290
  - lib/trmnlp/image_quantizer.rb