Upgrade to Pro — share decks privately, control downloads, hide ads and more …

TTY - Ruby alchemist’s secret potion

TTY - Ruby alchemist’s secret potion

What if you were told that there is a set of simple and potent gems developed to exponentially increase productivity when building modern terminal applications such as Bundler, in next to no time? Curious about how you can harness this power and become a command line applications alchemist?

Piotr Murach

May 31, 2018
Tweet

Other Decks in Programming

Transcript

  1. ▾ app/ ├── ▾ exe/ │ └── app ├── ▾

    lib/ │ ├── ▾ app/ │ │ ├── ▸ commands/ │ │ ├── ▸ templates/ │ │ ├── cli.rb │ │ ├── command.rb │ │ └── version.rb │ └── app.rb ... └── app.gemspec 30 — © Piotr Murach 2018
  2. CLI vs Web cli ↔ router command ↔ controller stdin

    ↔ request stdout ↔ response template ↔ view 31 — © Piotr Murach 2018
  3. $ teletype add config --args name # required argument $

    teletype add config --args "name = nil" # optional argument 34 — © Piotr Murach 2018
  4. $ teletype add config --args name # required argument $

    teletype add config --args "name = nil" # optional argument $ teletype add config --args *names # variadic argument 35 — © Piotr Murach 2018
  5. app/lib ▾ app/ ├── ▾ commands/ │ └── config.rb ├──

    ▾ templates/ │ └── ▸ config/ ├── cli.rb ├── cmd.rb └── version.rb 37 — © Piotr Murach 2018
  6. lib/app/cli.rb module App class CLI < Thor desc 'config NAME

    VALUE', 'Add configuration option' def config(name, value) if options[:help] invoke :help, ['config'] else require_relative 'commands/config' App::Commands::Config.new(name, value, options).execute end end end end 38 — © Piotr Murach 2018
  7. lib/app/commands/config.rb module App module Commands class Config < App::Cmd def

    initialize(name, value, options) @name = name @value = value @options = options end def execute(input: $stdin, output: $stdout) output.puts "OK" end end end end 39 — © Piotr Murach 2018
  8. lib/app/cmd.rb module App class Cmd ... def generator require 'tty-file'

    TTY::File end ... end end 40 — © Piotr Murach 2018
  9. ... ├── ▾ spec/ │ ├── ▾ integration/ │ │

    └── config_spec.rb │ ├── ▸ support/ │ ├── ▾ unit/ │ │ └── config_spec.rb │ ├── app_spec.rb │ └── spec_helper.rb ... 42 — © Piotr Murach 2018
  10. app/spec/integration/config_spec.rb RSpec.describe "`app config` command", type: :cli do it "executes

    `app help config` command successfully" do end end 44 — © Piotr Murach 2018
  11. app/spec/integration/config_spec.rb RSpec.describe "`app config` command", type: :cli do it "executes

    `app help config` command successfully" do output = `app help config` end end 45 — © Piotr Murach 2018
  12. app/spec/integration/config_spec.rb RSpec.describe "`app config` command", type: :cli do it "executes

    `app help config` command successfully" do output = `app help config` expected_output = <<-OUT Usage: app config NAME VALUE Options: -h, [--help], [--no-help] # Display usage information Add configuration option OUT end end 46 — © Piotr Murach 2018
  13. app/spec/integration/config_spec.rb RSpec.describe "`app config` command", type: :cli do it "executes

    `app help config` command successfully" do output = `app help config` expected_output = <<-OUT Usage: app config NAME VALUE Options: -h, [--help], [--no-help] # Display usage information Add configuration option OUT expect(output).to eq(expected_output) end end 47 — © Piotr Murach 2018
  14. app/spec/unit/config_spec.rb require 'app/commands/config' RSpec.describe App::Commands::Config do it "executes `app config`

    command successfully" do output = StringIO.new end end 50 — © Piotr Murach 2018
  15. app/spec/unit/config_spec.rb require 'app/commands/config' RSpec.describe App::Commands::Config do it "executes `app config`

    command successfully" do output = StringIO.new name = nil value = nil options = {} end end 51 — © Piotr Murach 2018
  16. app/spec/unit/config_spec.rb require 'app/commands/config' RSpec.describe App::Commands::Config do it "executes `app config`

    command successfully" do output = StringIO.new name = nil value = nil options = {} command = App::Commands::Config.new(name, value, options) end end 52 — © Piotr Murach 2018
  17. app/spec/unit/config_spec.rb require 'app/commands/config' RSpec.describe App::Commands::Config do it "executes `app config`

    command successfully" do output = StringIO.new name = nil value = nil options = {} command = App::Commands::Config.new(name, value, options) command.execute(output: output) end end 53 — © Piotr Murach 2018
  18. app/spec/unit/config_spec.rb require 'app/commands/config' RSpec.describe App::Commands::Config do it "executes `config` command

    successfully" do output = StringIO.new name = nil value = nil options = {} command = App::Commands::Config.new(name, value, options) command.execute(output: output) expect(output.string).to eq("OK\n") end end 54 — © Piotr Murach 2018
  19. Ask Prompt require 'tty-prompt' prompt = TTY::Prompt.new prompt.ask('What is your

    name?', default: ENV['USER']) 65 — © Piotr Murach 2018
  20. Ask Prompt prompt.ask('At what price per coin?') do |q| q.required(true,

    'You need to provide a price') end 67 — © Piotr Murach 2018
  21. Ask Prompt prompt.ask('At what price per coin?') do |q| q.required(true,

    'You need to provide a price') q.validate(/[\d.]+/, 'Invalid price provided') end 68 — © Piotr Murach 2018
  22. Ask Prompt prompt.ask('At what price per coin?') do |q| q.required(true,

    'You need to provide a price') q.validate(/[\d.]+/, 'Invalid price provided') q.convert ->(p) { p.to_f } end 69 — © Piotr Murach 2018
  23. Masked Prompt heart = prompt.decorate('❤', :magenta) res = prompt.mask('What is

    your secret?', mask: heart) do |q| ... end 71 — © Piotr Murach 2018
  24. Masked Prompt heart = prompt.decorate('❤', :magenta) res = prompt.mask('What is

    your secret?', mask: heart) do |q| q.validate(/[a-z\ ]{5,15}/) end puts "Secret: \"#{res}\"" 72 — © Piotr Murach 2018
  25. Multiple Choice Prompt drinks = %w(vodka beer wine whisky bourbon)

    prompt.multi_select('Choose your favourite drink?', drinks, help: '(Use arrow keys and Enter to finish)') 74 — © Piotr Murach 2018
  26. Prompt events (Vim style ;-) prompt.on(:keypress) do |event| if event.value

    == 'j' ... end if event.value == 'k' ... end end 77 — © Piotr Murach 2018
  27. Prompt events (Vim style ;-) prompt.on(:keypress) do |event| if event.value

    == 'j' prompt.trigger(:keydown) end if event.value == 'k' prompt.trigger(:keyup) end end 78 — © Piotr Murach 2018
  28. Collect Prompt prompt.collect do key(:name).ask('Name?') key(:age).ask('Age?', convert: :int) key(:address) do

    key(:street).ask('Street?', required: true) key(:city).ask('City?') key(:zip).ask('Zip?', validate: /\A\d{3}\Z/) end end 81 — © Piotr Murach 2018
  29. Collect Prompt { "name": "Piotr", "age": 33, "address": { "street":

    "West Street", "city": "Sheffield", "zip": "S1 EF" } } 82 — © Piotr Murach 2018
  30. Config require 'tty-config' config = TTY::Config.new config.filename = 'investments' config.extname

    = '.toml' config.append_path Dir.pwd 88 — © Piotr Murach 2018
  31. Config config.set(:settings, :base, value: 'USD') config.set("settings.exchange", value:'Bitfinex') config.set(:coins, value: ['BTC'])

    config.append('ETH', 'TRX', 'DASH', to: :coins) config.fetch(:settings, :base) # => 'USD' 93 — © Piotr Murach 2018
  32. Config config.set(:settings, :base, value: 'USD') config.set("settings.exchange", value:'Bitfinex') config.set(:coins, value: ['BTC'])

    config.append('ETH', 'TRX', 'DASH', to: :coins) config.fetch(:settings, :base) # => 'USD' config.fetch(:coins) # => ['BTC', 'ETH', 'TRX', 'DASH'] 94 — © Piotr Murach 2018
  33. Config config.validate(:settings, :base) do |key, value| if value.length != 3

    raise TTY::Config::ValidationError, "Currency code needs to be 3 chars long." end end 96 — © Piotr Murach 2018
  34. Config config.validate(:settings, :base) do |key, value| if value.length != 3

    raise TTY::Config::ValidationError, "Currency code needs to be 3 chars long." end end config.set(:settings, :base, value: 'PL') # raises TTY::Config::ValidationError ... 97 — © Piotr Murach 2018
  35. Progress Bar require 'pastel' pastel = Pastel.new green = pastel.on_green("

    ") red = pastel.on_red(" ") 102 — © Piotr Murach 2018
  36. Progress Bar require 'tty-progressbar' bar = TTY::ProgressBar.new( ":bar :percent :elapsed",

    total: 30, complete: green, incomplete: red, ) 106 — © Piotr Murach 2018
  37. Progress Bar require 'tty-progressbar' bar = TTY::ProgressBar.new( ":bar :percent :elapsed",

    total: 30, complete: green, incomplete: red, hide_cursor: true ) 107 — © Piotr Murach 2018
  38. Progress Bar require 'tty-progressbar' bar = TTY::ProgressBar.new( ":bar :percent :elapsed",

    total: 30, complete: green, incomplete: red, hide_cursor: true ) 30.times do sleep(0.1) bar.advance end 108 — © Piotr Murach 2018
  39. bars = TTY::ProgressBar::Multi.new("main [:bar] :percent") bar1 = bars.register "foo [:bar]

    :percent", total: 15 bar2 = bars.register "bar [:bar] :percent", total: 10 bar3 = bars.register "baz [:bar] :percent", total: 25 111 — © Piotr Murach 2018
  40. require 'tty-progressbar' bars = TTY::ProgressBar::Multi.new("main [:bar] :percent") bar1 = bars.register

    "foo [:bar] :percent", total: 15 bar2 = bars.register "bar [:bar] :percent", total: 10 bar3 = bars.register "baz [:bar] :percent", total: 25 th1 = Thread.new { 15.times { sleep(0.1); bar1.advance } } th2 = Thread.new { 10.times { sleep(0.1); bar2.advance } } th3 = Thread.new { 25.times { sleep(0.1); bar3.advance } } 112 — © Piotr Murach 2018
  41. require 'tty-progressbar' bars = TTY::ProgressBar::Multi.new("main [:bar] :percent") bar1 = bars.register

    "foo [:bar] :percent", total: 15 bar2 = bars.register "bar [:bar] :percent", total: 10 bar3 = bars.register "baz [:bar] :percent", total: 25 th1 = Thread.new { 15.times { sleep(0.1); bar1.advance } } th2 = Thread.new { 10.times { sleep(0.1); bar2.advance } } th3 = Thread.new { 25.times { sleep(0.1); bar3.advance } } [th1, th2, th3].each(&:join) 113 — © Piotr Murach 2018
  42. require 'tty-spinner' require 'pastel' pastel = Pastel.new spinners = TTY::Spinner::Multi.new("[:spinner]

    Downloading files...") ['file1', 'file2', 'file3'].each do |file| end 118 — © Piotr Murach 2018
  43. require 'tty-spinner' require 'pastel' pastel = Pastel.new spinners = TTY::Spinner::Multi.new("[:spinner]

    Downloading files...") ['file1', 'file2', 'file3'].each do |file| spinners.register("[:spinner] #{file}") do |sp| end end 119 — © Piotr Murach 2018
  44. require 'tty-spinner' require 'pastel' pastel = Pastel.new spinners = TTY::Spinner::Multi.new("[:spinner]

    Downloading files...") ['file1', 'file2', 'file3'].each do |file| spinners.register("[:spinner] #{file}") do |sp| sleep(rand * 5) end end 120 — © Piotr Murach 2018
  45. require 'tty-spinner' require 'pastel' pastel = Pastel.new spinners = TTY::Spinner::Multi.new("[:spinner]

    Downloading files...") ['file1', 'file2', 'file3'].each do |file| spinners.register("[:spinner] #{file}") do |sp| sleep(rand * 5) sp.success(pastel.green("success")) end end 121 — © Piotr Murach 2018
  46. require 'tty-spinner' require 'pastel' pastel = Pastel.new spinners = TTY::Spinner::Multi.new("[:spinner]

    Downloading files...") ['file1', 'file2', 'file3'].each do |file| spinners.register("[:spinner] #{file}") do |sp| sleep(rand * 5) sp.success(pastel.green("success")) end end spinners.auto_spin 122 — © Piotr Murach 2018
  47. TTY gems require 'pastel' require 'tty-config' require 'tty-cursor' require 'tty-editor'

    require 'tty-font' require 'tty-pager' require 'tty-prompt' require 'tty-spinner' require 'tty-table' 135 — © Piotr Murach 2018
  48. prompt.collect do key('settings') do key('base').ask('What base currency to convert holdings

    to?') do |q| q.default "USD" q.convert ->(b) { b.upcase } q.validate(/\w{3}/, 'Currency code needs to be 3 chars long') end end end 137 — © Piotr Murach 2018
  49. prompt.collect do key('settings') do key('base').ask('What base currency to convert holdings

    to?') do |q| q.default "USD" q.convert ->(b) { b.upcase } q.validate(/\w{3}/, 'Currency code needs to be 3 chars long') end key('exchange').ask('What exchange would you like to use?') do |q| q.default "CCCAGG" q.required true end end end 138 — © Piotr Murach 2018
  50. prompt.collect do key('settings') do key('base').ask('What base currency to convert holdings

    to?') do |q| ... end key('exchange').ask('What exchange would you like to use?') do |q| ... end end while prompt.yes?("Do you want to add coin to your altfolio?") end end 139 — © Piotr Murach 2018
  51. prompt.collect do key('settings') do key('base').ask('What base currency to convert holdings

    to?') do |q| ... end key('exchange').ask('What exchange would you like to use?') do |q| ... end end while prompt.yes?("Do you want to add coin to your altfolio?") key('holdings') end end 140 — © Piotr Murach 2018
  52. prompt.collect do key('settings') do key('base').ask('What base currency to convert holdings

    to?') do |q| ... end key('exchange').ask('What exchange would you like to use?') do |q| ... end end while prompt.yes?("Do you want to add coin to your altfolio?") key('holdings').values(&context.ask_coin) end end 141 — © Piotr Murach 2018
  53. def ask_coin -> (prompt) do key('name').ask('What coin do you own?')

    do |q| ... end key('amount').ask('What amount?') do |q| ... end key('price').ask('At what price per coin?') do |q| ... end end end 143 — © Piotr Murach 2018
  54. def ask_coin -> (prompt) do key('name').ask('What coin do you own?')

    do |q| q.default 'BTC' q.required(true, 'You need to provide a coin') q.validate(/\w{2,}/, 'Currency can only be chars.') q.convert ->(coin) { coin.upcase } end key('amount').ask('What amount?') do |q| q.required(true, 'You need to provide an amount') q.validate(/[\d.]+/, 'Invalid amount provided') q.convert ->(am) { am.to_f } end key('price').ask('At what price per coin?') do |q| q.required(true, 'You need to provide a price') q.validate(/[\d.]+/, 'Invalid prince provided') q.convert ->(p) { p.to_f } end end end 144 — © Piotr Murach 2018