├── exe └── spielbash ├── lib ├── spielbash │ ├── version.rb │ ├── model │ │ ├── action │ │ │ ├── base_action.rb │ │ │ ├── pause_action.rb │ │ │ ├── press_key_action.rb │ │ │ ├── message_context.rb │ │ │ ├── command_action.rb │ │ │ ├── delete_environment_action.rb │ │ │ ├── message_action.rb │ │ │ ├── new_environment_action.rb │ │ │ └── action_context.rb │ │ ├── context.rb │ │ ├── movie.rb │ │ └── session.rb │ ├── spielbash.rb │ ├── view │ │ └── cli.rb │ └── interactor │ │ └── record_interactor.rb └── spielbash.rb ├── sample └── sample.gif ├── .travis.yml ├── spec ├── spielbash_spec.rb ├── record_interactor_spec.rb ├── spec_helper.rb └── fixtures │ └── files │ └── scenario_1.yaml ├── bin ├── setup └── console ├── .rspec ├── .gitignore ├── Gemfile ├── .github └── dependabot.yml ├── Rakefile ├── .simplecov ├── LICENSE.txt ├── spielbash.gemspec ├── README.md └── CODE_OF_CONDUCT.md /exe/spielbash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "spielbash" 4 | -------------------------------------------------------------------------------- /lib/spielbash/version.rb: -------------------------------------------------------------------------------- 1 | module Spielbash 2 | VERSION = "0.1.4" 3 | end 4 | -------------------------------------------------------------------------------- /sample/sample.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Malinskiy/spielbash/HEAD/sample/sample.gif -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.4.2 5 | before_install: gem install bundler -v 1.16.1 6 | -------------------------------------------------------------------------------- /spec/spielbash_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Spielbash do 2 | it "has a version number" do 3 | expect(Spielbash::VERSION).not_to be nil 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | --format RspecJunitFormatter --out spec/reports/rspec.xml 5 | --format html --out spec/reports/rspec.html -------------------------------------------------------------------------------- /lib/spielbash.rb: -------------------------------------------------------------------------------- 1 | require 'spielbash/version' 2 | require 'spielbash/spielbash' 3 | require 'spielbash/view/cli' 4 | 5 | module Spielbash 6 | include Spielbash::CLI 7 | end 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /.idea/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | 11 | # rspec failure tracking 12 | .rspec_status 13 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in spielbash.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "19:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | begin 3 | require 'rspec/core/rake_task' 4 | RSpec::Core::RakeTask.new(:spec) 5 | rescue LoadError 6 | end 7 | 8 | task :default => :spec 9 | task :test => :spec -------------------------------------------------------------------------------- /lib/spielbash/model/action/base_action.rb: -------------------------------------------------------------------------------- 1 | module Spielbash 2 | class BaseAction 3 | attr_accessor :action_context 4 | 5 | def initialize(action_context) 6 | @action_context = action_context 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /.simplecov: -------------------------------------------------------------------------------- 1 | require 'simplecov-json' 2 | 3 | SimpleCov.formatters = [ 4 | SimpleCov::Formatter::HTMLFormatter, 5 | SimpleCov::Formatter::JSONFormatter 6 | ] 7 | SimpleCov.minimum_coverage 80 8 | SimpleCov.start do 9 | add_filter '/spec/' 10 | end -------------------------------------------------------------------------------- /lib/spielbash/model/action/pause_action.rb: -------------------------------------------------------------------------------- 1 | module Spielbash 2 | class PauseAction < Spielbash::BaseAction 3 | attr_accessor :length 4 | 5 | def initialize(length, action_context) 6 | super(action_context) 7 | @length = length 8 | end 9 | 10 | def execute(_session) 11 | sleep(length) 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /lib/spielbash/model/action/press_key_action.rb: -------------------------------------------------------------------------------- 1 | module Spielbash 2 | class PressKeyAction < Spielbash::BaseAction 3 | attr_accessor :key 4 | 5 | def initialize(key, action_context) 6 | super(action_context) 7 | @key = key 8 | end 9 | 10 | def execute(session) 11 | session.send_key(key) 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /lib/spielbash/model/action/message_context.rb: -------------------------------------------------------------------------------- 1 | module Spielbash 2 | class MessageContext < Spielbash::ActionContext 3 | attr_accessor :delete 4 | 5 | def initialize(base_context, typing_delay_s, reading_delay_s, wait, width, height, delete) 6 | super(base_context, typing_delay_s, reading_delay_s, wait, width, height) 7 | @delete = delete 8 | end 9 | end 10 | end -------------------------------------------------------------------------------- /spec/record_interactor_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe 'RecordInteractor' do 4 | 5 | before do 6 | File.delete('/tmp/output.json') if File.exists?('/tmp/output.json') 7 | end 8 | 9 | it 'should record movie' do 10 | interactor = Spielbash::RecordInteractor.new 11 | interactor.execute(file_fixture('scenario_1.yaml'), '/tmp/output.json') 12 | end 13 | end -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "spielbash" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /lib/spielbash/model/context.rb: -------------------------------------------------------------------------------- 1 | module Spielbash 2 | class Context 3 | attr_accessor :typing_delay_s, :reading_delay_s, :wait, :width, :height, :wait_check_cmd 4 | 5 | def initialize(typinig_delay_s, reading_delay_s, wait, width, height, wait_check_cmd = nil) 6 | @typing_delay_s = typinig_delay_s 7 | @reading_delay_s = reading_delay_s 8 | @wait = wait 9 | @width = width 10 | @height = height 11 | @wait_check_cmd = wait_check_cmd 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /lib/spielbash/model/action/command_action.rb: -------------------------------------------------------------------------------- 1 | module Spielbash 2 | class CommandAction < Spielbash::BaseAction 3 | attr_accessor :command 4 | 5 | def initialize(command, action_context) 6 | super(action_context) 7 | @command = command 8 | end 9 | 10 | def execute(session) 11 | command.each_char do |c| 12 | session.send_key(c) 13 | sleep(action_context.typing_delay_s) 14 | end 15 | session.send_key('C-m') 16 | 17 | session.wait if action_context.wait 18 | 19 | sleep(action_context.reading_delay_s) 20 | end 21 | end 22 | end -------------------------------------------------------------------------------- /lib/spielbash/model/action/delete_environment_action.rb: -------------------------------------------------------------------------------- 1 | module Spielbash 2 | class DeleteEnvironmentAction < Spielbash::BaseAction 3 | attr_accessor :command 4 | 5 | def initialize(command, root_context) 6 | super(root_context) 7 | @command = command 8 | end 9 | 10 | def execute(session) 11 | command.each_char do |c| 12 | session.send_key(c) 13 | sleep(action_context.typing_delay_s) 14 | end 15 | session.send_key('C-m') 16 | 17 | sleep(action_context.reading_delay_s) 18 | 19 | action_context.wait_check_cmd = nil 20 | end 21 | end 22 | end -------------------------------------------------------------------------------- /lib/spielbash/model/action/message_action.rb: -------------------------------------------------------------------------------- 1 | module Spielbash 2 | class MessageAction < Spielbash::BaseAction 3 | attr_accessor :message 4 | 5 | def initialize(message, action_context) 6 | super(action_context) 7 | @message = message 8 | end 9 | 10 | def execute(session) 11 | message.each_char do |c| 12 | session.send_key(c) 13 | sleep(action_context.typing_delay_s) 14 | end 15 | sleep(action_context.reading_delay_s) 16 | 17 | session.send_key('C-h', message.length) if (action_context.delete) 18 | sleep(action_context.typing_delay_s) 19 | end 20 | end 21 | end -------------------------------------------------------------------------------- /lib/spielbash/spielbash.rb: -------------------------------------------------------------------------------- 1 | require 'spielbash/interactor/record_interactor' 2 | require 'spielbash/model/movie' 3 | require 'spielbash/model/context' 4 | require 'spielbash/model/session' 5 | require 'spielbash/model/action/base_action' 6 | require 'spielbash/model/action/message_action' 7 | require 'spielbash/model/action/press_key_action' 8 | require 'spielbash/model/action/command_action' 9 | require 'spielbash/model/action/pause_action' 10 | require 'spielbash/model/action/action_context' 11 | require 'spielbash/model/action/message_context' 12 | require 'spielbash/model/action/new_environment_action' 13 | require 'spielbash/model/action/delete_environment_action' 14 | -------------------------------------------------------------------------------- /lib/spielbash/model/action/new_environment_action.rb: -------------------------------------------------------------------------------- 1 | module Spielbash 2 | class NewEnvironmentAction < Spielbash::BaseAction 3 | attr_accessor :command, :wait_check_cmd 4 | 5 | def initialize(command, wait_check_cmd, root_context) 6 | super(root_context) 7 | @command = command 8 | @wait_check_cmd = wait_check_cmd 9 | end 10 | 11 | def execute(session) 12 | command.each_char do |c| 13 | session.send_key(c) 14 | sleep(action_context.typing_delay_s) 15 | end 16 | session.send_key('C-m') 17 | 18 | sleep(action_context.reading_delay_s) 19 | 20 | action_context.wait_check_cmd = wait_check_cmd 21 | end 22 | end 23 | end -------------------------------------------------------------------------------- /lib/spielbash/model/action/action_context.rb: -------------------------------------------------------------------------------- 1 | module Spielbash 2 | class ActionContext < Spielbash::Context 3 | attr_accessor :base_context 4 | 5 | def initialize(base_context, typing_delay_s, reading_delay_s, wait, width, height) 6 | super(typing_delay_s, reading_delay_s, wait, width, height) 7 | @base_context = base_context 8 | end 9 | 10 | def typing_delay_s 11 | return @typing_delay_s.nil? ? base_context.typing_delay_s : @typing_delay_s 12 | end 13 | 14 | def reading_delay_s 15 | return @reading_delay_s.nil? ? base_context.reading_delay_s : @reading_delay_s 16 | end 17 | 18 | def wait 19 | return @wait.nil? ? base_context.wait : @wait 20 | end 21 | end 22 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | require 'bundler/setup' 3 | require 'spielbash/spielbash' 4 | 5 | module FileFixtures 6 | def file_fixture(fixture_name) 7 | path = Pathname.new(File.join('spec/fixtures/files', fixture_name)) 8 | 9 | if path.exist? 10 | path 11 | else 12 | msg = "the directory '%s' does not contain a file named '%s'" 13 | raise ArgumentError, msg % ['spec/fixtures/files', fixture_name] 14 | end 15 | end 16 | end 17 | 18 | RSpec.configure do |config| 19 | # Enable flags like --only-failures and --next-failure 20 | config.example_status_persistence_file_path = '.rspec_status' 21 | 22 | # Disable RSpec exposing methods globally on `Module` and `main` 23 | config.disable_monkey_patching! 24 | 25 | config.expect_with :rspec do |c| 26 | c.syntax = :expect 27 | end 28 | 29 | config.include(FileFixtures) 30 | end -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Anton Malinskiy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/spielbash/model/movie.rb: -------------------------------------------------------------------------------- 1 | module Spielbash 2 | class Movie 3 | attr_accessor :title, :pre_run_actions, :actions, :post_run_actions, :context, :output_path, :session 4 | 5 | def initialize(title, pre_run_actions, actions, post_run_actions, context, output_path) 6 | @title = title 7 | @pre_run_actions = pre_run_actions 8 | @actions = actions 9 | @post_run_actions = post_run_actions 10 | @context = context 11 | @output_path = output_path 12 | end 13 | 14 | def shoot 15 | session = Spielbash::Session.new(title.downcase.split.join('_'), output_path, context) 16 | session.new_session 17 | 18 | pre_run_actions.each do |action| 19 | action.execute(session) 20 | end 21 | 22 | session.start_recording 23 | 24 | actions.each do |action| 25 | action.execute(session) 26 | end 27 | 28 | session.stop_recording 29 | 30 | post_run_actions.each do |action| 31 | action.execute(session) 32 | end 33 | 34 | session.close_session 35 | end 36 | 37 | def interrupt 38 | Spielbash::PressKeyAction.new('C-c', context).execute(session) unless session.nil? 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/fixtures/files/scenario_1.yaml: -------------------------------------------------------------------------------- 1 | title: a beautiful movie 2 | options: 3 | width: 80 4 | height: 24 5 | wait: true 6 | typing_delay_s: 0.1 7 | reading_delay_s: 1 8 | pre-run: 9 | - pause: 1 10 | - new_env: 'docker run -it --rm --name ubuntu ubuntu bash' 11 | wait_check_cmd: 'docker exec ubuntu pgrep -P 1' 12 | - command: clear 13 | - pause: 1 14 | post-run: 15 | - delete_env: 'exit' 16 | - pause: 1 17 | scenes: 18 | - pause: 1 19 | - command: ls 20 | - message: 'Here is a list of files. Nice!' 21 | - pause: 3 22 | - command: sleep 5 23 | - message: 'Great Scott! We can wait for the end of command execution!' 24 | - command: sleep 7 25 | options: 26 | wait: false 27 | - message: 'Or not :) then you need to manually create pauses' 28 | options: 29 | delete: true 30 | reading_delay_s: 2 31 | - command: 'apt-get update' 32 | - command: 'apt-get -yq install vim' 33 | - pause: 5 34 | - command: vim 35 | options: 36 | wait: false 37 | - key: 'i' 38 | - message: 'Message action works inside apps too!' 39 | options: 40 | delete: false 41 | - key: 'Escape' 42 | - command: \:q! 43 | - message: 'You can override global options per command. For example typing delay' 44 | options: 45 | typing_delay_s: 0.03 46 | reading_delay_s: 2 47 | - message: 'I hope you enjoyed :)' 48 | options: 49 | reading_delay_s: 2 -------------------------------------------------------------------------------- /spielbash.gemspec: -------------------------------------------------------------------------------- 1 | 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "spielbash/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "spielbash" 8 | spec.version = Spielbash::VERSION 9 | spec.authors = ["Anton Malinskiy"] 10 | spec.email = ["anton@malinskiy.com"] 11 | 12 | spec.summary = %q{Spielbash helps you to automate asciicasts with asciinema.} 13 | spec.description = %q{Tool to automate bash movie-making with asciinema. Be the Spielberg of bash.} 14 | spec.homepage = "https://github.com/Malinskiy/spielbash" 15 | spec.license = "MIT" 16 | 17 | 18 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 19 | f.match(%r{^(test|coverage|spec|features)/}) 20 | end 21 | spec.bindir = "exe" 22 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 23 | spec.require_paths = ["lib"] 24 | 25 | spec.add_runtime_dependency 'gli', '~> 2.17' 26 | spec.add_runtime_dependency 'childprocess', '~> 0.9' 27 | 28 | spec.add_development_dependency 'bundler', '~> 1.16' 29 | spec.add_development_dependency 'rake', '~> 10.0' 30 | spec.add_development_dependency 'rspec', '~> 3.5' 31 | spec.add_development_dependency 'rspec_junit_formatter', '~> 0.2' 32 | spec.add_development_dependency 'simplecov', '~> 0.14' 33 | spec.add_development_dependency 'simplecov-json', '~> 0.2' 34 | end 35 | -------------------------------------------------------------------------------- /lib/spielbash/view/cli.rb: -------------------------------------------------------------------------------- 1 | module Spielbash 2 | module CLI 3 | require 'gli' 4 | 5 | include GLI::App 6 | extend self 7 | 8 | program_desc 'Tool to automate bash movie-making with asciinema. Be the Spielberg of bash' 9 | 10 | desc 'Be verbose' 11 | switch [:v, :verbose] 12 | 13 | # Cross-platform way of finding an executable in the $PATH. 14 | # 15 | # which('ruby') #=> /usr/bin/ruby 16 | def which(cmd) 17 | exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : [''] 18 | ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| 19 | exts.each {|ext| 20 | exe = File.join(path, "#{cmd}#{ext}") 21 | return true if File.executable?(exe) && !File.directory?(exe) 22 | } 23 | end 24 | false 25 | end 26 | 27 | desc 'Create a recording' 28 | command :record do |c| 29 | c.desc 'Script file path' 30 | c.flag [:script] 31 | c.desc 'Output file' 32 | c.flag [:o, :output] 33 | 34 | c.action do |_, options, _| 35 | script_path = options[:script] 36 | output_path = options[:output] 37 | 38 | help_now!('pgrep is not installed!') unless which('pgrep') 39 | help_now!('docker is not installed!') unless which('docker') 40 | help_now!('tmux is not installed!') unless which('tmux') 41 | help_now!('resize is not installed!') unless which('resize') 42 | help_now!('asciinema is not installed!') unless which('asciinema') 43 | 44 | Spielbash::RecordInteractor.new().execute(script_path, output_path) 45 | end 46 | end 47 | 48 | exit run(ARGV) 49 | end 50 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gem](https://img.shields.io/gem/v/spielbash.svg)](https://rubygems.org/gems/spielbash) 2 | [![Gem](https://img.shields.io/gem/dt/spielbash.svg)](https://rubygems.org/gems/spielbash) 3 | 4 | # Spielbash 5 | 6 | Spielbash helps you to automate asciicasts with asciinema. 7 | 8 | ![Sample output](sample/sample.gif?raw=true "Sample") 9 | 10 | Requirements 11 | ------------ 12 | 13 | * asciinema > 1.0 14 | * tmux 15 | * resize 16 | * pgrep 17 | 18 | ## Installation 19 | 20 | Add this line to your application's Gemfile: 21 | 22 | ```ruby 23 | gem 'spielbash' 24 | ``` 25 | 26 | And then execute: 27 | 28 | $ bundle 29 | 30 | Or install it yourself as: 31 | 32 | $ gem install spielbash 33 | 34 | ## Usage 35 | 36 | ```console 37 | $ spielbash -h 38 | NAME 39 | spielbash - Tool to automate bash movie-making with asciinema. Be the Spielberg of bash 40 | 41 | SYNOPSIS 42 | spielbash [global options] command [command options] [arguments...] 43 | 44 | GLOBAL OPTIONS 45 | --help - Show this message 46 | -v, --[no-]verbose - Be verbose 47 | 48 | COMMANDS 49 | help - Shows a list of commands or help for one command 50 | record - Create a recording 51 | ``` 52 | 53 | ```console 54 | $ spielbash record -h 55 | NAME 56 | record - Create a recording 57 | 58 | SYNOPSIS 59 | spielbash [global options] record [command options] 60 | 61 | COMMAND OPTIONS 62 | -o, --output=arg - Output file (default: none) 63 | --script=arg - Script file path (default: none) 64 | 65 | ``` 66 | 67 | See [scenario_1.yaml](spec/fixtures/files/scenario_1.yaml) for more detail on the script file 68 | 69 | ## Development 70 | 71 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 72 | 73 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 74 | 75 | ## Contributing 76 | 77 | Bug reports and pull requests are welcome on GitHub at https://github.com/Malinskiy/spielbash. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 78 | 79 | ## License 80 | 81 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 82 | 83 | This project is a complete rewrite of [spielbash by Red Hat Cloud Innovation Practice](https://github.com/redhat-cip/spielbash) in Ruby. 84 | 85 | ## Code of Conduct 86 | 87 | Everyone interacting in the Spielbash project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/spielbash/blob/master/CODE_OF_CONDUCT.md). 88 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at anton@malinskiy.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /lib/spielbash/interactor/record_interactor.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | module Spielbash 4 | class RecordInteractor 5 | def execute(script_path, output_path) 6 | script = YAML.load_file(script_path) 7 | title = script['title'] 8 | opts = script['options'] 9 | 10 | typing_delay_s = opts['typing_delay_s'] 11 | reading_delay_s = opts['reading_delay_s'] 12 | wait = opts['wait'] 13 | width = opts['width'] 14 | height = opts['height'] 15 | wait_check_cmd = opts['wait_check_cmd'] 16 | 17 | context = Spielbash::Context.new(typing_delay_s, reading_delay_s, wait, width, height, wait_check_cmd) 18 | 19 | actions = create_actions(context, script['scenes']) 20 | pre_run_actions = script['pre-run'].nil? ? [] : create_actions(context, script['pre-run']) 21 | post_run_actions = script['post-run'].nil? ? [] : create_actions(context, script['post-run']) 22 | 23 | movie = Spielbash::Movie.new(title, pre_run_actions, actions, post_run_actions, context, output_path) 24 | movie.shoot 25 | end 26 | 27 | private 28 | 29 | def create_actions(context, scenes) 30 | actions = [] 31 | for scene in scenes do 32 | action_opts = scene['options'].nil? ? Hash.new : scene['options'] 33 | action_context = Spielbash::ActionContext.new(context, 34 | action_opts['typing_delay_s'], 35 | action_opts['reading_delay_s'], 36 | action_opts['wait'], 37 | action_opts['width'], 38 | action_opts['height']) 39 | 40 | action = case 41 | when scene.has_key?('message') then 42 | value = scene['message'] 43 | message_context = Spielbash::MessageContext.new(context, 44 | action_opts['typing_delay_s'], 45 | action_opts['reading_delay_s'], 46 | action_opts['wait'], 47 | action_opts['width'], 48 | action_opts['height'], 49 | action_opts['delete'].nil? ? true : action_opts['delete']) 50 | Spielbash::MessageAction.new(value, message_context) 51 | when scene.has_key?('command') then 52 | value = scene['command'] 53 | Spielbash::CommandAction.new(value, action_context) 54 | when scene.has_key?('pause') then 55 | value = scene['pause'] 56 | Spielbash::PauseAction.new(value, action_context) 57 | when scene.has_key?('key') then 58 | value = scene['key'] 59 | Spielbash::PressKeyAction.new(value, action_context) 60 | when scene.has_key?('new_env') then 61 | cmd = scene['new_env'] 62 | wait_cmd = scene['wait_check_cmd'] 63 | Spielbash::NewEnvironmentAction.new(cmd, wait_cmd, context) 64 | when scene.has_key?('delete_env') then 65 | cmd = scene['delete_env'] 66 | Spielbash::DeleteEnvironmentAction.new(cmd, context) 67 | else 68 | not_implemented 69 | end 70 | 71 | actions << action 72 | end 73 | 74 | actions 75 | end 76 | end 77 | end -------------------------------------------------------------------------------- /lib/spielbash/model/session.rb: -------------------------------------------------------------------------------- 1 | require 'childprocess' 2 | require 'tempfile' 3 | require 'mkmf' 4 | 5 | module Spielbash 6 | class Session 7 | attr_accessor :context, :name, :output_path 8 | attr_reader :last_stdout, :last_stderr 9 | 10 | def initialize(name, output_path, context) 11 | @context = context 12 | @name = name 13 | @output_path = output_path 14 | 15 | ChildProcess.posix_spawn = true 16 | end 17 | 18 | def new_session 19 | execute_with('resize', "-s #{context.height} #{context.width}", false, false, false) 20 | execute_tmux_with("new-session -s #{name} -d") 21 | wait 22 | end 23 | 24 | def stop_recording 25 | send_key('C-d') 26 | end 27 | 28 | def close_session 29 | send_key("exit C-m") 30 | end 31 | 32 | # This will wait for tmux session pid to not have any child processes 33 | def wait 34 | execute_tmux_with('list-panes -F #{pane_pid}' + " -t #{name}", true) 35 | pid = last_stdout.strip 36 | return if pid.empty? 37 | 38 | loop do 39 | exec_wait_check_cmd(pid) 40 | children = last_stdout 41 | break if children.empty? 42 | end 43 | end 44 | 45 | def send_key(key, count=1) 46 | key = 'Space' if key == ' ' 47 | execute_tmux_with("send-keys -t #{name} -N #{count} #{key}", true) 48 | end 49 | 50 | def start_recording 51 | execute_tmux_with('list-panes -F #{pane_pid}' + " -t #{name}", true) 52 | pid = last_stdout.strip 53 | execute_with('pgrep', "-P #{pid}", true) 54 | execute_with_exactly('asciinema', false, true, false, "rec", "-y", "-c", "tmux attach -t #{name}", "#{output_path}") 55 | end 56 | 57 | private 58 | 59 | def exec_wait_check_cmd(pid) 60 | if is_real_environment 61 | execute_with('pgrep', "-P #{pid}", true) 62 | else 63 | cmd = context.wait_check_cmd.split 64 | execute_with_exactly(cmd.first, true, false, true, *cmd.drop(1)) 65 | end 66 | end 67 | 68 | def execute_tmux_with(arguments, wait = false) 69 | execute_with('tmux', arguments, wait) 70 | end 71 | 72 | def execute_with(cmd, arguments, wait = false, leader = true, io_inherit = false) 73 | args = arguments.split 74 | execute_with_exactly cmd, wait, io_inherit, leader, *args 75 | end 76 | 77 | def execute_with_exactly(cmd, wait, io_inherit, leader, *arguments) 78 | raise "Please install #{cmd}" if is_real_environment && which(cmd).nil? 79 | 80 | process = ChildProcess.build(cmd, *arguments) 81 | process.leader = leader 82 | 83 | if io_inherit 84 | process.io.inherit! 85 | process.start 86 | process.wait if wait 87 | else 88 | process.io.stdout, process.io.stderr = std_out_err(cmd) 89 | process.start 90 | process.wait if wait 91 | @last_stdout = output(process.io.stdout) 92 | @last_stderr = output(process.io.stderr) 93 | end 94 | 95 | process 96 | end 97 | 98 | def is_real_environment 99 | context.wait_check_cmd.nil? 100 | end 101 | 102 | def output(file) 103 | file.rewind 104 | out = file.read 105 | file.close 106 | file.unlink 107 | out 108 | end 109 | 110 | 111 | def std_out_err(cmd) 112 | return ::Tempfile.new(filename_for("#{cmd}-out")), ::Tempfile.new(filename_for("#{cmd}-err")) 113 | end 114 | 115 | def filename_for(prefix) 116 | "#{prefix}-#{Time.new.to_s.gsub(' ', '_').gsub(':', '_')}" 117 | end 118 | 119 | # Cross-platform way of finding an executable in the $PATH. 120 | # 121 | # which('ruby') #=> /usr/bin/ruby 122 | def which(cmd) 123 | exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : [''] 124 | ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| 125 | exts.each {|ext| 126 | exe = File.join(path, "#{cmd}#{ext}") 127 | return exe if File.executable?(exe) && !File.directory?(exe) 128 | } 129 | end 130 | return nil 131 | end 132 | end 133 | end 134 | --------------------------------------------------------------------------------