trmnl_preview 0.3.2 → 0.4.0

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.
@@ -0,0 +1,47 @@
1
+ require 'yaml'
2
+
3
+ module TRMNLP
4
+ class Config
5
+ class Project
6
+ attr_reader :paths
7
+
8
+ def initialize(paths)
9
+ @paths = paths
10
+ reload!
11
+ end
12
+
13
+ def reload!
14
+ if paths.trmnlp_config.exist?
15
+ @config = YAML.load_file(paths.trmnlp_config)
16
+ else
17
+ @config = {}
18
+ end
19
+ end
20
+
21
+ def user_filters = @config['custom_filters'] || []
22
+
23
+ def live_render? = !watch_paths.empty?
24
+
25
+ def watch_paths
26
+ (@config['watch'] || []).map { |watch_path| paths.expand(watch_path) }.uniq
27
+ end
28
+
29
+ def custom_fields = @config['custom_fields'] || {}
30
+
31
+ def user_data_overrides = @config['variables'] || {}
32
+
33
+ # for interpolating custom_fields into polling_* options
34
+ def with_custom_fields(value)
35
+ custom_fields_with_env = custom_fields.transform_values { |v| with_env(v) }
36
+ Liquid::Template.parse(value).render(custom_fields_with_env)
37
+ end
38
+
39
+ private
40
+
41
+ # for interpolating ENV vars into custom_fields
42
+ def with_env(value)
43
+ Liquid::Template.parse(value).render({ 'env' => ENV.to_h })
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,15 @@
1
+ require_relative 'config/app'
2
+ require_relative 'config/plugin'
3
+ require_relative 'config/project'
4
+
5
+ module TRMNLP
6
+ class Config
7
+ attr_reader :app, :project, :plugin
8
+
9
+ def initialize(path)
10
+ @app = App.new(path)
11
+ @project = Project.new(path)
12
+ @plugin = Plugin.new(path, @project)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,211 @@
1
+ require 'erb'
2
+ require 'faraday'
3
+ require 'filewatcher'
4
+ require 'json'
5
+ require 'liquid'
6
+
7
+ require_relative 'config'
8
+ require_relative 'custom_filters'
9
+ require_relative 'paths'
10
+
11
+ module TRMNLP
12
+ class Context
13
+ attr_reader :config, :paths
14
+
15
+ def initialize(root_dir)
16
+ @paths = Paths.new(root_dir)
17
+ @config = Config.new(paths)
18
+ end
19
+
20
+ def validate!
21
+ raise Error, "not a plugin directory (did not find #{paths.trmnlp_config})" unless paths.valid?
22
+ end
23
+
24
+ def start_filewatcher
25
+ @filewatcher_thread ||= Thread.new do
26
+ loop do
27
+ begin
28
+ Filewatcher.new(config.project.watch_paths).watch do |changes|
29
+ config.project.reload!
30
+ config.plugin.reload!
31
+ new_user_data = user_data
32
+
33
+ views = changes.map { |path, _change| File.basename(path, '.liquid') }
34
+ views.each do |view|
35
+ @view_change_callback.call(view, new_user_data) if @view_change_callback
36
+ end
37
+ end
38
+ rescue => e
39
+ puts "Error during live render: #{e}"
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ def on_view_change(&block)
46
+ @view_change_callback = block
47
+ end
48
+
49
+ def user_data
50
+ merged_data = base_trmnl_data
51
+
52
+ if config.plugin.static?
53
+ merged_data.merge!(config.plugin.static_data)
54
+ elsif paths.user_data.exist?
55
+ merged_data.merge!(JSON.parse(paths.user_data.read))
56
+ end
57
+
58
+ # Praise be to ActiveSupport
59
+ merged_data.deep_merge!(config.project.user_data_overrides)
60
+ end
61
+
62
+ def poll_data
63
+ return unless config.plugin.polling?
64
+
65
+ data = {}
66
+
67
+ if config.plugin.polling_urls.empty?
68
+ raise Error, "config must specify polling_url or polling_urls"
69
+ end
70
+
71
+ config.plugin.polling_urls.each.with_index do |url, i|
72
+ verb = config.plugin.polling_verb.upcase
73
+
74
+ print "#{verb} #{url}... "
75
+
76
+ conn = Faraday.new(url:, headers: config.plugin.polling_headers)
77
+
78
+ case verb
79
+ when 'GET'
80
+ response = conn.get
81
+ when 'POST'
82
+ response = conn.post do |req|
83
+ req.body = config.plugin.polling_body
84
+ end
85
+ end
86
+
87
+ puts "received #{response.body.length} bytes (#{response.status} status)"
88
+ if response.status == 200
89
+ json = wrap_array(JSON.parse(response.body))
90
+ else
91
+ json = {}
92
+ puts response.body
93
+ end
94
+
95
+ if config.plugin.polling_urls.count == 1
96
+ # For a single polling URL, we just return the JSON directly
97
+ data = json
98
+ break
99
+ else
100
+ # Multiple URLs are namespaced by index
101
+ data["IDX_#{i}"] = json
102
+ end
103
+ end
104
+
105
+ write_user_data(data)
106
+
107
+ data
108
+ rescue StandardError => e
109
+ puts "error: #{e.message}"
110
+ {}
111
+ end
112
+
113
+ def put_webhook(payload)
114
+ data = wrap_array(JSON.parse(payload))
115
+ write_user_data(data)
116
+ rescue
117
+ puts "webhook error: #{e.message}"
118
+ end
119
+
120
+ def render_template(view)
121
+ template_path = paths.template(view)
122
+ return "Missing template: #{template_path}" unless template_path.exist?
123
+
124
+ user_template = Liquid::Template.parse(template_path.read, environment: liquid_environment)
125
+ user_template.render(user_data)
126
+ rescue StandardError => e
127
+ e.message
128
+ end
129
+
130
+ def render_full_page(view)
131
+ template = paths.render_template.read
132
+
133
+ ERB.new(template).result(TemplateBinding.new(self, view).get_binding do
134
+ render_template(view)
135
+ end)
136
+ end
137
+
138
+ def screen_classes
139
+ classes = 'screen'
140
+ classes << ' screen--no-bleed' if config.plugin.no_screen_padding == 'yes'
141
+ classes << ' dark-mode' if config.plugin.dark_mode == 'yes'
142
+ classes
143
+ end
144
+
145
+ private
146
+
147
+ # bindings must match the `GET /render/{view}.html` route in app.rb
148
+ class TemplateBinding
149
+ def initialize(context, view)
150
+ @screen_classes = context.screen_classes
151
+ @view = view
152
+ end
153
+
154
+ def get_binding = binding
155
+ end
156
+
157
+ def wrap_array(json)
158
+ json.is_a?(Array) ? { data: json } : json
159
+ end
160
+
161
+ def base_trmnl_data
162
+ {
163
+ 'trmnl' => {
164
+ 'user' => {
165
+ 'name' => 'name',
166
+ 'first_name' => 'first_name',
167
+ 'last_name' => 'last_name',
168
+ 'locale' => 'en',
169
+ 'time_zone' => 'Eastern Time (US & Canada)',
170
+ 'time_zone_iana' => 'America/New_York',
171
+ 'utc_offset' => -14400
172
+ },
173
+ 'device' => {
174
+ 'friendly_id' => 'ABC123',
175
+ 'percent_charged' => 85.0,
176
+ 'wifi_strength' => 90,
177
+ 'height' => 480,
178
+ 'width' => 800
179
+ },
180
+ 'system' => {
181
+ 'timestamp_utc' => Time.now.utc.to_i,
182
+ },
183
+ 'plugin_settings' => {
184
+ 'instance_name' => 'instance_name',
185
+ 'strategy' => config.plugin.strategy,
186
+ 'dark_mode' => config.plugin.dark_mode,
187
+ 'polling_headers' => config.plugin.polling_headers_encoded,
188
+ 'polling_url' => config.plugin.polling_url_text,
189
+ 'custom_fields_values' => config.project.custom_fields
190
+ }
191
+ }
192
+ }
193
+ end
194
+
195
+ def liquid_environment
196
+ @liquid_environment ||= Liquid::Environment.build do |env|
197
+ env.register_filter(CustomFilters)
198
+
199
+ config.project.user_filters.each do |module_name, relative_path|
200
+ require paths.root_dir.join(relative_path)
201
+ env.register_filter(Object.const_get(module_name))
202
+ end
203
+ end
204
+ end
205
+
206
+ def write_user_data(data)
207
+ paths.create_cache_dir
208
+ paths.user_data.write(JSON.generate(data))
209
+ end
210
+ end
211
+ end
@@ -1,7 +1,8 @@
1
1
  require 'active_support'
2
2
 
3
- module TRMNLPreview
3
+ module TRMNLP
4
4
  module CustomFilters
5
+ # TODO: sync up with core
5
6
  def number_with_delimiter(*args)
6
7
  ActiveSupport::NumberHelper.number_to_delimited(*args)
7
8
  end
@@ -0,0 +1,50 @@
1
+ require 'xdg'
2
+
3
+ module TRMNLP
4
+ class Paths
5
+ attr_reader :root_dir
6
+
7
+ def initialize(root_dir)
8
+ @root_dir = Pathname.new(root_dir)
9
+ @xdg = XDG.new
10
+ end
11
+
12
+ # --- directories ---
13
+
14
+ def src_dir = root_dir.join('src')
15
+
16
+ def build_dir = root_dir.join('_build')
17
+ def create_build_dir = build_dir.mkpath
18
+
19
+ def app_config_dir = xdg.config_home.join('trmnlp')
20
+
21
+ def cache_dir = xdg.cache_home.join('trmnl')
22
+ def create_cache_dir = cache_dir.mkpath
23
+
24
+ def valid? = trmnlp_config.exist?
25
+
26
+ # --- files ---
27
+
28
+ def trmnlp_config = root_dir.join('.trmnlp.yml')
29
+
30
+ def plugin_config = src_dir.join('settings.yml')
31
+
32
+ def template(view) = src_dir.join("#{view}.liquid")
33
+
34
+ def app_config = app_config_dir.join('config.yml')
35
+
36
+ def user_data = cache_dir.join('data.json')
37
+
38
+ def render_template = Pathname.new(__dir__).join('..', '..', 'web', 'views', 'render_html.erb')
39
+
40
+ def src_files = src_dir.glob('*').select(&:file?)
41
+
42
+ # --- utilities ---
43
+
44
+ def expand(path) = Pathname.new(path).expand_path(root_dir)
45
+
46
+ private
47
+
48
+ attr_reader :xdg
49
+ end
50
+ end
@@ -3,7 +3,7 @@ require 'mini_magick'
3
3
  require 'puppeteer-ruby'
4
4
  require 'base64'
5
5
 
6
- module TRMNLPreview
6
+ module TRMNLP
7
7
  class ScreenGenerator
8
8
 
9
9
  def initialize(html, opts = {})
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRMNLP
4
+ VERSION = "0.4.0".freeze
5
+ end
data/lib/trmnlp.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRMNLP; end
4
+
5
+ require_relative "trmnlp/config"
6
+ require_relative "trmnlp/context"
7
+ require_relative "trmnlp/version"
8
+
9
+ module TRMNLP
10
+ VIEWS = %w{full half_horizontal half_vertical quadrant}
11
+
12
+ class Error < StandardError; end
13
+ end
@@ -1,33 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "lib/trmnl_preview/version"
3
+ require_relative "lib/trmnlp/version"
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "trmnl_preview"
7
- spec.version = TRMNLPreview::VERSION
7
+ spec.version = TRMNLP::VERSION
8
8
  spec.authors = ["Rockwell Schrock"]
9
9
  spec.email = ["rockwell@schrock.me"]
10
10
 
11
11
  spec.summary = "Local web server to preview TRMNL plugins"
12
12
  spec.description = "Automatically rebuild and preview TRNML plugins in multiple views"
13
- spec.homepage = "https://github.com/schrockwell/trmnl_preview"
13
+ spec.homepage = "https://github.com/usetrmnl/trmnlp"
14
14
  spec.license = "MIT"
15
15
  spec.required_ruby_version = ">= 3.0.0"
16
16
 
17
17
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
18
 
19
19
  spec.metadata["homepage_uri"] = spec.homepage
20
- spec.metadata["source_code_uri"] = "https://github.com/schrockwell/trmnl_preview"
20
+ spec.metadata["source_code_uri"] = "https://github.com/usetrmnl/trmnlp"
21
21
 
22
- # Specify which files should be added to the gem when it is released.
23
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
22
  spec.files = Dir.chdir(__dir__) do
25
- `git ls-files -z`.split("\x0").reject do |f|
26
- (File.expand_path(f) == __FILE__) ||
27
- f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile Dockerfile])
28
- end
23
+ [
24
+ 'bin/**/*',
25
+ 'lib/**/*',
26
+ 'web/**/*',
27
+ 'CHANGELOG.md',
28
+ 'LICENSE.txt',
29
+ 'README.md',
30
+ 'trmnl_preview.gemspec'
31
+ ].flat_map { |glob| Dir[glob] }
29
32
  end
30
- spec.bindir = "exe"
33
+ spec.bindir = "bin"
31
34
  spec.executables = ["trmnlp"]
32
35
  spec.require_paths = ["lib"]
33
36
 
@@ -48,8 +51,12 @@ Gem::Specification.new do |spec|
48
51
  spec.add_dependency 'mini_magick', '~> 4.12.0'
49
52
 
50
53
  # Utilities
51
- spec.add_dependency "toml-rb", "~> 3.0"
52
54
  spec.add_dependency "filewatcher", "~> 2.1"
55
+ spec.add_dependency "faraday", "~> 2.1"
56
+ spec.add_dependency "faraday-multipart", "~> 1.1"
57
+ spec.add_dependency "xdg", "~> 9.1"
58
+ spec.add_dependency "rubyzip", "~> 2.3.0"
59
+ spec.add_dependency "thor", "~> 1.3"
53
60
 
54
61
  # For more information and examples about making a new gem, check out our
55
62
  # guide at: https://bundler.io/guides/creating_gem.html
data/web/public/index.js CHANGED
@@ -8,8 +8,11 @@ trmnlp.connectLiveRender = function () {
8
8
  };
9
9
 
10
10
  ws.onmessage = function (msg) {
11
- if (msg.data === "reload") {
11
+ const payload = JSON.parse(msg.data);
12
+
13
+ if (payload.type === "reload") {
12
14
  trmnlp.setIframeSrc(trmnlp.iframe.src);
15
+ trmnlp.userData.textContent = JSON.stringify(payload.user_data, null, 2);
13
16
  }
14
17
  };
15
18
 
@@ -42,6 +45,7 @@ document.addEventListener("DOMContentLoaded", function () {
42
45
  trmnlp.iframe = document.querySelector("iframe");
43
46
  trmnlp.caseSelect = document.querySelector(".select-case");
44
47
  trmnlp.formatSelect = document.querySelector(".select-format");
48
+ trmnlp.userData = document.getElementById("user-data");
45
49
  trmnlp.isLiveReloadEnabled =
46
50
  document.querySelector("meta[name='live-reload']").content === "true";
47
51
 
data/web/views/index.erb CHANGED
@@ -41,6 +41,8 @@
41
41
  <iframe width="800" height="480"></iframe>
42
42
  <div class="case-overlay"></div>
43
43
  </div>
44
+
45
+ <pre id="user-data"><%= @user_data %></pre>
44
46
  </main>
45
47
  </body>
46
48
  </html>
@@ -11,7 +11,7 @@
11
11
  <!-- End Inter font -->
12
12
  </head>
13
13
  <body class="environment trmnl">
14
- <div class="screen">
14
+ <div class="<%= @screen_classes %>">
15
15
  <div class="view view--<%= @view %>">
16
16
  <%= yield %>
17
17
  </div>
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trmnl_preview
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rockwell Schrock
8
- bindir: exe
8
+ bindir: bin
9
9
  cert_chain: []
10
- date: 2025-01-08 00:00:00.000000000 Z
10
+ date: 2025-04-09 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: sinatra
@@ -136,21 +136,21 @@ dependencies:
136
136
  - !ruby/object:Gem::Version
137
137
  version: 4.12.0
138
138
  - !ruby/object:Gem::Dependency
139
- name: toml-rb
139
+ name: filewatcher
140
140
  requirement: !ruby/object:Gem::Requirement
141
141
  requirements:
142
142
  - - "~>"
143
143
  - !ruby/object:Gem::Version
144
- version: '3.0'
144
+ version: '2.1'
145
145
  type: :runtime
146
146
  prerelease: false
147
147
  version_requirements: !ruby/object:Gem::Requirement
148
148
  requirements:
149
149
  - - "~>"
150
150
  - !ruby/object:Gem::Version
151
- version: '3.0'
151
+ version: '2.1'
152
152
  - !ruby/object:Gem::Dependency
153
- name: filewatcher
153
+ name: faraday
154
154
  requirement: !ruby/object:Gem::Requirement
155
155
  requirements:
156
156
  - - "~>"
@@ -163,6 +163,62 @@ dependencies:
163
163
  - - "~>"
164
164
  - !ruby/object:Gem::Version
165
165
  version: '2.1'
166
+ - !ruby/object:Gem::Dependency
167
+ name: faraday-multipart
168
+ requirement: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - "~>"
171
+ - !ruby/object:Gem::Version
172
+ version: '1.1'
173
+ type: :runtime
174
+ prerelease: false
175
+ version_requirements: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - "~>"
178
+ - !ruby/object:Gem::Version
179
+ version: '1.1'
180
+ - !ruby/object:Gem::Dependency
181
+ name: xdg
182
+ requirement: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - "~>"
185
+ - !ruby/object:Gem::Version
186
+ version: '9.1'
187
+ type: :runtime
188
+ prerelease: false
189
+ version_requirements: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - "~>"
192
+ - !ruby/object:Gem::Version
193
+ version: '9.1'
194
+ - !ruby/object:Gem::Dependency
195
+ name: rubyzip
196
+ requirement: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - "~>"
199
+ - !ruby/object:Gem::Version
200
+ version: 2.3.0
201
+ type: :runtime
202
+ prerelease: false
203
+ version_requirements: !ruby/object:Gem::Requirement
204
+ requirements:
205
+ - - "~>"
206
+ - !ruby/object:Gem::Version
207
+ version: 2.3.0
208
+ - !ruby/object:Gem::Dependency
209
+ name: thor
210
+ requirement: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - "~>"
213
+ - !ruby/object:Gem::Version
214
+ version: '1.3'
215
+ type: :runtime
216
+ prerelease: false
217
+ version_requirements: !ruby/object:Gem::Requirement
218
+ requirements:
219
+ - - "~>"
220
+ - !ruby/object:Gem::Version
221
+ version: '1.3'
166
222
  description: Automatically rebuild and preview TRNML plugins in multiple views
167
223
  email:
168
224
  - rockwell@schrock.me
@@ -171,23 +227,30 @@ executables:
171
227
  extensions: []
172
228
  extra_rdoc_files: []
173
229
  files:
174
- - ".ruby-version"
175
230
  - CHANGELOG.md
176
231
  - LICENSE.txt
177
232
  - README.md
178
- - config.example.toml
179
- - docs/preview.png
180
- - exe/trmnlp
181
- - lib/trmnl_preview.rb
182
- - lib/trmnl_preview/app.rb
183
- - lib/trmnl_preview/cmd/build.rb
184
- - lib/trmnl_preview/cmd/serve.rb
185
- - lib/trmnl_preview/cmd/usage.rb
186
- - lib/trmnl_preview/cmd/version.rb
187
- - lib/trmnl_preview/context.rb
188
- - lib/trmnl_preview/custom_filters.rb
189
- - lib/trmnl_preview/screen_generator.rb
190
- - lib/trmnl_preview/version.rb
233
+ - bin/trmnlp
234
+ - lib/trmnlp.rb
235
+ - lib/trmnlp/api_client.rb
236
+ - lib/trmnlp/app.rb
237
+ - lib/trmnlp/cli.rb
238
+ - lib/trmnlp/commands.rb
239
+ - lib/trmnlp/commands/base.rb
240
+ - lib/trmnlp/commands/build.rb
241
+ - lib/trmnlp/commands/login.rb
242
+ - lib/trmnlp/commands/pull.rb
243
+ - lib/trmnlp/commands/push.rb
244
+ - lib/trmnlp/commands/serve.rb
245
+ - lib/trmnlp/config.rb
246
+ - lib/trmnlp/config/app.rb
247
+ - lib/trmnlp/config/plugin.rb
248
+ - lib/trmnlp/config/project.rb
249
+ - lib/trmnlp/context.rb
250
+ - lib/trmnlp/custom_filters.rb
251
+ - lib/trmnlp/paths.rb
252
+ - lib/trmnlp/screen_generator.rb
253
+ - lib/trmnlp/version.rb
191
254
  - trmnl_preview.gemspec
192
255
  - web/public/black-case.jpg
193
256
  - web/public/clear-case.jpg
@@ -196,13 +259,13 @@ files:
196
259
  - web/public/white-case.jpg
197
260
  - web/views/index.erb
198
261
  - web/views/render_html.erb
199
- homepage: https://github.com/schrockwell/trmnl_preview
262
+ homepage: https://github.com/usetrmnl/trmnlp
200
263
  licenses:
201
264
  - MIT
202
265
  metadata:
203
266
  allowed_push_host: https://rubygems.org
204
- homepage_uri: https://github.com/schrockwell/trmnl_preview
205
- source_code_uri: https://github.com/schrockwell/trmnl_preview
267
+ homepage_uri: https://github.com/usetrmnl/trmnlp
268
+ source_code_uri: https://github.com/usetrmnl/trmnlp
206
269
  rdoc_options: []
207
270
  require_paths:
208
271
  - lib
data/.ruby-version DELETED
@@ -1 +0,0 @@
1
- 3.4.1
data/config.example.toml DELETED
@@ -1,14 +0,0 @@
1
- # strategy = "polling" ==> the data will be fetched once, at server start-up
2
- # strategy = "webhook" ==> POST new data to /webhook
3
- strategy = "polling"
4
-
5
- # Poll URL (required for polling strategy)
6
- url = "https://example.com/data.json"
7
-
8
- # Automatically re-render the view when Liquid templates change (default: true)
9
- live_render = true
10
-
11
- # Polling headers (optional, for polling strategy)
12
- [polling_headers]
13
- authorization = "bearer 123"
14
- content-type = "application/json"
data/docs/preview.png DELETED
Binary file
data/exe/trmnlp DELETED
@@ -1,14 +0,0 @@
1
- #! /usr/bin/env ruby
2
-
3
- require_relative '../lib/trmnl_preview'
4
-
5
- case ARGV[0].to_s.downcase
6
- when 'serve'
7
- require_relative '../lib/trmnl_preview/cmd/serve'
8
- when 'build'
9
- require_relative '../lib/trmnl_preview/cmd/build'
10
- when 'version'
11
- require_relative '../lib/trmnl_preview/cmd/version'
12
- else
13
- require_relative '../lib/trmnl_preview/cmd/usage'
14
- end