Skip to content

Commit d32ed17

Browse files
authored
Add rprompt support for right-side prompt display (take 2) (#879)
* 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.readline("> ", rprompt: "[%H:%M]") The rprompt is rendered as part of Reline's normal render cycle, so it persists during line editing unlike workarounds using pre_input_hook. * Let readline and readmultiline accept both positional and keyword args for prompt, add_hist, and rprompt. * add_hist => add_history
1 parent 5d2edcc commit d32ed17

5 files changed

Lines changed: 97 additions & 12 deletions

File tree

lib/reline.rb

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -247,19 +247,19 @@ def get_screen_size
247247
} # :nodoc:
248248
Reline::DEFAULT_DIALOG_CONTEXT = Array.new # :nodoc:
249249

250-
def readmultiline(prompt = '', add_hist = false, &confirm_multiline_termination)
250+
def readmultiline(_prompt = '', _add_history = false, prompt: _prompt, add_history: _add_history, rprompt: nil, &confirm_multiline_termination)
251251
@mutex.synchronize do
252252
unless confirm_multiline_termination
253253
raise ArgumentError.new('#readmultiline needs block to confirm multiline termination')
254254
end
255255

256256
io_gate.with_raw_input do
257-
inner_readline(prompt, add_hist, true, &confirm_multiline_termination)
257+
inner_readline(prompt, add_history, true, rprompt: rprompt, &confirm_multiline_termination)
258258
end
259259

260260
whole_buffer = line_editor.whole_buffer.dup
261261
whole_buffer.taint if RUBY_VERSION < '2.7'
262-
if add_hist and whole_buffer and whole_buffer.chomp("\n").size > 0
262+
if add_history and whole_buffer and whole_buffer.chomp("\n").size > 0
263263
Reline::HISTORY << whole_buffer
264264
end
265265

@@ -273,15 +273,15 @@ def readmultiline(prompt = '', add_hist = false, &confirm_multiline_termination)
273273
end
274274
end
275275

276-
def readline(prompt = '', add_hist = false)
276+
def readline(_prompt = '', _add_history = false, prompt: _prompt, add_history: _add_history, rprompt: nil)
277277
@mutex.synchronize do
278278
io_gate.with_raw_input do
279-
inner_readline(prompt, add_hist, false)
279+
inner_readline(prompt, add_history, false, rprompt: rprompt)
280280
end
281281

282282
line = line_editor.line.dup
283283
line.taint if RUBY_VERSION < '2.7'
284-
if add_hist and line and line.chomp("\n").size > 0
284+
if add_history and line and line.chomp("\n").size > 0
285285
Reline::HISTORY << line.chomp("\n")
286286
end
287287

@@ -290,7 +290,7 @@ def readline(prompt = '', add_hist = false)
290290
end
291291
end
292292

293-
private def inner_readline(prompt, add_hist, multiline, &confirm_multiline_termination)
293+
private def inner_readline(prompt, add_history, multiline, rprompt: nil, &confirm_multiline_termination)
294294
if ENV['RELINE_STDERR_TTY']
295295
if io_gate.win?
296296
$stderr = File.open(ENV['RELINE_STDERR_TTY'], 'a')
@@ -323,6 +323,7 @@ def readline(prompt = '', add_hist = false)
323323
line_editor.prompt_proc = prompt_proc
324324
line_editor.auto_indent_proc = auto_indent_proc
325325
line_editor.dig_perfect_match_proc = dig_perfect_match_proc
326+
line_editor.rprompt = rprompt&.encode(encoding)
326327

327328
pre_input_hook&.call
328329

@@ -442,13 +443,13 @@ def ambiguous_width
442443
##
443444
# :singleton-method: readmultiline
444445
# :call-seq:
445-
# readmultiline(prompt = '', add_hist = false, &confirm_multiline_termination) -> string or nil
446+
# readmultiline(prompt = '', add_history = false, &confirm_multiline_termination) -> string or nil
446447
def_single_delegators :core, :readmultiline
447448

448449
##
449450
# :singleton-method: readline
450451
# :call-seq:
451-
# readline(prompt = '', add_hist = false) -> string or nil
452+
# readline(prompt = '', add_history = false) -> string or nil
452453
def_single_delegators :core, :readline
453454
def_single_delegators :core, :completion_case_fold, :completion_case_fold=
454455
def_single_delegators :core, :completion_quote_character

lib/reline/line_editor.rb

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class Reline::LineEditor
1313
attr_accessor :prompt_proc
1414
attr_accessor :auto_indent_proc
1515
attr_accessor :dig_perfect_match_proc
16+
attr_accessor :rprompt
1617

1718
VI_MOTIONS = %i{
1819
ed_prev_char
@@ -476,6 +477,20 @@ def render
476477
prompt_width = Reline::Unicode.calculate_width(prompt, true)
477478
[[0, prompt_width, prompt], [prompt_width, Reline::Unicode.calculate_width(line, true), line]]
478479
end
480+
481+
# Add rprompt to the first visible line if set and there's room
482+
if @rprompt && !@rprompt.empty? && new_lines[0]
483+
rprompt_width = Reline::Unicode.calculate_width(@rprompt, true)
484+
right_col = screen_width - rprompt_width
485+
first_line = new_lines[0]
486+
# Calculate the end of the current content (prompt + input)
487+
content_end = first_line.sum { |_, width, _| width }
488+
# Only show rprompt if there's at least 1 char gap between content and rprompt
489+
if right_col > content_end
490+
first_line << [right_col, rprompt_width, @rprompt]
491+
end
492+
end
493+
479494
if @menu_info
480495
@menu_info.lines(screen_width).each do |item|
481496
new_lines << [[0, Reline::Unicode.calculate_width(item), item]]
@@ -491,8 +506,8 @@ def render
491506
next if row < 0 || row >= screen_height
492507

493508
dialog_rows = new_lines[row] ||= []
494-
# index 0 is for prompt, index 1 is for line, index 2.. is for dialog
495-
dialog_rows[index + 2] = [x_range.begin, dialog.width, dialog.contents[row - y_range.begin]]
509+
# index 0 is for prompt, index 1 is for line, index 2 is for rprompt, index 3.. is for dialog
510+
dialog_rows[index + 3] = [x_range.begin, dialog.width, dialog.contents[row - y_range.begin]]
496511
end
497512
end
498513

test/reline/test_reline.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,42 @@ def test_pre_input_hook
232232
assert_equal(l, Reline.pre_input_hook)
233233
end
234234

235+
def test_readline_with_rprompt
236+
pend if win?
237+
lib = File.expand_path("../../lib", __dir__)
238+
code = "p result: Reline.readline('>', rprompt: '[TIME]')"
239+
out = IO.popen([Reline.test_rubybin, "-I#{lib}", "-rreline", "-e", code], "r+") do |io|
240+
io.write "a\n"
241+
io.close_write
242+
io.read
243+
end
244+
assert_include(out, { result: 'a' }.inspect)
245+
end
246+
247+
def test_readline_with_keyword_arguments
248+
pend if win?
249+
lib = File.expand_path("../../lib", __dir__)
250+
code = "p result: Reline.readline(prompt: '>', add_history: true, rprompt: '[TIME]')"
251+
out = IO.popen([Reline.test_rubybin, "-I#{lib}", "-rreline", "-e", code], "r+") do |io|
252+
io.write "a\n"
253+
io.close_write
254+
io.read
255+
end
256+
assert_include(out, { result: 'a' }.inspect)
257+
end
258+
259+
def test_readmultiline_with_keyword_arguments
260+
pend if win?
261+
lib = File.expand_path("../../lib", __dir__)
262+
code = "p result: Reline.readmultiline(prompt: '>', add_history: true, rprompt: '[TIME]') { true }"
263+
out = IO.popen([Reline.test_rubybin, "-I#{lib}", "-rreline", "-e", code], "r+") do |io|
264+
io.write "a\n"
265+
io.close_write
266+
io.read
267+
end
268+
assert_include(out, { result: 'a' }.inspect)
269+
end
270+
235271
def test_dig_perfect_match_proc
236272
assert_equal(nil, Reline.dig_perfect_match_proc)
237273

test/reline/yamatanooroti/multiline_repl

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,10 @@ opt.on('--autocomplete-width-long') {
212212
}.select{ |c| c.start_with?(target) }
213213
}
214214
}
215+
rprompt = nil
216+
opt.on('--rprompt VAL') { |v|
217+
rprompt = v
218+
}
215219
opt.parse!(ARGV)
216220

217221
begin
@@ -222,7 +226,7 @@ end
222226
begin
223227
prompt = ENV['RELINE_TEST_PROMPT'] || "\e[1mprompt>\e[m "
224228
puts 'Multiline REPL.'
225-
while code = Reline.readmultiline(prompt, true) { |code| TerminationChecker.terminated?(code) }
229+
while code = Reline.readmultiline(prompt, true, rprompt: rprompt) { |code| TerminationChecker.terminated?(code) }
226230
case code.chomp
227231
when 'exit', 'quit', 'q'
228232
exit 0

test/reline/yamatanooroti/test_rendering.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,35 @@ def test_prompt
183183
close
184184
end
185185

186+
def test_rprompt
187+
start_terminal(5, 40, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --rprompt [RPROMPT]}, startup_message: 'Multiline REPL.')
188+
assert_screen(<<~EOC)
189+
Multiline REPL.
190+
prompt> [RPROMPT]
191+
EOC
192+
close
193+
end
194+
195+
def test_rprompt_with_input
196+
start_terminal(5, 40, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --rprompt [RPROMPT]}, startup_message: 'Multiline REPL.')
197+
write("hello")
198+
assert_screen(<<~EOC)
199+
Multiline REPL.
200+
prompt> hello [RPROMPT]
201+
EOC
202+
close
203+
end
204+
205+
def test_rprompt_hides_when_input_reaches_rprompt
206+
start_terminal(5, 40, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl --rprompt [RPROMPT]}, startup_message: 'Multiline REPL.')
207+
write("a" * 30)
208+
assert_screen(<<~EOC)
209+
Multiline REPL.
210+
prompt> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
211+
EOC
212+
close
213+
end
214+
186215
def test_mode_string_emacs
187216
write_inputrc <<~LINES
188217
set show-mode-in-prompt on

0 commit comments

Comments
 (0)