Skip to content
6 changes: 6 additions & 0 deletions lib/ffi/io/console/bsd_console.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
require 'ffi'

tested_platforms = %w[i386 x86_64]

if RbConfig::CONFIG['host_os'].downcase =~ /darwin/ && FFI::Platform::ARCH !~ /#{tested_platforms.join('|')}/
raise LoadError.new("native console on MacOS only supported on #{tested_platforms.join(', ')}")
end

module IO::LibC
extend FFI::Library
ffi_lib FFI::Library::LIBC
Expand Down
116 changes: 116 additions & 0 deletions lib/ffi/io/console/common.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,47 @@
# Methods common to all backend impls
class IO
# TODO: Windows version uses "conin$" and "conout$" instead of /dev/tty
def self.console(sym = nil, *args)
raise TypeError, "expected Symbol, got #{sym.class}" unless sym.nil? || sym.kind_of?(Symbol)

# klass = self == IO ? File : self
if defined?(@console) # using ivar instead of hidden const as in MRI
con = @console
# MRI checks IO internals : (!RB_TYPE_P(con, T_FILE) || (!(fptr = RFILE(con)->fptr) || GetReadFD(fptr) == -1))
if !con.kind_of?(File) || (con.kind_of?(IO) && (con.closed? || !FileTest.readable?(con)))
remove_instance_variable :@console
con = nil
end
end

if sym
if sym == :close
if con
con.close
remove_instance_variable :@console if defined?(@console)
end
return nil
end
end

if !con
if $stdin.isatty && $stdout.isatty
begin
con = File.open('/dev/tty', 'r+')
rescue
return nil
end

con.sync = true
end

@console = con
end

return con.send(sym, *args) if sym
return con
end

def getch(*, **opts)
raw(**opts) do
getc
Expand All @@ -20,6 +62,80 @@ def getpass(prompt = nil)
str.chomp
end

def cursor
raw do
syswrite "\e[6n"

return nil if getbyte != 0x1b
return nil if getbyte != ?[.ord

num = 0
result = []

while b = getbyte
c = b.to_i
if c == ?;.ord
result.push num
num = 0
elsif c >= ?0.ord && c <= ?9.ord
num = num * 10 + c - ?0.ord
#elsif opt && c == opt
else
last = c
result.push num
b = last.chr
return nil unless b == ?R
break
end
end

result.map(&:pred)
end
end

def cursor=(pos)
pos = pos.to_ary if !pos.kind_of?(Array)

raise "expected 2D coordinates" unless pos.size == 2

x, y = pos
syswrite(format("\x1b[%d;%dH", x + 1, y + 1))

self
end

def cursor_down(n)
raw do
syswrite "\x1b[#{n}B"
end

self
end

def cursor_right(n)
raw do
syswrite "\x1b[#{n}C"
end

self
end

def cursor_left(n)
raw do
syswrite "\x1b[#{n}D"
end

self
end

def cursor_up(n)
raw do
syswrite "\x1b[#{n}A"
end

self
end

module GenericReadable
def getch(*)
getc
Expand Down
6 changes: 4 additions & 2 deletions lib/ffi/io/console/linux_console.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
require 'ffi'

unless FFI::Platform::ARCH =~ /i386|x86_64|powerpc64|aarch64|s390x/
raise LoadError.new("native console only supported on i386, x86_64, powerpc64, aarch64 and s390x")
tested_platforms = %w[i386 x86_64 powerpc64 aarch64 s390x]

unless FFI::Platform::ARCH =~ /#{tested_platforms.join('|')}/
warn "native console only tested on #{tested_platforms.join(', ')}"
end

module IO::LibC
Expand Down
117 changes: 2 additions & 115 deletions lib/ffi/io/console/native_console.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ def ttymode_yield(block, **opts, &setup)
end
end

def raw(*, **kwargs, &block)
ttymode_yield(block, **kwargs, &TTY_RAW)
def raw(*, min: 1, time: nil, intr: nil, &block)
ttymode_yield(block, min:, time:, intr:, &TTY_RAW)
end

def raw!(*)
Expand Down Expand Up @@ -137,117 +137,4 @@ def oflush
def ioflush
raise SystemCallError.new("tcflush(TCIOFLUSH)", FFI.errno) unless LibC.tcflush(self.fileno, LibC::TCIOFLUSH) == 0
end

def cursor
raw do
syswrite "\e[6n"

return nil if getbyte != 0x1b
return nil if getbyte != ?[.ord

num = 0
result = []

while b = getbyte
c = b.to_i
if c == ?;.ord
result.push num
num = 0
elsif c >= ?0.ord && c <= ?9.ord
num = num * 10 + c - ?0.ord
#elsif opt && c == opt
else
last = c
result.push num
b = last.chr
return nil unless b == ?R
break
end
end

result.map(&:pred)
end
end

def cursor=(pos)
pos = pos.to_ary if !pos.kind_of?(Array)

raise "expected 2D coordinates" unless pos.size == 2

x, y = pos
syswrite(format("\x1b[%d;%dH", x + 1, y + 1))

self
end

def cursor_down(n)
raw do
syswrite "\x1b[#{n}B"
end

self
end

def cursor_right(n)
raw do
syswrite "\x1b[#{n}C"
end

self
end

def cursor_left(n)
raw do
syswrite "\x1b[#{n}D"
end

self
end

def cursor_up(n)
raw do
syswrite "\x1b[#{n}A"
end

self
end

# TODO: Windows version uses "conin$" and "conout$" instead of /dev/tty
def self.console(sym = nil, *args)
raise TypeError, "expected Symbol, got #{sym.class}" unless sym.nil? || sym.kind_of?(Symbol)

# klass = self == IO ? File : self
if defined?(@console) # using ivar instead of hidden const as in MRI
con = @console
# MRI checks IO internals : (!RB_TYPE_P(con, T_FILE) || (!(fptr = RFILE(con)->fptr) || GetReadFD(fptr) == -1))
if !con.kind_of?(File) || (con.kind_of?(IO) && (con.closed? || !FileTest.readable?(con)))
remove_instance_variable :@console
con = nil
end
end

if sym
if sym == :close
if con
con.close
remove_instance_variable :@console if defined?(@console)
end
return nil
end
end

if !con
begin
con = File.open('/dev/tty', 'r+')
rescue
return nil
end

con.sync = true
@console = con
end

return con.send(sym, *args) if sym
return con
end
end
55 changes: 33 additions & 22 deletions lib/ffi/io/console/stty_console.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,66 +4,77 @@
raise "stty command returned nonzero exit status"
end

warn "io/console on JRuby shells out to stty for most operations"
warn "io/console on JRuby shells out to stty for most operations" if $VERBOSE

# Non-Windows assumes stty command is available
class IO
if RbConfig::CONFIG['host_os'].downcase =~ /linux/ && File.exists?("/proc/#{Process.pid}/fd")
def stty(*args)
`stty #{args.join(' ')} < /proc/#{Process.pid}/fd/#{fileno}`
protected def _io_console_stty(*args)
_io_console_stty_error { `stty #{args.join(' ')} < /proc/#{Process.pid}/fd/#{fileno}` }
end
else
def stty(*args)
`stty #{args.join(' ')}`
protected def _io_console_stty(*args)
_io_console_stty_error { `stty #{args.join(' ')}` }
end
end

def raw(*)
saved = stty('-g')
stty('raw')
protected def _io_console_stty_error
# pre-check to catch non-tty filenos we can't stty against anyway
raise Errno::ENOTTY, inspect if !tty?

result = yield

case result
when /Inappropriate ioctl for device/
raise Errno.ENOTTY, inspect
end

result
end

def raw(*, min: 1, time: nil, intr: nil)
saved = _io_console_stty('-g raw')
yield self
ensure
stty(saved)
_io_console_stty(saved)
end

def raw!(*)
stty('raw')
end

def cooked(*)
saved = stty('-g')
stty('-raw')
saved = _io_console_stty('-g', '-raw')
yield self
ensure
stty(saved)
_io_console_stty(saved)
end

def cooked!(*)
stty('-raw')
_io_console_stty('-raw')
end

def echo=(echo)
stty(echo ? 'echo' : '-echo')
_io_console_stty(echo ? 'echo' : '-echo')
end

def echo?
(stty('-a') =~ / -echo /) ? false : true
(_io_console_stty('-a') =~ / -echo /) ? false : true
end

def noecho
saved = stty('-g')
stty('-echo')
saved = _io_console_stty('-g', '-echo')
yield self
ensure
stty(saved)
_io_console_stty(saved)
end

# Not all systems return same format of stty -a output
IEEE_STD_1003_2 = '(?<rows>\d+) rows; (?<columns>\d+) columns'
UBUNTU = 'rows (?<rows>\d+); columns (?<columns>\d+)'

def winsize
match = stty('-a').match(/#{IEEE_STD_1003_2}|#{UBUNTU}/)
match = _io_console_stty('-a').match(/#{IEEE_STD_1003_2}|#{UBUNTU}/)
[match[:rows].to_i, match[:columns].to_i]
end

Expand All @@ -75,13 +86,13 @@ def winsize=(size)
raise ArgumentError.new("wrong number of arguments (given #{sizelen}, expected 2 or 4)")
end

row, col, xpixel, ypixel = size

if sizelen == 4
warn "stty io/console does not support pixel winsize"
end

stty("rows #{row} cols #{col}")
row, col, _, _ = size

_io_console_stty("rows #{row} cols #{col}")
end

def iflush
Expand Down
Loading