Repository: kovidgoyal/kitty

Issue:
Key sequence is not sent to children if a prefix is mapped by kitty
**Describe the bug**

Tmux by default accept `Ctrl+b > c` to create new tmux tab. However, if `Ctrl+b > l` is mapped in kitty, then `Ctrl+b > c` doesn't work for tmux any more.

**To Reproduce**
Steps to reproduce the behavior:
1. Create a kitty config with a single line `map ctrl+b>l kitten hints --type hyperlink`
2. Start kitty using this config
3. Start a fresh tmux inside kitty using default config: `tmux -f /dev/null -L test`
4. Type `Ctrl+b` then `c`, no tab is created. However, I expect kitty to recognize the key sequence and send `Ctrl+b>c` to tmux.


**Environment details**

<details>
<summary></summary>
<pre><code>
kitty 0.26.5 created by Kovid Goyal
Linux KeepRunning 5.15.85-1-lts #1 SMP Thu, 22 Dec 2022 06:22:00 +0000 x86_64
Arch Linux 5.15.85-1-lts (/dev/tty)

DISTRIB_ID="Arch"
DISTRIB_RELEASE="rolling"
DISTRIB_DESCRIPTION="Arch Linux"
Running under: X11
Frozen: False
Paths:
  kitty: /usr/bin/kitty
  base dir: /usr/lib/kitty
  extensions dir: /usr/lib/kitty/kitty
  system shell: /bin/zsh
Loaded config files:
  /home/USERNAME/.config/kitty/kitty.conf

Config options different from defaults:
active_tab_font_style             (True, False)
active_tab_title_template         {fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.tab}😸{sup.index}{title}
background_opacity                0.9
cursor_beam_thickness             4.5
cursor_shape                      2
cursor_text_color                 None
enable_audio_bell                 False
enabled_layouts                   ['splits']
focus_follows_mouse               True
font_size                         19.0
hide_window_decorations           1
linux_display_server              x11
scrollback_fill_enlarged_window   True
scrollback_lines                  10000
scrollback_pager_history_size     10485760
select_by_word_characters         @-._~?&%+#
select_by_word_characters_forward =/
strip_trailing_spaces             smart
tab_bar_style                     powerline
tab_title_template                {fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.tab}{sup.index}{title}
url_excluded_characters           >
Added shortcuts:
	alt+h →  kitten smart_tab.py previous_tab alt+h
	alt+l →  kitten smart_tab.py next_tab alt+l
	ctrl+q > " →  kitten smart_tab.py hsplit ctrl+q>\"
	ctrl+q > % →  kitten smart_tab.py vsplit ctrl+q>%
	ctrl+t →  kitten smart_tab.py new_tab ctrl+t
	kitty_mod+h > h →  show_scrollback
	kitty_mod+h > l →  show_last_command_output
	kitty_mod+p > o →  kitten hints --type path
	kitty_mod+p > u →  kitten hints --type regex --regex '(?:http://|ftp://|https://|file://|go/)[^\x00-\t\x0b-\x0c\x0e-\x20\x7f-\xa0\xad\u0600-\u0605\u061c\u06dd\u070f\u0890-\u0891\u08e2\u1680\u180e\u2000-\u200f\u2028-\u202f\u205f-\u2064\u2066-\u206f\u3000\ud800-\uf8ff\ufeff\ufff9-\ufffb\U000110bd\U000110cd\U00013430-\U0001343f\U0001bca0-\U0001bca3\U0001d173-\U0001d17a\U000e0001\U000e0020-\U000e007f\U000f0000-\U000ffffd\U00100000-\U0010fffd]{3,}|(?:b|cl)/[0-9]+|/google3/[a-zA-Z0-9_]{3,}/[a-zA-Z0-9\.\/\+-]{3,}' --program hyperlink-open.py --hints-text-color green
	shift+page_down →  scroll_page_down
	shift+page_up →  scroll_page_up
Removed shortcuts:
	ctrl+shift+tab →  previous_tab
	ctrl+tab →  next_tab
	kitty_mod+, →  move_tab_backward
	kitty_mod+. →  move_tab_forward
	kitty_mod+0 →  tenth_window
	kitty_mod+1 →  first_window
	kitty_mod+2 →  second_window
	kitty_mod+3 →  third_window
	kitty_mod+4 →  fourth_window
	kitty_mod+5 →  fifth_window
	kitty_mod+6 →  sixth_window
	kitty_mod+7 →  seventh_window
	kitty_mod+8 →  eighth_window
	kitty_mod+9 →  ninth_window
	kitty_mod+[ →  previous_window
	kitty_mod+] →  next_window
	kitty_mod+` →  move_window_to_top
	kitty_mod+a > 1 →  set_background_opacity 1
	kitty_mod+a > d →  set_background_opacity default
	kitty_mod+a > l →  set_background_opacity -0.1
	kitty_mod+a > m →  set_background_opacity +0.1
	kitty_mod+alt+t →  set_tab_title
	kitty_mod+b →  move_window_backward
	kitty_mod+delete →  clear_terminal reset active
	kitty_mod+e →  open_url_with_hints
	kitty_mod+end →  scroll_end
	kitty_mod+enter →  new_window
	kitty_mod+f →  move_window_forward
	kitty_mod+f1 →  show_kitty_doc overview
	kitty_mod+f10 →  toggle_maximized
	kitty_mod+f11 →  toggle_fullscreen
	kitty_mod+f2 →  edit_config_file
	kitty_mod+f7 →  focus_visible_window
	kitty_mod+f8 →  swap_with_window
	kitty_mod+g →  show_last_command_output
	kitty_mod+h →  show_scrollback
	kitty_mod+home →  scroll_home
	kitty_mod+j →  scroll_line_down
	kitty_mod+k →  scroll_line_up
	kitty_mod+l →  next_layout
	kitty_mod+left →  previous_tab
	kitty_mod+n →  new_os_window
	kitty_mod+o →  pass_selection_to_program
	kitty_mod+p > h →  kitten hints --type hash --program -
	kitty_mod+p > n →  kitten hints --type linenum
	kitty_mod+p > shift+f →  kitten hints --type path
	kitty_mod+p > w →  kitten hints --type word --program -
	kitty_mod+p > y →  kitten hints --type hyperlink
	kitty_mod+page_down →  scroll_page_down
	kitty_mod+page_up →  scroll_page_up
	kitty_mod+q →  close_tab
	kitty_mod+r →  start_resizing_window
	kitty_mod+right →  next_tab
	kitty_mod+s →  paste_from_selection
	kitty_mod+t →  new_tab
	kitty_mod+u →  kitten unicode_input
	kitty_mod+w →  close_window
	kitty_mod+x →  scroll_to_prompt 1
	kitty_mod+z →  scroll_to_prompt -1
	shift+insert →  paste_from_selection
Changed shortcuts:
	kitty_mod+down →  scroll_to_prompt 1
	kitty_mod+p > l →  kitten hints --type hyperlink
	kitty_mod+up →  scroll_to_prompt -1
Colors:
	active_tab_background             #0ab2ff   
	cursor                            #be5160   
	inactive_tab_background           #0a65ff   
	inactive_tab_foreground           #000000   

Important environment variables seen by the kitty process:
	PATH                                OMITTED
	LANG                                en_US.UTF-8
	SHELL                               /bin/zsh
	GLFW_IM_MODULE                      ibus
	DISPLAY                             :0.0
	USER                                USERNAME
	XCURSOR_SIZE                        24
	XDG_CONFIG_DIRS                     /home/USERNAME/.config/kdedefaults:/etc/xdg
	XDG_SESSION_PATH                    /org/freedesktop/DisplayManager/Session1
	XDG_SEAT                            seat0
	XDG_SESSION_DESKTOP                 KDE
	XDG_SESSION_TYPE                    x11
	XDG_CURRENT_DESKTOP                 KDE
	XDG_SEAT_PATH                       /org/freedesktop/DisplayManager/Seat0
	XDG_SESSION_CLASS                   user
	XDG_VTNR                            2
	XDG_SESSION_ID                      2
	XDG_RUNTIME_DIR                     /run/user/1000
	XDG_DATA_DIRS                       /home/USERNAME/.local/share/flatpak/exports/share:/var/lib/flatpak/exports/share:/usr/local/share:/usr/share:/var/lib/snapd/desktop

</code></pre>
</details>



Discussion:
Yes, since the keypress is part of a kitty sequence, it is not available
for terminal programs. I could technically send the events to the child
program if the sequence doesnt match anything, but this is pretty
complex, since one has to possibly send multiple press, release events.
So you would need to store all these while the sequence is in progress
and the encode them based on the current keyboard mode of the active
window and send them. Not worth the effort at least for me.


Resolving patch:
diff --git a/kitty/boss.py b/kitty/boss.py
--- a/kitty/boss.py
+++ b/kitty/boss.py
@@ -33,14 +33,14 @@
 )
 from .fast_data_types import (
     CLOSE_BEING_CONFIRMED, GLFW_MOD_ALT, GLFW_MOD_CONTROL, GLFW_MOD_SHIFT,
-    GLFW_MOD_SUPER, GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS, IMPERATIVE_CLOSE_REQUESTED,
+    GLFW_MOD_SUPER, GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS, GLFW_RELEASE, IMPERATIVE_CLOSE_REQUESTED,
     NO_CLOSE_REQUESTED, ChildMonitor, Color, EllipticCurveKey, KeyEvent, SingleKey,
     add_timer, apply_options_update, background_opacity_of, change_background_opacity,
     change_os_window_state, cocoa_hide_app, cocoa_hide_other_apps,
     cocoa_minimize_os_window, cocoa_set_menubar_title, create_os_window,
     current_application_quit_request, current_focused_os_window_id, current_os_window,
     destroy_global_data, focus_os_window, get_boss, get_options, get_os_window_size,
-    global_font_size, last_focused_os_window_id, mark_os_window_for_close,
+    glfw_is_modifier_key, global_font_size, last_focused_os_window_id, mark_os_window_for_close,
     os_window_font_size, patch_global_colors, redirect_mouse_handling, ring_bell,
     run_with_activation_token, safe_pipe, send_data_to_peer,
     set_application_quit_request, set_background_image, set_boss, set_in_sequence_mode,
@@ -254,6 +254,8 @@ def __init__(
         self.current_visual_select: Optional[VisualSelect] = None
         self.startup_cursor_text_color = opts.cursor_text_color
         self.pending_sequences: Optional[SubSequenceMap] = None
+        # A list of events received so far that are potentially part of a sequence keybinding.
+        self.current_sequence: List[KeyEvent] = []
         self.default_pending_action: str = ''
         self.cached_values = cached_values
         self.os_window_map: Dict[int, TabManager] = {}
@@ -1168,6 +1170,7 @@ def dispatch_possible_special_key(self, ev: KeyEvent) -> bool:
             sequences = get_shortcut(get_options().sequence_map, ev)
             if sequences and not isinstance(sequences, str):
                 self.set_pending_sequences(sequences)
+                self.current_sequence = [ev]
                 return True
             if self.global_shortcuts_map and get_shortcut(self.global_shortcuts_map, ev):
                 return True
@@ -1177,14 +1180,23 @@ def dispatch_possible_special_key(self, ev: KeyEvent) -> bool:
 
     def clear_pending_sequences(self) -> None:
         self.pending_sequences = None
+        self.current_sequence = []
         self.default_pending_action = ''
         set_in_sequence_mode(False)
 
-    def process_sequence(self, ev: KeyEvent) -> None:
+    def process_sequence(self, ev: KeyEvent) -> bool:
+        # Process an event as part of a sequence. Returns whether the key
+        # is consumed as part of a kitty sequence keybinding.
         if not self.pending_sequences:
             set_in_sequence_mode(False)
-            return
+            return False
 
+        if len(self.current_sequence):
+            self.current_sequence.append(ev)
+        if ev.action == GLFW_RELEASE or glfw_is_modifier_key(ev.key):
+            return True
+        # For a press/repeat event that's not a modifier, try matching with
+        # kitty bindings:
         remaining = {}
         matched_action = None
         for seq, key_action in self.pending_sequences.items():
@@ -1197,11 +1209,20 @@ def process_sequence(self, ev: KeyEvent) -> None:
 
         if remaining:
             self.pending_sequences = remaining
+            return True
         else:
             matched_action = matched_action or self.default_pending_action
-            self.clear_pending_sequences()
-            if matched_action is not None:
+            if matched_action is not None and matched_action != '':
+                self.clear_pending_sequences()
                 self.combine(matched_action)
+                return True
+            else:
+                w = self.active_window
+                if w is not None:
+                    for ev in self.current_sequence:
+                        w.write_to_child(w.encoded_key(ev))
+                self.clear_pending_sequences()
+                return False
 
     def cancel_current_visual_select(self) -> None:
         if self.current_visual_select:


Test patch: