Skip to content

Instantly share code, notes, and snippets.

@ttscoff
Last active July 11, 2025 21:43
Show Gist options
  • Save ttscoff/efe9c1284745c4df956457a5707e7450 to your computer and use it in GitHub Desktop.
Save ttscoff/efe9c1284745c4df956457a5707e7450 to your computer and use it in GitHub Desktop.
A command line tool (and library) for creating a ripple effect on text, for progress animation
#!/usr/bin/env ruby
# frozen_string_literal: true
# ripple - A simple Ruby script to create a text ripple effect in the terminal.
## From the command line, you can run:
# $ ruby ripple "Your Text Here" --speed fast --rainbow --direction bidirectional
## Run a command during the animation:
# $ ruby ripple "Your Text Here" --speed fast --rainbow --direction bidirectional --command "sleep 5"
# This will animate the text "Your Text Here" with the specified options, and run the command in the background.
# Can be loaded as a library
# require 'ripple'
# rippler = Ripple.new(ARGV.join(" "), { speed: options[:speed], format: options[:direction], rainbow: options[:rainbow] })
# rippler.hide_cursor
# while true do
# rippler.advance
# end
# rippler.show_cursor
## OR
# Ripple.progress("IN PROGRESS", :fast) { sleep 5 }
COLORS = {
"red" => "\e[31m",
"green" => "\e[32m",
"yellow" => "\e[33m",
"blue" => "\e[34m",
"magenta" => "\e[35m",
"cyan" => "\e[36m",
"white" => "\e[37m",
"dark_red" => "\e[31;1m",
"dark_green" => "\e[32;1m",
"dark_yellow" => "\e[33;1m",
"dark_blue" => "\e[34;1m",
"dark_magenta" => "\e[35;1m",
"dark_cyan" => "\e[36;1m",
"dark_white" => "\e[37;1m",
"light_red" => "\e[31;2m",
"light_green" => "\e[32;2m",
"light_yellow" => "\e[33;2m",
"light_blue" => "\e[34;2m",
"light_magenta" => "\e[35;2m",
"light_cyan" => "\e[36;2m",
"light_white" => "\e[37;2m",
"reset" => "\e[0m",
}.freeze
class ::String
COLORS.each do |color_name, color_code|
define_method(color_name) do
"#{color_code}#{self}#{COLORS['reset']}"
end
end
def rainbow(index = 0)
chars = split('')
colored_chars = chars.map.with_index do |char, idx|
color = COLORS.values[(idx + index) % COLORS.size]
"#{color}#{char}#{COLORS['reset']}"
end
colored_chars.join
end
end
class Ripple
attr_accessor :index, :string, :speed, :format, :inverse, :rainbow
def initialize(string, options = {})
defaults = {
speed: :medium,
format: :bidirectional,
rainbow: false,
inverse: false,
output: :error
}
@options = defaults.merge(options)
@string = string
@index = 0
@direction = :forward
@rainbow = @options[:rainbow]
@inverse = @options[:inverse]
end
def printout
letters = @string.dup.split('')
i = @index
if @inverse
pre = letters.slice!(0,i).join
pre = @rainbow ? pre.rainbow : pre.light_white
char = letters.slice!(0,2).join
char = char.dark_white
post = letters.slice!(0,letters.length).join
post = @rainbow ? post.rainbow : post.light_white
else
pre = letters.slice!(0,i).join.dark_white
char = letters.slice!(0,2).join
char = @rainbow ? char.rainbow(i) : char.light_white
post = letters.slice!(0,letters.length).join.dark_white
end
$stderr.print "\e[2K#{pre}#{char}#{post}\r"
end
# Hide or show the cursor
def self.hide_cursor
$stderr.print "\e[?25l"
end
def self.show_cursor
$stderr.print "\e[?25h"
end
def advance
if @index == @string.length - 1 && @options[:format] != :forward_only
@direction = :backward
elsif @index == @string.length && @options[:format] == :forward_only
@index = 0
elsif @index == 0
@direction = :forward
end
@index = @direction == :backward ? @index - 1 : @index + 1
printout
case @options[:speed]
when :fast
sleep 0.05
when :medium
sleep 0.1
else
sleep 0.2
end
end
def self.progress(string, options = {})
Signal.trap("INT") do
Thread.current.kill
nil
end
defaults = { speed: :medium, format: :bidirectional, rainbow: false, inverse: false, output: :error }
options = defaults.merge(options)
rippler = new(string, options)
Ripple.hide_cursor
begin
thread = Thread.new do
while true
rippler.advance
end
end
result = yield if block_given?
thread.kill
if @options[:output] == :error
$?.exitstatus.zero?
elsif @options[:output] == :stdout
result
else
nil
end
rescue StandardError
thread&.kill
nil
ensure
Ripple.show_cursor
end
end
end
if __FILE__ == $PROGRAM_NAME
require 'optparse'
trap("INT") {
Ripple.show_cursor
exit
}
options = { speed: :medium, direction: :bidirectional, rainbow: false, inverse: false, command: nil }
OptionParser.new do |opts|
opts.banner = "Usage: ripple [options] STRING"
opts.on("-s", "--speed SPEED", [:fast, :medium, :slow], "Set animation speed ((f)ast/(m)edium/(s)low)") do |s|
options[:speed] = case s
when /^f/ then :fast
when /^m/ then :medium
when /^s/ then :slow
else :slow
end
end
opts.on("-r", "--rainbow", "Enable rainbow mode") do
options[:rainbow] = true
end
opts.on("-d", "--direction DIRECTION", [:forward_only, :bidirectional], "Set animation format ((f)orward/(b)ack-and-forth)") do |f|
options[:direction] = f =~ /^f/ ? :forward_only : :bidirectional
end
opts.on("-i", "--inverse", "Enable inverse mode") do
options[:inverse] = true
end
opts.on("-c", "--command COMMAND", "Run a command during the animation") do |command|
options[:command] = command
end
opts.on("--stdout", "Output captured command result to STDOUT") do |output|
options[:output] = :stdout
end
opts.on("--quiet", "Suppress all output") do |quiet|
options[:output] = :quiet
end
opts.on("-v", "--version", "Display the version") do
puts "Ripple version #{Ripple::VERSION || "1.0.0"}"
exit
end
opts.on("-h", "--help", "Display this help message") do
puts opts
exit
end
end.parse!
if ARGV.empty?
puts "Please provide a string to animate as an argument."
exit 1
end
if options[:command]
captured_output = nil
res = Ripple.progress(ARGV.join(" "), options) do
captured_output = `#{options[:command]} 2>&1`
end
res = $?.success?
if options[:output] == :stdout
puts captured_output
end
exit res ? 0 : 1
end
rippler = Ripple.new(ARGV.join(" "), { speed: options[:speed], format: options[:direction], rainbow: options[:rainbow], inverse: options[:inverse] })
Ripple.hide_cursor
while true do
rippler.advance
end
Ripple.show_cursor
end
@AlloriMD
Copy link

AlloriMD commented Jul 2, 2025

Thanks for sharing. It inspired me to port it to Python as a context manager. You can check it out here: https://github.com/AlloriMD/koolkit/blob/main/src/koolkit/progress_bars/ripple_progress_bar.py

Usage is simply:

# This is before the context block.

with RippleProgressBar():
  # Your unit-of-work goes here, inside the context block.
  time.sleep(5)

# And back outside the context block.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment