Module: Przn

Defined in:
lib/przn.rb,
lib/przn/slide.rb,
lib/przn/theme.rb,
lib/przn/parser.rb,
lib/przn/version.rb,
lib/przn/renderer.rb,
lib/przn/terminal.rb,
lib/przn/controller.rb,
lib/przn/image_util.rb,
lib/przn/kitty_text.rb,
lib/przn/presentation.rb,
lib/przn/audience_link.rb,
lib/przn/echoes_client.rb,
lib/przn/prawn_pdf_exporter.rb,
lib/przn/presenter_renderer.rb,
lib/przn/screenshot_pdf_exporter.rb

Defined Under Namespace

Modules: AudienceLink, EchoesClient, ImageUtil, KittyText, Parser Classes: Controller, Error, PrawnPdfExporter, Presentation, PresenterRenderer, Renderer, ScreenshotPdfExporter, Slide, Terminal, Theme

Constant Summary collapse

VERSION =
'0.4.0'

Class Method Summary collapse

Class Method Details

.audience(file, socket:, theme: nil) ⇒ Object

Audience-side entry: opens the file, listens on ‘socket`, and renders whatever slide the presenter sends a `goto` for. Notes are stripped. Spawned by Echoes when the presenter requests an extended-display window.



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/przn.rb', line 37

def self.audience(file, socket:, theme: nil)
  markdown = File.read(file)
  presentation = Parser.parse(markdown)
  terminal = Terminal.new
  base_dir = File.dirname(File.expand_path(file))
  renderer = Renderer.new(terminal, base_dir: base_dir, theme: theme, mode: :audience)

  terminal.enter_alt_screen
  terminal.hide_cursor
  begin
    render = ->(idx, started_at) {
      presentation.go_to(idx)
      renderer.render(presentation.current_slide,
                      current: presentation.current,
                      total: presentation.total,
                      started_at: started_at)
    }
    render.call(0, nil)
    AudienceLink.serve(socket) do |msg|
      next unless msg[:type] == "goto" && msg[:index].is_a?(Integer)
      started_at = msg[:started_at] ? Time.at(msg[:started_at]) : nil
      render.call(msg[:index], started_at)
    end
  ensure
    terminal.write "\e]7772;bg-clear\a"
    terminal.show_cursor
    terminal.leave_alt_screen
  end
end

.export_pdf(file, output, theme: nil) ⇒ Object

Default PDF export: drives the live renderer, asks the terminal to save each rendered slide as a one-page vector PDF via OSC 7772 ‘capture`, then concatenates the per-slide PDFs into a single multi-page PDF. Requires Echoes (or any terminal that implements the same capture command); use `export_pdf_prawn` instead for environments where that’s not possible (CI, headless).



13
14
15
16
17
18
19
# File 'lib/przn/screenshot_pdf_exporter.rb', line 13

def self.export_pdf(file, output, theme: nil)
  markdown = File.read(file)
  presentation = Parser.parse(markdown)
  base_dir = File.dirname(File.expand_path(file))
  ScreenshotPdfExporter.new(presentation, base_dir: base_dir, theme: theme).export(output)
  puts "Generated: #{output}"
end

.export_pdf_prawn(file, output, theme: nil) ⇒ Object

Legacy PDF export via Prawn — renders the deck directly into a vector PDF without touching the terminal. Diverges from what’s on screen for any feature the live renderer adds (OSC 66 sized text, OSC 7772 backgrounds, proportional fonts) but works headlessly.



39
40
41
42
43
44
45
# File 'lib/przn/prawn_pdf_exporter.rb', line 39

def self.export_pdf_prawn(file, output, theme: nil)
  markdown = File.read(file)
  presentation = Parser.parse(markdown)
  base_dir = File.dirname(File.expand_path(file))
  PrawnPdfExporter.new(presentation, base_dir: base_dir, theme: theme).export(output)
  puts "Generated: #{output}"
end

.present(file, theme: nil, theme_path: nil) ⇒ Object

Presenter-side entry: detects a second display via Echoes, spawns the audience window on it, connects to the spawned process over a Unix socket, and returns a Controller wired up to drive both sides. Falls back to today’s mirror-mode (‘start`) when only one display is attached or Echoes is not the host terminal.



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/przn.rb', line 72

def self.present(file, theme: nil, theme_path: nil)
  info = EchoesClient.display_info
  if info.nil? || info.size < 2
    warn "przn: extended-display unavailable (no secondary display detected), falling back to mirror mode"
    return start(file, theme: theme)
  end

  socket_path = File.join(Dir.tmpdir, "przn-#{Process.pid}-#{SecureRandom.hex(4)}.sock")
  audience_argv = [File.expand_path($PROGRAM_NAME), '--audience', '--socket', socket_path]
  audience_argv += ['--theme', theme_path] if theme_path
  audience_argv << File.expand_path(file)
  EchoesClient.open_window(display: info.last[:index], argv: audience_argv)

  deadline = Time.now + 5
  sleep 0.1 until File.exist?(socket_path) || Time.now > deadline
  unless File.exist?(socket_path)
    warn "przn: audience window did not come up within 5s, falling back to mirror mode"
    return start(file, theme: theme)
  end

  link = AudienceLink.connect(socket_path)
  link.gets # discard the {"type":"ready"} handshake

  markdown = File.read(file)
  presentation = Parser.parse(markdown)
  terminal = Terminal.new
  base_dir = File.dirname(File.expand_path(file))
  renderer = PresenterRenderer.new(terminal, presentation: presentation, base_dir: base_dir, theme: theme)
  Controller.new(presentation, terminal, renderer, audience_link: link)
end

.start(file, theme: nil, start_at: nil) ⇒ Object



24
25
26
27
28
29
30
31
32
# File 'lib/przn.rb', line 24

def self.start(file, theme: nil, start_at: nil)
  markdown = File.read(file)
  presentation = Parser.parse(markdown)
  presentation.go_to(start_at - 1) if start_at
  terminal = Terminal.new
  base_dir = File.dirname(File.expand_path(file))
  renderer = Renderer.new(terminal, base_dir: base_dir, theme: theme)
  Controller.new(presentation, terminal, renderer)
end