require 'ostruct'
#
# Wirble: A collection of useful Irb features.
#
# To use, add the following to your ~/.irbrc:
#
# require 'rubygems'
# require 'wirble'
# Wirble.init
#
# If you want color in Irb, add this to your ~/.irbrc as well:
#
# Wirble.colorize
#
# Note: I spent a fair amount of time documenting this code in the
# README. If you've installed via RubyGems, root around your cache a
# little bit (or fire up gem_server) and read it before you tear your
# hair out sifting through the code below.
#
module Wirble
VERSION = '0.1.2'
#
# Load internal Ruby features, including tab-completion, rubygems,
# and a simple prompt.
#
module Internals
# list of internal libraries to automatically load
LIBRARIES = %w{pp irb/completion rubygems}
#
# load tab completion and rubygems
#
def self.init_libraries
LIBRARIES.each { |lib| require lib rescue nil }
end
#
# Set a simple prompt, unless a custom one has been specified.
#
def self.init_prompt
# set the prompt
if IRB.conf[:PROMPT_MODE] == :DEFAULT
IRB.conf[:PROMPT_MODE] = :SIMPLE
end
end
#
# Load all Ruby internal features.
#
def self.init(opt = nil)
init_libraries unless opt && opt[:skip_libraries]
init_prompt unless opt && opt[:skip_prompt]
end
end
#
# Basic IRB history support. This is based on the tips from
# http://wiki.rubygarden.org/Ruby/page/show/Irb/TipsAndTricks
#
class History
DEFAULTS = {
:history_path => ENV['IRB_HISTORY_FILE'] || "~/.irb_history",
:history_size => (ENV['IRB_HISTORY_SIZE'] || 1000).to_i,
:history_perms => File::WRONLY | File::CREAT | File::TRUNC,
}
private
def say(*args)
puts *args if @verbose
end
def cfg(key)
@opt["history_#{key}".intern]
end
def save_history
path, max_size, perms = %w{path size perms}.map { |v| cfg(v) }
# read lines from history, and truncate the list (if necessary)
lines = Readline::HISTORY.to_a.uniq
lines = lines[-max_size, -1] if lines.size > max_size
# write the history file
real_path = File.expand_path(path)
File.open(real_path, perms) { |fh| fh.puts lines }
say 'Saved %d lines to history file %s.' % [lines.size, path]
end
def load_history
# expand history file and make sure it exists
real_path = File.expand_path(cfg('path'))
unless File.exist?(real_path)
say "History file #{real_path} doesn't exist."
return
end
# read lines from file and add them to history
lines = File.readlines(real_path).map { |line| line.chomp }
Readline::HISTORY.push(*lines)
say 'Read %d lines from history file %s' % [lines.size, cfg('path')]
end
public
def initialize(opt = nil)
@opt = DEFAULTS.merge(opt || {})
return unless defined? Readline::HISTORY
load_history
Kernel.at_exit { save_history }
end
end
#
# Add color support to IRB.
#
module Colorize
#
# Tokenize an inspection string.
#
module Tokenizer
def self.tokenize(str)
raise 'missing block' unless block_given?
chars = str.split(//)
# $stderr.puts "DEBUG: chars = #{chars.join(',')}"
state, val, i, lc = [], '', 0, nil
while i <= chars.size
repeat = false
c = chars[i]
# $stderr.puts "DEBUG: state = #{state}"
case state[-1]
when nil
case c
when ':': state << :symbol
when '"': state << :string
when '#': state << :object
when /[a-z]/i
state << :keyword
repeat = true
when /[0-9-]/
state << :number
repeat = true
when '{': yield :open_hash, '{'
when '[': yield :open_array, '['
when ']': yield :close_array, ']'
when '}': yield :close_hash, '}'
when /\s/: yield :whitespace, c
when ',': yield :comma, ','
when '>'
yield :refers, '=>' if lc == '='
when '.'
yield :range, '..' if lc == '.'
when '='
# ignore these, they're used elsewhere
nil
else
# $stderr.puts "DEBUG: ignoring char #{c}"
end
when :symbol
case c
# XXX: should have =, but that messes up foo=>bar
when /[a-z0-9_!?]/
val << c
else
yield :symbol_prefix, ':'
yield state[-1], val
state.pop; val = ''
repeat = true
end
when :string
case c
when '"'
if lc == "\\"
val[-1] = ?"
else
yield :open_string, '"'
yield state[-1], val
state.pop; val = ''
yield :close_string, '"'
end
else
val << c
end
when :keyword
case c
when /[a-z0-9_]/i
val << c
else
# is this a class?
st = val =~ /^[A-Z]/ ? :class : state[-1]
yield st, val
state.pop; val = ''
repeat = true
end
when :number
case c
when /[0-9e-]/
val << c
when '.'
if lc == '.'
val[/\.$/] = ''
yield state[-1], val
state.pop; val = ''
yield :range, '..'
else
val << c
end
else
yield state[-1], val
state.pop; val = ''
repeat = true
end
when :object
case c
when '<':
yield :open_object, '#<'
state << :object_class
when ':':
state << :object_addr
when '@':
state << :object_line
when '>'
yield :close_object, '>'
state.pop; val = ''
end
when :object_class
case c
when ':'
yield state[-1], val
state.pop; val = ''
repeat = true
else
val << c
end
when :object_addr
case c
when '>'
when '@'
yield :object_addr_prefix, ':'
yield state[-1], val
state.pop; val = ''
repeat = true
else
val << c
end
when :object_line
case c
when '>'
yield :object_line_prefix, '@'
yield state[-1], val
state.pop; val = ''
repeat = true
else
val << c
end
else
raise "unknown state #{state}"
end
unless repeat
i += 1
lc = c
end
end
end
end
#
# Terminal escape codes for colors.
#
module Color
COLORS = {
:nothing => '0;0',
:black => '0;30',
:red => '0;31',
:green => '0;32',
:brown => '0;33',
:blue => '0;34',
:cyan => '0;36',
:purple => '0;35',
:light_gray => '0;37',
:dark_gray => '1;30',
:light_red => '1;31',
:light_green => '1;32',
:yellow => '1;33',
:light_blue => '1;34',
:light_cyan => '1;36',
:light_purple => '1;35',
:white => '1;37',
}
#
# Return the escape code for a given color.
#
def self.escape(key)
COLORS.key?(key) && "\033[#{COLORS[key]}m"
end
end
#
# Default Wirble color scheme.
#
DEFAULT_COLORS = {
# delimiter colors
:comma => :blue,
:refers => :blue,
# container colors (hash and array)
:open_hash => :green,
:close_hash => :green,
:open_array => :green,
:close_array => :green,
# object colors
:open_object => :light_red,
:object_class => :white,
:object_addr_prefix => :blue,
:object_line_prefix => :blue,
:close_object => :light_red,
# symbol colors
:symbol => :yellow,
:symbol_prefix => :yellow,
# string colors
:open_string => :red,
:string => :cyan,
:close_string => :red,
# misc colors
:number => :cyan,
:keyword => :green,
:class => :light_green,
:range => :red,
}
#
# Fruity testing colors.
#
TESTING_COLORS = {
:comma => :red,
:refers => :red,
:open_hash => :blue,
:close_hash => :blue,
:open_array => :green,
:close_array => :green,
:open_object => :light_red,
:object_class => :light_green,
:object_addr => :purple,
:object_line => :light_purple,
:close_object => :light_red,
:symbol => :yellow,
:symbol_prefix => :yellow,
:number => :cyan,
:string => :cyan,
:keyword => :white,
:range => :light_blue,
}
#
# Set color map to hash
#
def self.colors=(hash)
@colors = hash
end
#
# Get current color map
#
def self.colors
@colors ||= {}.update(DEFAULT_COLORS)
end
#
# Return a string with the given color.
#
def self.colorize_string(str, color)
col, nocol = [color, :nothing].map { |key| Color.escape(key) }
col ? "#{col}#{str}#{nocol}" : str
end
#
# Colorize the results of inspect
#
def self.colorize(str)
begin
ret, nocol = '', Color.escape(:nothing)
Tokenizer.tokenize(str) do |tok, val|
# c = Color.escape(colors[tok])
ret << colorize_string(val, colors[tok])
end
ret
rescue
# catch any errors from the tokenizer (just in case)
str
end
end
#
# Enable colorized IRB results.
#
def self.enable(custom_colors = nil)
# if there's a better way to do this, I'm all ears.
::IRB::Irb.class_eval do
alias :non_color_output_value :output_value
def output_value
if @context.inspect?
val = Colorize.colorize(@context.last_value.inspect)
printf @context.return_format, val
else
printf @context.return_format, @context.last_value
end
end
end
colors = custom_colors if custom_colors
end
#
# Disable colorized IRB results.
#
def self.disable
::IRB::Irb.class_eval do
alias :output_value :non_color_output_value
end
end
end
#
# Convenient shortcut methods.
#
module Shortcuts
#
# Print object methods, sorted by name. (excluding methods that
# exist in the class Object) .
#
def po(o)
o.methods.sort - Object.methods
end
#
# Print object constants, sorted by name.
#
def poc(o)
o.constants.sort
end
end
#
# Convenient shortcut for ri
#
module RiShortcut
def self.init
Kernel.class_eval {
def ri(arg)
puts `ri '#{arg}'`
end
}
Module.instance_eval {
def ri(meth=nil)
if meth
if instance_methods(false).include? meth.to_s
puts `ri #{self}##{meth}`
else
super
end
else
puts `ri #{self}`
end
end
}
end
end
#
# Enable color results.
#
def self.colorize(custom_colors = nil)
Colorize.enable(custom_colors)
end
#
# Load everything except color.
#
def self.init(opt = nil)
# make sure opt isn't nil
opt ||= {}
# load internal irb/ruby features
Internals.init(opt) unless opt && opt[:skip_internals]
# load the history
History.new(opt) unless opt && opt[:skip_history]
# load shortcuts
unless opt && opt[:skip_shortcuts]
# load ri shortcuts
RiShortcut.init
# include common shortcuts
Object.class_eval { include Shortcuts }
end
colorize(opt[:colors]) if opt && opt[:init_colors]
end
end
syntax highlighted by Code2HTML, v. 0.9.1