From 10b1b9e126fba13a8b420485899aaae7f30bdefc Mon Sep 17 00:00:00 2001 From: JoelTowell Date: Wed, 26 Aug 2026 01:03:41 +1000 Subject: [PATCH 1/3] Add failing test --- test/reline/test_reline.rb | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/test/reline/test_reline.rb b/test/reline/test_reline.rb index 49c736eb4a..f99faf3b1f 100644 --- a/test/reline/test_reline.rb +++ b/test/reline/test_reline.rb @@ -420,12 +420,22 @@ def test_dumb_terminal def test_readline_reads_piped_stdin out = readline_from_piped_stdin("input\n") - assert_include(out, { result: 'input' }.inspect) + assert_equal(">input\n#{ { result: 'input' }.inspect }\n", out) + end + + def test_readline_with_dumb_terminal_outputs_plain_piped_stdin + out = readline_from_dumb_terminal_piped_stdin("input\n") + assert_equal("> input\n#{ { result: 'input' }.inspect }\n", out) end def test_readline_returns_nil_on_piped_stdin_eof out = readline_from_piped_stdin("") - assert_include(out, { result: nil }.inspect) + assert_equal(">#{ { result: nil }.inspect }\n", out) + end + + def test_readline_with_dumb_terminal_outputs_plain_piped_stdin_eof + out = readline_from_dumb_terminal_piped_stdin("") + assert_equal("> #{ { result: nil }.inspect }\n", out) end def test_read_eof_returns_input @@ -488,6 +498,17 @@ def readline_from_piped_stdin(stdin) end end + def readline_from_dumb_terminal_piped_stdin(stdin) + lib = File.expand_path("../../lib", __dir__) + code = "p result: Reline.readline('> ')" + + IO.popen([{"TERM" => "dumb"}, Reline.test_rubybin, "-I#{lib}", "-rreline", "-e", code], "r+") do |io| + io.write stdin + io.close_write + io.read + end + end + def test_tty_ambiguous_width omit unless defined?(PTY) ruby_file = Tempfile.create('rubyfile') From 5513a1a2c080046ca47a01e3012b25ffe9b42ac1 Mon Sep 17 00:00:00 2001 From: JoelTowell Date: Wed, 26 Aug 2026 01:04:40 +1000 Subject: [PATCH 2/3] Use plain readline output outside TTYs --- lib/reline.rb | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/lib/reline.rb b/lib/reline.rb index 53adf1ad4d..d5460b123a 100644 --- a/lib/reline.rb +++ b/lib/reline.rb @@ -58,7 +58,7 @@ class Core attr_accessor :key_stroke attr_accessor :line_editor attr_accessor :last_incremental_search - attr_reader :output + attr_reader :input, :output extend Forwardable def_delegators :config, @@ -66,6 +66,7 @@ class Core :autocompletion= def initialize + self.input = STDIN self.output = STDOUT @mutex = Mutex.new @dialog_proc_list = {} @@ -175,6 +176,7 @@ def dialog_proc(name_sym) def input=(val) raise TypeError unless val.respond_to?(:getc) or val.nil? + @input = val if val.respond_to?(:getc) && io_gate.respond_to?(:input=) io_gate.input = val end @@ -290,6 +292,14 @@ def readline(_prompt = '', _add_history = false, prompt: _prompt, add_history: _ end end + private def terminal? + input.respond_to?(:tty?) && input.tty? && output.respond_to?(:tty?) && output.tty? + end + + private def terminal_rendering? + terminal? && !io_gate.dumb? + end + private def inner_readline(prompt, add_history, multiline, rprompt: nil, &confirm_multiline_termination) if ENV['RELINE_STDERR_TTY'] if io_gate.win? @@ -333,8 +343,12 @@ def readline(_prompt = '', _add_history = false, prompt: _prompt, add_history: _ end end - line_editor.update_dialogs - line_editor.rerender + if terminal_rendering? + line_editor.update_dialogs + line_editor.rerender + else + output.write(prompt) + end begin line_editor.set_signal_handlers @@ -354,13 +368,18 @@ def readline(_prompt = '', _add_history = false, prompt: _prompt, add_history: _ end } if line_editor.finished? - line_editor.render_finished + if terminal_rendering? + line_editor.render_finished + elsif !terminal? + line = line_editor.line + output.write("#{line}\n") if line + end break - else + elsif terminal_rendering? line_editor.rerender end end - io_gate.move_cursor_column(0) + io_gate.move_cursor_column(0) if terminal_rendering? rescue Errno::EIO # Maybe the I/O has been closed. ensure From 360edef0b3e027743da0599fd24b1824465b4ecc Mon Sep 17 00:00:00 2001 From: JoelTowell Date: Wed, 26 Aug 2026 02:00:38 +1000 Subject: [PATCH 3/3] Render only when readline is attached to TTYs --- lib/reline.rb | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/lib/reline.rb b/lib/reline.rb index d5460b123a..9dca0ef3ec 100644 --- a/lib/reline.rb +++ b/lib/reline.rb @@ -296,10 +296,6 @@ def readline(_prompt = '', _add_history = false, prompt: _prompt, add_history: _ input.respond_to?(:tty?) && input.tty? && output.respond_to?(:tty?) && output.tty? end - private def terminal_rendering? - terminal? && !io_gate.dumb? - end - private def inner_readline(prompt, add_history, multiline, rprompt: nil, &confirm_multiline_termination) if ENV['RELINE_STDERR_TTY'] if io_gate.win? @@ -343,7 +339,7 @@ def readline(_prompt = '', _add_history = false, prompt: _prompt, add_history: _ end end - if terminal_rendering? + if terminal? line_editor.update_dialogs line_editor.rerender else @@ -368,18 +364,18 @@ def readline(_prompt = '', _add_history = false, prompt: _prompt, add_history: _ end } if line_editor.finished? - if terminal_rendering? + if terminal? line_editor.render_finished - elsif !terminal? + else line = line_editor.line output.write("#{line}\n") if line end break - elsif terminal_rendering? + elsif terminal? line_editor.rerender end end - io_gate.move_cursor_column(0) if terminal_rendering? + io_gate.move_cursor_column(0) if terminal? rescue Errno::EIO # Maybe the I/O has been closed. ensure