From 43cf94c3b6ccb87bffe855a06ae50753408ba9aa Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Wed, 14 Jan 2026 00:11:03 +0900 Subject: [PATCH] Add rprompt support for right-side prompt display This adds support for displaying a right-aligned prompt (rprompt) similar to zsh's RPROMPT feature. The rprompt is displayed at the right edge of the terminal and automatically hides when the input line gets too long. Usage: Reline.rprompt = "[%H:%M]" Reline.readline("> ") The rprompt is rendered as part of Reline's normal render cycle, so it persists during line editing unlike workarounds using pre_input_hook. --- lib/reline.rb | 6 +++++ lib/reline/line_editor.rb | 15 +++++++++++ test/reline/test_reline.rb | 16 ++++++++++++ test/reline/yamatanooroti/multiline_repl | 3 +++ test/reline/yamatanooroti/test_rendering.rb | 29 +++++++++++++++++++++ 5 files changed, 69 insertions(+) diff --git a/lib/reline.rb b/lib/reline.rb index 03e4b745cf..07c69d0a87 100644 --- a/lib/reline.rb +++ b/lib/reline.rb @@ -50,6 +50,7 @@ class Core output_modifier_proc prompt_proc auto_indent_proc + rprompt pre_input_hook dig_perfect_match_proc ).each(&method(:attr_reader)) @@ -158,6 +159,10 @@ def dig_perfect_match_proc=(p) @dig_perfect_match_proc = p end + def rprompt=(val) + @rprompt = val&.encode(encoding) + end + DialogProc = Struct.new(:dialog_proc, :context) def add_dialog_proc(name_sym, p, context = nil) raise ArgumentError unless name_sym.instance_of?(Symbol) @@ -323,6 +328,7 @@ def readline(prompt = '', add_hist = false) line_editor.prompt_proc = prompt_proc line_editor.auto_indent_proc = auto_indent_proc line_editor.dig_perfect_match_proc = dig_perfect_match_proc + line_editor.rprompt = rprompt pre_input_hook&.call diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb index bfffd17d59..3aee849edc 100644 --- a/lib/reline/line_editor.rb +++ b/lib/reline/line_editor.rb @@ -13,6 +13,7 @@ class Reline::LineEditor attr_accessor :prompt_proc attr_accessor :auto_indent_proc attr_accessor :dig_perfect_match_proc + attr_accessor :rprompt VI_MOTIONS = %i{ ed_prev_char @@ -476,6 +477,20 @@ def render prompt_width = Reline::Unicode.calculate_width(prompt, true) [[0, prompt_width, prompt], [prompt_width, Reline::Unicode.calculate_width(line, true), line]] end + + # Add rprompt to the first visible line if set and there's room + if @rprompt && !@rprompt.empty? && new_lines[0] + rprompt_width = Reline::Unicode.calculate_width(@rprompt, true) + right_col = screen_width - rprompt_width + first_line = new_lines[0] + # Calculate the end of the current content (prompt + input) + content_end = first_line.sum { |_, width, _| width } + # Only show rprompt if there's at least 1 char gap between content and rprompt + if right_col > content_end + first_line << [right_col, rprompt_width, @rprompt] + end + end + if @menu_info @menu_info.lines(screen_width).each do |item| new_lines << [[0, Reline::Unicode.calculate_width(item), item]] diff --git a/test/reline/test_reline.rb b/test/reline/test_reline.rb index aa0fd7d29a..5d8300aa3a 100644 --- a/test/reline/test_reline.rb +++ b/test/reline/test_reline.rb @@ -19,6 +19,7 @@ def setup Reline.auto_indent_proc = nil Reline.pre_input_hook = nil Reline.dig_perfect_match_proc = nil + Reline.rprompt = nil end def teardown @@ -232,6 +233,21 @@ def test_pre_input_hook assert_equal(l, Reline.pre_input_hook) end + def test_rprompt + assert_equal(nil, Reline.rprompt) + + Reline.rprompt = "[Time]" + assert_equal("[Time]", Reline.rprompt) + assert_equal(get_reline_encoding, Reline.rprompt.encoding) + + Reline.rprompt = "[Time]".encode(Encoding::ASCII) + assert_equal("[Time]", Reline.rprompt) + assert_equal(get_reline_encoding, Reline.rprompt.encoding) + + Reline.rprompt = nil + assert_equal(nil, Reline.rprompt) + end + def test_dig_perfect_match_proc assert_equal(nil, Reline.dig_perfect_match_proc) diff --git a/test/reline/yamatanooroti/multiline_repl b/test/reline/yamatanooroti/multiline_repl index 4930f2e9d8..ed5800cfdf 100755 --- a/test/reline/yamatanooroti/multiline_repl +++ b/test/reline/yamatanooroti/multiline_repl @@ -212,6 +212,9 @@ opt.on('--autocomplete-width-long') { }.select{ |c| c.start_with?(target) } } } +opt.on('--rprompt VAL') { |v| + Reline.rprompt = v +} opt.parse!(ARGV) begin diff --git a/test/reline/yamatanooroti/test_rendering.rb b/test/reline/yamatanooroti/test_rendering.rb index 008ff4a5e2..a996b9ddd1 100644 --- a/test/reline/yamatanooroti/test_rendering.rb +++ b/test/reline/yamatanooroti/test_rendering.rb @@ -183,6 +183,35 @@ def test_prompt close end + def test_rprompt + start_terminal(5, 40, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --rprompt [RPROMPT]}, startup_message: 'Multiline REPL.') + assert_screen(<<~EOC) + Multiline REPL. + prompt> [RPROMPT] + EOC + close + end + + def test_rprompt_with_input + start_terminal(5, 40, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --rprompt [RPROMPT]}, startup_message: 'Multiline REPL.') + write("hello") + assert_screen(<<~EOC) + Multiline REPL. + prompt> hello [RPROMPT] + EOC + close + end + + def test_rprompt_hides_when_input_reaches_rprompt + start_terminal(5, 40, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --rprompt [RPROMPT]}, startup_message: 'Multiline REPL.') + write("a" * 30) + assert_screen(<<~EOC) + Multiline REPL. + prompt> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + EOC + close + end + def test_mode_string_emacs write_inputrc <<~LINES set show-mode-in-prompt on