From 5275faa7d32ead6c4da931629adb511a32bc042d Mon Sep 17 00:00:00 2001 From: JoelTowell Date: Tue, 1 Sep 2026 01:45:24 +1000 Subject: [PATCH 1/4] Add regression test for TERM=dumb TTY readline output --- test/reline/test_reline.rb | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/reline/test_reline.rb b/test/reline/test_reline.rb index 49c736eb4a..a0a8861e49 100644 --- a/test/reline/test_reline.rb +++ b/test/reline/test_reline.rb @@ -428,6 +428,26 @@ def test_readline_returns_nil_on_piped_stdin_eof assert_include(out, { result: nil }.inspect) end + def test_dumb_tty_buffered_output_keeps_first_and_finished_renders + input = StringIO.new + output = StringIO.new + + [input, output].each do |io| + def io.tty? + true + end + end + io_gate = Reline::Dumb.new + io_gate.input = input + io_gate.output = output + + io_gate.buffered_output { io_gate.write("first render") } + io_gate.buffered_output { io_gate.write("rerender") } + io_gate.buffered_output { io_gate.write("finished render\n") } + + assert_equal("first renderfinished render\n", output.string) + end + def test_read_eof_returns_input pend if win? lib = File.expand_path("../../lib", __dir__) From 8a1a7650ff3be8451fac9f13459af816c6d965fb Mon Sep 17 00:00:00 2001 From: JoelTowell Date: Tue, 1 Sep 2026 01:47:33 +1000 Subject: [PATCH 2/4] Avoid repeated readline renders with TERM=dumb --- lib/reline/io/dumb.rb | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/reline/io/dumb.rb b/lib/reline/io/dumb.rb index 0c04c755d2..dd1c31b478 100644 --- a/lib/reline/io/dumb.rb +++ b/lib/reline/io/dumb.rb @@ -9,9 +9,11 @@ def initialize(encoding: nil) @input = STDIN @output = STDOUT @buf = [] + @output_buffer = nil @pasting = false @encoding = encoding @screen_size = [24, 80] + @first_render = true end def dumb? @@ -43,11 +45,24 @@ def with_raw_input end def write(string) - @output.write(string) + if @output_buffer + @output_buffer << string + else + @output.write(string) + end end def buffered_output + return yield unless @input.respond_to?(:tty?) && @input.tty? && @output.respond_to?(:tty?) && @output.tty? + + @output_buffer = +'' yield + if @first_render + @output.write(@output_buffer) + @first_render = false + end + ensure + @output_buffer = nil end def getc(_timeout_second) @@ -113,6 +128,7 @@ def in_pasting? end def prep + @first_render = true end def deprep(otio) From 0098229a03760fbd3b253232df5de07a6cf605b1 Mon Sep 17 00:00:00 2001 From: JoelTowell Date: Wed, 2 Sep 2026 00:08:13 +1000 Subject: [PATCH 3/4] Drop incomplete dumb TTY redraws --- lib/reline/io/dumb.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/reline/io/dumb.rb b/lib/reline/io/dumb.rb index dd1c31b478..d389def4a7 100644 --- a/lib/reline/io/dumb.rb +++ b/lib/reline/io/dumb.rb @@ -53,11 +53,13 @@ def write(string) end def buffered_output - return yield unless @input.respond_to?(:tty?) && @input.tty? && @output.respond_to?(:tty?) && @output.tty? + return yield unless tty? @output_buffer = +'' yield - if @first_render + # render_finished writes complete transcript lines ending in CRLF. Keep those + # while dropping redraws that this IO gate cannot apply in place. + if @first_render || @output_buffer.end_with?("\n") @output.write(@output_buffer) @first_render = false end @@ -65,6 +67,10 @@ def buffered_output @output_buffer = nil end + private def tty? + @input.respond_to?(:tty?) && @input.tty? && @output.respond_to?(:tty?) && @output.tty? + end + def getc(_timeout_second) unless @buf.empty? return @buf.shift From 1bed023494663b3db6527a1dafe9cb26ba40d7df Mon Sep 17 00:00:00 2001 From: JoelTowell Date: Wed, 2 Sep 2026 00:16:35 +1000 Subject: [PATCH 4/4] slight cleanup --- lib/reline/io/dumb.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/reline/io/dumb.rb b/lib/reline/io/dumb.rb index d389def4a7..59e1630d7e 100644 --- a/lib/reline/io/dumb.rb +++ b/lib/reline/io/dumb.rb @@ -57,8 +57,7 @@ def buffered_output @output_buffer = +'' yield - # render_finished writes complete transcript lines ending in CRLF. Keep those - # while dropping redraws that this IO gate cannot apply in place. + if @first_render || @output_buffer.end_with?("\n") @output.write(@output_buffer) @first_render = false