diff --git a/lib/ffi/io/console/bsd_console.rb b/lib/ffi/io/console/bsd_console.rb index 1238fd9..092f7e8 100644 --- a/lib/ffi/io/console/bsd_console.rb +++ b/lib/ffi/io/console/bsd_console.rb @@ -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 diff --git a/lib/ffi/io/console/common.rb b/lib/ffi/io/console/common.rb index d45601a..832889a 100644 --- a/lib/ffi/io/console/common.rb +++ b/lib/ffi/io/console/common.rb @@ -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 @@ -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 diff --git a/lib/ffi/io/console/linux_console.rb b/lib/ffi/io/console/linux_console.rb index 54ab44a..ce71ecc 100644 --- a/lib/ffi/io/console/linux_console.rb +++ b/lib/ffi/io/console/linux_console.rb @@ -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 diff --git a/lib/ffi/io/console/native_console.rb b/lib/ffi/io/console/native_console.rb index c0fb87b..e5443c3 100644 --- a/lib/ffi/io/console/native_console.rb +++ b/lib/ffi/io/console/native_console.rb @@ -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!(*) @@ -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 diff --git a/lib/ffi/io/console/stty_console.rb b/lib/ffi/io/console/stty_console.rb index 95a22a0..c8e549b 100644 --- a/lib/ffi/io/console/stty_console.rb +++ b/lib/ffi/io/console/stty_console.rb @@ -4,26 +4,39 @@ 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!(*) @@ -31,31 +44,29 @@ def 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 @@ -63,7 +74,7 @@ def noecho UBUNTU = 'rows (?\d+); 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 @@ -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 diff --git a/lib/ffi/io/console/stub_console.rb b/lib/ffi/io/console/stub_console.rb index d2407b6..39a4876 100644 --- a/lib/ffi/io/console/stub_console.rb +++ b/lib/ffi/io/console/stub_console.rb @@ -2,7 +2,7 @@ # Windows version is always stubbed for now class IO - def raw(*) + def raw(*, min: 1, time: nil, intr: nil) yield self end diff --git a/test/io/console/test_io_console.rb b/test/io/console/test_io_console.rb index fd0e57b..7a9ddd0 100644 --- a/test/io/console/test_io_console.rb +++ b/test/io/console/test_io_console.rb @@ -51,7 +51,9 @@ def test_failed_path end def test_bad_keyword + # JRuby in CI still fails to reject this bad keyword argument omit if RUBY_ENGINE == 'jruby' + assert_raise_with_message(ArgumentError, /unknown keyword:.*bad/) do File.open(IO::NULL) do |f| f.raw(bad: 0)