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



70
71
72
73
74
75
76
77
# File 'lib/hammer/shell.rb', line 70

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


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
140
141
142
143
144
145
146
# File 'lib/hammer/shell.rb', line 91

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
  # In raw mode \n is not translated to \r\n, so the picker uses \r\n
  # explicitly. The initial draw happens in cooked mode but \r\n is
  # harmless there.
  redraw = lambda do |highlight = :cyan|
    items.each_with_index do |item, i|
      line = i == selected ? paint("> #{item}", highlight) : "  #{item}"
      $stdout.print "#{line}\r\n"
    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\r\e[J"
          $stdout.print "#{paint("> #{items[selected]}", :green)}\r\n"
          return selected
        when "\x03" # Ctrl-C
          $stdout.print "\e[#{items.size}A\r\e[J"
          raise Interrupt
        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\r\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\r\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.



149
150
151
152
153
154
155
156
# File 'lib/hammer/shell.rb', line 149

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



23
24
25
# File 'lib/hammer/shell.rb', line 23

def color!(value)
  @color = value
end

.color?Boolean

Returns:

  • (Boolean)


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

def color?
  # Only an explicit color!(value) override is sticky; otherwise the
  # tty decision is recomputed so a redirected $stdout (tests, capture
  # blocks) is honored instead of frozen at first read.
  return @color if defined?(@color) && !@color.nil?
  $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)


60
61
62
# File 'lib/hammer/shell.rb', line 60

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

.paint(text, color = nil) ⇒ Object



27
28
29
30
31
32
33
# File 'lib/hammer/shell.rb', line 27

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.



66
67
68
# File 'lib/hammer/shell.rb', line 66

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

.say(text = :_say_no_arg, 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.



37
38
39
40
# File 'lib/hammer/shell.rb', line 37

def say(text = :_say_no_arg, color = nil)
  return SayProxy.new if text == :_say_no_arg
  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.



160
161
162
163
164
# File 'lib/hammer/shell.rb', line 160

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

.yes?(prompt) ⇒ Boolean

Returns:

  • (Boolean)


79
80
81
82
83
# File 'lib/hammer/shell.rb', line 79

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