diff --git a/ext/readline/extconf.rb b/ext/readline/extconf.rb index 7bba386540..deef48a35e 100644 --- a/ext/readline/extconf.rb +++ b/ext/readline/extconf.rb @@ -101,6 +101,7 @@ def readline.have_type(type) readline.have_func("rl_redisplay") readline.have_func("rl_insert_text") readline.have_func("rl_delete_text") +readline.have_func("rl_callback_handler_install") unless readline.have_type("rl_hook_func_t*") # rl_hook_func_t is available since readline-4.2 (2001). # Function is removed at readline-6.3 (2014). diff --git a/ext/readline/readline.c b/ext/readline/readline.c index 253798f9e6..00eaf5700c 100644 --- a/ext/readline/readline.c +++ b/ext/readline/readline.c @@ -22,6 +22,7 @@ #include "ruby/config.h" #include #include +#include #include #ifdef HAVE_READLINE_READLINE_H #include @@ -1883,6 +1884,136 @@ username_completion_proc_call(VALUE self, VALUE str) return result; } +#ifdef HAVE_RL_CALLBACK_HANDLER_INSTALL +#define READ_CHAR_CB "read_char_cb_block" +static ID read_char_cb_block; +static bool readline_callback_add_history; +static char * readline_callback_line; +static VALUE readline_callback_ensure(VALUE val) { + free(readline_callback_line); + readline_callback_line = NULL; + return Qnil; +} +static VALUE readline_callback_call(VALUE line) { + VALUE proc = rb_attr_get(mReadline, read_char_cb_block); + rb_funcall(proc, id_call, 1, line); + return Qnil; +} +static void readline_callback_callback(char * line) { + if (readline_callback_add_history && line) { + add_history(line); + } + readline_callback_line = line; + rb_ensure( + readline_callback_call, line ? rb_tainted_str_new2(line) : Qnil, + readline_callback_ensure, Qnil + ); +} +/* + * call-seq: + * Readline.readline(prompt = "", add_hist = false) &block -> nil + + * Set up the terminal for readline I/O and display the initial expanded + * value of prompt. Save the provided block to use as a function to call when + * a complete line of input has been entered. The block should take the text + * of the line as an argument. + * + * = Example + * + * PROMPT = "rltest$ " + * + * $running = true + * $sigwinch_received = false + * + * Readline.handler_install(PROMPT, add_hist: true) do |line| + * # Can use ^D (stty eof) or `exit' to exit. + * if !line || line == "exit" + * puts unless line + * puts "exit" + * Readline.handler_remove + * $running = false + * else + * puts "input line: #{line}" + * end + * end + * + * Signal.trap('SIGWINCH') { $sigwinch_received = true } + * + * while $running do + * rs = IO.select([$stdin]) + * if $sigwinch_received + * Readline.resize_terminal + * $sigwinch_received = false + * end + * Readline.read_char if r = rs[0] + * end + * + * puts "rltest: Event loop has exited" + */ +static VALUE readline_callback_handler_install( + int argc, + VALUE * argv, + VALUE self +) { + VALUE tmp, add_hist, block; + char * prompt = NULL; + + rb_need_block(); + + if (rb_scan_args(argc, argv, "02&", &tmp, &add_hist, &block) > 0) { + prompt = RSTRING_PTR(tmp); + } + + if (RTEST(add_hist)) { + readline_callback_add_history = true; + } else { + readline_callback_add_history = false; + } + + rb_ivar_set(mReadline, read_char_cb_block, block); + + rl_callback_handler_install(prompt, readline_callback_callback); + + return Qnil; +} +/* + * call-seq: + * Readline.read_char -> nil + * + * Whenever an application determines that keyboard input is available, it + * should call read_char, which will read the next character from the current + * input source. If that character completes the line, read_char will invoke + * the handler function saved by handler_install to process the line. + * Before calling the handler function, the terminal settings are reset to + * the values they had before calling handler_install. If the handler function + * returns, the terminal settings are modified for Readline's use again. EOF + * is indicated by calling handler with a nil. + */ +static VALUE readline_callback_read_char(VALUE self) { + VALUE proc = rb_attr_get(mReadline, read_char_cb_block); + if (NIL_P(proc)) { + rb_raise(rb_eRuntimeError, "No handler installed."); + } + rl_callback_read_char(); + return Qnil; +} +/* + * call-seq: + * Readline.handler_remove -> nil + * + * Restore the terminal to its initial state and remove the line handler. + * This may be called from within a callback as well as independently. If + * the handler installed by handler_install does not exit the program, this + * function should be called before the program exits to reset the terminal + * settings. + */ +static VALUE readline_callback_handler_remove(VALUE self) { + rb_ivar_set(mReadline, read_char_cb_block, Qnil); + rl_callback_handler_remove(); + return Qnil; +} +#endif + #undef rb_intern void Init_readline(void) @@ -1906,6 +2037,9 @@ Init_readline(void) id_call = rb_intern("call"); completion_proc = rb_intern(COMPLETION_PROC); completion_case_fold = rb_intern(COMPLETION_CASE_FOLD); +#ifdef HAVE_RL_CALLBACK_HANDLER_INSTALL + read_char_cb_block = rb_intern(READ_CHAR_CB); +#endif #if defined(HAVE_RL_PRE_INPUT_HOOK) id_pre_input_hook = rb_intern("pre_input_hook"); #endif @@ -1993,6 +2127,26 @@ Init_readline(void) readline_s_set_special_prefixes, 1); rb_define_singleton_method(mReadline, "special_prefixes", readline_s_get_special_prefixes, 0); +#ifdef HAVE_RL_CALLBACK_HANDLER_INSTALL + rb_define_singleton_method( + mReadline, + "handler_install", + readline_callback_handler_install, + -1 + ); + rb_define_singleton_method( + mReadline, + "read_char", + readline_callback_read_char, + 0 + ); + rb_define_singleton_method( + mReadline, + "handler_remove", + readline_callback_handler_remove, + 0 + ); +#endif #if USE_INSERT_IGNORE_ESCAPE CONST_ID(id_orig_prompt, "orig_prompt"); @@ -2098,3 +2252,4 @@ Init_readline(void) * indent-tabs-mode: nil * end: */ +/* vim: set ts=8 sw=4 noexpandtab: */ diff --git a/test/readline/test_readline.rb b/test/readline/test_readline.rb index f50eafddf5..877e959fe1 100644 --- a/test/readline/test_readline.rb +++ b/test/readline/test_readline.rb @@ -30,6 +30,26 @@ def teardown SAVED_ENV.each_with_index {|k, i| ENV[k] = @saved_env[i] } end + def test_callback_interface + with_temp_stdio do |stdin, stdout| + stdin.write("hello\n") + stdin.close + stdout.flush + line = nil + replace_stdio(stdin.path, stdout.path) { + Readline.handler_install("> ", true) { |l| line = l if l } + 6.times { Readline.read_char } + Readline.handler_remove + } + assert_equal("hello", line) + assert_equal(true, line.tainted?) + stdout.rewind + assert_equal("> ", stdout.read(2)) + assert_equal(1, Readline::HISTORY.length) + assert_equal("hello", Readline::HISTORY[0]) + end + end + if !/EditLine/n.match(Readline::VERSION) def test_readline with_temp_stdio do |stdin, stdout| @@ -618,3 +638,5 @@ def assert_under_utf8 return true end end if defined?(::Readline) + +# vim: set nowrap tabstop=8 tw=0 sw=2 expandtab