Skip to content

Commit

Permalink
Add copy command
Browse files Browse the repository at this point in the history
Closes #753
  • Loading branch information
Prajjwal committed Jan 14, 2025
1 parent ce8fa68 commit f0faf98
Show file tree
Hide file tree
Showing 11 changed files with 186 additions and 10 deletions.
1 change: 1 addition & 0 deletions doc/Configurations.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Method `conf.rc?` returns `true` if a configuration file was read, `false` other
- `NO_COLOR`: Disables \IRB's colorization.
- `IRB_USE_AUTOCOMPLETE`: Setting to `false` disables autocompletion.
- `IRB_COMPLETOR`: Configures auto-completion behavior (`regexp` or `type`).
- `IRB_COPY_COMMAND`: Overrides the default program used to interface with the system clipboard.
- `VISUAL` / `EDITOR`: Specifies the editor for the `edit` command.
- `IRBRC`: Specifies the rc-file for configuration.
- `XDG_CONFIG_HOME`: Used to locate the rc-file if `IRBRC` is unset.
Expand Down
14 changes: 10 additions & 4 deletions lib/irb/color_printer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
module IRB
class ColorPrinter < ::PP
class << self
def pp(obj, out = $>, width = screen_width)
q = ColorPrinter.new(out, width)
def pp(obj, out = $>, width = screen_width, colorize: true)
q = ColorPrinter.new(out, width, colorize: colorize)
q.guard_inspect_key {q.pp obj}
q.flush
out << "\n"
Expand All @@ -21,6 +21,12 @@ def screen_width
end
end

def initialize(out, width, colorize: true)
@colorize = colorize

super(out, width)
end

def pp(obj)
if String === obj
# Avoid calling Ruby 2.4+ String#pretty_print that splits a string by "\n"
Expand All @@ -41,9 +47,9 @@ def text(str, width = nil)
when ',', '=>', '[', ']', '{', '}', '..', '...', /\A@\w+\z/
super(str, width)
when /\A#</, '=', '>'
super(Color.colorize(str, [:GREEN]), width)
super(@colorize ? Color.colorize(str, [:GREEN]) : str, width)
else
super(Color.colorize_code(str, ignore_error: true), width)
super(@colorize ? Color.colorize_code(str, ignore_error: true) : str, width)
end
end
end
Expand Down
58 changes: 58 additions & 0 deletions lib/irb/command/copy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# frozen_string_literal: true

module IRB
module Command
class Copy < Base
category "Workspace"
description "Copy command output to clipboard"

help_message(<<~HELP)
Usage: copy (command)
HELP

def execute(arg)
value = irb_context.workspace.binding.eval(arg)
output = irb_context.inspect_method.inspect_value(value, colorize: false)

if clipboard_available?
copy_to_clipboard(output)
else
warn "System clipboard not found"
end
rescue StandardError => e
warn "Error: #{e}"
warn "Is IRB.conf[:COPY_COMMAND] set to a bad value?"
end

private

def copy_to_clipboard(text)
IO.popen(clipboard_program, 'w') do |io|
io.write(text)
end

raise IOError.new("Copying to clipboard failed") unless $? == 0

puts "Copied to system clipboard"
end

def clipboard_program
@clipboard_program ||= if IRB.conf[:COPY_COMMAND]
IRB.conf[:COPY_COMMAND]
elsif executable?("pbcopy")
"pbcopy"
elsif executable?("xclip")
"xclip -selection clipboard"
end
end

def executable?(command)
system("which #{command} > /dev/null 2>&1")
end

def clipboard_available?
!!clipboard_program
end
end
end
end
2 changes: 2 additions & 0 deletions lib/irb/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,8 @@ def irb_path=(path)
attr_reader :use_autocomplete
# A copy of the default <code>IRB.conf[:INSPECT_MODE]</code>
attr_reader :inspect_mode
# Inspector for the current context
attr_reader :inspect_method

# A copy of the default <code>IRB.conf[:PROMPT_MODE]</code>
attr_reader :prompt_mode
Expand Down
2 changes: 2 additions & 0 deletions lib/irb/default_commands.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
require_relative "command/chws"
require_relative "command/context"
require_relative "command/continue"
require_relative "command/copy"
require_relative "command/debug"
require_relative "command/delete"
require_relative "command/disable_irb"
Expand Down Expand Up @@ -250,6 +251,7 @@ def load_command(command)
)

register(:cd, Command::CD)
register(:copy, Command::Copy)
end

ExtendCommand = Command
Expand Down
2 changes: 2 additions & 0 deletions lib/irb/init.rb
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ def IRB.init_config(ap_path)
:'$' => :show_source,
:'@' => :whereami,
}

@CONF[:COPY_COMMAND] = ENV.fetch("IRB_COPY_COMMAND", nil)
end

def IRB.set_measure_callback(type = nil, arg = nil, &block)
Expand Down
12 changes: 6 additions & 6 deletions lib/irb/inspector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ def init
end

# Proc to call when the input is evaluated and output in irb.
def inspect_value(v)
@inspect.call(v)
def inspect_value(v, colorize: true)
@inspect.call(v, colorize: colorize)
rescue => e
puts "An error occurred when inspecting the object: #{e.inspect}"

Expand All @@ -110,11 +110,11 @@ def inspect_value(v)
end

Inspector.def_inspector([false, :to_s, :raw]){|v| v.to_s}
Inspector.def_inspector([:p, :inspect]){|v|
Color.colorize_code(v.inspect, colorable: Color.colorable? && Color.inspect_colorable?(v))
Inspector.def_inspector([:p, :inspect]){|v, colorize: true|
Color.colorize_code(v.inspect, colorable: colorize && Color.colorable? && Color.inspect_colorable?(v))
}
Inspector.def_inspector([true, :pp, :pretty_inspect], proc{require_relative "color_printer"}){|v|
IRB::ColorPrinter.pp(v, +'').chomp
Inspector.def_inspector([true, :pp, :pretty_inspect], proc{require_relative "color_printer"}){|v, colorize: true|
IRB::ColorPrinter.pp(v, +'', colorize: colorize).chomp
}
Inspector.def_inspector([:yaml, :YAML], proc{require "yaml"}){|v|
begin
Expand Down
2 changes: 2 additions & 0 deletions man/irb.1
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ or
.Sy type
.
.Pp
.It Ev IRB_COPY_COMMAND
Overrides the default program used to interface with the system clipboard.
.El
.Pp
Also
Expand Down
70 changes: 70 additions & 0 deletions test/irb/command/test_copy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# frozen_string_literal: true

require 'irb'

require_relative "../helper"

module TestIRB
class CopyTest < IntegrationTestCase
def setup
super
@envs['IRB_COPY_COMMAND'] = "ruby -e \"puts 'foo' + STDIN.read\""
end

def test_copy_with_pbcopy
write_ruby <<~'ruby'
class Answer
def initialize(answer)
@answer = answer
end
end
binding.irb
ruby

output = run_ruby_file do
type "copy Answer.new(42)"
type "exit"
end

assert_match(/foo#<Answer:0x[0-9a-f]+ @answer=42/, output)
assert_match(/Copied to system clipboard/, output)
end

# copy puts 5 should:
# - Print value to the console
# - Copy nil to clipboard, since that is what the puts call evaluates to
def test_copy_when_expression_has_side_effects
write_ruby <<~'ruby'
binding.irb
ruby

output = run_ruby_file do
type "copy puts 42"
type "exit"
end

assert_match(/^42\r\n/, output)
assert_match(/foonil/, output)
assert_match(/Copied to system clipboard/, output)
refute_match(/foo42/, output)
end

def test_copy_when_copy_command_is_invalid
@envs['IRB_COPY_COMMAND'] = "lulz"

write_ruby <<~'ruby'
binding.irb
ruby

output = run_ruby_file do
type "copy 42"
type "exit"
end

assert_match(/Error: No such file or directory - lulz/, output)
assert_match(/Is IRB\.conf\[:COPY_COMMAND\] set to a bad value/, output)
refute_match(/Copied to system clipboard/, output)
end
end
end
13 changes: 13 additions & 0 deletions test/irb/test_color_printer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ def test_color_printer
end
end

def test_colorization_disabled
{
1 => "1\n",
"a\nb" => %["a\\nb"\n],
IRBTestColorPrinter.new('test') => "#<struct TestIRB::ColorPrinterTest::IRBTestColorPrinter a=\"test\">\n",
Ripper::Lexer.new('1').scan => "[#<Ripper::Lexer::Elem: on_int@1:0 END token: \"1\">]\n",
Class.new{define_method(:pretty_print){|q| q.text("[__FILE__, __LINE__, __ENCODING__]")}}.new => "[__FILE__, __LINE__, __ENCODING__]\n",
}.each do |object, result|
actual = with_term { IRB::ColorPrinter.pp(object, '', colorize: false) }
assert_equal(result, actual, "Case: IRB::ColorPrinter.pp(#{object.inspect}, '')")
end
end

private

def with_term
Expand Down
20 changes: 20 additions & 0 deletions test/irb/test_init.rb
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,26 @@ def test_use_autocomplete_environment_variable
IRB.conf[:USE_AUTOCOMPLETE] = orig_use_autocomplete_conf
end

def test_copy_command_environment_variable
orig_copy_command_env = ENV['IRB_COPY_COMMAND']
orig_copy_command_conf = IRB.conf[:COPY_COMMAND]

ENV['IRB_COPY_COMMAND'] = nil
IRB.setup(__FILE__)
refute IRB.conf[:COPY_COMMAND]

ENV['IRB_COPY_COMMAND'] = ''
IRB.setup(__FILE__)
assert_equal('', IRB.conf[:COPY_COMMAND])

ENV['IRB_COPY_COMMAND'] = 'blah'
IRB.setup(__FILE__)
assert_equal('blah', IRB.conf[:COPY_COMMAND])
ensure
ENV['IRB_COPY_COMMAND'] = orig_copy_command_env
IRB.conf[:COPY_COMMAND] = orig_copy_command_conf
end

def test_completor_environment_variable
orig_use_autocomplete_env = ENV['IRB_COMPLETOR']
orig_use_autocomplete_conf = IRB.conf[:COMPLETOR]
Expand Down

0 comments on commit f0faf98

Please sign in to comment.