Skip to content

Commit

Permalink
Add automatic presentation refresh when terminal screen size changes
Browse files Browse the repository at this point in the history
  • Loading branch information
piotrmurach committed Jul 8, 2023
1 parent c090ea1 commit c1b7dda
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 32 deletions.
43 changes: 41 additions & 2 deletions lib/slideck/presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ module Slideck
#
# @api private
class Presenter
# Terminal screen size change signal
#
# @return [String]
#
# @api private
TERM_SCREEN_SIZE_CHANGE_SIG = "WINCH"
private_constant :TERM_SCREEN_SIZE_CHANGE_SIG

# Create a Presenter
#
# @param [TTY::Reader] reader
Expand All @@ -13,16 +21,19 @@ class Presenter
# the slides renderer
# @param [Slideck::Tracker] tracker
# the tracker for slides
# @param [TTY::Screen] screen
# the terminal screen size
# @param [IO] output
# the output stream for the slides
# @param [Proc] reloader
# the metadata and slides reloader
#
# @api public
def initialize(reader, renderer, tracker, output, &reloader)
def initialize(reader, renderer, tracker, screen, output, &reloader)
@reader = reader
@renderer = renderer
@tracker = tracker
@screen = screen
@output = output
@reloader = reloader
@stop = false
Expand Down Expand Up @@ -55,6 +66,7 @@ def start
reload
@reader.subscribe(self)
hide_cursor
subscribe_to_screen_resize { resize.render }

until @stop
render
Expand All @@ -79,9 +91,12 @@ def stop

# Render presentation on cleared screen
#
# @example
# presenter.render
#
# @return [void]
#
# @api private
# @api public
def render
clear_screen
render_slide
Expand Down Expand Up @@ -127,6 +142,30 @@ def show_cursor
@output.print @renderer.cursor.show
end

# Subscribe to the terminal screen size change signal
#
# @param [Proc] resizer
# the presentation resizer
#
# @return [void]
#
# @api private
def subscribe_to_screen_resize(&resizer)
return if @screen.windows?

Signal.trap(TERM_SCREEN_SIZE_CHANGE_SIG, &resizer)
end

# Resize presentation
#
# @return [Slideck::Presenter]
#
# @api private
def resize
@renderer = @renderer.resize(@screen.width, @screen.height)
self
end

# Handle a keypress event
#
# @param [TTY::Reader::KeyEvent] event
Expand Down
17 changes: 17 additions & 0 deletions lib/slideck/renderer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,23 @@ def initialize(converter, ansi, cursor, width: nil, height: nil)
freeze
end

# Create a Renderer with a new screen size
#
# @example
# renderer.resize(200, 50)
#
# @param [Integer] width
# the screen width
# @param [Integer] height
# the screen height
#
# @return [Slideck::Renderer]
#
# @api public
def resize(width, height)
self.class.new(@converter, @ansi, @cursor, width: width, height: height)
end

# Render a slide
#
# @example
Expand Down
2 changes: 1 addition & 1 deletion lib/slideck/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ def build_presenter(color, &reloader)
renderer = Renderer.new(converter, Strings::ANSI, TTY::Cursor,
width: @screen.width, height: @screen.height)
tracker = Tracker.for(0)
Presenter.new(reader, renderer, tracker, @output, &reloader)
Presenter.new(reader, renderer, tracker, @screen, @output, &reloader)
end

# Build a listener for changes in a filename
Expand Down
4 changes: 3 additions & 1 deletion spec/unit/cli_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
let(:output) { StringIO.new("".dup, "w+") }
let(:error_output) { StringIO.new("".dup, "w+") }
let(:env) { {"TTY_TEST" => true} }
let(:screen) { class_double(TTY::Screen, width: 40, height: 20) }
let(:windows?) { RSpec::Support::OS.windows? }
let(:screen_methods) { {width: 40, height: 20, windows?: windows?} }
let(:screen) { class_double(TTY::Screen, **screen_methods) }
let(:runner) { Slideck::Runner.new(screen, input, output, env) }

it "starts with a slides file and --color=always option and quits" do
Expand Down
100 changes: 79 additions & 21 deletions spec/unit/presenter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
let(:meta_defaults) { Slideck::MetadataDefaults.new(alignment, margin) }
let(:metadata) { Slideck::Metadata.from(meta_converter, {}, meta_defaults) }
let(:slide_metadata) { Slideck::Metadata.from(meta_converter, {}, {}) }
let(:windows?) { RSpec::Support::OS.windows? }
let(:screen_methods) { {width: 20, height: 8, windows?: windows?} }
let(:screen) { class_double(TTY::Screen, **screen_methods) }

def build_metadata(custom_metadata)
Slideck::Metadata.from(meta_converter, custom_metadata, meta_defaults)
Expand All @@ -26,11 +29,12 @@ def build_metadata(custom_metadata)
{content: "slide#{i + 1}", metadata: slide_metadata}
end
tracker = Slideck::Tracker.for(slides.size)
renderer = Slideck::Renderer.new(converter, ansi, cursor,
width: 20, height: 8)
renderer = Slideck::Renderer.new(
converter, ansi, cursor, width: screen.width, height: screen.height)
reloaded_metadata = build_metadata({theme: {strong: :cyan}})
reloaded_slides = [{content: "**Reloaded**", metadata: slide_metadata}]
presenter = described_class.new(reader, renderer, tracker, output) do
presenter = described_class.new(
reader, renderer, tracker, screen, output) do
[reloaded_metadata, reloaded_slides]
end

Expand All @@ -50,9 +54,10 @@ def build_metadata(custom_metadata)
{content: "slide2", metadata: slide_metadata},
{content: "slide3", metadata: slide_metadata}]
tracker = Slideck::Tracker.for(slides.size)
renderer = Slideck::Renderer.new(converter, ansi, cursor,
width: 20, height: 8)
presenter = described_class.new(reader, renderer, tracker, output) do
renderer = Slideck::Renderer.new(
converter, ansi, cursor, width: screen.width, height: screen.height)
presenter = described_class.new(
reader, renderer, tracker, screen, output) do
[metadata, slides]
end
input << "q"
Expand All @@ -73,9 +78,10 @@ def build_metadata(custom_metadata)
{content: "slide2", metadata: slide_metadata},
{content: "slide3", metadata: slide_metadata}]
tracker = Slideck::Tracker.for(slides.size)
renderer = Slideck::Renderer.new(converter, ansi, cursor,
width: 20, height: 8)
presenter = described_class.new(reader, renderer, tracker, output) do
renderer = Slideck::Renderer.new(
converter, ansi, cursor, width: screen.width, height: screen.height)
presenter = described_class.new(
reader, renderer, tracker, screen, output) do
[metadata, slides]
end
input << "n" << "l" << "p" << "h" << ?\C-x
Expand Down Expand Up @@ -108,9 +114,10 @@ def build_metadata(custom_metadata)
{content: "slide2", metadata: slide_metadata},
{content: "slide3", metadata: slide_metadata}]
tracker = Slideck::Tracker.for(slides.size)
renderer = Slideck::Renderer.new(converter, ansi, cursor,
width: 20, height: 8)
presenter = described_class.new(reader, renderer, tracker, output) do
renderer = Slideck::Renderer.new(
converter, ansi, cursor, width: screen.width, height: screen.height)
presenter = described_class.new(
reader, renderer, tracker, screen, output) do
[metadata, slides]
end
reader.on(:keypress) do |event|
Expand Down Expand Up @@ -156,9 +163,10 @@ def build_metadata(custom_metadata)
{content: "slide2", metadata: slide_metadata},
{content: "slide3", metadata: slide_metadata}]
tracker = Slideck::Tracker.for(slides.size)
renderer = Slideck::Renderer.new(converter, ansi, cursor,
width: 20, height: 8)
presenter = described_class.new(reader, renderer, tracker, output) do
renderer = Slideck::Renderer.new(
converter, ansi, cursor, width: screen.width, height: screen.height)
presenter = described_class.new(
reader, renderer, tracker, screen, output) do
[metadata, slides]
end
input << "$" << "^" << "\e"
Expand All @@ -185,9 +193,10 @@ def build_metadata(custom_metadata)
{content: "slide#{i + 1}", metadata: slide_metadata}
end
tracker = Slideck::Tracker.for(slides.size)
renderer = Slideck::Renderer.new(converter, ansi, cursor,
width: 20, height: 8)
presenter = described_class.new(reader, renderer, tracker, output) do
renderer = Slideck::Renderer.new(
converter, ansi, cursor, width: screen.width, height: screen.height)
presenter = described_class.new(
reader, renderer, tracker, screen, output) do
[metadata, slides]
end
input << "1" << "3" << "g" << "q" << ?\C-c
Expand Down Expand Up @@ -216,10 +225,11 @@ def build_metadata(custom_metadata)
slides = [{content: "slide1", metadata: slide_metadata},
{content: "slide2", metadata: slide_metadata}]
tracker = Slideck::Tracker.for(slides.size)
renderer = Slideck::Renderer.new(converter, ansi, cursor,
width: 20, height: 8)
renderer = Slideck::Renderer.new(
converter, ansi, cursor, width: screen.width, height: screen.height)
i = -1
presenter = described_class.new(reader, renderer, tracker, output) do
presenter = described_class.new(
reader, renderer, tracker, screen, output) do
if (i += 1).zero?
[metadata, slides]
else
Expand All @@ -244,5 +254,53 @@ def build_metadata(custom_metadata)
"\e[2J\e[1;1H\e[?25h"
].join.inspect)
end

it "refreshes the slides when the screen size changes",
unless: RSpec::Support::OS.windows? do
slides = Array.new(5) do |i|
{content: "slide#{i + 1}", metadata: slide_metadata}
end
tracker = Slideck::Tracker.for(slides.size)
renderer = Slideck::Renderer.new(
converter, ansi, cursor, width: screen.width, height: screen.height)
allow(Signal).to receive(:trap).with("WINCH").and_yield
presenter = described_class.new(
reader, renderer, tracker, screen, output) do
[metadata, slides]
end
input << "q"
input.rewind

presenter.start

expect(output.string.inspect).to eq([
"\e[?25l\e[2J\e[1;1H",
"\e[1;1Hslide1\n",
"\e[8;16H1 / 5",
"\e[2J\e[1;1H",
"\e[1;1Hslide1\n",
"\e[8;16H1 / 5",
"\e[2J\e[1;1H\e[?25h"
].join.inspect)
end

it "doesn't subscribe to the screen size change signal on Windows" do
slides = []
tracker = Slideck::Tracker.for(slides.size)
screen = class_double(TTY::Screen, width: 20, height: 8, windows?: true)
renderer = Slideck::Renderer.new(
converter, ansi, cursor, width: screen.width, height: screen.height)
allow(Signal).to receive(:trap)
presenter = described_class.new(
reader, renderer, tracker, screen, output) do
[metadata, slides]
end
input << "q"
input.rewind

presenter.start

expect(Signal).not_to have_received(:trap)
end
end
end
16 changes: 16 additions & 0 deletions spec/unit/renderer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,22 @@ def build_slide_metadata(custom_metadata)
Slideck::Metadata.from(meta_converter, custom_metadata, {})
end

describe "#resize" do
it "creates an instance with a new screen size" do
metadata = build_metadata({})
slide = {content: "content", metadata: build_slide_metadata({})}
renderer = described_class.new(converter, ansi, cursor,
width: 20, height: 8)

new_renderer = renderer.resize(40, 16)

expect(new_renderer.render(metadata, slide, 1, 1).inspect).to eq([
"\e[1;1Hcontent\n",
"\e[16;36H1 / 1"
].join.inspect)
end
end

describe "#render" do
it "renders page number without slide content" do
metadata = build_metadata({})
Expand Down
10 changes: 3 additions & 7 deletions spec/unit/runner_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
let(:output) { StringIO.new("".dup, "w+") }
let(:input) { StringIO.new("".dup, "w+") }
let(:env) { {"TTY_TEST" => true} }
let(:windows?) { RSpec::Support::OS.windows? }
let(:screen_methods) { {width: 40, height: 10, windows?: windows?} }
let(:screen) { class_double(TTY::Screen, **screen_methods) }

it "displays no slides and quits" do
screen = class_double(TTY::Screen, width: 40, height: 10)
runner = described_class.new(screen, input, output, env)
input << "q"
input.rewind
Expand All @@ -21,7 +23,6 @@
end

it "displays slides with color and quits" do
screen = class_double(TTY::Screen, width: 40, height: 10)
runner = described_class.new(screen, input, output, env)
input << "q"
input.rewind
Expand All @@ -43,7 +44,6 @@
end

it "displays slides without color and quits" do
screen = class_double(TTY::Screen, width: 40, height: 10)
runner = described_class.new(screen, input, output, env)
input << "q"
input.rewind
Expand All @@ -65,7 +65,6 @@
end

it "reloads slides from watched file and quits" do
screen = class_double(TTY::Screen, width: 40, height: 10)
listener = instance_spy(Listen::Listener)
slides_file = fixtures_path("empty.md")
allow(Listen).to receive(:to).and_yield([slides_file], [], [])
Expand All @@ -86,7 +85,6 @@
end

it "doesn't reload slides from an unchanged file and quits" do
screen = class_double(TTY::Screen, width: 40, height: 10)
listener = instance_spy(Listen::Listener)
allow(Listen).to receive(:to).and_yield([], [], []).and_return(listener)
runner = described_class.new(screen, input, output, env)
Expand All @@ -103,7 +101,6 @@
end

it "stops the listener before quitting" do
screen = class_double(TTY::Screen, width: 40, height: 10)
listener = instance_spy(Listen::Listener)
allow(Listen).to receive(:to).and_return(listener)
runner = described_class.new(screen, input, output, env)
Expand All @@ -116,7 +113,6 @@
end

it "doesn't watch for changes in file with slides" do
screen = class_double(TTY::Screen, width: 40, height: 10)
allow(Listen).to receive(:to)
runner = described_class.new(screen, input, output, env)
input << "q"
Expand Down

0 comments on commit c1b7dda

Please sign in to comment.