lib/reline/config.rb in reline-0.0.0 vs lib/reline/config.rb in reline-0.0.1

- old
+ new

@@ -1,25 +1,72 @@ require 'pathname' class Reline::Config - DEFAULT_PATH = Pathname.new(Dir.home).join('.inputrc') + attr_reader :test_mode + DEFAULT_PATH = '~/.inputrc' + + KEYSEQ_PATTERN = /\\(?:C|Control)-[A-Za-z_]|\\(?:M|Meta)-[0-9A-Za-z_]|\\(?:C|Control)-(?:M|Meta)-[A-Za-z_]|\\(?:M|Meta)-(?:C|Control)-[A-Za-z_]|\\e|\\[\\\"\'abdfnrtv]|\\\d{1,3}|\\x\h{1,2}|./ + + class InvalidInputrc < RuntimeError + attr_accessor :file, :lineno + end + + VARIABLE_NAMES = %w{ + bind-tty-special-chars + blink-matching-paren + byte-oriented + completion-ignore-case + convert-meta + disable-completion + enable-keypad + expand-tilde + history-preserve-point + history-size + horizontal-scroll-mode + input-meta + keyseq-timeout + mark-directories + mark-modified-lines + mark-symlinked-directories + match-hidden-files + meta-flag + output-meta + page-completions + prefer-visible-bell + print-completions-horizontally + show-all-if-ambiguous + show-all-if-unmodified + visible-stats + } + VARIABLE_NAME_SYMBOLS = VARIABLE_NAMES.map { |v| :"#{v.tr(?-, ?_)}" } + VARIABLE_NAME_SYMBOLS.each do |v| + attr_accessor v + end + def initialize + @additional_key_bindings = {} # from inputrc + @default_key_bindings = {} # environment-dependent @skip_section = nil - @if_stack = [] + @if_stack = nil @editing_mode_label = :emacs @keymap_label = :emacs @key_actors = {} @key_actors[:emacs] = Reline::KeyActor::Emacs.new @key_actors[:vi_insert] = Reline::KeyActor::ViInsert.new @key_actors[:vi_command] = Reline::KeyActor::ViCommand.new + @history_size = 500 + @keyseq_timeout = 500 + @test_mode = false end def reset if editing_mode_is?(:vi_command) @editing_mode_label = :vi_insert end + @additional_key_bindings = {} + @default_key_bindings = {} end def editing_mode @key_actors[@editing_mode_label] end @@ -34,33 +81,55 @@ def keymap @key_actors[@keymap_label] end - def read(file = DEFAULT_PATH) + def read(file = nil) + file ||= File.expand_path(ENV['INPUTRC'] || DEFAULT_PATH) begin if file.respond_to?(:readlines) lines = file.readlines else - File.open(file, 'rt') do |f| - lines = f.readlines - end + lines = File.readlines(file) end rescue Errno::ENOENT - $stderr.puts "no such file #{file}" return nil end - read_lines(lines) + read_lines(lines, file) self + rescue InvalidInputrc => e + warn e.message + nil end - def read_lines(lines) - lines.each do |line| - line = line.chomp.gsub(/^\s*/, '') + def key_bindings + # override @default_key_bindings with @additional_key_bindings + @default_key_bindings.merge(@additional_key_bindings) + end + + def add_default_key_binding(keystroke, target) + @default_key_bindings[keystroke] = target + end + + def reset_default_key_bindings + @default_key_bindings = {} + end + + def read_lines(lines, file = nil) + conditions = [@skip_section, @if_stack] + @skip_section = nil + @if_stack = [] + + lines.each_with_index do |line, no| + next if line.match(/\A\s*#/) + + no += 1 + + line = line.chomp.lstrip if line[0, 1] == '$' - handle_directive(line[1..-1]) + handle_directive(line[1..-1], file, no) next end next if @skip_section @@ -68,72 +137,57 @@ var, value = $1.downcase, $2.downcase bind_variable(var, value) next end - if line =~ /\s*(.*)\s*:\s*(.*)\s*$/ + if line =~ /\s*("#{KEYSEQ_PATTERN}+")\s*:\s*(.*)\s*$/ key, func_name = $1, $2 - bind_key(key, func_name) + keystroke, func = bind_key(key, func_name) + next unless keystroke + @additional_key_bindings[keystroke] = func end end + unless @if_stack.empty? + raise InvalidInputrc, "#{file}:#{@if_stack.last[1]}: unclosed if" + end + ensure + @skip_section, @if_stack = conditions end - def handle_directive(directive) + def handle_directive(directive, file, no) directive, args = directive.split(' ') case directive when 'if' condition = false case args # TODO: variables when 'mode' when 'term' when 'version' else # application name condition = true if args == 'Ruby' + condition = true if args == 'Reline' end - unless @skip_section.nil? - @if_stack << @skip_section - end + @if_stack << [file, no, @skip_section] @skip_section = !condition when 'else' + if @if_stack.empty? + raise InvalidInputrc, "#{file}:#{no}: unmatched else" + end @skip_section = !@skip_section when 'endif' - @skip_section = nil - unless @if_stack.empty? - @skip_section = @if_stack.pop + if @if_stack.empty? + raise InvalidInputrc, "#{file}:#{no}: unmatched endif" end + @skip_section = @if_stack.pop when 'include' read(args) end end def bind_variable(name, value) case name - when %w{ - bind-tty-special-chars - blink-matching-paren - byte-oriented - completion-ignore-case - convert-meta - disable-completion - enable-keypad - expand-tilde - history-preserve-point - horizontal-scroll-mode - input-meta - mark-directories - mark-modified-lines - mark-symlinked-directories - match-hidden-files - meta-flag - output-meta - page-completions - prefer-visible-bell - print-completions-horizontally - show-all-if-ambiguous - show-all-if-unmodified - visible-stats - } then + when VARIABLE_NAMES then variable_name = :"@#{name.tr(?-, ?_)}" instance_variable_set(variable_name, value.nil? || value == '1' || value == 'on') when 'bell-style' @bell_style = case value @@ -168,68 +222,66 @@ when 'vi', 'vi-move', 'vi-command' @keymap_label = :vi_command when 'vi-insert' @keymap_label = :vi_insert end + when 'keyseq-timeout' + @keyseq_timeout = value.to_i end end def bind_key(key, func_name) - if key =~ /"(.*)"/ - keyseq = parse_keyseq($1).force_encoding('ASCII-8BIT') + if key =~ /\A"(.*)"\z/ + keyseq = parse_keyseq($1) else keyseq = nil end if func_name =~ /"(.*)"/ - func = parse_keyseq($1).force_encoding('ASCII-8BIT') + func = parse_keyseq($1) else - func = func_name.to_sym # It must be macro. + func = func_name.tr(?-, ?_).to_sym # It must be macro. end [keyseq, func] end - def key_notation_to_char(notation) + def key_notation_to_code(notation) case notation - when /\\C-([A-Za-z_])/ - (1 + $1.downcase.ord - ?a.ord).chr('ASCII-8BIT') - when /\\M-([0-9A-Za-z_])/ + when /\\(?:C|Control)-([A-Za-z_])/ + (1 + $1.downcase.ord - ?a.ord) + when /\\(?:M|Meta)-([0-9A-Za-z_])/ modified_key = $1 - code = - case $1 - when /[0-9]/ - ?\M-0.bytes.first + (modified_key.ord - ?0.ord) - when /[A-Z]/ - ?\M-A.bytes.first + (modified_key.ord - ?A.ord) - when /[a-z]/ - ?\M-a.bytes.first + (modified_key.ord - ?a.ord) - end - code.chr('ASCII-8BIT') - when /\\C-M-[A-Za-z_]/, /\\M-C-[A-Za-z_]/ + case $1 + when /[0-9]/ + ?\M-0.bytes.first + (modified_key.ord - ?0.ord) + when /[A-Z]/ + ?\M-A.bytes.first + (modified_key.ord - ?A.ord) + when /[a-z]/ + ?\M-a.bytes.first + (modified_key.ord - ?a.ord) + end + when /\\(?:C|Control)-(?:M|Meta)-[A-Za-z_]/, /\\(?:M|Meta)-(?:C|Control)-[A-Za-z_]/ # 129 M-^A - when /\\(\d{1,3})/ then $1.to_i(8).chr # octal - when /\\x(\h{1,2})/ then $1.to_i(16).chr # hexadecimal - when "\\e" then ?\e - when "\\\\" then ?\\ - when "\\\"" then ?" - when "\\'" then ?' - when "\\a" then ?\a - when "\\b" then ?\b - when "\\d" then ?\d - when "\\f" then ?\f - when "\\n" then ?\n - when "\\r" then ?\r - when "\\t" then ?\t - when "\\v" then ?\v - else notation + when /\\(\d{1,3})/ then $1.to_i(8) # octal + when /\\x(\h{1,2})/ then $1.to_i(16) # hexadecimal + when "\\e" then ?\e.ord + when "\\\\" then ?\\.ord + when "\\\"" then ?".ord + when "\\'" then ?'.ord + when "\\a" then ?\a.ord + when "\\b" then ?\b.ord + when "\\d" then ?\d.ord + when "\\f" then ?\f.ord + when "\\n" then ?\n.ord + when "\\r" then ?\r.ord + when "\\t" then ?\t.ord + when "\\v" then ?\v.ord + else notation.ord end end def parse_keyseq(str) - # TODO: Control- and Meta- - ret = String.new(encoding: 'ASCII-8BIT') - while str =~ /(\\C-[A-Za-z_]|\\M-[0-9A-Za-z_]|\\C-M-[A-Za-z_]|\\M-C-[A-Za-z_]|\\e|\\\\|\\"|\\'|\\a|\\b|\\d|\\f|\\n|\\r|\\t|\\v|\\\d{1,3}|\\x\h{1,2}|.)/ - ret << key_notation_to_char($&) - str = $' + ret = [] + str.scan(KEYSEQ_PATTERN) do + ret << key_notation_to_code($&) end ret end end