trmnl_preview 0.8.0 → 0.8.1

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: 0f80ac8d03e9b67e4ea6c7f18e8cd7745a76c910e9801f910662cc46b5661eb0
4
+ data.tar.gz: 1022cfae134c303ba7d00201d2d6ca7acb7c23c927371585b9e63a00fa28bfa2
5
5
  SHA512:
6
- metadata.gz: 39963f376ff877a4170ce57564876e5cb4bd6eacc23de4b1792a6cba6cd01f4252579fe19c8d678f21144f37e9ba050ed03866b38a3f7aaa9a67521aa324fd04
7
- data.tar.gz: 835b494bb56d29e9e4b607a482ae05466b08aa527ff38c00fae2a98e87524bbcbb6db78c9abd5dc8288c3b36692008107a1c9bbbdd298c36ddf1d435cd0d2e65
6
+ metadata.gz: 6667ad132d9b601073db2010c524f8322e6a7fd2b5425d248cf3a53398166d1f579171e8c1142da146247f7fcb96192c50c7fa0e8d9158797bc98d7bc9755eb2
7
+ data.tar.gz: 189d830c8ee5b2ed2ccd2006ef1fda9c6dadd258488a74d4925ac4476762c581fd4df3961fb94f771ff0943f3ba57cd62d0294cff6e5e1c5f79558c763d534a1
data/CHANGELOG.md CHANGED
@@ -1,6 +1,24 @@
1
1
 
2
2
  # Changelog
3
3
 
4
+ ## 0.8.1
5
+
6
+ ### Added
7
+
8
+ - `trmnlp build --png` renders a PNG for every view alongside the HTML, with `--width`, `--height`, and `--color-depth` flags to override the defaults (#92)
9
+ - 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)
10
+ - Added colour to CLI output — `lint` results, warnings, and errors — suppressed automatically when output is piped or redirected (#33)
11
+
12
+ ### Fixed
13
+
14
+ - `trmnlp init` no longer produces read-only project files when trmnlp itself is installed read-only, such as on NixOS (#83)
15
+ - Non-JSON polling responses (`text/html`, `text/plain`) are exposed to templates as `{{ data }}`, matching the hosted service — previously `{{ text }}` (#81)
16
+
17
+ ### Housekeeping
18
+
19
+ - Added SimpleCov coverage tracking, gated in CI at a 90% floor, plus dedicated specs for every lint check
20
+ - Extracted the headless-Firefox driver into a shared `FirefoxDriver` module used by both `serve` and `build --png`
21
+
4
22
  ## 0.8.0
5
23
 
6
24
  ### 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,6 +91,30 @@ 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, 2, or 4 — 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`.
@@ -310,6 +334,8 @@ To test, run:
310
334
  bin/rake
311
335
  ```
312
336
 
337
+ Specs run under SimpleCov; a coverage report is written to `coverage/`.
338
+
313
339
  ## Contributing
314
340
 
315
341
  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, 2, or 4 (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
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
@@ -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)
@@ -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.1'
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.1
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