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
- .ask(prompt, default: nil) ⇒ Object
-
.choose(prompt, items) ⇒ Object
Arrow-key picker.
-
.choose_numbered(items) ⇒ Object
Fallback for non-TTY stdin (pipes, tests).
- .color!(value) ⇒ Object
- .color? ⇒ Boolean
-
.error(text) ⇒ Object
Raise a controlled Hammer::Error.
- .paint(text, color = nil) ⇒ Object
-
.print_error(text) ⇒ Object
Print a red [error] line to stderr (does not exit).
-
.say(text = :_say_no_arg, color = nil) ⇒ Object
‘say` with no args returns a proxy so you can write `say.cyan ’hi’‘.
-
.sh(cmd) ⇒ Object
Run a shell command.
- .yes?(prompt) ⇒ Boolean
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
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)
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_error(text) ⇒ Object
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
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 |