Skip to content

Commit

Permalink
Display show_cmds's output in a pager when in TTY environment
Browse files Browse the repository at this point in the history
This can:

- Make it easier to scroll up and down the commands list
- Avoid pushing up users' previous output
- Allow users to do basic search with `/<word>`
  • Loading branch information
st0012 committed Jul 22, 2023
1 parent 18bb402 commit 20505ee
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 2 deletions.
26 changes: 24 additions & 2 deletions lib/irb/cmd/show_cmds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require "stringio"
require_relative "nop"
require_relative "../pager"

module IRB
# :stopdoc:
Expand All @@ -28,9 +29,30 @@ def execute(*args)
output.puts
end

puts output.string
display_content(output.string)
end

nil
private

def display_content(content)
if STDIN.tty?
begin
pid = nil
Pager.page do |io|
pid = io.pid
io.puts content
end
# When user presses Ctrl-C, IRB would raise `IRB::Abort`
# But since Pager is implemented by running paging commands like `less` in another process with `IO.popen`,
# the `IRB::Abort` exception only interrupts IRB's execution but doesn't affect the pager
# So to properly terminate the pager with Ctrl-C, we need to catch `IRB::Abort` and kill the pager process
rescue IRB::Abort
Process.kill("TERM", pid) if pid
nil
end
else
puts content
end
end
end
end
Expand Down
49 changes: 49 additions & 0 deletions lib/irb/pager.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

module IRB
# The implementation of this class is borrowed from RDoc's lib/rdoc/ri/driver.rb.
# Please do NOT use this class directly outside of IRB.
class Pager
PAGE_COMMANDS = [ENV['RI_PAGER'], ENV['PAGER'], 'pager', 'less -r', 'more -r'].compact.uniq

class << self
def page
if pager = setup_pager
begin
yield pager
ensure
pager.close
end
else
yield $stdout
end
rescue Errno::EPIPE
end

private

def setup_pager
require 'shellwords'

PAGE_COMMANDS.each do |pager|
pager = Shellwords.split(pager)
next if pager.empty?

begin
io = IO.popen(pager, 'w')
rescue
next
end

if $? && $?.pid == io.pid && $?.exited? # pager didn't work
next
end

return io
end

nil
end
end
end
end
10 changes: 10 additions & 0 deletions test/irb/test_cmd.rb
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,16 @@ def test_whereami_alias


class ShowCmdsTest < CommandTestCase
def setup
STDIN.singleton_class.define_method :tty? do
false
end
end

def teardown
STDIN.singleton_class.remove_method :tty?
end

def test_show_cmds
out, err = execute_lines(
"show_cmds\n"
Expand Down
18 changes: 18 additions & 0 deletions test/irb/yamatanooroti/test_rendering.rb
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,24 @@ def test_assignment_expression_truncate
EOC
end

def test_show_cmds_with_pager_can_quit_with_ctrl_c
write_irbrc <<~'LINES'
puts 'start IRB'
LINES
start_terminal(40, 80, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: 'start IRB')
write("show_cmds\n")
write("G") # move to the end of the screen
write("\C-c") # quit pager
write("'foo' + 'bar'\n") # eval something to make sure IRB resumes
close

screen = result.join("\n").sub(/\n*\z/, "\n")
# IRB::Abort should be rescued
assert_not_match(/IRB::Abort/, screen)
# IRB should resume
assert_match(/foobar/, screen)
end

private

def write_irbrc(content)
Expand Down

0 comments on commit 20505ee

Please sign in to comment.