Search code examples
rubyinputioconsoleconsole-application

Ruby Console Input Character by Character


I'd like to get an input string of digits (multiples of 5) in one line, but after each block of 5 digits, a space separator character would print to make the input line easier to read and count.

For instance:

Enter random numeric key with a length of 25 digits below:

45325 89345 98834 23453 23453

Rather than:

Enter random numeric key with a length of 25 digits below:

4532589345988342345323453

Is this at all possible?


I tried using getc to read in a character but am getting an error message:

undefined local variable or method `getc' for main:Object (NameError)


Solution

  • You could do something like this:

    require 'io/console'
    
    cnt=0
    buffer=[]
    while cnt<25 do
        c=STDIN.getch 
        if c=~/[0-9]/ then 
            print c
            print " " if (cnt+=1)%5==0
            buffer<<c
        else
            IO::console::beep
        end
    end    
    
    puts "\n#{buffer.join}"
    

    As written, it does what you stated. But typically something would also need to support delete, esc and ^ c at a minimum.

    Here is a way to handle those (but I bet there is a better way):

    #!/usr/bin/env ruby
    
    require 'io/console'
    
    def get_line(txt, len=25, skip=5)
        def left
            IO::console::cursor_left(1)
        end 
        
        def pr(c)
            print c
        end 
        
        def del(x)
            x.times{left()} 
            x.times{pr(' ')}
            x.times{left()} 
        end         
        
        def beep
            IO::console::beep
        end 
            
        cnt=col=0
        buf=[]
        breaks=["\u0003"]
        bs=["\x0008", "\u007F"]
        print txt
        line=""
        
        while buf.length<len do
            c=STDIN.getch 
            if c=~/[0-9]/ then 
                buf << c
            elsif bs.include?(c) 
                buf.length>0 ? buf.pop : beep()
            elsif breaks.include?(c) 
                buf=[]
                break
            elsif c=="\e" 
                buf=[]
            else     
                beep()
            end
            del(line.length)
            line=buf.map.with_index(1){|c,i| 
                "#{c}#{(i%skip==0) ? " " : ""}"
            }.join
            line.chars.each{|c| pr(c) }
        end    
        buf.join
    end 
    
    p get_line("Enter numbers: ")
    

    Or an alternate version that uses a case statement rather than the if elsif else ladder:

    require 'io/console'
    
    def get_line(txt, len=25, skip=5)
        def left 
            IO::console::cursor_left(1) 
        end 
        def pr(c) 
            print c 
        end 
        def del(x)
            x.times{left()} 
            x.times{pr(' ')}
            x.times{left()} 
        end         
        
        beep=-> {IO::console::beep}
        backspace=lambda{|c| ["\x0008", "\u007F"].include?(c) }
        brk=lambda{|c| ["\u0003"].include?(c) }
            
        cnt=col=0; buf=[]; line=""
    
        print txt
        
        while buf.length<len do
            c=STDIN.getch 
            case c
            when /[0-9]/ 
                buf << c
                
            when backspace
                buf.length>0 ? buf.pop : beep[]
                
            when brk 
                buf=[]
                break
                
            when "\e" 
                buf=[]
                
            else     
                beep[]
            end
            del(line.length)
            line=buf.map.with_index(1){|c,i| 
                "#{c}#{(i%skip==0) ? " " : ""}"
            }.join
            line.chars.each{|c| pr(c) }
        end    
        buf.join
    end 
    # works the same
    

    The problem is that there is a great deal of variability in text terminal types, compliance and capabilities. These functions only needs left, pr and beep to work properly on the terminal you use so not that hard. You could do it with ANSI codes as well.