/extensions/wirble/lib/wirble.rb

http://github.com/alloy/dietrb · Ruby · 541 lines · 367 code · 57 blank · 117 comment · 33 complexity · d5e5715db619a8e62fd35caa539c57de MD5 · raw file

  1. require 'ostruct'
  2. #
  3. # Wirble: A collection of useful Irb features.
  4. #
  5. # To use, add the following to your ~/.irbrc:
  6. #
  7. # require 'rubygems'
  8. # require 'wirble'
  9. # Wirble.init
  10. #
  11. # If you want color in Irb, add this to your ~/.irbrc as well:
  12. #
  13. # Wirble.colorize
  14. #
  15. # Note: I spent a fair amount of time documenting this code in the
  16. # README. If you've installed via RubyGems, root around your cache a
  17. # little bit (or fire up gem_server) and read it before you tear your
  18. # hair out sifting through the code below.
  19. #
  20. module Wirble
  21. VERSION = '0.1.3.2'
  22. #
  23. # Load internal Ruby features, including pp, tab-completion,
  24. # and a simple prompt.
  25. #
  26. module Internals
  27. # list of internal libraries to automatically load
  28. LIBRARIES = %w{pp irb/completion}
  29. #
  30. # load libraries
  31. #
  32. def self.init_libraries
  33. LIBRARIES.each do |lib|
  34. begin
  35. require lib
  36. rescue LoadError
  37. nil
  38. end
  39. end
  40. end
  41. #
  42. # Set a simple prompt, unless a custom one has been specified.
  43. #
  44. def self.init_prompt
  45. # set the prompt
  46. if IRB.conf[:PROMPT_MODE] == :DEFAULT
  47. IRB.conf[:PROMPT_MODE] = :SIMPLE
  48. end
  49. end
  50. #
  51. # Load all Ruby internal features.
  52. #
  53. def self.init(opt = nil)
  54. init_libraries unless opt && opt[:skip_libraries]
  55. init_prompt unless opt && opt[:skip_prompt]
  56. end
  57. end
  58. #
  59. # Basic IRB history support. This is based on the tips from
  60. # http://wiki.rubygarden.org/Ruby/page/show/Irb/TipsAndTricks
  61. #
  62. class History
  63. DEFAULTS = {
  64. :history_path => ENV['IRB_HISTORY_FILE'] || "~/.irb_history",
  65. :history_size => (ENV['IRB_HISTORY_SIZE'] || 1000).to_i,
  66. :history_perms => File::WRONLY | File::CREAT | File::TRUNC,
  67. :history_uniq => true,
  68. }
  69. private
  70. def say(*args)
  71. puts(*args) if @verbose
  72. end
  73. def cfg(key)
  74. @opt["history_#{key}".intern]
  75. end
  76. def save_history
  77. path, max_size, perms, uniq = %w{path size perms uniq}.map { |v| cfg(v) }
  78. # read lines from history, and truncate the list (if necessary)
  79. lines = Readline::HISTORY.to_a
  80. lines.reverse! if reverse = uniq.to_s == 'reverse'
  81. lines.uniq! if uniq
  82. lines.reverse! if reverse
  83. lines.slice!(0, lines.size - max_size) if lines.size > max_size
  84. # write the history file
  85. real_path = File.expand_path(path)
  86. File.open(real_path, perms) { |fh| fh.puts lines }
  87. say 'Saved %d lines to history file %s.' % [lines.size, path]
  88. end
  89. def load_history
  90. # expand history file and make sure it exists
  91. real_path = File.expand_path(cfg('path'))
  92. unless File.exist?(real_path)
  93. say "History file #{real_path} doesn't exist."
  94. return
  95. end
  96. # read lines from file and add them to history
  97. lines = File.readlines(real_path).map { |line| line.chomp }
  98. Readline::HISTORY.push(*lines)
  99. say 'Read %d lines from history file %s' % [lines.size, cfg('path')]
  100. end
  101. public
  102. def initialize(opt = nil)
  103. @opt = DEFAULTS.merge(opt || {})
  104. return unless defined? Readline::HISTORY
  105. load_history
  106. Kernel.at_exit { save_history }
  107. end
  108. end
  109. #
  110. # Add color support to IRB.
  111. #
  112. module Colorize
  113. #
  114. # Tokenize an inspection string.
  115. #
  116. module Tokenizer
  117. def self.tokenize(str)
  118. raise 'missing block' unless block_given?
  119. chars = str.split(//)
  120. # $stderr.puts "DEBUG: chars = #{chars.join(',')}"
  121. state, val, i, lc = [], '', 0, nil
  122. while i <= chars.size
  123. repeat = false
  124. c = chars[i]
  125. # $stderr.puts "DEBUG: state = #{state}"
  126. case state[-1]
  127. when nil
  128. case c
  129. when ':'
  130. state << :symbol
  131. when '"'
  132. state << :string
  133. when '#'
  134. state << :object
  135. when /[a-z]/i
  136. state << :keyword
  137. repeat = true
  138. when /[0-9-]/
  139. state << :number
  140. repeat = true
  141. when '{'
  142. yield :open_hash, '{'
  143. when '['
  144. yield :open_array, '['
  145. when ']'
  146. yield :close_array, ']'
  147. when '}'
  148. yield :close_hash, '}'
  149. when /\s/
  150. yield :whitespace, c
  151. when ','
  152. yield :comma, ','
  153. when '>'
  154. yield :refers, '=>' if lc == '='
  155. when '.'
  156. yield :range, '..' if lc == '.'
  157. when '='
  158. # ignore these, they're used elsewhere
  159. nil
  160. else
  161. # $stderr.puts "DEBUG: ignoring char #{c}"
  162. end
  163. when :symbol
  164. case c
  165. # XXX: should have =, but that messes up foo=>bar
  166. when /[a-z0-9_!?]/
  167. val << c
  168. else
  169. yield :symbol_prefix, ':'
  170. yield state[-1], val
  171. state.pop; val = ''
  172. repeat = true
  173. end
  174. when :string
  175. case c
  176. when '"'
  177. if lc == "\\"
  178. val[-1] = ?"
  179. else
  180. yield :open_string, '"'
  181. yield state[-1], val
  182. state.pop; val = ''
  183. yield :close_string, '"'
  184. end
  185. else
  186. val << c
  187. end
  188. when :keyword
  189. case c
  190. when /[a-z0-9_]/i
  191. val << c
  192. else
  193. # is this a class?
  194. st = val =~ /^[A-Z]/ ? :class : state[-1]
  195. yield st, val
  196. state.pop; val = ''
  197. repeat = true
  198. end
  199. when :number
  200. case c
  201. when /[0-9e-]/
  202. val << c
  203. when '.'
  204. if lc == '.'
  205. val[/\.$/] = ''
  206. yield state[-1], val
  207. state.pop; val = ''
  208. yield :range, '..'
  209. else
  210. val << c
  211. end
  212. else
  213. yield state[-1], val
  214. state.pop; val = ''
  215. repeat = true
  216. end
  217. when :object
  218. case c
  219. when '<'
  220. yield :open_object, '#<'
  221. state << :object_class
  222. when ':'
  223. state << :object_addr
  224. when '@'
  225. state << :object_line
  226. when '>'
  227. yield :close_object, '>'
  228. state.pop; val = ''
  229. end
  230. when :object_class
  231. case c
  232. when ':'
  233. yield state[-1], val
  234. state.pop; val = ''
  235. repeat = true
  236. else
  237. val << c
  238. end
  239. when :object_addr
  240. case c
  241. when '>'
  242. when '@'
  243. yield :object_addr_prefix, ':'
  244. yield state[-1], val
  245. state.pop; val = ''
  246. repeat = true
  247. else
  248. val << c
  249. end
  250. when :object_line
  251. case c
  252. when '>'
  253. yield :object_line_prefix, '@'
  254. yield state[-1], val
  255. state.pop; val = ''
  256. repeat = true
  257. else
  258. val << c
  259. end
  260. else
  261. raise "unknown state #{state}"
  262. end
  263. unless repeat
  264. i += 1
  265. lc = c
  266. end
  267. end
  268. end
  269. end
  270. #
  271. # Terminal escape codes for colors.
  272. #
  273. module Color
  274. COLORS = {
  275. :nothing => '0;0',
  276. :black => '0;30',
  277. :red => '0;31',
  278. :green => '0;32',
  279. :brown => '0;33',
  280. :blue => '0;34',
  281. :cyan => '0;36',
  282. :purple => '0;35',
  283. :light_gray => '0;37',
  284. :dark_gray => '1;30',
  285. :light_red => '1;31',
  286. :light_green => '1;32',
  287. :yellow => '1;33',
  288. :light_blue => '1;34',
  289. :light_cyan => '1;36',
  290. :light_purple => '1;35',
  291. :white => '1;37',
  292. }
  293. #
  294. # Return the escape code for a given color.
  295. #
  296. def self.escape(key)
  297. COLORS.key?(key) && "\033[#{COLORS[key]}m"
  298. end
  299. end
  300. #
  301. # Default Wirble color scheme.
  302. #
  303. DEFAULT_COLORS = {
  304. # delimiter colors
  305. :comma => :blue,
  306. :refers => :blue,
  307. # container colors (hash and array)
  308. :open_hash => :green,
  309. :close_hash => :green,
  310. :open_array => :green,
  311. :close_array => :green,
  312. # object colors
  313. :open_object => :light_red,
  314. :object_class => :white,
  315. :object_addr_prefix => :blue,
  316. :object_line_prefix => :blue,
  317. :close_object => :light_red,
  318. # symbol colors
  319. :symbol => :yellow,
  320. :symbol_prefix => :yellow,
  321. # string colors
  322. :open_string => :red,
  323. :string => :cyan,
  324. :close_string => :red,
  325. # misc colors
  326. :number => :cyan,
  327. :keyword => :green,
  328. :class => :light_green,
  329. :range => :red,
  330. }
  331. #
  332. # Fruity testing colors.
  333. #
  334. TESTING_COLORS = {
  335. :comma => :red,
  336. :refers => :red,
  337. :open_hash => :blue,
  338. :close_hash => :blue,
  339. :open_array => :green,
  340. :close_array => :green,
  341. :open_object => :light_red,
  342. :object_class => :light_green,
  343. :object_addr => :purple,
  344. :object_line => :light_purple,
  345. :close_object => :light_red,
  346. :symbol => :yellow,
  347. :symbol_prefix => :yellow,
  348. :number => :cyan,
  349. :string => :cyan,
  350. :keyword => :white,
  351. :range => :light_blue,
  352. }
  353. #
  354. # Set color map to hash
  355. #
  356. def self.colors=(hash)
  357. @colors = hash
  358. end
  359. #
  360. # Get current color map
  361. #
  362. def self.colors
  363. @colors ||= {}.update(DEFAULT_COLORS)
  364. end
  365. #
  366. # Return a string with the given color.
  367. #
  368. def self.colorize_string(str, color)
  369. col, nocol = [color, :nothing].map { |key| Color.escape(key) }
  370. col ? "#{col}#{str}#{nocol}" : str
  371. end
  372. #
  373. # Colorize the results of inspect
  374. #
  375. def self.colorize(str)
  376. begin
  377. ret, nocol = '', Color.escape(:nothing)
  378. Tokenizer.tokenize(str) do |tok, val|
  379. # c = Color.escape(colors[tok])
  380. ret << colorize_string(val, colors[tok])
  381. end
  382. ret
  383. rescue
  384. # catch any errors from the tokenizer (just in case)
  385. str
  386. end
  387. end
  388. #
  389. # Enable colorized IRB results.
  390. #
  391. def self.enable(custom_colors = nil)
  392. # if there's a better way to do this, I'm all ears.
  393. ::IRB::Irb.class_eval do
  394. alias :non_color_output_value :output_value
  395. def output_value
  396. if @context.inspect?
  397. val = Colorize.colorize(@context.last_value.inspect)
  398. p val
  399. printf @context.return_format, val
  400. else
  401. printf @context.return_format, @context.last_value
  402. end
  403. end
  404. end
  405. self.colors = custom_colors if custom_colors
  406. end
  407. #
  408. # Disable colorized IRB results.
  409. #
  410. def self.disable
  411. ::IRB::Irb.class_eval do
  412. alias :output_value :non_color_output_value
  413. end
  414. end
  415. end
  416. #
  417. # Convenient shortcut methods.
  418. #
  419. module Shortcuts
  420. #
  421. # Print object methods, sorted by name. (excluding methods that
  422. # exist in the class Object) .
  423. #
  424. def po(o)
  425. o.methods.sort - Object.methods
  426. end
  427. #
  428. # Print object constants, sorted by name.
  429. #
  430. def poc(o)
  431. o.constants.sort
  432. end
  433. end
  434. #
  435. # Convenient shortcut for ri
  436. #
  437. module RiShortcut
  438. def self.init
  439. Kernel.class_eval {
  440. def ri(arg)
  441. puts `ri '#{arg}'`
  442. end
  443. }
  444. Module.instance_eval {
  445. def ri(meth=nil)
  446. if meth
  447. if instance_methods(false).include? meth.to_s
  448. puts `ri #{self}##{meth}`
  449. else
  450. super
  451. end
  452. else
  453. puts `ri #{self}`
  454. end
  455. end
  456. }
  457. end
  458. end
  459. #
  460. # Enable color results.
  461. #
  462. def self.colorize(custom_colors = nil)
  463. Colorize.enable(custom_colors)
  464. end
  465. #
  466. # Load everything except color.
  467. #
  468. def self.init(opt = nil)
  469. # make sure opt isn't nil
  470. opt ||= {}
  471. # load internal irb/ruby features
  472. Internals.init(opt) unless opt && opt[:skip_internals]
  473. # load the history
  474. History.new(opt) unless opt && opt[:skip_history]
  475. # load shortcuts
  476. unless opt && opt[:skip_shortcuts]
  477. # load ri shortcuts
  478. RiShortcut.init
  479. # include common shortcuts
  480. Object.class_eval { include Shortcuts }
  481. end
  482. colorize(opt[:colors]) if opt && opt[:init_colors]
  483. end
  484. end