ruby/ext/io/console/console.c
aycabta 0057fe4063
Ruby 2.7 backport about IRB (#2990)
* [ruby/reline] Sort completion list

#### Legacy mode:

  ```console
  $ irb --legacy
  irb(main):001:0> l[TAB][TAB]
  lambda           load             local_variables  loop
  ```

#### Before this patch:

  ```console
  $ irb
  irb(main):001:0> l[TAB][TAB]
  local_variables
  loop
  lambda
  load
  ```

#### After this patch:

  ```console
  $ irb
  irb(main):001:0> l[TAB][TAB]
  lambda
  load
  local_variables
  loop
  ```

6074069c7d

* Drop an invalid char as UTF-8

* Add test_completion_with_indent_and_completer_quote_characters

This is for 8a705245e5.

* [ruby/irb] Add tests for RubyLex

The set_auto_indent method calculates the correct number of spaces for
indenting a line. We think there might be a few bugs in this method so
we are testing the current functionality to make sure nothing breaks
when we address those bugs.

Example test failure:

```
  1) Failure:
TestIRB::TestRubyLex#test_auto_indent [/Users/Ben/Projects/irb/test/irb/test_ruby_lex.rb:75]:
Calculated the wrong number of spaces for:
 def each_top_level_statement
  initialize_input
  catch(:TERM_INPUT) do
    loop do
      begin
        prompt
        unless l = lex
          throw :TERM_INPUT if @line == ''
        else
.
<10> expected but was
<12>.
```

752d5597ab

* [ruby/reline] Degenerate the terminal size to [$LINES, $COLUMNS] if it is unknown

This is a workaround for https://github.com/ruby/irb/issues/50

5725677d1a

* [ruby/irb] Fix newline depth with multiple braces

This commit fixes the check_newline_depth_difference method to multiple
open braces on one line into account. Before this change we were
subtracting from the depth in check_newline_depth_difference on
every open brace. This is the right thing to do if the opening and
closing brace are on the same line. For example in a method definition we
have an opening and closing parentheses we want to add 1 to our depth,
and then remove it.

```
def foo()
end
```

However this isn't the correct behavior when the brace spans multiple
lines. If a brace spans multiple lines we don't want to subtract from
check_newline_depth_difference and we want to treat the braces the same
way as we do `end` and allow check_corresponding_token_depth to pop the
correct depth.

Example of bad behavior:

```
def foo()
  [
  ]
puts 'bar'
end
```

Example of desired behavior:

```
def foo()
  [
  ]
  puts 'bar'
end
```

7dc8af01e0

* text/readline/test_readline.rb - fix skip on Reline (#2743)

TestRelineAsReadline#test_input_metachar passes on MinGW

* Add "require 'openstruct'" what is forgotten

* [ruby/irb] Fix lib name of OpenStruct

1f3a84ab6b

* Add load path and require for ruby/ruby

* Rescue EOFError

If C-d is pressed before IRB is ready, IRB crashes because EOFError occurs.

* Complete indented and quoted string correctly

  def foo
    ''.upca[TAB]

This will be completed to be:

  def foo
  ''.upcase

The indent was gone. This commit fixes the bug.

* [ruby/irb] Fix crashing when multiple open braces per line

https://github.com/ruby/irb/issues/55

If we had put multiple open braces on a line the with no closing brace
spaces_of_nest array keeps getting '0' added to it. This means that when
we pop off of this array we are saying that we should be in position zero
for the next line. This is an issue because we don't always want to be
in position 0 after a closing brace.

Example:
```
[[[
]
]
]
```
In the above example the 'spaces_of_nest' array looks like this after
the first line is entered: [0,0,0]. We really want to be indented 4
spaces for the 1st closing brace 2 for the 2nd and 0 for the 3rd. i.e.
we want it to be: [0,2,4].

We also saw this issue with a heredoc inside of an array.

```
[<<FOO]
hello
FOO
```

80c69c8272

* Support history-size in .inputrc correctly

* Introduce an abstracted structure about the encoding of Reline

The command prompt on Windows always uses Unicode to take input and print
output but most Reline implementation depends on Encoding.default_external.
This commit introduces an abstracted structure about the encoding of Reline.

* Remove an unused setting variable

* Use Reline.encoding_system_needs if exists

* Add tests for vi_insert and vi_add

* Implement vi_insert_at_bol and vi_add_at_eol

* [ruby/reline] Implement vi_to_next_char

066ecb0a21

* [ruby/reline] Implement vi_prev_char and vi_to_prev_char

0ad3ee63fa

* [ruby/readline-ext] Include ruby/assert.h in ruby/ruby.h so that assertions can be there

4d44c12832

* Stop using minitest dependent methods

* Skip a test that uses assert_ruby_status if it doesn't exist

* Use omit instead of skip

* Check DONT_RUN_RELINE_TEST envvar

* [ruby/irb] Add newline_before_multiline_output

9eb1801a66

* [ruby/irb] Fix compatibility with rails before 5.2

Rails before 5.2 added Array#append as an alias to Array#<< ,
so that it expects only one argument.
However ruby-2.5 added Array#append as an alias to Array#push
which takes any number of arguments.

If irb completion is used in `rails c` (for example "IO.<tab>")
it fails with:
  irb/completion.rb:206:in `<<': wrong number of arguments (given 3, expected 1) (ArgumentError)

Using Array#push instead of Array#append fixes compatibility.

5b7bbf9c34

* Reline: Use a more robust detection of MinTTY

The previous detection per get_screen_size fails when stdout is passed
to a pipe. That is the case when running ruby tests in parallel ("-j" switch).
In this case Reline believes that it's running on MinTTY and the tests
are running with ANSI IOGate instead of the Windows adapter on MINGW.
So parallel test results were different to that of a single process.
This commit fixes these differencies.

The code is taken from git sources and translated to ruby.
NtQueryObject() is replaced by GetFileInformationByHandleEx(), because
NtQueryObject() is undocumented and is more difficult to use:
  c5a03b1e29/compat/winansi.c (L558)

* Reline: Fix changed test results due to change to UTF-8 on Windows

In commit f8ea2860b0 the Reline encoding
for native windows console was changed to hardcoded UTF-8.
This caused failures in reline and readline tests, but they were hidden,
because parallel ruby tests incorrectly used Reline::ANSI as IOGate.
Tests failures were raised in single process mode, but not with -j switch.

This patch corrects encodings on native Windows console.

* [ruby/irb] [ruby/irb] Rewrite an expression to detect multiline

ed5cf375a6

5b7bbf9c34

* [ruby/reline] Implement vi_change_meta

8538e0e10f

* Always refer to Reline::IOGate.encoding

* Always use UTF-8 for Reline::GeneralIO on Windows

* Use test_mode on Reline::History::Test for encoding

* [ruby/reline] Support GNOME style Home/End key sequences [Bug #16510]

788f0df845

* [ruby/irb] Add a new easter egg: dancing ruby

e37dc7e58e

* [ruby/irb] Exclude useless files from RDoc

8f1ab2400c

* [ruby/irb] Exclude useless files from RDoc

* Fix inaccuracy in encoding tests

These tests assume
  Encoding.find('locale') == Encoding.find('external')
and fail if they are distinct.

* [ruby/reline] Fix Reline::Windows#scroll_down

I mistook Right and Bottom.

8be401c5f5

* [ruby/reline] Bypass cursor down when a char is rendered at eol on Windows

A newline is automatically inserted if a character is rendered at eol on
Windows command prompt.

4bfea07e4a

* [ruby/reline] Organize special keys escape sequences

41deb1a3d9

* [ruby/readline-ext] Remove unnecessary -I$(top_srcdir) when it's an individual gem

efaca4a5f4

* [ruby/readline-ext] Check TestRelineAsReadline existance

c0a6303168

* [ruby/readline-ext] The ruby/assert.h is adopted by Ruby 2.7 or later

106c31fc1b

* Revert "[ruby/readline-ext] Include ruby/assert.h in ruby/ruby.h so that assertions can be there"

This reverts commit 425b2064d3.

This cherry-pick was a mistake.

* [ruby/readline-ext] Use require check instead of DONT_RUN_RELINE_TEST env

1df99d1481

* [ruby/readline-ext] Add spec.extensions

8c33abb13c

* [ruby/readline-ext] Use rake/extensiokntask to build

b0b5f709bd

* Fix readline build dependency

* [ruby/irb] Add test_complete_symbol

dbbf086c1f

* [ruby/irb] Check doc namespace correctly

IRB::InputCompletor::PerfectMatchedProc crashes when doc not found because a
variable name was incorrect.

889fd4928f

* [ruby/irb] Fix auto indent with closed brace

A closed brace in auto-indent shouldn't affect the next brace in the same line,
but it behaves like below:

  p() {
    }

It's a bug.

fbe59e344f

* [ruby/irb] Use 0.step instead of (..0).each for Ruby 2.5

5d628ca40e

* Revert "[ruby/irb] Add test_complete_symbol"

This reverts commit 3af3431c2c.

* [ruby/irb] fix reserved words and completion for them

6184b227ad

* Add test_complete_symbol

The previous version of the test method used a symbol, ":abcdefg" to complete
but longer symbols that can be completed are defined by other test methods of
other libs.

* test/irb/test_completion.rb: suppress a warning: unused literal ignored

* [ruby/reline] Use IO#write instead of IO#print

IO#print always adds a string of $\ automatically.

a93119c847

* [ruby/irb] Version 1.2.2

a71753f15a

* [ruby/reline] Version 0.1.3

ea2b182466

* [ruby/irb] Include easter-egg.rb in gemspec

`irb` doesn't run because this file isn't included in the gem.
73cda56d25

* [ruby/irb] Version 1.2.3

dd56e06df5

* support multi-run test for test_readline.rb

* [ruby/irb] `yield` outside method definition is a syntax error

dbc7b059c7

* test/readline - allow ENV control of test class creation

In ruby/ruby, the tests run on both readline & reline by creating four test classes:
```
TestReadline
TestReadlineHistory

TestRelineAsReadline
TestRelineAsReadlineHistory
```

Reline inports the test files and uses them in its CI.  Adding the ENV control allows it to only run the `TestRelineAsReadline` classes.

* Omit test_using_quoting_detection_proc_with_multibyte_input temporarily for random order test

* support random order test.

test_readline:
  HISTORY should be empty.

test_using_quoting_detection_proc:
test_using_quoting_detection_proc_with_multibyte_input:
  Readline.completer_quote_characters= and
  Readline.completer_word_break_characters= doesn't accept nil,
  so skip if previous values are nil.

* Set Readline.completion_append_character = nil always

GNU Readline add a white space when Readline.completion_append_character is
not initialized.

* Fix a typo [ci skip]

* skip test if Reline.completion_proc is nil.

Some other tests can set Reline.completion_proc, so if it is nil,
simply skip this test.

* Reset Reline.point

TestRelineAsReadline#test_insert_text expects Readline.point == 0
at the beginning of the test, but a test violate this assumption.

* Convert incompatible encoding symbol names

* Ignore incompatible convert of symbols

* Add workaround for test-bundler failure

500526558 (step):16:127
```
Failures:

  1) Bundler.setup when Bundler is bundled doesn't blow up
     Failure/Error: expect(err).to be_empty

       expected `"fatal: not a git repository (or any of the parent directories): .git\nfatal: not a git repository (o...the parent directories): .git\nfatal: not a git repository (or any of the parent directories): .git".empty?` to return true, got false

       Commands:
       $ /home/runner/work/actions/actions/snapshot-master/ruby \
         -I/home/runner/work/actions/actions/snapshot-master/lib:/home/runner/work/actions/actions/snapshot-master/spec/bundler \
         -rsupport/hax -rsupport/artifice/fail \
         /home/runner/work/actions/actions/snapshot-master/libexec/bundle install --retry 0
       Resolving dependencies...
       Using bundler 2.1.4
       Bundle complete! 1 Gemfile dependency, 1 gem now installed.
       Use `bundle info [gemname]` to see where a bundled gem is installed.
       fatal: not a git repository (or any of the parent directories): .git
       fatal: not a git repository (or any of the parent directories): .git
       fatal: not a git repository (or any of the parent directories): .git
       # $? => 0

       $ /home/runner/work/actions/actions/snapshot-master/ruby \
         -I/home/runner/work/actions/actions/snapshot-master/lib:/home/runner/work/actions/actions/snapshot-master/spec/bundler \
         -rsupport/hax -rsupport/artifice/fail \
         /home/runner/work/actions/actions/snapshot-master/libexec/bundle exec ruby -e \
         require\ \'bundler\'\;\ Bundler.setup
       fatal: not a git repository (or any of the parent directories): .git
       fatal: not a git repository (or any of the parent directories): .git
       fatal: not a git repository (or any of the parent directories): .git
       # $? => 0
     # ./spec/bundler/runtime/setup_spec.rb:1056:in `block (3 levels) in <top (required)>'
     # ./spec/bundler/spec_helper.rb:111:in `block (3 levels) in <top (required)>'
     # ./spec/bundler/spec_helper.rb:111:in `block (2 levels) in <top (required)>'
     # ./spec/bundler/spec_helper.rb:78:in `block (2 levels) in <top (required)>'
make: *** [yes-test-bundler] Error 1
```

* [ruby/irb] Unnamed groups are not captured when named groups are used

0a641a69b0

* [ruby/reline] Work with wrong $/ value correctly

962ebf5a1b

* [ruby/irb] Detect multiple lines output simplify

The old implementation performance test code:

    require 'objspace'
    puts "%.5g MB" % (ObjectSpace.memsize_of_all * 0.001 * 0.001)
    /\A.*\Z/ !~ ('abc' * 20_000_000)
    puts "%.5g MB" % (ObjectSpace.memsize_of_all * 0.001 * 0.001)

and run `time test.rb`:

    2.5868 MB
    62.226 MB

    real    0m1.307s
    user    0m0.452s
    sys     0m0.797s

The new implementation performance test code:

    require 'objspace'
    puts "%.5g MB" % (ObjectSpace.memsize_of_all * 0.001 * 0.001)
    ('abc' * 20_000_000).include?("\n")
    puts "%.5g MB" % (ObjectSpace.memsize_of_all * 0.001 * 0.001)

and run `time test.rb`:

    2.5861 MB
    62.226 MB

    real    0m0.132s
    user    0m0.088s
    sys     0m0.042s

40d6610baf

* [ruby/reline] Suppress error in case INPUTRC env is empty

bce7e7562b

* [ruby/reline] Add yamatanooroti rendering test

f092519525

* [ruby/reline] Rename test suite name of yamatanooroti test

b0f32f5de4

* [ruby/reline] Add a comment why rescue yamatanooroti loading error on the test

2a8061daec

* [ruby/irb] Suppress crashing when EncodingError has occurred without lineno

13572d8cdc

* [ruby/reline] Suppress error when check ambiguous char width in LANG=C

623dffdd75

* [ruby/io-console] Enable only interrupt bits on `intr: true`

baaf929041

* [ruby/io-console] bump up to 0.5.4

* [ruby/io-console] Update the minimum requirement of Ruby version

73e7b6318a

* [ruby/io-console] Filter Ruby engine name rather than just /ruby/

This breaks tests using this path on JRuby because the `jruby`
executable turns into `jjruby` after the sub.

e5951aa34c

* [ruby/io-console] bump up to 0.5.5

* [ruby/io-console] Prefer keyword arguments

5facbfc4c8

* [ruby/io-console] [DOC] Improved about `intr:`

82b630cd79

* [ruby/io-console] Just ignore the extension on other than CRuby

41b6f09574

* [ruby/io-console] bump up to 0.5.6

Co-authored-by: KOBAYASHI Shuji <shuujii@gmail.com>
Co-authored-by: Ben <kanobt61@gmail.com>
Co-authored-by: Yusuke Endoh <mame@ruby-lang.org>
Co-authored-by: MSP-Greg <MSP-Greg@users.noreply.github.com>
Co-authored-by: Nobuyoshi Nakada <nobu@ruby-lang.org>
Co-authored-by: Kenta Murata <mrkn@mrkn.jp>
Co-authored-by: Lars Kanis <lars@greiz-reinsdorf.de>
Co-authored-by: Lars Kanis <kanis@comcard.de>
Co-authored-by: Alan Wu <XrXr@users.noreply.github.com>
Co-authored-by: Hiroshi SHIBATA <hsbt@ruby-lang.org>
Co-authored-by: Nobuhiro IMAI <nov@yo.rim.or.jp>
Co-authored-by: Nick Lewis <nick@puppet.com>
Co-authored-by: S-H-GAMELINKS <gamelinks007@gmail.com>
Co-authored-by: Koichi Sasada <ko1@atdot.net>
Co-authored-by: Kazuhiro NISHIYAMA <zn@mbf.nifty.com>
Co-authored-by: Charles Oliver Nutter <headius@headius.com>
2020-03-30 19:09:50 +09:00

1661 lines
38 KiB
C

/* -*- c-file-style: "ruby" -*- */
/*
* console IO module
*/
#include "ruby.h"
#include "ruby/io.h"
#include "ruby/thread.h"
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#ifdef HAVE_FCNTL_H
#include <fcntl.h>
#endif
#ifdef HAVE_SYS_IOCTL_H
#include <sys/ioctl.h>
#endif
#if defined HAVE_TERMIOS_H
# include <termios.h>
typedef struct termios conmode;
static int
setattr(int fd, conmode *t)
{
while (tcsetattr(fd, TCSANOW, t)) {
if (errno != EINTR) return 0;
}
return 1;
}
# define getattr(fd, t) (tcgetattr(fd, t) == 0)
#elif defined HAVE_TERMIO_H
# include <termio.h>
typedef struct termio conmode;
# define setattr(fd, t) (ioctl(fd, TCSETAF, t) == 0)
# define getattr(fd, t) (ioctl(fd, TCGETA, t) == 0)
#elif defined HAVE_SGTTY_H
# include <sgtty.h>
typedef struct sgttyb conmode;
# ifdef HAVE_STTY
# define setattr(fd, t) (stty(fd, t) == 0)
# else
# define setattr(fd, t) (ioctl((fd), TIOCSETP, (t)) == 0)
# endif
# ifdef HAVE_GTTY
# define getattr(fd, t) (gtty(fd, t) == 0)
# else
# define getattr(fd, t) (ioctl((fd), TIOCGETP, (t)) == 0)
# endif
#elif defined _WIN32
#include <winioctl.h>
#include <conio.h>
typedef DWORD conmode;
#define LAST_ERROR rb_w32_map_errno(GetLastError())
#define SET_LAST_ERROR (errno = LAST_ERROR, 0)
static int
setattr(int fd, conmode *t)
{
int x = SetConsoleMode((HANDLE)rb_w32_get_osfhandle(fd), *t);
if (!x) errno = LAST_ERROR;
return x;
}
static int
getattr(int fd, conmode *t)
{
int x = GetConsoleMode((HANDLE)rb_w32_get_osfhandle(fd), t);
if (!x) errno = LAST_ERROR;
return x;
}
#endif
#ifndef SET_LAST_ERROR
#define SET_LAST_ERROR (0)
#endif
static ID id_getc, id_console, id_close, id_min, id_time, id_intr;
#if ENABLE_IO_GETPASS
static ID id_gets;
#endif
#ifndef HAVE_RB_F_SEND
static ID id___send__;
static VALUE
rb_f_send(int argc, VALUE *argv, VALUE recv)
{
VALUE sym = argv[0];
ID vid = rb_check_id(&sym);
if (vid) {
--argc;
++argv;
}
else {
vid = id___send__;
}
return rb_funcallv(recv, vid, argc, argv);
}
#endif
typedef struct {
int vmin;
int vtime;
int intr;
} rawmode_arg_t;
static rawmode_arg_t *
rawmode_opt(int *argcp, VALUE *argv, int min_argc, int max_argc, rawmode_arg_t *opts)
{
int argc = *argcp;
rawmode_arg_t *optp = NULL;
VALUE vopts = Qnil;
#ifdef RB_SCAN_ARGS_PASS_CALLED_KEYWORDS
argc = rb_scan_args(argc, argv, "*:", NULL, &vopts);
#else
if (argc > min_argc) {
vopts = rb_check_hash_type(argv[argc-1]);
if (!NIL_P(vopts)) {
argv[argc-1] = vopts;
vopts = rb_extract_keywords(&argv[argc-1]);
if (!argv[argc-1]) *argcp = --argc;
if (!vopts) vopts = Qnil;
}
}
#endif
rb_check_arity(argc, min_argc, max_argc);
if (!NIL_P(vopts)) {
VALUE vmin = rb_hash_aref(vopts, ID2SYM(id_min));
VALUE vtime = rb_hash_aref(vopts, ID2SYM(id_time));
VALUE intr = rb_hash_aref(vopts, ID2SYM(id_intr));
/* default values by `stty raw` */
opts->vmin = 1;
opts->vtime = 0;
opts->intr = 0;
if (!NIL_P(vmin)) {
opts->vmin = NUM2INT(vmin);
optp = opts;
}
if (!NIL_P(vtime)) {
VALUE v10 = INT2FIX(10);
vtime = rb_funcall3(vtime, '*', 1, &v10);
opts->vtime = NUM2INT(vtime);
optp = opts;
}
switch (intr) {
case Qtrue:
opts->intr = 1;
optp = opts;
break;
case Qfalse:
opts->intr = 0;
optp = opts;
break;
case Qnil:
break;
default:
rb_raise(rb_eArgError, "true or false expected as intr: %"PRIsVALUE,
intr);
}
}
return optp;
}
static void
set_rawmode(conmode *t, void *arg)
{
#ifdef HAVE_CFMAKERAW
cfmakeraw(t);
t->c_lflag &= ~(ECHOE|ECHOK);
#elif defined HAVE_TERMIOS_H || defined HAVE_TERMIO_H
t->c_iflag &= ~(IGNBRK|BRKINT|IGNPAR|PARMRK|INPCK|ISTRIP|INLCR|IGNCR|ICRNL|IXON|IXOFF|IXANY|IMAXBEL);
t->c_oflag &= ~OPOST;
t->c_lflag &= ~(ECHO|ECHOE|ECHOK|ECHONL|ICANON|ISIG|IEXTEN|XCASE);
t->c_cflag &= ~(CSIZE|PARENB);
t->c_cflag |= CS8;
t->c_cc[VMIN] = 1;
t->c_cc[VTIME] = 0;
#elif defined HAVE_SGTTY_H
t->sg_flags &= ~ECHO;
t->sg_flags |= RAW;
#elif defined _WIN32
*t = 0;
#endif
if (arg) {
const rawmode_arg_t *r = arg;
#ifdef VMIN
if (r->vmin >= 0) t->c_cc[VMIN] = r->vmin;
#endif
#ifdef VTIME
if (r->vtime >= 0) t->c_cc[VTIME] = r->vtime;
#endif
#ifdef ISIG
if (r->intr) {
t->c_iflag |= BRKINT;
t->c_lflag |= ISIG;
t->c_oflag |= OPOST;
}
#endif
(void)r;
}
}
static void
set_cookedmode(conmode *t, void *arg)
{
#if defined HAVE_TERMIOS_H || defined HAVE_TERMIO_H
t->c_iflag |= (BRKINT|ISTRIP|ICRNL|IXON);
t->c_oflag |= OPOST;
t->c_lflag |= (ECHO|ECHOE|ECHOK|ECHONL|ICANON|ISIG|IEXTEN);
#elif defined HAVE_SGTTY_H
t->sg_flags |= ECHO;
t->sg_flags &= ~RAW;
#elif defined _WIN32
*t |= ENABLE_ECHO_INPUT|ENABLE_LINE_INPUT|ENABLE_PROCESSED_INPUT;
#endif
}
static void
set_noecho(conmode *t, void *arg)
{
#if defined HAVE_TERMIOS_H || defined HAVE_TERMIO_H
t->c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL);
#elif defined HAVE_SGTTY_H
t->sg_flags &= ~ECHO;
#elif defined _WIN32
*t &= ~ENABLE_ECHO_INPUT;
#endif
}
static void
set_echo(conmode *t, void *arg)
{
#if defined HAVE_TERMIOS_H || defined HAVE_TERMIO_H
t->c_lflag |= (ECHO | ECHOE | ECHOK | ECHONL);
#elif defined HAVE_SGTTY_H
t->sg_flags |= ECHO;
#elif defined _WIN32
*t |= ENABLE_ECHO_INPUT;
#endif
}
static int
echo_p(conmode *t)
{
#if defined HAVE_TERMIOS_H || defined HAVE_TERMIO_H
return (t->c_lflag & (ECHO | ECHONL)) != 0;
#elif defined HAVE_SGTTY_H
return (t->sg_flags & ECHO) != 0;
#elif defined _WIN32
return (*t & ENABLE_ECHO_INPUT) != 0;
#endif
}
static int
set_ttymode(int fd, conmode *t, void (*setter)(conmode *, void *), void *arg)
{
conmode r;
if (!getattr(fd, t)) return 0;
r = *t;
setter(&r, arg);
return setattr(fd, &r);
}
#define GetReadFD(fptr) ((fptr)->fd)
static inline int
get_write_fd(const rb_io_t *fptr)
{
VALUE wio = fptr->tied_io_for_writing;
rb_io_t *ofptr;
if (!wio) return fptr->fd;
GetOpenFile(wio, ofptr);
return ofptr->fd;
}
#define GetWriteFD(fptr) get_write_fd(fptr)
#define FD_PER_IO 2
static VALUE
ttymode(VALUE io, VALUE (*func)(VALUE), VALUE farg, void (*setter)(conmode *, void *), void *arg)
{
rb_io_t *fptr;
int status = -1;
int error = 0;
int fd[FD_PER_IO];
conmode t[FD_PER_IO];
VALUE result = Qnil;
GetOpenFile(io, fptr);
fd[0] = GetReadFD(fptr);
if (fd[0] != -1) {
if (set_ttymode(fd[0], t+0, setter, arg)) {
status = 0;
}
else {
error = errno;
fd[0] = -1;
}
}
fd[1] = GetWriteFD(fptr);
if (fd[1] != -1 && fd[1] != fd[0]) {
if (set_ttymode(fd[1], t+1, setter, arg)) {
status = 0;
}
else {
error = errno;
fd[1] = -1;
}
}
if (status == 0) {
result = rb_protect(func, farg, &status);
}
GetOpenFile(io, fptr);
if (fd[0] != -1 && fd[0] == GetReadFD(fptr)) {
if (!setattr(fd[0], t+0)) {
error = errno;
status = -1;
}
}
if (fd[1] != -1 && fd[1] != fd[0] && fd[1] == GetWriteFD(fptr)) {
if (!setattr(fd[1], t+1)) {
error = errno;
status = -1;
}
}
if (status) {
if (status == -1) {
rb_syserr_fail(error, 0);
}
rb_jump_tag(status);
}
return result;
}
#if !defined _WIN32
struct ttymode_callback_args {
VALUE (*func)(VALUE, VALUE);
VALUE io;
VALUE farg;
};
static VALUE
ttymode_callback(VALUE args)
{
struct ttymode_callback_args *argp = (struct ttymode_callback_args *)args;
return argp->func(argp->io, argp->farg);
}
static VALUE
ttymode_with_io(VALUE io, VALUE (*func)(VALUE, VALUE), VALUE farg, void (*setter)(conmode *, void *), void *arg)
{
struct ttymode_callback_args cargs;
cargs.func = func;
cargs.io = io;
cargs.farg = farg;
return ttymode(io, ttymode_callback, (VALUE)&cargs, setter, arg);
}
#endif
/*
* call-seq:
* io.raw(min: nil, time: nil, intr: nil) {|io| }
*
* Yields +self+ within raw mode, and returns the result of the block.
*
* STDIN.raw(&:gets)
*
* will read and return a line without echo back and line editing.
*
* The parameter +min+ specifies the minimum number of bytes that
* should be received when a read operation is performed. (default: 1)
*
* The parameter +time+ specifies the timeout in _seconds_ with a
* precision of 1/10 of a second. (default: 0)
*
* If the parameter +intr+ is +true+, enables break, interrupt, quit,
* and suspend special characters.
*
* Refer to the manual page of termios for further details.
*
* You must require 'io/console' to use this method.
*/
static VALUE
console_raw(int argc, VALUE *argv, VALUE io)
{
rawmode_arg_t opts, *optp = rawmode_opt(&argc, argv, 0, 0, &opts);
return ttymode(io, rb_yield, io, set_rawmode, optp);
}
/*
* call-seq:
* io.raw!(min: nil, time: nil, intr: nil) -> io
*
* Enables raw mode, and returns +io+.
*
* If the terminal mode needs to be back, use <code>io.raw { ... }</code>.
*
* See IO#raw for details on the parameters.
*
* You must require 'io/console' to use this method.
*/
static VALUE
console_set_raw(int argc, VALUE *argv, VALUE io)
{
conmode t;
rb_io_t *fptr;
int fd;
rawmode_arg_t opts, *optp = rawmode_opt(&argc, argv, 0, 0, &opts);
GetOpenFile(io, fptr);
fd = GetReadFD(fptr);
if (!getattr(fd, &t)) rb_sys_fail(0);
set_rawmode(&t, optp);
if (!setattr(fd, &t)) rb_sys_fail(0);
return io;
}
/*
* call-seq:
* io.cooked {|io| }
*
* Yields +self+ within cooked mode.
*
* STDIN.cooked(&:gets)
*
* will read and return a line with echo back and line editing.
*
* You must require 'io/console' to use this method.
*/
static VALUE
console_cooked(VALUE io)
{
return ttymode(io, rb_yield, io, set_cookedmode, NULL);
}
/*
* call-seq:
* io.cooked!
*
* Enables cooked mode.
*
* If the terminal mode needs to be back, use io.cooked { ... }.
*
* You must require 'io/console' to use this method.
*/
static VALUE
console_set_cooked(VALUE io)
{
conmode t;
rb_io_t *fptr;
int fd;
GetOpenFile(io, fptr);
fd = GetReadFD(fptr);
if (!getattr(fd, &t)) rb_sys_fail(0);
set_cookedmode(&t, NULL);
if (!setattr(fd, &t)) rb_sys_fail(0);
return io;
}
#ifndef _WIN32
static VALUE
getc_call(VALUE io)
{
return rb_funcallv(io, id_getc, 0, 0);
}
#else
static void *
nogvl_getch(void *p)
{
int len = 0;
wint_t *buf = p, c = _getwch();
switch (c) {
case WEOF:
break;
case 0x00:
case 0xe0:
buf[len++] = c;
c = _getwch();
/* fall through */
default:
buf[len++] = c;
break;
}
return (void *)(VALUE)len;
}
#endif
/*
* call-seq:
* io.getch(min: nil, time: nil, intr: nil) -> char
*
* Reads and returns a character in raw mode.
*
* See IO#raw for details on the parameters.
*
* You must require 'io/console' to use this method.
*/
static VALUE
console_getch(int argc, VALUE *argv, VALUE io)
{
rawmode_arg_t opts, *optp = rawmode_opt(&argc, argv, 0, 0, &opts);
#ifndef _WIN32
return ttymode(io, getc_call, io, set_rawmode, optp);
#else
rb_io_t *fptr;
VALUE str;
wint_t c;
int w, len;
char buf[8];
wint_t wbuf[2];
struct timeval *to = NULL, tv;
GetOpenFile(io, fptr);
if (optp) {
if (optp->vtime) {
to = &tv;
tv.tv_sec = optp->vtime / 10;
tv.tv_usec = (optp->vtime % 10) * 100000;
}
if (optp->vmin != 1) {
rb_warning("min option ignored");
}
if (optp->intr) {
w = rb_wait_for_single_fd(fptr->fd, RB_WAITFD_IN, to);
if (w < 0) rb_eof_error();
if (!(w & RB_WAITFD_IN)) return Qnil;
}
else {
rb_warning("vtime option ignored if intr flag is unset");
}
}
len = (int)(VALUE)rb_thread_call_without_gvl(nogvl_getch, wbuf, RUBY_UBF_IO, 0);
switch (len) {
case 0:
return Qnil;
case 2:
buf[0] = (char)wbuf[0];
c = wbuf[1];
len = 1;
do {
buf[len++] = (unsigned char)c;
} while ((c >>= CHAR_BIT) && len < (int)sizeof(buf));
return rb_str_new(buf, len);
default:
c = wbuf[0];
len = rb_uv_to_utf8(buf, c);
str = rb_utf8_str_new(buf, len);
return rb_str_conv_enc(str, NULL, rb_default_external_encoding());
}
#endif
}
/*
* call-seq:
* io.noecho {|io| }
*
* Yields +self+ with disabling echo back.
*
* STDIN.noecho(&:gets)
*
* will read and return a line without echo back.
*
* You must require 'io/console' to use this method.
*/
static VALUE
console_noecho(VALUE io)
{
return ttymode(io, rb_yield, io, set_noecho, NULL);
}
/*
* call-seq:
* io.echo = flag
*
* Enables/disables echo back.
* On some platforms, all combinations of this flags and raw/cooked
* mode may not be valid.
*
* You must require 'io/console' to use this method.
*/
static VALUE
console_set_echo(VALUE io, VALUE f)
{
conmode t;
rb_io_t *fptr;
int fd;
GetOpenFile(io, fptr);
fd = GetReadFD(fptr);
if (!getattr(fd, &t)) rb_sys_fail(0);
if (RTEST(f))
set_echo(&t, NULL);
else
set_noecho(&t, NULL);
if (!setattr(fd, &t)) rb_sys_fail(0);
return io;
}
/*
* call-seq:
* io.echo? -> true or false
*
* Returns +true+ if echo back is enabled.
*
* You must require 'io/console' to use this method.
*/
static VALUE
console_echo_p(VALUE io)
{
conmode t;
rb_io_t *fptr;
int fd;
GetOpenFile(io, fptr);
fd = GetReadFD(fptr);
if (!getattr(fd, &t)) rb_sys_fail(0);
return echo_p(&t) ? Qtrue : Qfalse;
}
static const rb_data_type_t conmode_type = {
"console-mode",
{0, RUBY_TYPED_DEFAULT_FREE,},
0, 0,
RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED,
};
static VALUE cConmode;
static VALUE
conmode_alloc(VALUE klass)
{
return rb_data_typed_object_zalloc(klass, sizeof(conmode), &conmode_type);
}
static VALUE
conmode_new(VALUE klass, const conmode *t)
{
VALUE obj = conmode_alloc(klass);
*(conmode *)DATA_PTR(obj) = *t;
return obj;
}
static VALUE
conmode_init_copy(VALUE obj, VALUE obj2)
{
conmode *t = rb_check_typeddata(obj, &conmode_type);
conmode *t2 = rb_check_typeddata(obj2, &conmode_type);
*t = *t2;
return obj;
}
static VALUE
conmode_set_echo(VALUE obj, VALUE f)
{
conmode *t = rb_check_typeddata(obj, &conmode_type);
if (RTEST(f))
set_echo(t, NULL);
else
set_noecho(t, NULL);
return obj;
}
static VALUE
conmode_set_raw(int argc, VALUE *argv, VALUE obj)
{
conmode *t = rb_check_typeddata(obj, &conmode_type);
rawmode_arg_t opts, *optp = rawmode_opt(&argc, argv, 0, 0, &opts);
set_rawmode(t, optp);
return obj;
}
static VALUE
conmode_raw_new(int argc, VALUE *argv, VALUE obj)
{
conmode *r = rb_check_typeddata(obj, &conmode_type);
conmode t = *r;
rawmode_arg_t opts, *optp = rawmode_opt(&argc, argv, 0, 0, &opts);
set_rawmode(&t, optp);
return conmode_new(rb_obj_class(obj), &t);
}
/*
* call-seq:
* io.console_mode -> mode
*
* Returns a data represents the current console mode.
*
* You must require 'io/console' to use this method.
*/
static VALUE
console_conmode_get(VALUE io)
{
conmode t;
rb_io_t *fptr;
int fd;
GetOpenFile(io, fptr);
fd = GetReadFD(fptr);
if (!getattr(fd, &t)) rb_sys_fail(0);
return conmode_new(cConmode, &t);
}
/*
* call-seq:
* io.console_mode = mode
*
* Sets the console mode to +mode+.
*
* You must require 'io/console' to use this method.
*/
static VALUE
console_conmode_set(VALUE io, VALUE mode)
{
conmode *t, r;
rb_io_t *fptr;
int fd;
TypedData_Get_Struct(mode, conmode, &conmode_type, t);
r = *t;
GetOpenFile(io, fptr);
fd = GetReadFD(fptr);
if (!setattr(fd, &r)) rb_sys_fail(0);
return mode;
}
#if defined TIOCGWINSZ
typedef struct winsize rb_console_size_t;
#define getwinsize(fd, buf) (ioctl((fd), TIOCGWINSZ, (buf)) == 0)
#define setwinsize(fd, buf) (ioctl((fd), TIOCSWINSZ, (buf)) == 0)
#define winsize_row(buf) (buf)->ws_row
#define winsize_col(buf) (buf)->ws_col
#elif defined _WIN32
typedef CONSOLE_SCREEN_BUFFER_INFO rb_console_size_t;
#define getwinsize(fd, buf) ( \
GetConsoleScreenBufferInfo((HANDLE)rb_w32_get_osfhandle(fd), (buf)) || \
SET_LAST_ERROR)
#define winsize_row(buf) ((buf)->srWindow.Bottom - (buf)->srWindow.Top + 1)
#define winsize_col(buf) (buf)->dwSize.X
#endif
#if defined TIOCGWINSZ || defined _WIN32
#define USE_CONSOLE_GETSIZE 1
#endif
#ifdef USE_CONSOLE_GETSIZE
/*
* call-seq:
* io.winsize -> [rows, columns]
*
* Returns console size.
*
* You must require 'io/console' to use this method.
*/
static VALUE
console_winsize(VALUE io)
{
rb_io_t *fptr;
int fd;
rb_console_size_t ws;
GetOpenFile(io, fptr);
fd = GetWriteFD(fptr);
if (!getwinsize(fd, &ws)) rb_sys_fail(0);
return rb_assoc_new(INT2NUM(winsize_row(&ws)), INT2NUM(winsize_col(&ws)));
}
/*
* call-seq:
* io.winsize = [rows, columns]
*
* Tries to set console size. The effect depends on the platform and
* the running environment.
*
* You must require 'io/console' to use this method.
*/
static VALUE
console_set_winsize(VALUE io, VALUE size)
{
rb_io_t *fptr;
rb_console_size_t ws;
#if defined _WIN32
HANDLE wh;
int newrow, newcol;
BOOL ret;
#endif
VALUE row, col, xpixel, ypixel;
const VALUE *sz;
int fd;
long sizelen;
GetOpenFile(io, fptr);
size = rb_Array(size);
if ((sizelen = RARRAY_LEN(size)) != 2 && sizelen != 4) {
rb_raise(rb_eArgError,
"wrong number of arguments (given %ld, expected 2 or 4)",
sizelen);
}
sz = RARRAY_CONST_PTR(size);
row = sz[0], col = sz[1], xpixel = ypixel = Qnil;
if (sizelen == 4) xpixel = sz[2], ypixel = sz[3];
fd = GetWriteFD(fptr);
#if defined TIOCSWINSZ
ws.ws_row = ws.ws_col = ws.ws_xpixel = ws.ws_ypixel = 0;
#define SET(m) ws.ws_##m = NIL_P(m) ? 0 : (unsigned short)NUM2UINT(m)
SET(row);
SET(col);
SET(xpixel);
SET(ypixel);
#undef SET
if (!setwinsize(fd, &ws)) rb_sys_fail(0);
#elif defined _WIN32
wh = (HANDLE)rb_w32_get_osfhandle(fd);
#define SET(m) new##m = NIL_P(m) ? 0 : (unsigned short)NUM2UINT(m)
SET(row);
SET(col);
#undef SET
if (!NIL_P(xpixel)) (void)NUM2UINT(xpixel);
if (!NIL_P(ypixel)) (void)NUM2UINT(ypixel);
if (!GetConsoleScreenBufferInfo(wh, &ws)) {
rb_syserr_fail(LAST_ERROR, "GetConsoleScreenBufferInfo");
}
ws.dwSize.X = newcol;
ret = SetConsoleScreenBufferSize(wh, ws.dwSize);
ws.srWindow.Left = 0;
ws.srWindow.Top = 0;
ws.srWindow.Right = newcol-1;
ws.srWindow.Bottom = newrow-1;
if (!SetConsoleWindowInfo(wh, TRUE, &ws.srWindow)) {
rb_syserr_fail(LAST_ERROR, "SetConsoleWindowInfo");
}
/* retry when shrinking buffer after shrunk window */
if (!ret && !SetConsoleScreenBufferSize(wh, ws.dwSize)) {
rb_syserr_fail(LAST_ERROR, "SetConsoleScreenBufferInfo");
}
/* remove scrollbar if possible */
if (!SetConsoleWindowInfo(wh, TRUE, &ws.srWindow)) {
rb_syserr_fail(LAST_ERROR, "SetConsoleWindowInfo");
}
#endif
return io;
}
#endif
#ifdef _WIN32
static VALUE
console_check_winsize_changed(VALUE io)
{
rb_io_t *fptr;
HANDLE h;
DWORD num;
GetOpenFile(io, fptr);
h = (HANDLE)rb_w32_get_osfhandle(GetReadFD(fptr));
while (GetNumberOfConsoleInputEvents(h, &num) && num > 0) {
INPUT_RECORD rec;
if (ReadConsoleInput(h, &rec, 1, &num)) {
if (rec.EventType == WINDOW_BUFFER_SIZE_EVENT) {
rb_yield(Qnil);
}
}
}
return io;
}
#else
#define console_check_winsize_changed rb_f_notimplement
#endif
/*
* call-seq:
* io.iflush
*
* Flushes input buffer in kernel.
*
* You must require 'io/console' to use this method.
*/
static VALUE
console_iflush(VALUE io)
{
rb_io_t *fptr;
int fd;
GetOpenFile(io, fptr);
fd = GetReadFD(fptr);
#if defined HAVE_TERMIOS_H || defined HAVE_TERMIO_H
if (tcflush(fd, TCIFLUSH)) rb_sys_fail(0);
#endif
(void)fd;
return io;
}
/*
* call-seq:
* io.oflush
*
* Flushes output buffer in kernel.
*
* You must require 'io/console' to use this method.
*/
static VALUE
console_oflush(VALUE io)
{
rb_io_t *fptr;
int fd;
GetOpenFile(io, fptr);
fd = GetWriteFD(fptr);
#if defined HAVE_TERMIOS_H || defined HAVE_TERMIO_H
if (tcflush(fd, TCOFLUSH)) rb_sys_fail(0);
#endif
(void)fd;
return io;
}
/*
* call-seq:
* io.ioflush
*
* Flushes input and output buffers in kernel.
*
* You must require 'io/console' to use this method.
*/
static VALUE
console_ioflush(VALUE io)
{
rb_io_t *fptr;
#if defined HAVE_TERMIOS_H || defined HAVE_TERMIO_H
int fd1, fd2;
#endif
GetOpenFile(io, fptr);
#if defined HAVE_TERMIOS_H || defined HAVE_TERMIO_H
fd1 = GetReadFD(fptr);
fd2 = GetWriteFD(fptr);
if (fd2 != -1 && fd1 != fd2) {
if (tcflush(fd1, TCIFLUSH)) rb_sys_fail(0);
if (tcflush(fd2, TCOFLUSH)) rb_sys_fail(0);
}
else {
if (tcflush(fd1, TCIOFLUSH)) rb_sys_fail(0);
}
#endif
return io;
}
static VALUE
console_beep(VALUE io)
{
rb_io_t *fptr;
int fd;
GetOpenFile(io, fptr);
fd = GetWriteFD(fptr);
#ifdef _WIN32
(void)fd;
MessageBeep(0);
#else
if (write(fd, "\a", 1) < 0)
rb_sys_fail(0);
#endif
return io;
}
static int
mode_in_range(VALUE val, int high, const char *modename)
{
int mode;
if (NIL_P(val)) return 0;
if (!RB_INTEGER_TYPE_P(val)) {
wrong_value:
rb_raise(rb_eArgError, "wrong %s mode: %"PRIsVALUE, modename, val);
}
if ((mode = NUM2INT(val)) < 0 || mode > high) {
goto wrong_value;
}
return mode;
}
#if defined _WIN32
static VALUE
console_goto(VALUE io, VALUE y, VALUE x)
{
rb_io_t *fptr;
int fd;
COORD pos;
GetOpenFile(io, fptr);
fd = GetWriteFD(fptr);
pos.X = NUM2UINT(x);
pos.Y = NUM2UINT(y);
if (!SetConsoleCursorPosition((HANDLE)rb_w32_get_osfhandle(fd), pos)) {
rb_syserr_fail(LAST_ERROR, 0);
}
return io;
}
static VALUE
console_cursor_pos(VALUE io)
{
rb_io_t *fptr;
int fd;
rb_console_size_t ws;
GetOpenFile(io, fptr);
fd = GetWriteFD(fptr);
if (!GetConsoleScreenBufferInfo((HANDLE)rb_w32_get_osfhandle(fd), &ws)) {
rb_syserr_fail(LAST_ERROR, 0);
}
return rb_assoc_new(UINT2NUM(ws.dwCursorPosition.Y), UINT2NUM(ws.dwCursorPosition.X));
}
static VALUE
console_move(VALUE io, int y, int x)
{
rb_io_t *fptr;
HANDLE h;
rb_console_size_t ws;
COORD *pos = &ws.dwCursorPosition;
GetOpenFile(io, fptr);
h = (HANDLE)rb_w32_get_osfhandle(GetWriteFD(fptr));
if (!GetConsoleScreenBufferInfo(h, &ws)) {
rb_syserr_fail(LAST_ERROR, 0);
}
pos->X += x;
pos->Y += y;
if (!SetConsoleCursorPosition(h, *pos)) {
rb_syserr_fail(LAST_ERROR, 0);
}
return io;
}
static VALUE
console_goto_column(VALUE io, VALUE val)
{
rb_io_t *fptr;
HANDLE h;
rb_console_size_t ws;
COORD *pos = &ws.dwCursorPosition;
GetOpenFile(io, fptr);
h = (HANDLE)rb_w32_get_osfhandle(GetWriteFD(fptr));
if (!GetConsoleScreenBufferInfo(h, &ws)) {
rb_syserr_fail(LAST_ERROR, 0);
}
pos->X = NUM2INT(val);
if (!SetConsoleCursorPosition(h, *pos)) {
rb_syserr_fail(LAST_ERROR, 0);
}
return io;
}
static void
constat_clear(HANDLE handle, WORD attr, DWORD len, COORD pos)
{
DWORD written;
FillConsoleOutputAttribute(handle, attr, len, pos, &written);
FillConsoleOutputCharacterW(handle, L' ', len, pos, &written);
}
static VALUE
console_erase_line(VALUE io, VALUE val)
{
rb_io_t *fptr;
HANDLE h;
rb_console_size_t ws;
COORD *pos = &ws.dwCursorPosition;
DWORD w;
int mode = mode_in_range(val, 2, "line erase");
GetOpenFile(io, fptr);
h = (HANDLE)rb_w32_get_osfhandle(GetWriteFD(fptr));
if (!GetConsoleScreenBufferInfo(h, &ws)) {
rb_syserr_fail(LAST_ERROR, 0);
}
w = winsize_col(&ws);
switch (mode) {
case 0: /* after cursor */
w -= pos->X;
break;
case 1: /* before *and* cursor */
w = pos->X + 1;
pos->X = 0;
break;
case 2: /* entire line */
pos->X = 0;
break;
}
constat_clear(h, ws.wAttributes, w, *pos);
return io;
}
static VALUE
console_erase_screen(VALUE io, VALUE val)
{
rb_io_t *fptr;
HANDLE h;
rb_console_size_t ws;
COORD *pos = &ws.dwCursorPosition;
DWORD w;
int mode = mode_in_range(val, 3, "screen erase");
GetOpenFile(io, fptr);
h = (HANDLE)rb_w32_get_osfhandle(GetWriteFD(fptr));
if (!GetConsoleScreenBufferInfo(h, &ws)) {
rb_syserr_fail(LAST_ERROR, 0);
}
w = winsize_col(&ws);
switch (mode) {
case 0: /* erase after cursor */
w = (w * (ws.srWindow.Bottom - pos->Y + 1) - pos->X);
break;
case 1: /* erase before *and* cursor */
w = (w * (pos->Y - ws.srWindow.Top) + pos->X + 1);
pos->X = 0;
pos->Y = ws.srWindow.Top;
break;
case 2: /* erase entire screen */
w = (w * winsize_row(&ws));
pos->X = 0;
pos->Y = ws.srWindow.Top;
break;
case 3: /* erase entire screen */
w = (w * ws.dwSize.Y);
pos->X = 0;
pos->Y = 0;
break;
}
constat_clear(h, ws.wAttributes, w, *pos);
return io;
}
static VALUE
console_scroll(VALUE io, int line)
{
rb_io_t *fptr;
HANDLE h;
rb_console_size_t ws;
GetOpenFile(io, fptr);
h = (HANDLE)rb_w32_get_osfhandle(GetWriteFD(fptr));
if (!GetConsoleScreenBufferInfo(h, &ws)) {
rb_syserr_fail(LAST_ERROR, 0);
}
if (line) {
SMALL_RECT scroll;
COORD destination;
CHAR_INFO fill;
scroll.Left = 0;
scroll.Top = line > 0 ? line : 0;
scroll.Right = winsize_col(&ws) - 1;
scroll.Bottom = winsize_row(&ws) - 1 + (line < 0 ? line : 0);
destination.X = 0;
destination.Y = line < 0 ? -line : 0;
fill.Char.UnicodeChar = L' ';
fill.Attributes = ws.wAttributes;
ScrollConsoleScreenBuffer(h, &scroll, NULL, destination, &fill);
}
return io;
}
#include "win32_vk.inc"
static VALUE
console_key_pressed_p(VALUE io, VALUE k)
{
int vk = -1;
if (FIXNUM_P(k)) {
vk = NUM2UINT(k);
}
else {
const struct vktable *t;
const char *kn;
if (SYMBOL_P(k)) {
k = rb_sym2str(k);
kn = RSTRING_PTR(k);
}
else {
kn = StringValuePtr(k);
}
t = console_win32_vk(kn, RSTRING_LEN(k));
if (!t || (vk = (short)t->vk) == -1) {
rb_raise(rb_eArgError, "unknown virtual key code: % "PRIsVALUE, k);
}
}
return GetKeyState(vk) & 0x80 ? Qtrue : Qfalse;
}
#else
struct query_args {
const char *qstr;
int opt;
};
static int
direct_query(VALUE io, const struct query_args *query)
{
if (RB_TYPE_P(io, T_FILE)) {
rb_io_t *fptr;
VALUE wio;
GetOpenFile(io, fptr);
wio = fptr->tied_io_for_writing;
if (wio) {
VALUE s = rb_str_new_cstr(query->qstr);
rb_io_write(wio, s);
rb_io_flush(wio);
return 1;
}
if (write(fptr->fd, query->qstr, strlen(query->qstr)) != -1) {
return 1;
}
if (fptr->fd == 0 &&
write(1, query->qstr, strlen(query->qstr)) != -1) {
return 1;
}
}
return 0;
}
static VALUE
read_vt_response(VALUE io, VALUE query)
{
struct query_args *qargs = (struct query_args *)query;
VALUE result, b;
int opt = 0;
int num = 0;
if (qargs) {
opt = qargs->opt;
if (!direct_query(io, qargs)) return Qnil;
}
if (rb_io_getbyte(io) != INT2FIX(0x1b)) return Qnil;
if (rb_io_getbyte(io) != INT2FIX('[')) return Qnil;
result = rb_ary_new();
while (!NIL_P(b = rb_io_getbyte(io))) {
int c = NUM2UINT(b);
if (c == ';') {
rb_ary_push(result, INT2NUM(num));
num = 0;
}
else if (ISDIGIT(c)) {
num = num * 10 + c - '0';
}
else if (opt && c == opt) {
opt = 0;
}
else {
char last = (char)c;
rb_ary_push(result, INT2NUM(num));
b = rb_str_new(&last, 1);
break;
}
}
return rb_ary_push(result, b);
}
static VALUE
console_vt_response(int argc, VALUE *argv, VALUE io, const struct query_args *qargs)
{
rawmode_arg_t opts, *optp = rawmode_opt(&argc, argv, 0, 1, &opts);
VALUE query = (VALUE)qargs;
VALUE ret = ttymode_with_io(io, read_vt_response, query, set_rawmode, optp);
return ret;
}
static VALUE
console_cursor_pos(VALUE io)
{
static const struct query_args query = {"\033[6n", 0};
VALUE resp = console_vt_response(0, 0, io, &query);
VALUE row, column, term;
unsigned int r, c;
if (!RB_TYPE_P(resp, T_ARRAY) || RARRAY_LEN(resp) != 3) return Qnil;
term = RARRAY_AREF(resp, 2);
if (!RB_TYPE_P(term, T_STRING) || RSTRING_LEN(term) != 1) return Qnil;
if (RSTRING_PTR(term)[0] != 'R') return Qnil;
row = RARRAY_AREF(resp, 0);
column = RARRAY_AREF(resp, 1);
rb_ary_resize(resp, 2);
r = NUM2UINT(row) - 1;
c = NUM2UINT(column) - 1;
RARRAY_ASET(resp, 0, INT2NUM(r));
RARRAY_ASET(resp, 1, INT2NUM(c));
return resp;
}
static VALUE
console_goto(VALUE io, VALUE y, VALUE x)
{
rb_io_write(io, rb_sprintf("\x1b[%d;%dH", NUM2UINT(y)+1, NUM2UINT(x)+1));
return io;
}
static VALUE
console_move(VALUE io, int y, int x)
{
if (x || y) {
VALUE s = rb_str_new_cstr("");
if (y) rb_str_catf(s, "\x1b[%d%c", y < 0 ? -y : y, y < 0 ? 'A' : 'B');
if (x) rb_str_catf(s, "\x1b[%d%c", x < 0 ? -x : x, x < 0 ? 'D' : 'C');
rb_io_write(io, s);
rb_io_flush(io);
}
return io;
}
static VALUE
console_goto_column(VALUE io, VALUE val)
{
rb_io_write(io, rb_sprintf("\x1b[%dG", NUM2UINT(val)+1));
return io;
}
static VALUE
console_erase_line(VALUE io, VALUE val)
{
int mode = mode_in_range(val, 2, "line erase");
rb_io_write(io, rb_sprintf("\x1b[%dK", mode));
return io;
}
static VALUE
console_erase_screen(VALUE io, VALUE val)
{
int mode = mode_in_range(val, 3, "screen erase");
rb_io_write(io, rb_sprintf("\x1b[%dJ", mode));
return io;
}
static VALUE
console_scroll(VALUE io, int line)
{
if (line) {
VALUE s = rb_sprintf("\x1b[%d%c", line < 0 ? -line : line,
line < 0 ? 'T' : 'S');
rb_io_write(io, s);
}
return io;
}
# define console_key_pressed_p rb_f_notimplement
#endif
static VALUE
console_cursor_set(VALUE io, VALUE cpos)
{
cpos = rb_convert_type(cpos, T_ARRAY, "Array", "to_ary");
if (RARRAY_LEN(cpos) != 2) rb_raise(rb_eArgError, "expected 2D coordinate");
return console_goto(io, RARRAY_AREF(cpos, 0), RARRAY_AREF(cpos, 1));
}
static VALUE
console_cursor_up(VALUE io, VALUE val)
{
return console_move(io, -NUM2INT(val), 0);
}
static VALUE
console_cursor_down(VALUE io, VALUE val)
{
return console_move(io, +NUM2INT(val), 0);
}
static VALUE
console_cursor_left(VALUE io, VALUE val)
{
return console_move(io, 0, -NUM2INT(val));
}
static VALUE
console_cursor_right(VALUE io, VALUE val)
{
return console_move(io, 0, +NUM2INT(val));
}
static VALUE
console_scroll_forward(VALUE io, VALUE val)
{
return console_scroll(io, +NUM2INT(val));
}
static VALUE
console_scroll_backward(VALUE io, VALUE val)
{
return console_scroll(io, -NUM2INT(val));
}
static VALUE
console_clear_screen(VALUE io)
{
console_erase_screen(io, INT2FIX(2));
console_goto(io, INT2FIX(0), INT2FIX(0));
return io;
}
/*
* call-seq:
* IO.console -> #<File:/dev/tty>
* IO.console(sym, *args)
*
* Returns an File instance opened console.
*
* If +sym+ is given, it will be sent to the opened console with
* +args+ and the result will be returned instead of the console IO
* itself.
*
* You must require 'io/console' to use this method.
*/
static VALUE
console_dev(int argc, VALUE *argv, VALUE klass)
{
VALUE con = 0;
rb_io_t *fptr;
VALUE sym = 0;
rb_check_arity(argc, 0, UNLIMITED_ARGUMENTS);
if (argc) {
Check_Type(sym = argv[0], T_SYMBOL);
}
if (klass == rb_cIO) klass = rb_cFile;
if (rb_const_defined(klass, id_console)) {
con = rb_const_get(klass, id_console);
if (!RB_TYPE_P(con, T_FILE) ||
(!(fptr = RFILE(con)->fptr) || GetReadFD(fptr) == -1)) {
rb_const_remove(klass, id_console);
con = 0;
}
}
if (sym) {
if (sym == ID2SYM(id_close) && argc == 1) {
if (con) {
rb_io_close(con);
rb_const_remove(klass, id_console);
con = 0;
}
return Qnil;
}
}
if (!con) {
VALUE args[2];
#if defined HAVE_TERMIOS_H || defined HAVE_TERMIO_H || defined HAVE_SGTTY_H
# define CONSOLE_DEVICE "/dev/tty"
#elif defined _WIN32
# define CONSOLE_DEVICE "con$"
# define CONSOLE_DEVICE_FOR_READING "conin$"
# define CONSOLE_DEVICE_FOR_WRITING "conout$"
#endif
#ifndef CONSOLE_DEVICE_FOR_READING
# define CONSOLE_DEVICE_FOR_READING CONSOLE_DEVICE
#endif
#ifdef CONSOLE_DEVICE_FOR_WRITING
VALUE out;
rb_io_t *ofptr;
#endif
int fd;
#ifdef CONSOLE_DEVICE_FOR_WRITING
fd = rb_cloexec_open(CONSOLE_DEVICE_FOR_WRITING, O_RDWR, 0);
if (fd < 0) return Qnil;
rb_update_max_fd(fd);
args[1] = INT2FIX(O_WRONLY);
args[0] = INT2NUM(fd);
out = rb_class_new_instance(2, args, klass);
#endif
fd = rb_cloexec_open(CONSOLE_DEVICE_FOR_READING, O_RDWR, 0);
if (fd < 0) {
#ifdef CONSOLE_DEVICE_FOR_WRITING
rb_io_close(out);
#endif
return Qnil;
}
rb_update_max_fd(fd);
args[1] = INT2FIX(O_RDWR);
args[0] = INT2NUM(fd);
con = rb_class_new_instance(2, args, klass);
GetOpenFile(con, fptr);
fptr->pathv = rb_obj_freeze(rb_str_new2(CONSOLE_DEVICE));
#ifdef CONSOLE_DEVICE_FOR_WRITING
GetOpenFile(out, ofptr);
ofptr->pathv = fptr->pathv;
fptr->tied_io_for_writing = out;
ofptr->mode |= FMODE_SYNC;
#endif
fptr->mode |= FMODE_SYNC;
rb_const_set(klass, id_console, con);
}
if (sym) {
return rb_f_send(argc, argv, con);
}
return con;
}
/*
* call-seq:
* io.getch(min: nil, time: nil, intr: nil) -> char
*
* See IO#getch.
*/
static VALUE
io_getch(int argc, VALUE *argv, VALUE io)
{
return rb_funcallv(io, id_getc, argc, argv);
}
#if ENABLE_IO_GETPASS
static VALUE
puts_call(VALUE io)
{
return rb_io_write(io, rb_default_rs);
}
static VALUE
getpass_call(VALUE io)
{
return ttymode(io, rb_io_gets, io, set_noecho, NULL);
}
static void
prompt(int argc, VALUE *argv, VALUE io)
{
if (argc > 0 && !NIL_P(argv[0])) {
VALUE str = argv[0];
StringValueCStr(str);
rb_io_write(io, str);
}
}
static VALUE
str_chomp(VALUE str)
{
if (!NIL_P(str)) {
str = rb_funcallv(str, rb_intern("chomp!"), 0, 0);
}
return str;
}
/*
* call-seq:
* io.getpass(prompt=nil) -> string
*
* Reads and returns a line without echo back.
* Prints +prompt+ unless it is +nil+.
*
* You must require 'io/console' to use this method.
*/
static VALUE
console_getpass(int argc, VALUE *argv, VALUE io)
{
VALUE str, wio;
rb_check_arity(argc, 0, 1);
wio = rb_io_get_write_io(io);
if (wio == io && io == rb_stdin) wio = rb_stderr;
prompt(argc, argv, wio);
str = rb_ensure(getpass_call, io, puts_call, wio);
return str_chomp(str);
}
/*
* call-seq:
* io.getpass(prompt=nil) -> string
*
* See IO#getpass.
*/
static VALUE
io_getpass(int argc, VALUE *argv, VALUE io)
{
VALUE str;
rb_check_arity(argc, 0, 1);
prompt(argc, argv, io);
str = str_chomp(rb_funcallv(io, id_gets, 0, 0));
puts_call(io);
return str;
}
#endif
/*
* IO console methods
*/
void
Init_console(void)
{
#undef rb_intern
id_getc = rb_intern("getc");
#if ENABLE_IO_GETPASS
id_gets = rb_intern("gets");
#endif
id_console = rb_intern("console");
id_close = rb_intern("close");
id_min = rb_intern("min");
id_time = rb_intern("time");
id_intr = rb_intern("intr");
#ifndef HAVE_RB_F_SEND
id___send__ = rb_intern("__send__");
#endif
InitVM(console);
}
void
InitVM_console(void)
{
rb_define_method(rb_cIO, "raw", console_raw, -1);
rb_define_method(rb_cIO, "raw!", console_set_raw, -1);
rb_define_method(rb_cIO, "cooked", console_cooked, 0);
rb_define_method(rb_cIO, "cooked!", console_set_cooked, 0);
rb_define_method(rb_cIO, "getch", console_getch, -1);
rb_define_method(rb_cIO, "echo=", console_set_echo, 1);
rb_define_method(rb_cIO, "echo?", console_echo_p, 0);
rb_define_method(rb_cIO, "console_mode", console_conmode_get, 0);
rb_define_method(rb_cIO, "console_mode=", console_conmode_set, 1);
rb_define_method(rb_cIO, "noecho", console_noecho, 0);
rb_define_method(rb_cIO, "winsize", console_winsize, 0);
rb_define_method(rb_cIO, "winsize=", console_set_winsize, 1);
rb_define_method(rb_cIO, "iflush", console_iflush, 0);
rb_define_method(rb_cIO, "oflush", console_oflush, 0);
rb_define_method(rb_cIO, "ioflush", console_ioflush, 0);
rb_define_method(rb_cIO, "beep", console_beep, 0);
rb_define_method(rb_cIO, "goto", console_goto, 2);
rb_define_method(rb_cIO, "cursor", console_cursor_pos, 0);
rb_define_method(rb_cIO, "cursor=", console_cursor_set, 1);
rb_define_method(rb_cIO, "cursor_up", console_cursor_up, 1);
rb_define_method(rb_cIO, "cursor_down", console_cursor_down, 1);
rb_define_method(rb_cIO, "cursor_left", console_cursor_left, 1);
rb_define_method(rb_cIO, "cursor_right", console_cursor_right, 1);
rb_define_method(rb_cIO, "goto_column", console_goto_column, 1);
rb_define_method(rb_cIO, "erase_line", console_erase_line, 1);
rb_define_method(rb_cIO, "erase_screen", console_erase_screen, 1);
rb_define_method(rb_cIO, "scroll_forward", console_scroll_forward, 1);
rb_define_method(rb_cIO, "scroll_backward", console_scroll_backward, 1);
rb_define_method(rb_cIO, "clear_screen", console_clear_screen, 0);
rb_define_method(rb_cIO, "pressed?", console_key_pressed_p, 1);
rb_define_method(rb_cIO, "check_winsize_changed", console_check_winsize_changed, 0);
#if ENABLE_IO_GETPASS
rb_define_method(rb_cIO, "getpass", console_getpass, -1);
#endif
rb_define_singleton_method(rb_cIO, "console", console_dev, -1);
{
VALUE mReadable = rb_define_module_under(rb_cIO, "generic_readable");
rb_define_method(mReadable, "getch", io_getch, -1);
#if ENABLE_IO_GETPASS
rb_define_method(mReadable, "getpass", io_getpass, -1);
#endif
}
{
/* :stopdoc: */
cConmode = rb_define_class_under(rb_cIO, "ConsoleMode", rb_cObject);
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_set_echo, 1);
rb_define_method(cConmode, "raw!", conmode_set_raw, -1);
rb_define_method(cConmode, "raw", conmode_raw_new, -1);
/* :startdoc: */
}
}