From 54a68ca244882eb4cba97038345b219c40c8d3f5 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Sun, 16 Jul 2023 12:22:01 +0100 Subject: [PATCH] Display `show_cmds`'s output in a pager when in TTY environment 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 `/` --- lib/irb/cmd/show_cmds.rb | 7 +-- lib/irb/pager.rb | 57 ++++++++++++++++++++++++ test/irb/test_cmd.rb | 10 +++++ test/irb/yamatanooroti/test_rendering.rb | 18 ++++++++ 4 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 lib/irb/pager.rb diff --git a/lib/irb/cmd/show_cmds.rb b/lib/irb/cmd/show_cmds.rb index 490561825..1c9f1ea19 100644 --- a/lib/irb/cmd/show_cmds.rb +++ b/lib/irb/cmd/show_cmds.rb @@ -2,6 +2,7 @@ require "stringio" require_relative "nop" +require_relative "../pager" module IRB # :stopdoc: @@ -28,9 +29,9 @@ def execute(*args) output.puts end - puts output.string - - nil + Pager.page do |io| + io.puts output.string + end end end end diff --git a/lib/irb/pager.rb b/lib/irb/pager.rb new file mode 100644 index 000000000..d216e1374 --- /dev/null +++ b/lib/irb/pager.rb @@ -0,0 +1,57 @@ +# 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 STDIN.tty? && pager = setup_pager + begin + pid = pager.pid + yield pager + ensure + pager.close + end + else + yield $stdout + 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 + 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 diff --git a/test/irb/test_cmd.rb b/test/irb/test_cmd.rb index 71df60db1..71d64a0ec 100644 --- a/test/irb/test_cmd.rb +++ b/test/irb/test_cmd.rb @@ -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" diff --git a/test/irb/yamatanooroti/test_rendering.rb b/test/irb/yamatanooroti/test_rendering.rb index d2342d6a2..12467b091 100644 --- a/test/irb/yamatanooroti/test_rendering.rb +++ b/test/irb/yamatanooroti/test_rendering.rb @@ -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)