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)


31
32
33
# File 'lib/vizcore/cli.rb', line 31

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



447
448
449
450
451
452
453
454
455
456
457
458
# File 'lib/vizcore/cli.rb', line 447

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),
    wait_for_frame: options.fetch(:wait_for_frame),
    frame_timeout: options.fetch(:frame_timeout)
  )
end

#calibrate(command = nil) ⇒ void

This method returns an undefined value.

Run calibration helpers.

Parameters:

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

Raises:

  • (Thor::Error)

    when arguments are invalid



308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/vizcore/cli.rb', line 308

def calibrate(command = nil)
  raise Thor::Error, "Unknown calibrate command: #{command || '(nil)'}. Use `vizcore calibrate audio`." unless command.to_s == "audio"

  result = Vizcore::Audio::Calibration.new(
    source: options.fetch(:audio_source),
    file_path: options[:audio_file],
    audio_device: options[:audio_device],
    duration: options.fetch(:duration),
    fps: options.fetch(:fps)
  ).call
  print_calibration_result(result, format: options.fetch(:format))
rescue ArgumentError => e
  raise Thor::Error, e.message
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



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
508
509
510
511
512
513
514
515
516
# File 'lib/vizcore/cli.rb', line 482

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,
    allow_public_control: options.fetch(:allow_public_control)
  )
  validate_snapshot_config!(config)
  warn_untrusted_scene(config.scene_file) unless options.fetch(:trust)

  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),
      wait_for_frame: options.fetch(:wait_for_frame),
      frame_timeout: options.fetch(:frame_timeout)
    )
  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.



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/vizcore/cli.rb', line 183

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],
    scene_switch_effect: options[:scene_switch_effect],
    scene_switch_effect_duration: options[:scene_switch_duration],
    projector_mode: options.fetch(:projector),
    allow_public_control: options.fetch(:allow_public_control)
  )
  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



249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/vizcore/cli.rb', line 249

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



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

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.



380
381
382
# File 'lib/vizcore/cli.rb', line 380

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

#featuresvoid

This method returns an undefined value.

Print optional dependency feature flags for automation and doctor-style checks.

Raises:

  • (Thor::Error)

    when the output format is unsupported



282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/vizcore/cli.rb', line 282

def features
  payload = Vizcore.features
  case options.fetch(:format).to_s
  when "json"
    say(JSON.pretty_generate(payload.transform_keys(&:to_s)))
  when "text"
    payload.each do |name, available|
      say("#{available ? '[ok]' : '[warn]'} #{name}: #{available ? 'available' : 'unavailable'}")
    end
  else
    raise Thor::Error, "unsupported features format: #{options.fetch(:format)}"
  end
end

This method returns an undefined value.

Start a browser gallery for bundled example scenes.



211
212
213
214
215
216
# File 'lib/vizcore/cli.rb', line 211

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



331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
# File 'lib/vizcore/cli.rb', line 331

def inspect_scene(scene_file)
  format = options.fetch(:format).to_s
  diagnostics = Vizcore::CLISupport::SceneDiagnostics.new(scene_file: scene_file)
  result = diagnostics.validate
  if format == "json"
    payload = {
      issues: result.issues.map(&:to_h),
      definition: result.definition ? Vizcore::CLISupport::SceneInspector.new(definition: result.definition).to_h : nil
    }
    say(JSON.pretty_generate(payload))
    raise Thor::Error, "scene inspection failed" unless result.definition
    return
  end
  raise Thor::Error, "unsupported inspect format: #{format}" unless format == "text"

  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.



371
372
373
# File 'lib/vizcore/cli.rb', line 371

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



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/vizcore/cli.rb', line 227

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



420
421
422
423
424
425
426
427
428
429
430
431
# File 'lib/vizcore/cli.rb', line 420

def plugin(command = nil, name = nil)
  case command.to_s
  when "new"
    create_plugin_scaffold(name)
  when "check"
    check_plugin_scaffold(name)
  else
    raise Thor::Error, "Unknown plugin command: #{command || '(nil)'}. Use `vizcore plugin new NAME` or `vizcore plugin check PATH`."
  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



649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
# File 'lib/vizcore/cli.rb', line 649

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),
    cache_root: feature_record_cache_root
  ).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



590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
# File 'lib/vizcore/cli.rb', line 590

def render(scene_file)
  feature_file = resolve_render_feature_cache if feature_cache_enabled?(scene_file: 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),
    feature_file: feature_file
  )
  validate_snapshot_config!(config)
  warn_untrusted_scene(config.scene_file) unless options.fetch(:trust)

  result = Vizcore::Renderer::RenderSequence.new(
    config: config,
    frames: options.fetch(:frames),
    fps: options.fetch(:fps),
    width: options.fetch(:width),
    height: options.fetch(:height),
    duration: options[:duration],
    from_frame: options.fetch(:from_frame),
    to_frame: options[:to_frame],
    resume: options.fetch(:resume),
    seed: options[:seed],
    transparent: options.fetch(:transparent),
    video_codec: options[:codec],
    video_bitrate: options[:bitrate],
    video_crf: options[:crf],
    pixel_format: options[:pix_fmt],
    progress_reporter: render_progress_reporter
  ).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



401
402
403
404
405
406
407
408
409
410
# File 'lib/vizcore/cli.rb', line 401

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.



389
390
391
# File 'lib/vizcore/cli.rb', line 389

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



535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
# File 'lib/vizcore/cli.rb', line 535

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)
  warn_untrusted_scene(config.scene_file) unless options.fetch(:trust)

  result = Vizcore::Renderer::Snapshot.new(
    config: config,
    width: options.fetch(:width),
    height: options.fetch(:height),
    transparent: options.fetch(:transparent)
  ).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



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/vizcore/cli.rb', line 137

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],
    scene_switch_effect: options[:scene_switch_effect] || defaults[:scene_switch_effect],
    scene_switch_effect_duration: options[:scene_switch_duration] || defaults[:scene_switch_effect_duration],
    reload: options.fetch(:reload),
    projector_mode: options.fetch(:projector),
    allow_public_control: options.fetch(:allow_public_control)
  )
  warn_untrusted_scene(config.scene_file, project_root: manifest&.root || Dir.pwd) unless options.fetch(:trust)
  Server::Runner.new(config, manifest: manifest, initial_profile: profile).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



359
360
361
362
363
364
365
# File 'lib/vizcore/cli.rb', line 359

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

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