Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 39 additions & 35 deletions lib/reline/ansi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,21 @@ class Reline::ANSI
'kcud1' => :ed_next_history,
'kcuf1' => :ed_next_char,
'kcub1' => :ed_prev_char,
'cuu' => :ed_prev_history,
'cud' => :ed_next_history,
'cuf' => :ed_next_char,
'cub' => :ed_prev_char,
}

ANSI_CURSOR_KEY_BINDINGS = {
# Up
'A' => [:ed_prev_history, {}],
# Down
'B' => [:ed_next_history, {}],
# Right
'C' => [:ed_next_char, { ctrl: :em_next_word, meta: :em_next_word }],
# Left
'D' => [:ed_prev_char, { ctrl: :ed_prev_word, meta: :ed_prev_word }],
# End
'F' => [:ed_move_to_end, {}],
# Home
'H' => [:ed_move_to_beg, {}],
}

if Reline::Terminfo.enabled?
Expand All @@ -33,22 +44,12 @@ def self.win?
end

def self.set_default_key_bindings(config, allow_terminfo: true)
set_default_key_bindings_ansi_cursor(config)
if allow_terminfo && Reline::Terminfo.enabled?
set_default_key_bindings_terminfo(config)
else
set_default_key_bindings_comprehensive_list(config)
end
{
# extended entries of terminfo
[27, 91, 49, 59, 53, 67] => :em_next_word, # Ctrl+→, extended entry
[27, 91, 49, 59, 53, 68] => :ed_prev_word, # Ctrl+←, extended entry
[27, 91, 49, 59, 51, 67] => :em_next_word, # Meta+→, extended entry
[27, 91, 49, 59, 51, 68] => :ed_prev_word, # Meta+←, extended entry
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"\e[1;5C", "\e[1;5D", "\e[1;3C", "\e[1;3D" will be bound in set_default_key_bindings_ansi_cursor

}.each_pair do |key, func|
config.add_default_key_binding_by_keymap(:emacs, key, func)
config.add_default_key_binding_by_keymap(:vi_insert, key, func)
config.add_default_key_binding_by_keymap(:vi_command, key, func)
end
{
[27, 91, 90] => :completion_journey_up, # S-Tab
}.each_pair do |key, func|
Expand All @@ -64,18 +65,33 @@ def self.set_default_key_bindings(config, allow_terminfo: true)
end
end

def self.set_default_key_bindings_ansi_cursor(config)
ANSI_CURSOR_KEY_BINDINGS.each do |char, (default_func, modifiers)|
bindings = [["\e[#{char}", default_func]] # CSI + char
if modifiers[:ctrl]
# CSI + ctrl_key_modifier + char
bindings << ["\e[1;5#{char}", modifiers[:ctrl]]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In https://mdfs.net/Docs/Comp/Comms/ANSIKeys, it says "\e[1;5D" and also "\e[5D" is valid as Ctrl+Left.
Although, I think we only need 1;5 style because set_default_key_bindings_comprehensive_list only had \e[1;5D, emacs and readline only supports \e[1;5D style.

end
if modifiers[:meta]
# CSI + meta_key_modifier + char
bindings << ["\e[1;3#{char}", modifiers[:meta]]
# Meta(ESC) + CSI + char
bindings << ["\e\e[#{char}", modifiers[:meta]]
end
bindings.each do |sequence, func|
key = sequence.bytes
config.add_default_key_binding_by_keymap(:emacs, key, func)
config.add_default_key_binding_by_keymap(:vi_insert, key, func)
config.add_default_key_binding_by_keymap(:vi_command, key, func)
end
end
end

def self.set_default_key_bindings_terminfo(config)
key_bindings = CAPNAME_KEY_BINDINGS.map do |capname, key_binding|
begin
key_code = Reline::Terminfo.tigetstr(capname)
case capname
# Escape sequences that omit the move distance and are set to defaults
# value 1 may be sometimes sent by pressing the arrow-key.
when 'cuu', 'cud', 'cuf', 'cub'
[ key_code.sub(/%p1%d/, '').bytes, key_binding ]
Copy link
Member Author

@tompng tompng Jul 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

# searched cuu from terminfo
cuu=\E[%p1%dA
cuu=\233%p1%dA
cuu=\Ep-%p1%d;
cuu=\E&a-%p1%dR
cuu=\E[%p1%dA$<5>
cuu=\E[%p1%dA$<30>
cuu=\037up %p1%d\r
cuu=\035up %p1%d;

these are not for input sequence. It's for output sequence like \e[42A = move cursor up 42 rows.
This pattern does not match to \e[A (UP) nor \e[1;2A (UP+SHIFT).
We need to explicitly bind "\e[A", not irrelevant_value.gsub(replace_it, to_match_CSI_A)

else
[ key_code.bytes, key_binding ]
end
[ key_code.bytes, key_binding ]
rescue Reline::Terminfo::TerminfoError
# capname is undefined
end
Expand All @@ -94,14 +110,8 @@ def self.set_default_key_bindings_comprehensive_list(config)
[27, 91, 49, 126] => :ed_move_to_beg, # Home
[27, 91, 52, 126] => :ed_move_to_end, # End
[27, 91, 51, 126] => :key_delete, # Del
[27, 91, 65] => :ed_prev_history, # ↑
[27, 91, 66] => :ed_next_history, # ↓
[27, 91, 67] => :ed_next_char, # →
[27, 91, 68] => :ed_prev_char, # ←

# KDE
[27, 91, 72] => :ed_move_to_beg, # Home
[27, 91, 70] => :ed_move_to_end, # End
# Del is 0x08
[27, 71, 65] => :ed_prev_history, # ↑
[27, 71, 66] => :ed_next_history, # ↓
Expand All @@ -118,12 +128,6 @@ def self.set_default_key_bindings_comprehensive_list(config)
# Del is 0x08
# Arrow keys are the same of KDE

# iTerm2
[27, 27, 91, 67] => :em_next_word, # Option+→, extended entry
[27, 27, 91, 68] => :ed_prev_word, # Option+←, extended entry
[195, 166] => :em_next_word, # Option+f
[195, 162] => :ed_prev_word, # Option+b
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[195, 166] and [195,162] are unicode bytes for æ and â. It's not an escape sequence.

In mac, option+f option+b will insert ƒ in any textarea and also in iTerm2, but we don't want to add 'ƒ'.bytes '∫'.bytes to this list. Same for æ and â.


[27, 79, 65] => :ed_prev_history, # ↑
[27, 79, 66] => :ed_next_history, # ↓
[27, 79, 67] => :ed_next_char, # →
Expand Down
16 changes: 16 additions & 0 deletions test/reline/test_ansi_with_terminfo.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,28 @@ def test_kcub1
omit e.message
end

# Home and End; always mapped regardless of terminfo enabled or not
def test_home_end
assert_key_binding("\e[H", :ed_move_to_beg)
assert_key_binding("\e[F", :ed_move_to_end)
end

# Arrow; always mapped regardless of terminfo enabled or not
def test_arrow
assert_key_binding("\e[A", :ed_prev_history)
assert_key_binding("\e[B", :ed_next_history)
assert_key_binding("\e[C", :ed_next_char)
assert_key_binding("\e[D", :ed_prev_char)
end

# Ctrl+arrow and Meta+arrow; always mapped regardless of terminfo enabled or not
def test_extended
assert_key_binding("\e[1;5C", :em_next_word) # Ctrl+→
assert_key_binding("\e[1;5D", :ed_prev_word) # Ctrl+←
assert_key_binding("\e[1;3C", :em_next_word) # Meta+→
assert_key_binding("\e[1;3D", :ed_prev_word) # Meta+←
assert_key_binding("\e\e[C", :em_next_word) # Meta+→
assert_key_binding("\e\e[D", :ed_prev_word) # Meta+←
end

# Shift-Tab; always mapped regardless of terminfo enabled or not
Expand Down
2 changes: 2 additions & 0 deletions test/reline/test_ansi_without_terminfo.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ def test_extended
assert_key_binding("\e[1;5D", :ed_prev_word) # Ctrl+←
assert_key_binding("\e[1;3C", :em_next_word) # Meta+→
assert_key_binding("\e[1;3D", :ed_prev_word) # Meta+←
assert_key_binding("\e\e[C", :em_next_word) # Meta+→
assert_key_binding("\e\e[D", :ed_prev_word) # Meta+←
end

# Shift-Tab; always mapped regardless of terminfo enabled or not
Expand Down