Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/dry/cli/ansi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ def self.carriage_return = CR
EL2 = "\e[2K"
def self.erase_line = EL2

# Erase from the cursor to the end of the line
# @api public
# @since x.y.z
EL0 = "\e[K"
def self.erase_to_end_of_line = EL0

# Reset all styles
# @api public
# @since x.y.z
Expand Down
6 changes: 5 additions & 1 deletion lib/dry/cli/spinner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,18 @@ def run(format = "%{spinner}", fps: self.fps, stream: $stderr, &)
frames = self.frames.cycle

ticker = Ticker.start(interval: 1.0 / fps) do
stream.raw.print ANSI.clear_line
# Draw over the previous frame, then erase what is left of it. Erasing the whole line
# first would show a blank line in terminals that redraw between writes.
stream.raw.print ANSI.carriage_return

data = {spinner: frames.next}
before_tick.call(stream, data)

stream.print format % data

after_tick.call(stream)

stream.raw.print ANSI.erase_to_end_of_line
end
end

Expand Down
5 changes: 3 additions & 2 deletions lib/dry/cli/spinner/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ def handlers
# @since x.y.z
def run(&block) = @events[:run] = block

# Executed before each tick of the spinner. The line has been erased, so the current state
# of the stream will be a blank slate.
# Executed before each tick of the spinner. The cursor is at the start of the line. The line
# still shows the previous frame, but anything you write draws over it, and any old text left
# to the right is erased after {#after_tick} runs.
#
# This runs in a separate thread, so do not `throw :exit` or raise {Dry::CLI::ExitError} from
# it. Do that from the {#run} block instead.
Expand Down
7 changes: 7 additions & 0 deletions spec/unit/dry/cli/ansi_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@
end
end

describe ".erase_to_end_of_line" do
it "returns the ANSI escape sequence to erase from the cursor to the end of the line" do
expect(described_class.erase_to_end_of_line).to eq("\e[K")
end
end

describe ".enter_alt_screen" do
it "returns the ANSI escape sequence to switch to the alternate screen buffer" do
expect(described_class.enter_alt_screen).to eq("\e[?1049h")
Expand Down Expand Up @@ -143,6 +149,7 @@
expect(described_class::CNORM).to eq("\e[?25h")
expect(described_class::CR).to eq("\r")
expect(described_class::EL2).to eq("\e[2K")
expect(described_class::EL0).to eq("\e[K")
expect(described_class::SGR0).to eq("\e[0m")
expect(described_class::SMCUP).to eq("\e[?1049h")
expect(described_class::RMCUP).to eq("\e[?1049l")
Expand Down
27 changes: 24 additions & 3 deletions spec/unit/dry/cli/spinner_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def render_ticks(count = 1)
end
render_ticks

expect(terminal.string).to include "#{Dry::CLI::ANSI.clear_line}<frame>"
expect(terminal.string).to include "\r<frame>\e[K"
end

it "does not need the DSL to register a worker" do
Expand All @@ -175,8 +175,29 @@ def render_ticks(count = 1)
it "cycles through the frames on each tick" do
spinner.run(stream: terminal) { render_ticks(2) }

expect(terminal.string).to include "#{Dry::CLI::ANSI.clear_line}|"
expect(terminal.string).to include "#{Dry::CLI::ANSI.clear_line}/"
expect(terminal.string).to include "\r|\e[K"
expect(terminal.string).to include "\r/\e[K"
end

it "draws over the previous frame and then erases to the end of the line" do
spinner.run("%{spinner} Loading", stream: terminal) { render_ticks(2) }

expect(terminal.string).to eq(
Dry::CLI::ANSI.hide_cursor +
"\r| Loading\e[K" \
"\r/ Loading\e[K" +
Dry::CLI::ANSI.clear_line +
Dry::CLI::ANSI.show_cursor
)
end

it "does not erase the whole line before drawing a frame" do
spinner.run(stream: terminal) { render_ticks(2) }

ticks = terminal.string.delete_prefix(Dry::CLI::ANSI.hide_cursor)
.delete_suffix(Dry::CLI::ANSI.clear_line + Dry::CLI::ANSI.show_cursor)

expect(ticks).to_not include Dry::CLI::ANSI.erase_line
end
end

Expand Down
Loading