Module: Hammer::Shell

Included in:
Hammer
Defined in:
lib/hammer/shell.rb

Overview

ANSI color/output helpers. Mixed into command instances; also callable directly as ‘Hammer::Shell.say(…)`.

Defined Under Namespace

Classes: SayProxy

Class Method Summary collapse

Class Method Details

.ask(prompt, default: nil) ⇒ Object



67
68
69
70
71
72
73
74
# File 'lib/hammer/shell.rb', line 67

def ask(prompt, default: nil)
  suffix = default ? " [#{default}]" : ''
  print paint("#{prompt}#{suffix}: ", :cyan)
  line = $stdin.gets
  return default if line.nil?
  line = line.chomp
  line.empty? ? default : line
end

.choose(prompt, items) ⇒ Object

Arrow-key picker. Returns the chosen index, or nil on cancel (ESC, Ctrl-C, q). Non-TTY input falls back to a numbered prompt so this stays scriptable.

idx = choose 'Pick env', %w[dev staging prod]
say.green "chose #{ %w[dev staging prod][idx] }" if idx


88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/hammer/shell.rb', line 88

def choose(prompt, items)
  items = items.to_a
  error 'choose needs at least one item' if items.empty?

  say.cyan prompt

  return choose_numbered(items) unless $stdin.tty? && $stdin.respond_to?(:raw)

  selected = 0
  redraw = lambda do |highlight = :cyan|
    items.each_with_index do |item, i|
      puts(i == selected ? paint("> #{item}", highlight) : "  #{item}")
    end
  end
  redraw.call

  $stdout.print "\e[?25l" # hide cursor
  begin
    $stdin.raw do |io|
      loop do
        ch = io.getch
        case ch
        when "\r", "\n"
          # Collapse the list to the chosen line, in green.
          $stdout.print "\e[#{items.size}A\e[J"
          puts paint("> #{items[selected]}", :green)
          return selected
        when "\x03", 'q' # Ctrl-C, q
          $stdout.print "\e[#{items.size}A\e[J"
          return nil
        when "\e"
          # ESC may stand alone or start an arrow sequence \e[A / \e[B.
          if IO.select([io], nil, nil, 0.01) && io.getch == '['
            case io.getch
            when 'A' then selected = (selected - 1) % items.size
            when 'B' then selected = (selected + 1) % items.size
            end
          else
            $stdout.print "\e[#{items.size}A\e[J"
            return nil
          end
        when 'k' then selected = (selected - 1) % items.size
        when 'j' then selected = (selected + 1) % items.size
        end
        $stdout.print "\e[#{items.size}A\e[J"
        redraw.call
      end
    end
  ensure
    $stdout.print "\e[?25h" # show cursor
  end
end

.choose_numbered(items) ⇒ Object

Fallback for non-TTY stdin (pipes, tests). Returns the index or nil.



142
143
144
145
146
147
148
149
# File 'lib/hammer/shell.rb', line 142

def choose_numbered(items)
  items.each_with_index { |item, i| puts "  #{i + 1}) #{item}" }
  print paint("select [1-#{items.size}]: ", :cyan)
  line = $stdin.gets
  return nil if line.nil?
  idx = line.strip.to_i - 1
  idx.between?(0, items.size - 1) ? idx : nil
end

.color!(value) ⇒ Object



20
21
22
# File 'lib/hammer/shell.rb', line 20

def color!(value)
  @color = value
end

.color?Boolean

Returns:

  • (Boolean)


15
16
17
18
# File 'lib/hammer/shell.rb', line 15

def color?
  return @color if defined?(@color)
  @color = $stdout.tty? && ENV['NO_COLOR'].nil?
end

.error(text) ⇒ Object

Raise a controlled Hammer::Error. If unhandled, the dispatcher prints the message in red and exits 1 - no backtrace, no help spam.

error 'config file missing' unless File.exist?(path)

Raises:

  • (Hammer::Error)


57
58
59
# File 'lib/hammer/shell.rb', line 57

def error(text)
  raise Hammer::Error, text
end

.paint(text, color = nil) ⇒ Object



24
25
26
27
28
29
30
# File 'lib/hammer/shell.rb', line 24

def paint(text, color = nil)
  if color && !COLORS.key?(color)
    raise Hammer::Error, "unknown color #{color.inspect} (valid: #{COLORS.keys.join(', ')})"
  end
  return text.to_s unless color? && color
  "\e[#{COLORS[color]}m#{text}\e[0m"
end

Print a red [error] line to stderr (does not exit). Used internally by the dispatcher to render Hammer::Error messages.



63
64
65
# File 'lib/hammer/shell.rb', line 63

def print_error(text)
  warn paint("[error] #{text}", :red)
end

.say(text = nil, color = nil) ⇒ Object

‘say` with no args returns a proxy so you can write `say.cyan ’hi’‘. `say(”)` still prints a blank line; `say(’x’, :cyan)‘ is unchanged.



34
35
36
37
# File 'lib/hammer/shell.rb', line 34

def say(text = nil, color = nil)
  return SayProxy.new if text.nil?
  puts paint(text, color)
end

.sh(cmd) ⇒ Object

Run a shell command. Echoes the command in gray, raises Hammer::Error on non-zero exit. Returns true on success.



153
154
155
156
157
# File 'lib/hammer/shell.rb', line 153

def sh(cmd)
  say "$ #{cmd}", :gray
  error "command failed: #{cmd}" unless system(cmd)
  true
end

.yes?(prompt) ⇒ Boolean

Returns:

  • (Boolean)


76
77
78
79
80
# File 'lib/hammer/shell.rb', line 76

def yes?(prompt)
  answer = ask("#{prompt} (y/N)")
  return false if answer.nil?
  answer.to_s.strip.downcase.start_with?('y')
end