CLI - GIF recording with Ruby and SCROT
Tags: ruby, linux, tutorial, opensource
Published: 2021-03-31 20:14:14

> _Preview GIF created with ScreenScrot:_

![Preview recorded with screenscrot](https://raw.githubusercontent.com/decentralizuj/moriarty/main/docs/moriarty.gif)

_Take a look about **Moriarty** on [GitHub](https://github.com/decentralizuj/moriarty):_
{% github decentralizuj/moriarty no-readme %}  
  
  
>_Open-Source Development need donations, in my case that's link/repo sharing, instead of Bitcoin. Comments, discussions and opinions are highly appreciated (good or bad)._  

---

# Introduction


GIF recording is great to show your friends how the script work. I was looking for an easy way to capture my script output into GIF, but somehow I couldn't find anything that suit my needs. I decided to write few lines of code, and to make it work just like I want.

First, I want to capture screenshots from my terminal, and to have it saved in PNG, so I can use those photos as background or something. I want to start it from terminal, and to record only active window, but with ability to record everything sometimes (or to select with mouse). At the end, I want to enter for how long to capture screenshots, before gif creation. Let's start!  
  
  
# Install SCROT and ImageMagick  
  
  
On ubuntu-like linux:
```bash
sudo apt update -qq
sudo apt install scrot imagemagick -y
```  
Now it's time to write some ruby code to do what we want. Create file called 'screenscrot.rb'.  
  
  
# Create class ScreenScrot
  
  
Create class, define constants and make initializer accept args or use those constants.
  
```ruby
# capture screenshots on linux with ruby and scrot
# save photos as .png
# create gif with imagemagick

class ScreenScrot

  TITLE = 'screenscrot' # name of file and folder to create
  PATH  = Dir.pwd    # define path to root directory
  EXT   = :png  # screenshots extension

  attr_reader :title, :ext, :path, :directory, :c

  def initialize( title = TITLE, path = PATH, ext = EXT )

    @c = 0
    @path, @ext, @title = path.to_s, ext.to_s, title.to_s

    @path     +=  '/' unless @path.end_with?('/')
    @filename  =  @title + '_' + rand(999).to_s
    @directory =  @path + @filename + '/'

    system "mkdir -p #{@directory}" unless Dir.exist?(@directory)
    Dir.chdir(@directory)

  end
end
```  
  
Here, TITLE is constant to use if parameter title is not defined. Path is path to the working directory, and extension is for captured screenshots. Our new object accept all those parameters, or will use this ones if not defined. After title I added random number, so I do not overwrite past screenshots if I want to record it again (so if I use 'recorder', it will become 'recorder_456'. That is saved in variable @filename. We add this filename to path, together with '/' at the end, and we get @directory variable, which is path where we will save our screenshots and gif at the end). If directory not exist, it will be created and entered.
  
# Capture screenshots
  
This method is here to save .png screenshot that we will use later with imagemagick.
  
```ruby
  # record active window
  # use :all to capture everything
  # use :select to capture select rec window
  # >> @screen.capture(:all)

  def capture( win = :active )

    case win.to_sym
      when :all      then scr = ''    # rec everything
      when :select   then scr = '-s'  # select with mouse
      else scr = '-u'                 # active window (default)
    end

    # without name scrot add timestamp, but that is too slow
    # if use .to_i we get 1 screenshot per second only
    # so we use .to_f and then remove '.' from name
    screenshot = @directory + Time.now.to_f.to_s.gsub!('.', '')

    # prepend command for scrot, add counter to the name
    # so we can save all frames without problems
    exec = "scrot #{scr} -q 10 #{screenshot + @c.to_s}.#{@ext}"

    `exec`

    @c += 1    # captured frames counter

  end
```   
  
With this we have option to save screenshots into folder, with chosen quality, extension and part of screen to save. Default is active window, so part of screen that is active when recording start. If you change terminal, do it on same part of screen. Otherwise use :select, so you click with mouse on window you want to record. Last option is :all, that will record entire screen.
  
# Convert into GIF
  
After getting screenshots, we will use ImageMagick's `convert` to create animated GIF.
  
```ruby
  def to_gif( name = nil, opts = {} )

    # if name param is nil or '' use default title
    name  = @filename if name.empty?
    @gif = name.to_s

    # accept options hash or use default values
    opts[:dir]   ||= @directory
    opts[:ext]   ||= @ext
    opts[:delay] ||= 20
    opts[:loop]  ||= 2

    Dir.chdir(opts[:dir])

    # add .gif to the filename, if do not contain 
    @gif += '.gif' unless @gif.end_with? '.gif'

    # construct ImageMagick's convert command from parameters
    exec = "convert -delay #{opts[:delay]} -loop #{opts[:loop]} *.#{opts[:ext]} #{@gif}"

    # create GIF
    `exec`
  end
```  
  
That's it! Method `#to_gif` accept filename and options hash as parameters, or will use default values. Default filename is @title, and delay is pause in microseconds between two frames. I will add some sleep between capturing screenshots because I do not need all those frames for terminal recording. Loop param define how many times to repeat animation. Zero is infinite loop, but all other values will be increased by one (if loop is 1, it's played once. If loop is 2, animation is played and repeated two more times, which is 3 times at all).

GIF created this way is high quality, even when png quality in scrot command is set to 10 (100 is max). This lead to pretty big file, but still it's ok for short videos. This can be fixed with more arguments in convert command.
  
# Run script
  
To run everything, I just made it simple. Accept time to record, gif name and part of screen to record as arguments. First argument is time to record, default is 60. Second is name, default is screenscrot. Third arg is part of screen, default is active (that's why script count 5 seconds, so you can activate window to rec). 
  
```ruby
  # enter name as second argument or use hardcoded TITLE
  puts "Start Recording Screenshots..."
  name = ARGV[1] or ScreenScrot::TITLE
  
  # initialize new object with name
  @screen = ScreenScrot.new name

  # clean terminal and count 5 seconds before start
  system 'clear'

  cnt = 5
  cnt.times do
    puts "Starting in #{cnt -= 1}"
    sleep 1
    system 'clear'
  end

  puts "[Screenshot Recording Started]"  
     
  str_time = Time.now

  # add recording time as first argument, in seconds
  # default is 60
  max_time = ARGV.empty? ? 60 : ARGV[0].to_i
      
  # check args for window recording type
  # if contain --all or --select, use it, else use :active
  display = :active
  display = :select if desplay.include?('--select')
  display = :all    if display.include?('--all')
    
  # save screenshots untill time elapsed
  # add small sleep because we record terminal
  until (Time.now - str_time).to_i >= max_time do
    @screen.capture(display)
    # edit or remove sleep, per use-case
    sleep 0.2
  end

  # notify about end, print time in minutes and number of frames
  print "[Screenshot Recording Stopped] after "
  puts  "#{(Time.now - str_time).to_f.round(2)} seconds"

  # at the end, create GIF animation
  puts "\n [GIF!] - Start creating gif animation:"
  puts "   Name: #{@screen.gif}"
  puts "   Path: #{@screen.directory}\n"

  @screen.to_gif
    
  # notify user about end, print file name and directory
  print "[CREATED!] - #{@screen.c.to_s} frame"
  print "s" unless @screen.c.to_s.end_with?('1')
  puts  "collected into GIF animation."
  puts
```  
  
With this you can make nice gif animation of terminal, for showcase or to record tests...
  
```bash
ruby screenscrot.rb 60 myfilename --all
```
  
# Full Script
  
{% gist https://gist.github.com/decentralizuj/8c2e9a49a96f227d9227d1839774ef82 %}