Class: Vizcore::CLI

Inherits:
Thor
  • Object
show all
Defined in:
lib/vizcore/cli.rb

Overview

Thor-based CLI entrypoint for Vizcore.

Constant Summary collapse

SCAFFOLD_TEMPLATES =
{
  "standard" => {
    label: "standard",
    start_scene: "scenes/basic.rb",
    files: [
      ["basic_scene.rb", "scenes/basic.rb", "Minimal wireframe starter"],
      ["intro_drop_scene.rb", "scenes/intro_drop.rb", "Transition flow with beat trigger"],
      ["midi_control_scene.rb", "scenes/midi_control.rb", "MIDI note/CC mapping example"],
      ["custom_shader_scene.rb", "scenes/custom_shader.rb", "Custom GLSL + post/VJ effect example"],
      ["custom_wave.frag", "shaders/custom_wave.frag", "Custom GLSL fragment shader"]
    ],
    notes: [
      "`scenes/custom_shader.rb` references `shaders/custom_wave.frag`.",
      "Use `vizcore devices midi` before running `scenes/midi_control.rb`."
    ]
  },
  "minimal" => {
    label: "minimal",
    start_scene: "scenes/basic.rb",
    files: [
      ["basic_scene.rb", "scenes/basic.rb", "Minimal wireframe starter"]
    ],
    notes: []
  },
  "shader" => {
    label: "shader",
    start_scene: "scenes/custom_shader.rb",
    files: [
      ["custom_shader_scene.rb", "scenes/custom_shader.rb", "Custom GLSL + post/VJ effect example"],
      ["custom_wave.frag", "shaders/custom_wave.frag", "Custom GLSL fragment shader"]
    ],
    notes: [
      "`scenes/custom_shader.rb` references `shaders/custom_wave.frag`."
    ]
  },
  "midi" => {
    label: "midi",
    start_scene: "scenes/midi_control.rb",
    files: [
      ["midi_control_scene.rb", "scenes/midi_control.rb", "MIDI note/CC mapping example"]
    ],
    notes: [
      "Run `vizcore devices midi` before starting the MIDI scene."
    ]
  },
  "live-set" => {
    label: "live-set",
    start_scene: "scenes/live_set.rb",
    files: [
      ["intro_drop_scene.rb", "scenes/live_set.rb", "Two-scene transition flow with beat trigger"]
    ],
    notes: [
      "Use file audio or a microphone input with clear beats for transition triggers."
    ]
  },
  "rubykaigi" => {
    label: "rubykaigi",
    start_scene: "scenes/rubykaigi.rb",
    files: [
      ["rubykaigi_scene.rb", "scenes/rubykaigi.rb", "Ruby conference visual starter"]
    ],
    notes: [
      "This scene uses Ruby-red text and audio-reactive geometry for talk or event visuals."
    ]
  }
}.freeze
PLUGIN_SCAFFOLD_FILES =
[
  ["plugin_readme.md", "README.md"],
  ["plugin_layer.rb", "lib/{{plugin_name}}.rb"],
  ["plugin_renderer.js", "frontend/{{plugin_name}}-renderer.js"],
  ["plugin_scene.rb", "examples/{{plugin_name}}_scene.rb"]
].freeze
DEFAULT_CAPTURE_PORT =
4579

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.exit_on_failure?Boolean

Exit with non-zero status when a Thor command fails.

Returns:

  • (Boolean)


28
29
30
# File 'lib/vizcore/cli.rb', line 28

def self.exit_on_failure?
  true
end

Instance Method Details

#browser_capture(url) ⇒ void

This method returns an undefined value.

Capture browser-rendered output from a running Vizcore server.

Parameters:

  • url (String)

Raises:

  • (Thor::Error)

    when Playwright capture fails



365
366
367
368
369
370
371
372
373
374
# File 'lib/vizcore/cli.rb', line 365

def browser_capture(url)
  run_browser_capture(
    url,
    out: options.fetch(:out),
    selector: options.fetch(:selector),
    wait: options.fetch(:wait),
    width: options.fetch(:width),
    height: options.fetch(:height)
  )
end

#capture(scene_file) ⇒ void

This method returns an undefined value.

Start Vizcore and capture a browser-rendered canvas from the projector route.

Parameters:

  • scene_file (String)

Raises:

  • (Thor::Error)

    when server startup or capture fails



394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
# File 'lib/vizcore/cli.rb', line 394

def capture(scene_file)
  config = Config.new(
    scene_file: scene_file,
    host: options.fetch(:host),
    port: options.fetch(:port),
    audio_source: options.fetch(:audio_source),
    audio_file: options[:audio_file],
    feature_file: options[:feature_file],
    control_preset: options[:control_preset],
    reload: false,
    projector_mode: true
  )
  validate_snapshot_config!(config)

  pid = Kernel.spawn(*temporary_server_command(config), out: File::NULL, err: File::NULL)
  begin
    wait_for_http("http://#{config.host}:#{config.port}/health", timeout: options.fetch(:timeout))
    run_browser_capture(
      "http://#{config.host}:#{config.port}/projector",
      out: options.fetch(:out),
      selector: options.fetch(:selector),
      wait: options.fetch(:wait),
      width: options.fetch(:width),
      height: options.fetch(:height)
    )
  ensure
    stop_temporary_server(pid)
  end
rescue StandardError => e
  raise Thor::Error, e.message
end

#demovoid

This method returns an undefined value.

Start a bundled scene with bundled audio for first-run verification.



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/vizcore/cli.rb', line 169

def demo
  config = Config.new(
    scene_file: Vizcore.root.join("examples", "rhythm_geometry.rb"),
    host: options.fetch(:host),
    port: options.fetch(:port),
    audio_source: :file,
    audio_file: Vizcore.root.join("examples", "assets", "complex_demo_loop.wav"),
    noise_gate: options.fetch(:noise_gate),
    bpm: options[:bpm],
    bpm_lock: options.fetch(:bpm_lock),
    control_preset: options[:control_preset],
    osc_port: options[:osc_port],
    projector_mode: options.fetch(:projector)
  )
  Server::Runner.new(config).run
rescue ArgumentError => e
  raise Thor::Error, e.message
end

#devices(type = nil) ⇒ void

This method returns an undefined value.

Print audio and/or MIDI devices detected by the runtime.

Parameters:

  • type (String, nil) (defaults to: nil)

    ‘audio`, `midi`, or nil for both

Raises:

  • (Thor::Error)

    when an unknown type is provided



232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/vizcore/cli.rb', line 232

def devices(type = nil)
  case type
  when nil
    print_audio_devices
    print_midi_devices
  when "audio"
    print_audio_devices
  when "midi"
    print_midi_devices
  else
    raise Thor::Error, "Unknown type: #{type}. Use `audio` or `midi`."
  end
end

#doctorvoid

This method returns an undefined value.

Print local environment checks for Vizcore runtime dependencies.

Raises:

  • (Thor::Error)

    when a required check fails



251
252
253
254
255
256
257
# File 'lib/vizcore/cli.rb', line 251

def doctor
  report = Vizcore::CLISupport::Doctor.new.call
  report.checks.each do |check|
    say("#{status_label(check.status)} #{check.name}: #{check.message}")
  end
  raise Thor::Error, "vizcore doctor found required failures" if report.failure?
end

#dsl_docsvoid

This method returns an undefined value.

Print generated documentation for the Ruby scene DSL.



302
303
304
# File 'lib/vizcore/cli.rb', line 302

def dsl_docs
  Vizcore::CLISupport::DslReference.new.lines.each { |line| say(line) }
end

This method returns an undefined value.

Start a browser gallery for bundled example scenes.



194
195
196
197
198
199
# File 'lib/vizcore/cli.rb', line 194

def gallery
  Vizcore::Server::GalleryRunner.new(
    host: options.fetch(:host),
    port: options.fetch(:port)
  ).run
end

#inspect_scene(scene_file) ⇒ void

This method returns an undefined value.

Load a scene DSL file and print its runtime structure.

Parameters:

  • scene_file (String)

    path to a Ruby scene DSL file

Raises:

  • (Thor::Error)

    when scene loading fails



266
267
268
269
270
271
272
273
# File 'lib/vizcore/cli.rb', line 266

def inspect_scene(scene_file)
  diagnostics = Vizcore::CLISupport::SceneDiagnostics.new(scene_file: scene_file)
  result = diagnostics.validate
  print_issues(result.issues)
  raise Thor::Error, "scene inspection failed" unless result.definition

  diagnostics.inspect_lines(result.definition).each { |line| say(line) }
end

#layersvoid

This method returns an undefined value.

Print supported layer types, params, and browser-side capabilities.



293
294
295
# File 'lib/vizcore/cli.rb', line 293

def layers
  Vizcore::CLISupport::LayerDocs.new.lines.each { |line| say(line) }
end

#new(name) ⇒ void

This method returns an undefined value.

Generate a new Vizcore project scaffold.

Parameters:

  • name (String)

    directory name for the new project



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/vizcore/cli.rb', line 210

def new(name)
  scaffold = scaffold_template(options.fetch(:template))
  root = Pathname.new(name).expand_path
  FileUtils.mkdir_p(root)

  write_project_readme(root.join("README.md"), project_name: name, scaffold: scaffold)
  scaffold.fetch(:files).each do |template_name, destination, _description|
    write_template(template_name, root.join(destination), project_name: name)
  end

  say("Created project scaffold (#{scaffold.fetch(:label)}): #{root}")
  say("Next: cd #{name} && vizcore start #{scaffold.fetch(:start_scene)}")
rescue ArgumentError => e
  raise Thor::Error, e.message
end

#plugin(command = nil, name = nil) ⇒ void

This method returns an undefined value.

Run plugin helper commands.

Parameters:

  • command (String, nil) (defaults to: nil)
  • name (String, nil) (defaults to: nil)

Raises:

  • (Thor::Error)

    when the subcommand or arguments are invalid



342
343
344
345
346
347
348
349
350
351
# File 'lib/vizcore/cli.rb', line 342

def plugin(command = nil, name = nil)
  case command.to_s
  when "new"
    create_plugin_scaffold(name)
  else
    raise Thor::Error, "Unknown plugin command: #{command || '(nil)'}. Use `vizcore plugin new NAME`."
  end
rescue ArgumentError => e
  raise Thor::Error, e.message
end

#record_features(audio_file) ⇒ void

This method returns an undefined value.

Analyze an audio file and persist feature frames as JSON.

Parameters:

  • audio_file (String)

    path to WAV/MP3/FLAC audio file

Raises:

  • (Thor::Error)

    when audio loading or JSON writing fails



523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
# File 'lib/vizcore/cli.rb', line 523

def record_features(audio_file)
  result = Vizcore::Analysis::FeatureRecorder.new(
    audio_file: audio_file,
    frames: options.fetch(:frames),
    fps: options.fetch(:fps),
    noise_gate: options.fetch(:noise_gate),
    audio_normalize: feature_audio_normalize_setting,
    bpm: options[:bpm],
    bpm_lock: options.fetch(:bpm_lock)
  ).write(out: options.fetch(:out))
  say(
    "Features written: #{result[:path]} " \
    "(frames=#{result[:frames]}, fps=#{result[:fps]}, sample_rate=#{result[:sample_rate]})"
  )
rescue StandardError => e
  raise Thor::Error, e.message
end

#render(scene_file) ⇒ void

This method returns an undefined value.

Load a scene DSL file and write a software-rendered PNG image sequence or MP4.

Parameters:

  • scene_file (String)

    path to a Ruby scene DSL file

Raises:

  • (Thor::Error)

    when scene loading or frame writing fails



480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
# File 'lib/vizcore/cli.rb', line 480

def render(scene_file)
  config = Config.new(
    scene_file: scene_file,
    audio_source: options.fetch(:audio_source),
    audio_file: options[:audio_file],
    audio_device: options[:audio_device],
    noise_gate: options.fetch(:noise_gate),
    bpm: options[:bpm],
    bpm_lock: options.fetch(:bpm_lock)
  )
  validate_snapshot_config!(config)

  result = Vizcore::Renderer::RenderSequence.new(
    config: config,
    frames: options.fetch(:frames),
    fps: options.fetch(:fps),
    width: options.fetch(:width),
    height: options.fetch(:height)
  ).write(out: options.fetch(:out))
  return say(render_video_message(result)) if result[:format] == :mp4

  say(
    "Frames written: #{result[:path]} " \
    "(scene=#{result[:scene]}, frames=#{result[:frames]}, fps=#{result[:fps]}, #{result[:width]}x#{result[:height]})"
  )
rescue StandardError => e
  raise Thor::Error, e.message
end

#shader(command = nil, name = nil) ⇒ void

This method returns an undefined value.

Run custom shader helper commands.

Parameters:

  • command (String) (defaults to: nil)
  • name (String, nil) (defaults to: nil)

Raises:

  • (Thor::Error)

    when the subcommand or arguments are invalid



323
324
325
326
327
328
329
330
331
332
# File 'lib/vizcore/cli.rb', line 323

def shader(command = nil, name = nil)
  case command.to_s
  when "new"
    create_shader_template(name)
  else
    raise Thor::Error, "Unknown shader command: #{command || '(nil)'}. Use `vizcore shader new NAME`."
  end
rescue ArgumentError => e
  raise Thor::Error, e.message
end

#shader_docsvoid

This method returns an undefined value.

Print generated documentation for custom GLSL uniforms.



311
312
313
# File 'lib/vizcore/cli.rb', line 311

def shader_docs
  Vizcore::CLISupport::ShaderUniformDocs.new.lines.each { |line| say(line) }
end

#snapshot(scene_file) ⇒ void

This method returns an undefined value.

Load a scene DSL file and write a software-rendered PNG preview.

Parameters:

  • scene_file (String)

    path to a Ruby scene DSL file

Raises:

  • (Thor::Error)

    when scene loading or snapshot writing fails



441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
# File 'lib/vizcore/cli.rb', line 441

def snapshot(scene_file)
  config = Config.new(
    scene_file: scene_file,
    audio_source: options.fetch(:audio_source),
    audio_file: options[:audio_file],
    audio_device: options[:audio_device],
    noise_gate: options.fetch(:noise_gate),
    bpm: options[:bpm],
    bpm_lock: options.fetch(:bpm_lock)
  )
  validate_snapshot_config!(config)

  result = Vizcore::Renderer::Snapshot.new(
    config: config,
    width: options.fetch(:width),
    height: options.fetch(:height)
  ).write(out: options.fetch(:out))
  say("Snapshot written: #{result[:path]} (scene=#{result[:scene]}, #{result[:width]}x#{result[:height]})")
rescue StandardError => e
  raise Thor::Error, e.message
end

#start(scene_file = nil) ⇒ void

This method returns an undefined value.

Start the Vizcore server with the given scene file.

Parameters:

  • scene_file (String) (defaults to: nil)

    path to a Ruby scene DSL file

Raises:

  • (Thor::Error)

    when CLI arguments are invalid



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/vizcore/cli.rb', line 130

def start(scene_file = nil)
  manifest = load_project_manifest(options[:manifest])
  profile = options[:profile]
  load_manifest_plugins(manifest, profile: profile)
  defaults = manifest&.config_defaults(profile: profile) || {}
  config = Config.new(
    scene_file: scene_file || defaults[:scene_file],
    host: options.fetch(:host),
    port: options.fetch(:port),
    audio_source: options[:audio_source] || defaults[:audio_source] || Config::DEFAULT_AUDIO_SOURCE,
    audio_file: options[:audio_file] || defaults[:audio_file],
    audio_device: options[:audio_device] || defaults[:audio_device],
    feature_file: options[:feature_file] || defaults[:feature_file],
    control_preset: options[:control_preset] || defaults[:control_preset],
    plugin_assets: defaults[:plugin_assets],
    noise_gate: options.fetch(:noise_gate),
    bpm: options[:bpm],
    bpm_lock: options.fetch(:bpm_lock),
    osc_port: options[:osc_port] || defaults[:osc_port],
    reload: options.fetch(:reload),
    projector_mode: options.fetch(:projector)
  )
  Server::Runner.new(config).run
rescue ArgumentError => e
  raise Thor::Error, e.message
end

#validate(scene_file) ⇒ void

This method returns an undefined value.

Load and validate a scene DSL file without starting the server.

Parameters:

  • scene_file (String)

    path to a Ruby scene DSL file

Raises:

  • (Thor::Error)

    when validation fails



281
282
283
284
285
286
287
# File 'lib/vizcore/cli.rb', line 281

def validate(scene_file)
  result = Vizcore::CLISupport::SceneDiagnostics.new(scene_file: scene_file).validate
  print_issues(result.issues)
  raise Thor::Error, "scene validation failed" unless result.valid?

  say("Scene valid: #{scene_file}")
end