diff --git a/lib/reline_pac.rb b/lib/reline_pac.rb index 08e3586..bb5cff6 100644 --- a/lib/reline_pac.rb +++ b/lib/reline_pac.rb @@ -4,6 +4,9 @@ require_relative 'reline_pac/version' require_relative 'reline_pac/packages' require_relative 'reline_pac/config' +require_relative 'reline_pac/introspection' +# Load commands only if IRB is available +require_relative 'reline_pac/irb_commands' if defined?(IRB) # RelinePac provides Reline extensions for completion, history search, and clipboard helpers. module RelinePac diff --git a/lib/reline_pac/introspection.rb b/lib/reline_pac/introspection.rb new file mode 100644 index 0000000..3fa58e5 --- /dev/null +++ b/lib/reline_pac/introspection.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +module RelinePac + # Introspection provides utilities to inspect and debug Reline keybindings. + module Introspection + class << self + # Display all current keybindings in a human-readable format. + def keymap + config = Reline.core.config + bindings = {} + + config.keymap.instance_variable_get(:@key_bindings).each do |key, method| + # Skip ed_insert as it's used for regular character input + next if %i[ed_insert ed_digit].include?(method) + + key_str = format_key_sequence(key) + + # Skip if we've already displayed this key + next if bindings[key_str] + + bindings[key_str] = method + end + bindings + end + + private + + # Format a key sequence (byte array) into a readable string. + # @param key [Array] array of bytes representing the key sequence + # @return [String] formatted key representation + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity + def format_key_sequence(key) + sequence = key.pack('C*') + + case sequence + when "\e[A", "\eOA" then '↑' + when "\e[B", "\eOB" then '↓' + when "\e[C", "\eOC" then '→' + when "\e[D", "\eOD" then '←' + when "\e[F", "\eOF" then 'End' + when "\e[H", "\eOH" then 'Home' + when "\e[1~", "\e[7~" then 'Home' # rubocop:disable Lint/DuplicateBranch + when "\e[4~", "\e[8~" then 'End' # rubocop:disable Lint/DuplicateBranch + when "\e[3~" then 'Delete' + when "\e[5~" then 'PageUp' + when "\e[6~" then 'PageDown' + when "\e[Z" then '⇧Tab' + when "\e[200~" then 'BracketedPaste' + when /\e\[1;5([ABCD])/ then "⌃#{arrow_symbol(::Regexp.last_match(1))}" + when /\e\[1;3([ABCD])/ then "⌥#{arrow_symbol(::Regexp.last_match(1))}" + when "\e\x1B[C" then '⌃→' + when "\e\x1B[D" then '⌃←' + else + if key.first == 27 && key.size > 1 + rest = key[1..].map { |byte| format_byte(byte) }.join + "⎋#{rest}" + else + key.map { |byte| format_byte(byte) }.join + end + end + end + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity + + # Convert arrow key letter to symbol. + # @param letter [String] 'A', 'B', 'C', or 'D' + # @return [String] arrow symbol + def arrow_symbol(letter) + { 'A' => '↑', 'B' => '↓', 'C' => '→', 'D' => '←' }[letter] + end + + # Format a single byte into a readable representation. + # @param byte [Integer] byte value (0-255) + # @return [String] formatted byte representation + def format_byte(byte) + case byte + when 27 + '⎋' + when 0..31 # Control キー + "⌃#{(byte + 64).chr}" + when 32 + '␣' + when 127 + '⌫' + else + byte.chr.match?(/[[:print:]]/) ? byte.chr : format('\\x%02X', byte) + end + end + end + end +end diff --git a/lib/reline_pac/irb_commands.rb b/lib/reline_pac/irb_commands.rb new file mode 100644 index 0000000..dc8706b --- /dev/null +++ b/lib/reline_pac/irb_commands.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'irb' + +module RelinePac + module IrbCommands + # IRB command to display Reline keybindings + class Keybinds < ::IRB::Command::Base + category 'Context' + description 'Show Reline keybindings' + + def execute(_arg) + bindings = RelinePac::Introspection.keymap + max_key_length = bindings.keys.map(&:length).max + + bindings.each do |key, method| + puts "#{key.ljust(max_key_length + 2)} #{method}" + end + end + end + end +end + +# IRB が読み込まれている場合のみ登録 +IRB::Command.register(:show_keybinds, RelinePac::IrbCommands::Keybinds) if defined?(IRB::Command) diff --git a/spec/reline_pac/introspection_spec.rb b/spec/reline_pac/introspection_spec.rb new file mode 100644 index 0000000..1d2c9d4 --- /dev/null +++ b/spec/reline_pac/introspection_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe RelinePac::Introspection do + describe '.keymap' do + it 'returns a hash without errors' do + expect { described_class.keymap }.not_to raise_error + end + + it 'returns a hash of keybindings' do + result = described_class.keymap + expect(result).to be_a(Hash) + end + + it 'filters out ed_insert and ed_digit bindings' do + result = described_class.keymap + expect(result.values).not_to include(:ed_insert) + expect(result.values).not_to include(:ed_digit) + end + + it 'includes known keybindings' do + result = described_class.keymap + # Control-A should map to ed_move_to_beg + expect(result).to include('⌃A' => :ed_move_to_beg) + end + + it 'removes duplicate keys' do + result = described_class.keymap + # All keys should be unique + expect(result.keys.uniq.length).to eq(result.keys.length) + end + + it 'formats special keys correctly' do + result = described_class.keymap + # Should have Control and Escape keys formatted with symbols + expect(result.keys.grep(/⌃/).any?).to be true + expect(result.keys.grep(/⎋/).any?).to be true + end + end +end