diff --git a/ext/io/console/console.c b/ext/io/console/console.c index eb1c876..d3f0a7d 100644 --- a/ext/io/console/console.c +++ b/ext/io/console/console.c @@ -178,6 +178,22 @@ typedef struct { # define NIL_OR_UNDEF_P(obj) (NIL_P(obj) || UNDEF_P(obj)) #endif +static int +to_vtime(VALUE vtime) +{ + VALUE v10 = INT2FIX(10); + vtime = rb_funcall3(vtime, '*', 1, &v10); + return NUM2INT(vtime); +} + +static unsigned char +clamp_uchar(int n) +{ + if ((unsigned int)n >= UCHAR_MAX) n = UCHAR_MAX; + else if (n < 0) n = 0; + return (unsigned char)n; +} + static rawmode_arg_t * rawmode_opt(int *argcp, VALUE *argv, int min_argc, int max_argc, rawmode_arg_t *opts) { @@ -209,13 +225,11 @@ rawmode_opt(int *argcp, VALUE *argv, int min_argc, int max_argc, rawmode_arg_t * opts->vtime = 0; opts->intr = 0; if (!NIL_OR_UNDEF_P(vmin)) { - opts->vmin = NUM2INT(vmin); + opts->vmin = clamp_uchar(NUM2INT(vmin)); optp = opts; } if (!NIL_OR_UNDEF_P(vtime)) { - VALUE v10 = INT2FIX(10); - vtime = rb_funcall3(vtime, '*', 1, &v10); - opts->vtime = NUM2INT(vtime); + opts->vtime = clamp_uchar(to_vtime(vtime)); optp = opts; } switch (intr) { @@ -762,6 +776,13 @@ conmode_init_copy(VALUE obj, VALUE obj2) return obj; } +static VALUE +conmode_get_echo(VALUE obj) +{ + conmode *t = rb_check_typeddata(obj, &conmode_type); + return echo_p(t) ? Qtrue : Qfalse; +} + static VALUE conmode_set_echo(VALUE obj, VALUE f) { @@ -794,6 +815,58 @@ conmode_raw_new(int argc, VALUE *argv, VALUE obj) return conmode_new(rb_obj_class(obj), &t); } +static VALUE +conmode_get_min(VALUE obj) +{ + conmode *t = rb_check_typeddata(obj, &conmode_type); +#ifdef VMIN + return INT2FIX(t->c_cc[VMIN]); +#else + (void)t; + return Qnil; +#endif +} + +static VALUE +conmode_set_min(VALUE obj, VALUE min) +{ + conmode *t = rb_check_typeddata(obj, &conmode_type); + int vmin = NIL_P(min) ? 1 : clamp_uchar(NUM2INT(min)); +#ifdef VMIN + t->c_cc[VMIN] = vmin; +#else + (void)t; + (void)vmin; +#endif + return min; +} + +static VALUE +conmode_get_time(VALUE obj) +{ + conmode *t = rb_check_typeddata(obj, &conmode_type); +#ifdef VTIME + return rb_rational_new(INT2FIX(t->c_cc[VTIME]), INT2FIX(10)); +#else + (void)t; + return Qnil; +#endif +} + +static VALUE +conmode_set_time(VALUE obj, VALUE time) +{ + conmode *t = rb_check_typeddata(obj, &conmode_type); + int vtime = NIL_P(time) ? 0 : clamp_uchar(to_vtime(time)); +#ifdef VTIME + t->c_cc[VTIME] = vtime; +#else + (void)t; + (void)vtime; +#endif + return time; +} + #ifdef _WIN32 /* * call-seq: @@ -2508,9 +2581,15 @@ InitVM_console(void) rb_define_alloc_func(cConmode, conmode_alloc); rb_undef_method(cConmode, "initialize"); rb_define_method(cConmode, "initialize_copy", conmode_init_copy, 1); + rb_define_method(cConmode, "echo?", conmode_get_echo, 0); + rb_define_method(cConmode, "echo", conmode_get_echo, 0); rb_define_method(cConmode, "echo=", conmode_set_echo, 1); rb_define_method(cConmode, "raw!", conmode_set_raw, -1); rb_define_method(cConmode, "raw", conmode_raw_new, -1); + rb_define_method(cConmode, "min", conmode_get_min, 0); + rb_define_method(cConmode, "min=", conmode_set_min, 1); + rb_define_method(cConmode, "time", conmode_get_time, 0); + rb_define_method(cConmode, "time=", conmode_set_time, 1); #ifdef _WIN32 rb_define_method(cConmode, "virtual_terminal_processing?", conmode_virtual_terminal_processing_p, 0); rb_define_method(cConmode, "virtual_terminal_processing=", conmode_set_virtual_terminal_processing, 1); diff --git a/jruby/lib/io/console/backend/ffi/termios.rb b/jruby/lib/io/console/backend/ffi/termios.rb index 4528e5f..5349db5 100644 --- a/jruby/lib/io/console/backend/ffi/termios.rb +++ b/jruby/lib/io/console/backend/ffi/termios.rb @@ -102,13 +102,19 @@ def ttymode_yield(block, **opts, &setup) end private :ttymode_yield - TTY_RAW = Proc.new do |t, min: 1, time: nil, intr: nil| + TTY_SET_MIN = Proc.new do |t, min| + t[:c_cc][LibC::VMIN] = min ? min.to_i.clamp(0, 255) : 1 + end + + TTY_SET_TIME = Proc.new do |t, time| + t[:c_cc][LibC::VTIME] = time ? (time * 10).to_i.clamp(0, 255) : 0 + end + + TTY_RAW = Proc.new do |t, min: nil, time: nil, intr: nil| LibC.cfmakeraw(t.pointer) t[:c_lflag] &= ~(LibC::ECHOE|LibC::ECHOK) - if min && min >= 0 - t[:c_cc][LibC::VMIN] = min - end - t[:c_cc][LibC::VTIME] = ((time || 0) * 10).to_i + TTY_SET_MIN[t, min] + TTY_SET_TIME[t, time] if intr t[:c_iflag] |= LibC::BRKINT t[:c_lflag] |= LibC::ISIG @@ -116,7 +122,7 @@ def ttymode_yield(block, **opts, &setup) end end - def raw(*, min: 1, time: nil, intr: nil, &block) + def raw(*, min: nil, time: nil, intr: nil, &block) ttymode_yield(block, min:, time:, intr:, &TTY_RAW) end @@ -128,7 +134,7 @@ def raw!(*, min: 1, time: nil, intr: nil) TTY_COOKED = Proc.new do |t| t[:c_iflag] |= (LibC::BRKINT|LibC::ISTRIP|LibC::ICRNL|LibC::IXON) t[:c_oflag] |= LibC::OPOST - t[:c_lflag] |= (LibC::ECHO|LibC::ECHOE|LibC::ECHOK|LibC::ECHONL|LibC::ICANON|LibC::ISIG|LibC::IEXTEN) + t[:c_lflag] |= (TTY_ECHO|LibC::ICANON|LibC::ISIG|LibC::IEXTEN) end def cooked(*, &block) @@ -158,6 +164,8 @@ def noecho(&block) ttymode_yield(block) { |t| t[:c_lflag] &= ~(TTY_ECHO) } end + private_constant :TTY_SET_TIME, :TTY_SET_MIN, :TTY_RAW, :TTY_COOKED, :TTY_ECHO + class Mode attr_reader :termios @@ -169,6 +177,11 @@ def initialize_copy(m) @termios = m.termios.dup end + def echo? + !(@termios[:c_lflag] & TTY_ECHO).zero? + end + alias echo echo? + def echo=(echo) if echo @termios[:c_lflag] |= TTY_ECHO @@ -187,6 +200,22 @@ def raw(min: 1, time: nil, intr: nil) TTY_RAW[new_mode.termios, min:, time:, intr:] new_mode end + + def min + @termios[:c_cc][LibC::VMIN] + end + + def min=(min) + TTY_SET_MIN[@termios, min] + end + + def time + @termios[:c_cc][LibC::VTIME].quo(10) + end + + def time=(time) + TTY_SET_TIME[@termios, time] + end end def console_mode diff --git a/jruby/lib/io/console/backend/ffi/windows.rb b/jruby/lib/io/console/backend/ffi/windows.rb index 79b9a47..1cef04e 100644 --- a/jruby/lib/io/console/backend/ffi/windows.rb +++ b/jruby/lib/io/console/backend/ffi/windows.rb @@ -118,11 +118,30 @@ def tty?(*types) IO.prepend(IO::Console::Windows::TTY) -class IO::ConsoleMode +class IO::Console::Mode def initialize(mode) @mode = mode end + def echo? + end + alias echo echo? + + def echo=(echo) + end + + def min + end + + def min=(min) + end + + def time + end + + def time=(time) + end + def virtual_terminal_processing? @mode & IO::Console::Windows::ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0 end @@ -151,7 +170,7 @@ def wrap_at_eol_output=(enabled) class IO def console_mode - IO::ConsoleMode.new(IO::Console::Windows.console_mode(self)) + IO::Console::Mode.new(IO::Console::Windows.console_mode(self)) end def console_mode=(mode) diff --git a/jruby/lib/io/console/backend/stty.rb b/jruby/lib/io/console/backend/stty.rb index 4ef97e3..b9af183 100644 --- a/jruby/lib/io/console/backend/stty.rb +++ b/jruby/lib/io/console/backend/stty.rb @@ -8,37 +8,97 @@ module IO::Console warn "io/console on JRuby shells out to stty for most operations" if $VERBOSE class IO::Console::Mode - def initialize(saved) - @saved = saved + STTY_PATTERNS = / + (?[a-z]\w*)\s(?:=\s(?:(?\d+)|(?\^.|.)|)|(?\d+)(?:\s+\w+)?); | + (?\d+)\s+(?[a-z]\w*); | + (?-)?(?[a-z]\w*)(?=\s|$) + /x + private_constant :STTY_PATTERNS + + def initialize(saved, attrs) + @saved = saved.chomp + @attrs = {} + attrs.scan(STTY_PATTERNS) do + m = $~ + case attr = m[:a] + when "echo" + @attrs[attr.to_sym] = !m[:f] + when "min", "time" + @attrs[attr.to_sym] = m[:n]&.to_i + when *%w[brkint icanon isig opost] + @attrs[attr.to_sym] = !m[:f] + end + end @args = [] + @changes = {} end def initialize_copy(mode) super - @saved = mode.__send__(:saved).dup - @args = mode.__send__(:args).dup + @attrs = @attrs.dup + @saved = @saved.dup + @args = @args.dup + @changes = @changes.dup + end + + def arguments + [ + @saved, + *@args, + *@changes.flat_map {|a, v| + case v + when true + a.to_s + when false + "-#{a}" + when nil + else + [a.to_s, v.to_s] + end + }, + ] + end + + def echo? + @changes.fetch(:echo) {@attrs[:echo]} end + alias echo echo? def echo=(echo) - @args << (echo ? 'echo' : '-echo') - echo + @changes[:echo] = echo end - def raw(min: 1, time: nil, intr: nil) + def raw(min: nil, time: nil, intr: nil) dup.raw!(min:, time:, intr:) end - def raw!(min: 1, time: nil, intr: nil) + def raw!(min: nil, time: nil, intr: nil) @args << 'raw' - @args.push('min', min.to_s) if min && min >= 0 - @args.push('time', ((time || 0) * 10).to_i.to_s) - @args.concat(%w[brkint isig opost]) if intr + self.min = min + self.time = time + %i[brkint isig opost].each {|a| @changes[a] = true} if intr self end + def min + @changes.fetch(:min) {@attrs[:min]} + end + + def min=(min) + @changes[:min] = (min || 1).to_i.clamp(0, 255) + end + + def time + @changes.fetch(:time) {@attrs[:time]}&.quo(10) + end + + def time=(time) + @changes[:time] = ((time || 0) * 10).to_i.clamp(0, 255) + end + private - attr_reader :saved, :args + attr_reader :attrs, :saved, :args, :changes end # Non-Windows assumes stty command is available @@ -106,20 +166,20 @@ def noecho end def console_mode - Console::Mode.new(_io_console_stty('-g').chomp) + Console::Mode.new(_io_console_stty('-g'), _io_console_stty('-a')) end def console_mode=(mode) - _io_console_stty(mode.__send__(:saved), *mode.__send__(:args)) + _io_console_stty(*mode.arguments) mode end - # Not all systems return same format of stty -a output - IEEE_STD_1003_2 = '(?\d+) rows; (?\d+) columns' - UBUNTU = 'rows (?\d+); columns (?\d+)' - def winsize - match = _io_console_stty('-a').match(/#{IEEE_STD_1003_2}|#{UBUNTU}/o) + # Pattern for rows/columns; all systems may not return in these formats. + match = / + (?\d+)\s+rows;\s*(?\d+)\s+columns | + rows\s+(?\d+);\s*columns\s+(?\d+) + /x.match(_io_console_stty('-a')) [match[:rows].to_i, match[:columns].to_i] end @@ -128,7 +188,7 @@ def winsize=(size) sizelen = size.size if sizelen != 2 && sizelen != 4 - raise ArgumentError.new("wrong number of arguments (given #{sizelen}, expected 2 or 4)") + raise ArgumentError, "wrong number of arguments (given #{sizelen}, expected 2 or 4)" end if sizelen == 4 diff --git a/jruby/lib/io/console/backend/stub.rb b/jruby/lib/io/console/backend/stub.rb index 39a4876..36a20c8 100644 --- a/jruby/lib/io/console/backend/stub.rb +++ b/jruby/lib/io/console/backend/stub.rb @@ -27,6 +27,18 @@ def noecho yield self end + def min + end + + def min=(n) + end + + def time + end + + def time=(t) + end + def winsize [25, 80] end diff --git a/test/io/console/test_io_console.rb b/test/io/console/test_io_console.rb index af207c8..1878264 100644 --- a/test/io/console/test_io_console.rb +++ b/test/io/console/test_io_console.rb @@ -77,6 +77,18 @@ def test_bad_keyword TTY_ENHANCED = IO.instance_method(:tty?).arity != 0 TTY_MODE_STTY = IO.private_method_defined?(:_io_console_stty) + def test_stty_mode_arguments + omit "stty backend only" unless TTY_MODE_STTY + + mode = IO::Console::Mode.new( + "saved\n", + "echo icanon isig opost; min = 1; time = 0;", + ) + mode.raw!(min: 2, time: 0.3) + + assert_equal(["saved", "raw", "min", "2", "time", "3"], mode.arguments) + end + def test_tty? pend "not supported" unless TTY_ENHANCED @@ -294,8 +306,10 @@ def test_console_mode assert_same(IO::Console::Mode, IO.const_get(:ConsoleMode)) end + assert_predicate(original, :echo?) noecho = original.dup noecho.echo = false + assert_not_predicate(noecho, :echo?) assert_same(noecho, s.send(:console_mode=, noecho)) assert_not_predicate(s, :echo?) @@ -305,6 +319,15 @@ def test_console_mode assert_same(raw, s.send(:console_mode=, raw)) s.print "raw\n" assert_equal("raw\n", m.gets) + + if min = s.console_mode.min + assert_equal(1, min) + assert_equal(2, s.raw(min: 2) {s.console_mode.min}) + end + if time = s.console_mode.time + assert_equal(0, time) + assert_equal(3.1, s.raw(time: 3.14r) {s.console_mode.time}) + end ensure s.console_mode = original if original end