Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package fzf for openSUSE:Factory checked in 
at 2026-10-21 20:57:22
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/fzf (Old)
 and      /work/SRC/openSUSE:Factory/.fzf.new.2252680 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "fzf"

Wed Oct 21 20:57:22 2026 rev:87 rq:1386650 version:0.74.5

Changes:
--------
--- /work/SRC/openSUSE:Factory/fzf/fzf.changes  2026-09-21 12:21:41.510461242 
+0200
+++ /work/SRC/openSUSE:Factory/.fzf.new.2252680/fzf.changes     2026-10-21 
20:59:02.672899660 +0200
@@ -1,0 +2,29 @@
+Tue Oct 20 05:31:56 UTC 2026 - Kyle Scheuing <[email protected]>
+
+- Update to version 0.74.5:
+  * Fixed --popup unzooming the window when opened over a zoomed pane on tmux
+       3.8 or above (#4927, junegunn/fzf.vim#1625) (@jereviikari)
+       - On tmux 3.7, the window is still unzoomed, as tmux 3.7b crashes when a
+         floating pane is created over a zoomed window
+  * Fixed --gap-line cutting a grapheme cluster when filling the last cells of
+       the line (#4920)
+  * Fixed an escape sequence fzf does not recognize being partly consumed,
+       which leaked the rest into the query (#4926)
+  * Fixed a key lost when it arrived in the same read as an escape sequence
+  * Fixed focus event not triggered after reload when the new item is at the
+       same position as the previous one (#4930)
+       - With --id-nth, the event is not triggered if the focus moves directly
+         from an item to another item with the same key
+       - A focus binding that runs reload now triggers itself again after each
+         reload, unless --id-nth is set and the key does not change
+  * Security fixes
+       - fzf-tmux no longer evaluates an attached layout size (e.g. -l50) via 
bash
+         arithmetic, which could run commands or inject tmux flags
+         (GHSA-w6x2-f5m4-xw82)
+       - zsh completion now creates its FIFO in a private mktemp -d directory
+         instead of a predictable /tmp path (GHSA-7rj6-7pqx-v7rm)
+       - Reported by Sai Sreewathsa Kovalluri, Shon Babu, and Kannan K
+         (Researchers) of Innspark Solutions
+
+
+-------------------------------------------------------------------

Old:
----
  fzf-0.74.4.tar.gz

New:
----
  fzf-0.74.5.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ fzf.spec ++++++
--- /var/tmp/diff_new_pack.n7DG4K/_old  2026-10-21 20:59:03.601938608 +0200
+++ /var/tmp/diff_new_pack.n7DG4K/_new  2026-10-21 20:59:03.603938692 +0200
@@ -19,7 +19,7 @@
 
 %global _lto_cflags %{nil}
 Name:           fzf
-Version:        0.74.4
+Version:        0.74.5
 Release:        0
 Summary:        A command-line fuzzy finder
 License:        MIT

++++++ fzf-0.74.4.tar.gz -> fzf-0.74.5.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/fzf-0.74.4/.github/ISSUE_TEMPLATE/issue_template.yml 
new/fzf-0.74.5/.github/ISSUE_TEMPLATE/issue_template.yml
--- old/fzf-0.74.4/.github/ISSUE_TEMPLATE/issue_template.yml    2026-09-12 
02:21:00.000000000 +0200
+++ new/fzf-0.74.5/.github/ISSUE_TEMPLATE/issue_template.yml    2026-10-11 
04:29:16.000000000 +0200
@@ -3,10 +3,6 @@
 description: Report a problem or bug related to fzf to help us improve
 
 body:
-  - type: markdown
-    attributes:
-      value: ISSUES NOT FOLLOWING THIS TEMPLATE WILL BE CLOSED AND DELETED
-
   - type: checkboxes
     attributes:
       label: Checklist
@@ -32,7 +28,6 @@
         - label: Linux
         - label: macOS
         - label: Windows
-        - label: Etc.
 
   - type: checkboxes
     attributes:
@@ -41,9 +36,23 @@
         - label: bash
         - label: zsh
         - label: fish
+        - label: nushell
+        - label: PowerShell
 
   - type: textarea
     attributes:
       label: Problem / Steps to reproduce
     validations:
       required: true
+
+  - type: textarea
+    attributes:
+      label: How did you run into this?
+      description: |
+        What were you actually trying to do? Include the command line or
+        configuration from your real setup.
+
+        If you did not hit this in actual use, say how you found it
+        (reading the code, automated analysis, etc.).
+    validations:
+      required: true
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/fzf-0.74.4/CHANGELOG.md new/fzf-0.74.5/CHANGELOG.md
--- old/fzf-0.74.4/CHANGELOG.md 2026-09-12 02:21:00.000000000 +0200
+++ new/fzf-0.74.5/CHANGELOG.md 2026-10-11 04:29:16.000000000 +0200
@@ -1,6 +1,21 @@
 CHANGELOG
 =========
 
+0.74.5
+------
+- Fixed `--popup` unzooming the window when opened over a zoomed pane on tmux 
3.8 or above (#4927, junegunn/fzf.vim#1625) (@jereviikari)
+    - On tmux 3.7, the window is still unzoomed, as tmux 3.7b crashes when a 
floating pane is created over a zoomed window
+- Fixed `--gap-line` cutting a grapheme cluster when filling the last cells of 
the line (#4920)
+- Fixed an escape sequence fzf does not recognize being partly consumed, which 
leaked the rest into the query (#4926)
+- Fixed a key lost when it arrived in the same read as an escape sequence
+- Fixed `focus` event not triggered after `reload` when the new item is at the 
same position as the previous one (#4930)
+    - With `--id-nth`, the event is not triggered if the focus moves directly 
from an item to another item with the same key
+    - A `focus` binding that runs `reload` now triggers itself again after 
each reload, unless `--id-nth` is set and the key does not change
+- Security fixes
+    - fzf-tmux no longer evaluates an attached layout size (e.g. `-l50`) via 
bash arithmetic, which could run commands or inject tmux flags 
(GHSA-w6x2-f5m4-xw82)
+    - zsh completion now creates its FIFO in a private `mktemp -d` directory 
instead of a predictable `/tmp` path (GHSA-7rj6-7pqx-v7rm)
+    - Reported by Sai Sreewathsa Kovalluri, Shon Babu, and Kannan K 
(Researchers) of Innspark Solutions
+
 0.74.4
 ------
 - Fixed an escape sequence split across reads being parsed as a fragment, 
which leaked the rest into the query (#4899)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/fzf-0.74.4/Makefile new/fzf-0.74.5/Makefile
--- old/fzf-0.74.4/Makefile     2026-09-12 02:21:00.000000000 +0200
+++ new/fzf-0.74.5/Makefile     2026-10-11 04:29:16.000000000 +0200
@@ -94,8 +94,12 @@
                                github.com/junegunn/fzf/src/tui \
                                github.com/junegunn/fzf/src/util
 
+# Run in a separate tmux server without user config, like CI (80x24)
 itest:
-       ruby test/runner.rb
+       @dir=$$(mktemp -d) && \
+       env -u TMUX TMUX_TMPDIR=$$dir sh -c \
+               'tmux -f /dev/null new-session -d && ruby test/runner.rb; 
s=$$?; tmux kill-server; exit $$s'; \
+       s=$$?; rm -rf $$dir; exit $$s
 
 # Actively fuzz the matcher fast paths against the general algorithm.
 # Go fuzzes one target at a time, so iterate. Override duration with
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/fzf-0.74.4/RELEASE.md new/fzf-0.74.5/RELEASE.md
--- old/fzf-0.74.4/RELEASE.md   2026-09-12 02:21:00.000000000 +0200
+++ new/fzf-0.74.5/RELEASE.md   2026-10-11 04:29:16.000000000 +0200
@@ -7,7 +7,11 @@
 
 ## Steps
 
-1. Update version in the following files and commit on `master`:
+1. Sign in at https://developer.apple.com/account and accept any pending
+   agreements (e.g. an updated Apple Developer Program License Agreement).
+   Otherwise notarization fails.
+
+2. Update version in the following files and commit on `master`:
     - `CHANGELOG.md`
     - `main.go`
     - `install`
@@ -15,10 +19,10 @@
     - `man/man1/fzf.1`
     - `man/man1/fzf-tmux.1`
 
-2. Verify file consistency, sign the tag, and push the tag.
+3. Verify file consistency, sign the tag, and push the tag.
 
     ```sh
-    make tag VERSION=0.74.4
+    make tag VERSION=0.74.5
     ```
 
     `make tag` runs `prerelease` first (checks that the version
@@ -29,10 +33,10 @@
     old version, so `/master/install` keeps resolving against existing
     binaries during the publish window.
 
-3. The workflow fires on the tag push and pauses on the `release`
+4. The workflow fires on the tag push and pauses on the `release`
    environment gate. Approve it in the Actions tab to release.
 
-4. After the GitHub release is published, fast-forward `master`:
+5. After the GitHub release is published, fast-forward `master`:
 
     ```sh
     git push origin master
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/fzf-0.74.4/bin/fzf-tmux new/fzf-0.74.5/bin/fzf-tmux
--- old/fzf-0.74.4/bin/fzf-tmux 2026-09-12 02:21:00.000000000 +0200
+++ new/fzf-0.74.5/bin/fzf-tmux 2026-10-11 04:29:16.000000000 +0200
@@ -78,6 +78,7 @@
       fi
       if [[ ${#arg} -gt 2 ]]; then
         size="${arg:2}"
+        [[ $size =~ ^[0-9%,]+$ || $size =~ ^[A-Z]$ ]] || continue
       else
         if [[ $1 =~ ^[0-9%,]+$ ]] || [[ $1 =~ ^[A-Z]$ ]]; then
           size="$1"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/fzf-0.74.4/install new/fzf-0.74.5/install
--- old/fzf-0.74.4/install      2026-09-12 02:21:00.000000000 +0200
+++ new/fzf-0.74.5/install      2026-10-11 04:29:16.000000000 +0200
@@ -2,7 +2,7 @@
 
 set -u
 
-version=0.74.4
+version=0.74.5
 auto_completion=
 key_bindings=
 update_config=2
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/fzf-0.74.4/install.ps1 new/fzf-0.74.5/install.ps1
--- old/fzf-0.74.4/install.ps1  2026-09-12 02:21:00.000000000 +0200
+++ new/fzf-0.74.5/install.ps1  2026-10-11 04:29:16.000000000 +0200
@@ -1,4 +1,4 @@
-$version="0.74.4"
+$version="0.74.5"
 
 $fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/fzf-0.74.4/man/man1/fzf-tmux.1 
new/fzf-0.74.5/man/man1/fzf-tmux.1
--- old/fzf-0.74.4/man/man1/fzf-tmux.1  2026-09-12 02:21:00.000000000 +0200
+++ new/fzf-0.74.5/man/man1/fzf-tmux.1  2026-10-11 04:29:16.000000000 +0200
@@ -21,7 +21,7 @@
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
 ..
-.TH fzf\-tmux 1 "Sep 2026" "fzf 0.74.4" "fzf\-tmux - open fzf in tmux split 
pane"
+.TH fzf\-tmux 1 "Oct 2026" "fzf 0.74.5" "fzf\-tmux - open fzf in tmux split 
pane"
 
 .SH NAME
 fzf\-tmux - open fzf in tmux split pane
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/fzf-0.74.4/man/man1/fzf.1 
new/fzf-0.74.5/man/man1/fzf.1
--- old/fzf-0.74.4/man/man1/fzf.1       2026-09-12 02:21:00.000000000 +0200
+++ new/fzf-0.74.5/man/man1/fzf.1       2026-10-11 04:29:16.000000000 +0200
@@ -21,7 +21,7 @@
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
 ..
-.TH fzf 1 "Sep 2026" "fzf 0.74.4" "fzf - a command-line fuzzy finder"
+.TH fzf 1 "Oct 2026" "fzf 0.74.5" "fzf - a command-line fuzzy finder"
 
 .SH NAME
 fzf - a command-line fuzzy finder
@@ -1909,7 +1909,10 @@
 \fIfocus\fR
 .RS
 Triggered when the focus changes due to a vertical cursor movement or a search
-result update.
+result update. After a reload, the focused item is considered new unless
+\fB\-\-id\-nth\fR is set and the item has the same key as the previously
+focused item. Because of this, binding \fBreload\fR to this event can cause an
+infinite loop.
 
 e.g.
      \fBfzf \-\-bind 'focus:transform\-preview\-label:echo [ {} ]' \-\-preview 
'cat {}'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/fzf-0.74.4/shell/completion.zsh 
new/fzf-0.74.5/shell/completion.zsh
--- old/fzf-0.74.4/shell/completion.zsh 2026-09-12 02:21:00.000000000 +0200
+++ new/fzf-0.74.5/shell/completion.zsh 2026-10-11 04:29:16.000000000 +0200
@@ -215,7 +215,7 @@
 
 _fzf_feed_fifo() {
   command rm -f "$1"
-  mkfifo "$1"
+  command mkfifo "$1" || return
   cat <&0 > "$1" &|
 }
 
@@ -242,8 +242,9 @@
     rest=("$@")
   fi
 
-  local fifo lbuf matches post
-  fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$"
+  local fifo fifo_dir lbuf matches post
+  fifo_dir=$(command mktemp -d "${TMPDIR:-/tmp}/fzf-completion-XXXXXX") || 
return
+  fifo="$fifo_dir/fifo"
   lbuf=${rest[0]}
   post="${funcstack[1]}_post"
   type $post > /dev/null 2>&1 || post=cat
@@ -256,7 +257,7 @@
   if [ -n "$matches" ]; then
     LBUFFER="$lbuf$matches"
   fi
-  command rm -f "$fifo"
+  command rm -rf "$fifo_dir"
 }
 
 # To use custom hostname lists, override __fzf_list_hosts.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/fzf-0.74.4/src/terminal.go 
new/fzf-0.74.5/src/terminal.go
--- old/fzf-0.74.4/src/terminal.go      2026-09-12 02:21:00.000000000 +0200
+++ new/fzf-0.74.5/src/terminal.go      2026-10-11 04:29:16.000000000 +0200
@@ -467,7 +467,7 @@
        termSize             tui.TermSize
        lastAction           actionType
        lastKey              string
-       lastFocus            int32
+       lastFocus            focusedItem
        areaLines            int
        areaColumns          int
        forcePreview         bool
@@ -492,6 +492,11 @@
        item *Item
 }
 
+type focusedItem struct {
+       major int // revision.major; changes on reload
+       item  *Item
+}
+
 type byTimeOrder []selectedItem
 
 func (a byTimeOrder) Len() int {
@@ -1186,7 +1191,7 @@
                initFunc:           func() error { return renderer.Init() },
                executing:          util.NewAtomicBool(false),
                lastAction:         actStart,
-               lastFocus:          minItem.Index(),
+               lastFocus:          focusedItem{},
                lastActivity:       time.Now(),
                numLinesCache:      make(map[int32]numLinesCacheValue),
                // The initial load counts as a search in progress 
('start:wait').
@@ -6066,6 +6071,26 @@
        return minItem.Index()
 }
 
+func (t *Terminal) currentFocus() focusedItem {
+       return focusedItem{t.revision.major, t.currentItem()}
+}
+
+// Records the current focus and returns whether it differs from the last one.
+// Indexes restart after a reload, so items are compared by --id-nth key, if
+// set. Without it, any focused item after a reload is a new one.
+func (t *Terminal) updateFocus() bool {
+       curr := t.currentFocus()
+       last := t.lastFocus
+       t.lastFocus = curr
+       if curr.item == nil || last.item == nil {
+               return curr.item != last.item
+       }
+       if curr.major == last.major {
+               return curr.item.Index() != last.item.Index()
+       }
+       return len(t.idNth) == 0 || t.trackKeyFor(curr.item, t.idNth) != 
t.trackKeyFor(last.item, t.idNth)
+}
+
 func (t *Terminal) trackKeyFor(item *Item, nth []Range) string {
        tokens := Tokenize(item.AsString(t.ansi), t.delimiter)
        return StripLastDelimiter(JoinTokens(Transform(tokens, nth)), 
t.delimiter)
@@ -6631,8 +6656,7 @@
                                                        info = true
                                                }
                                                focusChanged := focusedIndex != 
currentIndex
-                                               if (t.hasFocusActions || 
t.infoCommand != "") && focusChanged && currentIndex != t.lastFocus {
-                                                       t.lastFocus = 
currentIndex
+                                               if (t.hasFocusActions || 
t.infoCommand != "") && !t.trackBlocked && t.updateFocus() {
                                                        t.eventChan <- 
tui.Focus.AsEvent()
                                                        if t.infoCommand != "" {
                                                                info = true
@@ -7017,9 +7041,11 @@
                                        }
                                }
 
-                               if onFocus, prs := 
t.keymap[tui.Focus.AsEvent()]; prs && iter < maxFocusEvents {
-                                       if newIndex := t.currentIndex(); 
newIndex != currentIndex {
-                                               t.lastFocus = newIndex
+                               // Actions are ignored while blocked. The 
render loop will
+                               // report the focus after the reload.
+                               if onFocus, prs := 
t.keymap[tui.Focus.AsEvent()]; prs && iter < maxFocusEvents && !t.trackBlocked {
+                                       if t.currentIndex() != currentIndex {
+                                               t.lastFocus = t.currentFocus()
                                                if t.infoCommand != "" {
                                                        req(reqInfo)
                                                }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/fzf-0.74.4/src/tmux.go new/fzf-0.74.5/src/tmux.go
--- old/fzf-0.74.4/src/tmux.go  2026-09-12 02:21:00.000000000 +0200
+++ new/fzf-0.74.5/src/tmux.go  2026-10-11 04:29:16.000000000 +0200
@@ -5,14 +5,19 @@
        "os"
        "os/exec"
        "path/filepath"
+       "regexp"
        "strings"
 
        "github.com/junegunn/fzf/src/tui"
 )
 
+// The flag group of new-pane usage includes -A (tmux 3.8 or above)
+var tmuxOverZoomFlag = regexp.MustCompile(`new-pane.*\[-[[:alpha:]]*A`)
+
 // Returns the size of the current window if the tmux server supports
-// floating panes (tmux 3.7 or above)
-func tmuxFloatingPaneInfo() (int, int, bool) {
+// floating panes (tmux 3.7 or above), and whether a floating pane can stay
+// above a zoomed pane
+func tmuxFloatingPaneInfo() (int, int, bool, bool) {
        // TMUX_PANE is not set when fzf is not started from within a pane
        // (e.g. 'bind-key 0 run-shell "fzf --popup"'). Do not use a floating
        // pane there; a blocking run-shell suspends key processing for the
@@ -25,7 +30,7 @@
        //   https://github.com/tmux/tmux/issues/5384
        target := os.Getenv("TMUX_PANE")
        if target == "" {
-               return 0, 0, false
+               return 0, 0, false, false
        }
        // A single invocation for both checks. Cannot rely on the exit status;
        // tmux versions before 3.7 exit normally with empty output for an
@@ -33,17 +38,17 @@
        out, err := exec.Command("tmux", "display-message", "-p", "-t", target,
                "#{window_width} #{window_height}", ";", "list-commands", 
"new-pane").Output()
        if err != nil || !strings.Contains(string(out), "new-pane") {
-               return 0, 0, false
+               return 0, 0, false, false
        }
        var width, height int
        if _, err := fmt.Sscanf(string(out), "%d %d", &width, &height); err != 
nil {
-               return 0, 0, false
+               return 0, 0, false, false
        }
        // The window is too small to fit a floating pane of the minimum size
        if width < 3 || height < 3 {
-               return 0, 0, false
+               return 0, 0, false, false
        }
-       return width, height, true
+       return width, height, tmuxOverZoomFlag.Match(out), true
 }
 
 // tmux ends a command at an argument ending in ';', so a trailing one is
@@ -85,7 +90,7 @@
        return max(3, min(dim, window))
 }
 
-func runTmuxFloatingPane(argStr string, dir string, windowWidth int, 
windowHeight int, opts *Options) (int, error) {
+func runTmuxFloatingPane(argStr string, dir string, windowWidth int, 
windowHeight int, overZoom bool, opts *Options) (int, error) {
        // Unlike display-popup, the size of a floating pane does not account 
for
        // the border around it, and the position is that of the content area. 
To
        // stay consistent with popups, treat the requested size as the total
@@ -166,13 +171,15 @@
                }
                paneCmd := fmt.Sprintf("%s%s %s; echo $? > %s; tmux wait-for -S 
%s",
                        setup, escapeSingleQuote(sh), escapeSingleQuote(temp), 
code, signal)
-               // Unzoom the window first; creating a floating pane over a 
zoomed
-               // window crashes the tmux server on 3.7b, and newer versions of
-               // tmux unzoom the window anyway.
                target := os.Getenv("TMUX_PANE")
-               newPane := fmt.Sprintf(
-                       "tmux if -F -t %s '#{window_zoomed_flag}' %s ';' 
new-pane -P -F '#{pane_id}' -t %s -c %s -x %d -y %d -X %d -Y %d %s -c %s",
-                       escapeSingleQuote(target), 
escapeSingleQuote("resize-pane -Z -t "+target),
+               newPane := "tmux new-pane -A"
+               if !overZoom {
+                       // Unzoom the window first; creating a floating pane 
over a
+                       // zoomed window crashes the tmux server on 3.7b
+                       newPane = fmt.Sprintf("tmux if -F -t %s 
'#{window_zoomed_flag}' %s ';' new-pane",
+                               escapeSingleQuote(target), 
escapeSingleQuote("resize-pane -Z -t "+target))
+               }
+               newPane += fmt.Sprintf(" -P -F '#{pane_id}' -t %s -c %s -x %d 
-y %d -X %d -Y %d %s -c %s",
                        escapeSingleQuote(target), escapeSingleQuote(dir), 
width-2, height-2, x, y,
                        escapeSingleQuote(sh), escapeSingleQuote(paneCmd))
 
@@ -222,10 +229,10 @@
        // popup is used instead so that the fzf-drawn border is the only border
        // shown; the native border of a tmux floating pane cannot be removed.
        if nativeBorder(opts) {
-               if windowWidth, windowHeight, ok := tmuxFloatingPaneInfo(); ok {
+               if windowWidth, windowHeight, overZoom, ok := 
tmuxFloatingPaneInfo(); ok {
                        opts.Tmux.border = true
                        argStr, dir := popupArgStr(args, opts)
-                       return runTmuxFloatingPane(argStr, dir, windowWidth, 
windowHeight, opts)
+                       return runTmuxFloatingPane(argStr, dir, windowWidth, 
windowHeight, overZoom, opts)
                }
        }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/fzf-0.74.4/src/tui/light.go 
new/fzf-0.74.5/src/tui/light.go
--- old/fzf-0.74.4/src/tui/light.go     2026-09-12 02:21:00.000000000 +0200
+++ new/fzf-0.74.5/src/tui/light.go     2026-10-11 04:29:16.000000000 +0200
@@ -348,9 +348,125 @@
 func csiContinues(b byte) bool { return b >= 0x20 && b <= 0x3f }
 func csiFinal(b byte) bool     { return b >= 0x40 && b <= 0x7e }
 
-// incompleteEscape reports whether the buffer ends in an escape sequence that
-// has not been terminated yet. The read loop keeps waiting in that case, so 
the
-// parser is never handed a fragment to guess at.
+// csiEnd returns the length of the CSI or SS3 sequence at the start of the
+// buffer, 0 if it has no final byte yet, or -1 if it is malformed.
+func csiEnd(buffer []byte) int {
+       digits := true
+       for i := 2; i < len(buffer); i++ {
+               b := buffer[i]
+               // rxvt ends keys with $ after at least one digit, as in \e[7$ 
for
+               // SHIFT-HOME and \e[23$ for SHIFT-F11. Elsewhere $ is an 
intermediate byte.
+               if csiFinal(b) || b == '$' && digits && i > 2 {
+                       return i + 1
+               }
+               if !csiContinues(b) {
+                       return -1
+               }
+               digits = digits && b >= '0' && b <= '9'
+       }
+       return 0
+}
+
+// stringEnd returns the length of the string sequence (DCS, OSC or APC) at the
+// start of the buffer, or 0 while nothing has ended it. A terminator is
+// included, an ESC that introduces another sequence is not.
+func stringEnd(buffer []byte) int {
+       if len(buffer) < 2 {
+               return 0
+       }
+       // Every one of them ends with ST. BEL ends an OSC as well, because 
xterm has
+       // always allowed it, but stopping at a BEL inside a DCS or APC payload 
would
+       // frame only its first half and leave the rest to be typed into the 
query.
+       bel := buffer[1] == ']'
+       for i := 2; i < len(buffer); i++ {
+               switch buffer[i] {
+               case '\a':
+                       if bel {
+                               return i + 1
+                       }
+               case Esc.Byte():
+                       if i+1 == len(buffer) {
+                               return 0 // ST may still be arriving
+                       }
+                       if buffer[i+1] == '\\' {
+                               return i + 2
+                       }
+                       // Any other ESC ends the string and introduces a 
sequence of its
+                       // own, so frame only what precedes it and leave the 
ESC to be
+                       // parsed again. Scanning past it would swallow that 
sequence too.
+                       return i
+               }
+       }
+       return 0
+}
+
+// rxvtKeyEnd reports whether the byte ends a key such as \e[7~ for Home.
+// rxvt marks modifiers with the final byte: $ for SHIFT, ^ for CTRL, @ for 
both.
+func rxvtKeyEnd(b byte) bool {
+       return b == '~' || b == '$' || b == '^' || b == '@'
+}
+
+// csiIntroducer reports whether the byte after ESC starts a sequence that is
+// framed by a final byte. The parser routes CSI and SS3 through the same
+// parameterized subcases, so both are framed by csiEnd.
+func csiIntroducer(b byte) bool {
+       return b == '[' || b == 'O'
+}
+
+// stringIntroducer reports whether the byte after ESC starts a string 
sequence.
+// Only the three that terminals reply with: OSC for colors, title and 
clipboard,
+// DCS for XTVERSION, DECRQSS and XTGETTCAP, APC for Kitty graphics. Nothing
+// sends SOS or PM.
+func stringIntroducer(b byte) bool {
+       switch b {
+       case 'P', ']', '_':
+               return true
+       }
+       return false
+}
+
+// stringReply reports whether a framed string sequence starts the way a
+// terminal's reply does. Its introducer is also ALT-], ALT-P or ALT-_, and 
typed
+// text after one, ended by CTRL-G or another key, frames like a reply when it
+// all arrives in one read.
+func stringReply(buffer []byte) bool {
+       payload := buffer[2:]
+       switch buffer[1] {
+       case ']': // Ps ; ...
+               i := 0
+               for i < len(payload) && payload[i] >= '0' && payload[i] <= '9' {
+                       i++
+               }
+               return i > 0 && i < len(payload) && payload[i] == ';'
+       case 'P': // >| or !| for the version, [01]$r or [01]+r for settings
+               if len(payload) < 2 {
+                       return false
+               }
+               if payload[1] == '|' {
+                       return payload[0] == '>' || payload[0] == '!'
+               }
+               return len(payload) >= 3 && (payload[0] == '0' || payload[0] == 
'1') &&
+                       (payload[1] == '$' || payload[1] == '+') && payload[2] 
== 'r'
+       case '_': // G, then key=value
+               return len(payload) >= 3 && payload[0] == 'G' &&
+                       (payload[1] >= 'a' && payload[1] <= 'z' || payload[1] 
>= 'A' && payload[1] <= 'Z') &&
+                       payload[2] == '='
+       }
+       return false
+}
+
+// stillArriving reports whether the CSI or SS3 sequence at the start of the
+// buffer may still be completed by more input. It must be the only one in the 
buffer:
+// whatever follows it, another sequence or a byte that ended it, means it is 
not
+// waiting for anything.
+func stillArriving(buffer []byte) bool {
+       return bytes.IndexByte(buffer[1:], Esc.Byte()) < 0 && 
incompleteEscape(buffer)
+}
+
+// incompleteEscape reports whether the buffer ends in a CSI or SS3 sequence 
that
+// has not been terminated yet. The read loop keeps waiting while it does, so a
+// fragment reaches the parser only once that wait has run out. A string 
sequence
+// is never waited for, because its introducer is also ALT-], ALT-P or ALT-_.
 func incompleteEscape(buffer []byte) bool {
        // Only the tail can hold a sequence still arriving. This runs once per 
byte
        // read, so scanning all of a large paste would make the read quadratic.
@@ -362,21 +478,10 @@
        if start < 0 || len(tail)-start < 2 {
                return false
        }
-       switch tail[start+1] {
-       case '[':
-               for _, b := range tail[start+2:] {
-                       if csiFinal(b) {
-                               return false
-                       }
-                       if !csiContinues(b) {
-                               return false // malformed, do not wait for a 
terminator
-                       }
-               }
-               return true
-       case 'O':
-               return len(tail)-start < 3
-       }
-       return false
+       // SS3 is judged like CSI: declaring it finished after one byte made the
+       // parser give up on a split \eO1;5A. A malformed sequence is not 
waited for,
+       // as nothing can complete it.
+       return csiIntroducer(tail[start+1]) && csiEnd(tail[start:]) == 0
 }
 
 func (r *LightRenderer) getBytes(cancellable bool) ([]byte, getCharResult, 
error) {
@@ -483,17 +588,27 @@
                return Event{CtrlSlash, 0, nil}
        case Esc.Byte():
                ev := r.escSequence(&sz)
-               // Second chance
-               if ev.Type == Invalid {
-                       r.buffer, result, err = r.getBytes(true)
+               // Second chance, but only for an unfinished sequence. 
Re-reading
+               // otherwise blocks until the next keystroke, holding back 
whatever
+               // follows in the buffer.
+               if ev.Type == Invalid && stillArriving(r.buffer) {
+                       waited := len(r.buffer)
+                       var buffer []byte
+                       buffer, result, err = r.getBytes(true)
                        if err != nil {
                                return Event{Fatal, 0, nil}
                        }
+                       r.buffer = buffer
                        if result == getCharCancelled {
                                return Event{Invalid, 0, nil}
                        }
 
                        ev = r.escSequence(&sz)
+                       // This read has no time limit, so what it returned may 
be the next
+                       // keystroke rather than the rest of the sequence. 
Never drop it.
+                       if ev.Type == Invalid && sz > waited {
+                               sz = waited
+                       }
                }
                return ev
        }
@@ -525,7 +640,41 @@
        r.mutex.Unlock()
 }
 
+// escSequence parses an escape sequence, then checks a CSI or SS3 result
+// against its frame. parseEscSequence can stop after a few bytes, and 
consuming
+// only those would leave the rest to be typed into the query. Complete
+// sequences it does not recognize are dropped there, unless they could be an
+// ALT key followed by typed text.
 func (r *LightRenderer) escSequence(sz *int) Event {
+       ev := r.parseEscSequence(sz)
+       if len(r.buffer) < 3 || !csiIntroducer(r.buffer[1]) {
+               return ev
+       }
+       // A frame missing its final byte may still be arriving
+       end := csiEnd(r.buffer)
+       if end <= 0 {
+               return ev
+       }
+       // The parser can also set the size before checking the bytes and then 
give
+       // up, counting typed input past the frame, as in \e[3;1u followed by ~
+       if ev.Type == Invalid && *sz > end {
+               *sz = end
+       }
+       // Four bytes or fewer could be ALT-[ or ALT-O and typed text, and rxvt 
sends
+       // keys of that size fzf does not know, such as \e[3^ for CTRL-DELETE
+       if end <= 4 {
+               return ev
+       }
+       // A key matched on a prefix, such as Home for \e[70;5u, or a sequence 
given
+       // up on partway
+       if end > *sz {
+               *sz = end
+               return Event{Invalid, 0, nil}
+       }
+       return ev
+}
+
+func (r *LightRenderer) parseEscSequence(sz *int) Event {
        if len(r.buffer) < 2 {
                return Event{Esc, 0, nil}
        }
@@ -606,6 +755,11 @@
                        if len(r.buffer) < 4 {
                                return Event{Invalid, 0, nil}
                        }
+                       // A byte that cannot continue the sequence, such as 
Enter after
+                       // ALT-[ and a digit, is a key of its own
+                       if csiEnd(r.buffer) < 0 {
+                               break
+                       }
                        *sz = 4
                        switch r.buffer[2] {
                        case '2':
@@ -638,7 +792,7 @@
                                if r.buffer[3] == '~' {
                                        return Event{Delete, 0, nil}
                                }
-                               if len(r.buffer) == 7 && r.buffer[6] == '~' && 
r.buffer[4] == '1' {
+                               if len(r.buffer) > 6 && r.buffer[6] == '~' && 
r.buffer[4] == '1' {
                                        *sz = 7
                                        switch r.buffer[5] {
                                        case '0':
@@ -657,7 +811,7 @@
                                                return 
Event{CtrlAltShiftDelete, 0, nil}
                                        }
                                }
-                               if len(r.buffer) == 6 && r.buffer[5] == '~' {
+                               if len(r.buffer) > 5 && r.buffer[5] == '~' {
                                        *sz = 6
                                        switch r.buffer[4] {
                                        case '2':
@@ -680,12 +834,14 @@
                                }
                                return Event{Invalid, 0, nil}
                        case '4':
-                               return Event{End, 0, nil}
+                               if rxvtKeyEnd(r.buffer[3]) {
+                                       return Event{End, 0, nil}
+                               }
                        case '5':
                                if r.buffer[3] == '~' {
                                        return Event{PageUp, 0, nil}
                                }
-                               if len(r.buffer) == 7 && r.buffer[6] == '~' && 
r.buffer[4] == '1' {
+                               if len(r.buffer) > 6 && r.buffer[6] == '~' && 
r.buffer[4] == '1' {
                                        *sz = 7
                                        switch r.buffer[5] {
                                        case '0':
@@ -704,7 +860,7 @@
                                                return 
Event{CtrlAltShiftPageUp, 0, nil}
                                        }
                                }
-                               if len(r.buffer) == 6 && r.buffer[5] == '~' {
+                               if len(r.buffer) > 5 && r.buffer[5] == '~' {
                                        *sz = 6
                                        switch r.buffer[4] {
                                        case '2':
@@ -730,7 +886,7 @@
                                if r.buffer[3] == '~' {
                                        return Event{PageDown, 0, nil}
                                }
-                               if len(r.buffer) == 7 && r.buffer[6] == '~' && 
r.buffer[4] == '1' {
+                               if len(r.buffer) > 6 && r.buffer[6] == '~' && 
r.buffer[4] == '1' {
                                        *sz = 7
                                        switch r.buffer[5] {
                                        case '0':
@@ -749,7 +905,7 @@
                                                return 
Event{CtrlAltShiftPageDown, 0, nil}
                                        }
                                }
-                               if len(r.buffer) == 6 && r.buffer[5] == '~' {
+                               if len(r.buffer) > 5 && r.buffer[5] == '~' {
                                        *sz = 6
                                        switch r.buffer[4] {
                                        case '2':
@@ -772,15 +928,19 @@
                                }
                                return Event{Invalid, 0, nil}
                        case '7':
-                               return Event{Home, 0, nil}
+                               if rxvtKeyEnd(r.buffer[3]) {
+                                       return Event{Home, 0, nil}
+                               }
                        case '8':
-                               return Event{End, 0, nil}
+                               if rxvtKeyEnd(r.buffer[3]) {
+                                       return Event{End, 0, nil}
+                               }
                        case '1':
                                switch r.buffer[3] {
                                case '~':
                                        return Event{Home, 0, nil}
                                case '1', '2', '3', '4', '5', '7', '8', '9':
-                                       if len(r.buffer) == 5 && r.buffer[4] == 
'~' {
+                                       if len(r.buffer) > 4 && r.buffer[4] == 
'~' {
                                                *sz = 5
                                                switch r.buffer[3] {
                                                case '1':
@@ -987,6 +1147,24 @@
                        } // r.buffer[2]
                } // r.buffer[2]
        } // r.buffer[1]
+       // Nothing matched. A framed sequence is dropped whole: reading its
+       // introducer as an ALT-key below would type the rest into the query.
+       // Unterminated ones are left alone, as that is how ALT-[, ALT-O, ALT-],
+       // ALT-P and ALT-_ arrive.
+       if csiIntroducer(r.buffer[1]) {
+               // ALT-[ or ALT-O and one or two typed characters can look like 
a short
+               // sequence, so ask for at least two bytes before the final byte
+               if end := csiEnd(r.buffer); end > 4 {
+                       *sz = end
+                       return Event{Invalid, 0, nil}
+               }
+       } else if stringIntroducer(r.buffer[1]) {
+               if end := stringEnd(r.buffer); end > 0 && stringReply(r.buffer) 
{
+                       *sz = end
+                       return Event{Invalid, 0, nil}
+               }
+       }
+
        rest := bytes.NewBuffer(r.buffer[1:])
        c, size, err := rest.ReadRune()
        if err == nil {
@@ -1003,11 +1181,13 @@
                return Event{Invalid, 0, nil}
        }
 
-       rest := r.buffer[*sz:]
-       end := bytes.IndexAny(rest, "mM")
-       if end == -1 {
+       // Not past the frame, where M or m can be a key typed later
+       frame := csiEnd(r.buffer)
+       if frame <= 0 || r.buffer[frame-1] != 'm' && r.buffer[frame-1] != 'M' {
                return Event{Invalid, 0, nil}
        }
+       rest := r.buffer[*sz:]
+       end := frame - 1 - *sz
 
        elems := strings.SplitN(string(rest[:end]), ";", 3)
        if len(elems) != 3 {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/fzf-0.74.4/src/tui/light_csi_test.go 
new/fzf-0.74.5/src/tui/light_csi_test.go
--- old/fzf-0.74.4/src/tui/light_csi_test.go    1970-01-01 01:00:00.000000000 
+0100
+++ new/fzf-0.74.5/src/tui/light_csi_test.go    2026-10-11 04:29:16.000000000 
+0200
@@ -0,0 +1,274 @@
+package tui
+
+import "testing"
+
+// An unrecognized escape sequence must be consumed whole, CSI and string
+// sequences alike. Consuming only part of one leaves the rest to be read as
+// input and typed into the query.
+func TestUnknownEscapeSequence(t *testing.T) {
+       for _, c := range []struct {
+               sequence string
+               event    EventType
+               size     int
+       }{
+               // Key encodings fzf does not implement
+               {"\x1b[97;5u", Invalid, 7},
+               {"\x1b[70;5u", Invalid, 7}, // not Home, which its prefix \e[7 
matches
+               {"\x1b[42;5u", Invalid, 7}, // nor End
+               {"\x1b[4;5~", Invalid, 6},
+               {"\x1b[127;5u", Invalid, 8},
+               {"\x1b[27;5;127~", Invalid, 11},
+               {"\x1b[57441;1u", Invalid, 10},
+               {"\x1b\x1b[97;5u", Invalid, 7}, // ALT prefixed, the first ESC 
is dropped
+
+               // Replies to queries fzf did not send, or sent and stopped 
waiting for
+               {"\x1b[?1;2c", Invalid, 7},
+               {"\x1b[>0;95;0c", Invalid, 10},
+
+               // Mouse report arriving while mouse input is off
+               {"\x1b[<0;1;1M", Invalid, 9},
+
+               // String sequences
+               {"\x1b]11;rgb:4a4a/4a4a/4a4a\x1b\\", Invalid, 25}, // 
background color reply
+               {"\x1b]0;a title\a", Invalid, 12},                 // BEL 
terminated
+               {"\x1bP>|kitty(0.48.2)\x1b\\", Invalid, 19},       // XTVERSION 
reply
+               {"\x1b_Gi=1;OK\x1b\\", Invalid, 11},               // Kitty 
graphics reply
+               {"\x1bP1$r\a\x1b\\", Invalid, 8},                  // BEL is 
payload in a DCS
+               {"\x1b]11;x\x1bX\x1b\\", Invalid, 6},              // ESC ends 
it, framing "\e]11;x"
+
+               // SS3 is framed like CSI, since the parser routes both the 
same way
+               {"\x1bO9;5u", Invalid, 6},
+               {"\x1bO1;5X", Invalid, 6}, // unknown final byte
+               {"\x1bO1;5A", CtrlUp, 6},  // recognized, unchanged
+               {"\x1bOP", F1, 3},
+
+               // Left alone: this is how ALT-[, ALT-O, ALT-], ALT-P and ALT-_ 
arrive
+               {"\x1b[a", Alt, 2},
+               {"\x1b[9A", Alt, 2}, // ALT-[ and two characters can look like 
a CSI
+               {"\x1b[1m", Alt, 2},
+               {"\x1b[ x", Alt, 2},
+               {"\x1bOx", Alt, 2},
+               {"\x1bO9A", Alt, 2},
+
+               // rxvt keys fzf does not know: dropped, not typed
+               {"\x1b[3^", Invalid, 4}, // CTRL-DELETE
+               {"\x1b[5^", Invalid, 4}, // CTRL-PAGEUP
+               {"\x1b[3@", Invalid, 4}, // CTRL-SHIFT-DELETE
+               {"\x1b[11^", Invalid, 5},
+
+               // rxvt ends keys with $ after digits, so what follows is typed
+               {"\x1b[7$x", Home, 4}, // SHIFT-HOME, then x
+               {"\x1b[7$1x", Home, 4},
+               {"\x1b[3$x", Invalid, 4},
+               {"\x1b[3$1x", Invalid, 4},
+               {"\x1b[23$x", Invalid, 5}, // SHIFT-F11, then x
+               {"\x1b[24$1x", Invalid, 5},
+
+               // A parse that gives up past the frame consumes only the 
frame, so the
+               // typed ~ after these stays in the buffer
+               {"\x1b[3;1u~", Invalid, 6},
+               {"\x1b[5;1u~", Invalid, 6},
+               {"\x1b[2$~", Invalid, 4}, // rxvt SHIFT-INSERT
+               {"\x1b[2^~", Invalid, 4}, // rxvt CTRL-INSERT
+               {"\x1b[2A~", Invalid, 4},
+
+               // Elsewhere $ and other intermediate bytes do not end a 
sequence
+               {"\x1b[4;0;10;20;0&w", Invalid, 15}, // not End, which \e[4 
matches
+               {"\x1b[4;2$y", Invalid, 7},
+               {"\x1b[2 q", Invalid, 5},
+               {"\x1b]abc", Alt, 2},
+               {"\x1b]11;rgb:", Alt, 2}, // terminator has not arrived
+               {"\x1b]\x1b]", Alt, 2},   // a second sequence must not swallow 
the ALT key
+               {"\x1b]\x1b[A", Alt, 2},
+               {"\x1bP\x1bP", Alt, 2},
+               {"\x1b_\x1b_", Alt, 2},
+               {"\x1b]\x1b\\", Alt, 2}, // ALT-] then ALT-backslash, not an 
empty OSC
+               {"\x1b]\a", Alt, 2},     // ALT-] then CTRL-G, which must still 
abort
+
+               // Typed text after the introducer is not a reply, even when a
+               // terminator follows in the same read
+               {"\x1b]a\a", Alt, 2},
+               {"\x1b]1\a", Alt, 2},
+               {"\x1b]a\x1b[A", Alt, 2},
+               {"\x1bPx\x1b[A", Alt, 2},
+               {"\x1b_ab\x1bb", Alt, 2},
+
+               // SOS and PM are not framed
+               {"\x1bXsos\x1b\\", Alt, 2},
+               {"\x1b^status\x1b\\", Alt, 2},
+
+               // Left alone: no final byte yet, so the sequence may still be 
arriving
+               {"\x1b[", Invalid, 2},
+
+               // Recognized sequences keep their existing parsing
+               {"\x1b[1;5A", CtrlUp, 6},
+               {"\x1b[3;5~", CtrlDelete, 6},
+               {"\x1b[2~", Insert, 4},
+               {"\x1b[200~", BracketedPasteBegin, 6},
+               {"\x1b[Z", ShiftTab, 3},
+               {"\x1bOA", Up, 3},
+               {"\x1b[12;34R", Invalid, 8},
+               {"\x1b[?2004;2$y", Invalid, 11},
+       } {
+               r := &LightRenderer{buffer: []byte(c.sequence)}
+               sz := 1
+               event := r.escSequence(&sz)
+               if event.Type != c.event {
+                       t.Errorf("escSequence(%q) = %s, want %s",
+                               c.sequence, event.Type.String(), 
c.event.String())
+               }
+               if sz != c.size {
+                       t.Errorf("escSequence(%q) consumed %d bytes, want %d", 
c.sequence, sz, c.size)
+               }
+       }
+}
+
+func TestStringEnd(t *testing.T) {
+       for _, c := range []struct {
+               buffer string
+               want   int
+       }{
+               // Terminated
+               {"\x1b]0;t\a", 6},
+               {"\x1b]0;t\x1b\\", 7},
+               {"\x1b_G\x1b\\", 5},
+               {"\x1bP1$r\a\x1b\\", 8}, // BEL is payload in a DCS, ST ends it
+
+               // BEL terminates an OSC only
+               {"\x1bP\a", 0},
+               {"\x1b_Gi=1\a", 0},
+
+               // An ESC ends the string and introduces a sequence of its own
+               {"\x1b]foo\x1bX\x1b\\", 5},
+               {"\x1b]foo\x1bP", 5},
+               {"\x1b]\x1b]", 2},
+
+               // Nothing has ended it yet
+               {"\x1b]0;t", 0},
+               {"\x1b]0;t\x1b", 0}, // ST half arrived
+               {"\x1b]foo\x1b", 0},
+               {"\x1b]", 0},
+               {"\x1b", 0}, // shorter than an introducer
+
+       } {
+               if got := stringEnd([]byte(c.buffer)); got != c.want {
+                       t.Errorf("stringEnd(%q) = %d, want %d", c.buffer, got, 
c.want)
+               }
+       }
+}
+
+// A dropped sequence followed by ALT-[ is not waiting for anything, so the
+// bytes in between must not be held back until the next keystroke
+func TestStillArriving(t *testing.T) {
+       for _, c := range []struct {
+               buffer string
+               want   bool
+       }{
+               {"\x1b[97;5u\a\x1b[", false},
+               {"\x1b[97;5u\x1b[", false},
+               {"\x1b[3;\a\x1b[", false}, // the BEL ended it, the trailing 
\e[ is another one
+               {"\x1b[1;\a", false},      // the BEL has already ended it
+               {"\x1b[1;", true},
+               {"\x1b[", true},
+               {"\x1bO1", true},
+               {"\x1b[<0;1", true},
+       } {
+               if got := stillArriving([]byte(c.buffer)); got != c.want {
+                       t.Errorf("stillArriving(%q) = %v, want %v", c.buffer, 
got, c.want)
+               }
+       }
+}
+
+func TestStringReply(t *testing.T) {
+       for _, c := range []struct {
+               buffer string
+               want   bool
+       }{
+               {"\x1b]11;rgb:1/2/3\a", true},
+               {"\x1b]0;title\a", true},
+               {"\x1b]52;c;YWJj\x1b\\", true},
+               {"\x1bP>|kitty(0.48.2)\x1b\\", true},
+               {"\x1bP!|0\x1b\\", true},
+               {"\x1bP1$r0m\x1b\\", true},
+               {"\x1bP0+r\x1b\\", true},
+               {"\x1b_Gi=1;OK\x1b\\", true},
+
+               // What typed text after ALT-], ALT-P or ALT-_ looks like
+               {"\x1b]\a", false},
+               {"\x1b]a\a", false},
+               {"\x1b]1\a", false},
+               {"\x1b];\a", false},
+               {"\x1bPx\x1b\\", false},
+               {"\x1bP1\x1b\\", false},
+               {"\x1b_ab\x1b\\", false},
+               {"\x1b_G\x1b\\", false},
+       } {
+               if got := stringReply([]byte(c.buffer)); got != c.want {
+                       t.Errorf("stringReply(%q) = %v, want %v", c.buffer, 
got, c.want)
+               }
+       }
+}
+
+func TestCsiEnd(t *testing.T) {
+       for _, c := range []struct {
+               buffer string
+               want   int
+       }{
+               {"\x1b[97;5u", 7},
+               {"\x1b[A", 3},
+               {"\x1b[<0;1;1M", 9},
+               {"\x1b[?2004;2$y", 11},
+               {"\x1b[7$x", 4}, // rxvt ends keys with $ after digits
+               {"\x1b[23$", 5},
+               {"\x1b[4;2$y", 7},   // but $ is intermediate elsewhere
+               {"\x1b[97;5", 0},    // no final byte
+               {"\x1b[", 0},        // no final byte
+               {"\x1b[1\x01A", -1}, // malformed, do not frame it
+               {"\x1b[\a", -1},
+       } {
+               if got := csiEnd([]byte(c.buffer)); got != c.want {
+                       t.Errorf("csiEnd(%q) = %d, want %d", c.buffer, got, 
c.want)
+               }
+       }
+}
+
+// A key that follows a sequence in the same read must not be consumed with it
+func TestKeyAfterSequence(t *testing.T) {
+       for _, c := range []struct {
+               sequence string
+               mouse    bool
+               event    EventType
+               size     int
+       }{
+               // Recognized when more input follows, as from a paste or tmux 
send-keys
+               {"\x1b[15~x", false, F5, 5},
+               {"\x1b[3;5~x", false, CtrlDelete, 6},
+               {"\x1b[5;13~x", false, CtrlAltPageUp, 7},
+               {"\x1b[6;2~\x1b[A", false, ShiftPageDown, 6},
+
+               // ALT-[ or ALT-O and a digit, then a key that cannot continue 
a sequence
+               {"\x1b[2\r", false, Alt, 2},
+               {"\x1b[12\x03", false, Alt, 2},
+               {"\x1bO2\a", false, Alt, 2},
+               {"\x1b[4x", false, Alt, 2},  // not End
+               {"\x1b[7\r", false, Alt, 2}, // not Home
+               {"\x1b[7~x", false, Home, 4},
+               {"\x1b[8^x", false, End, 4}, // rxvt CTRL-END
+
+               // A mouse report ends at its own final byte
+               {"\x1b[<0;5;3Mx", true, Mouse, 9},
+               {"\x1b[<0;1;\x1b[AM", true, Invalid, 3},
+               {"\x1b[<0;1;1xM", true, Invalid, 9},
+       } {
+               r := &LightRenderer{buffer: []byte(c.sequence), mouse: c.mouse}
+               sz := 1
+               event := r.escSequence(&sz)
+               if event.Type != c.event {
+                       t.Errorf("escSequence(%q) = %s, want %s",
+                               c.sequence, event.Type.String(), 
c.event.String())
+               }
+               if sz != c.size {
+                       t.Errorf("escSequence(%q) consumed %d bytes, want %d", 
c.sequence, sz, c.size)
+               }
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/fzf-0.74.4/src/tui/light_escape_test.go 
new/fzf-0.74.5/src/tui/light_escape_test.go
--- old/fzf-0.74.4/src/tui/light_escape_test.go 2026-09-12 02:21:00.000000000 
+0200
+++ new/fzf-0.74.5/src/tui/light_escape_test.go 2026-10-11 04:29:16.000000000 
+0200
@@ -51,3 +51,46 @@
                }
        }
 }
+
+// A string sequence is not waited for. Its introducer is also ALT-], ALT-P or
+// ALT-_, and holding the read open on those both delayed the key and let typed
+// bytes accumulate into something that looked like a sequence.
+func TestIncompleteStringEscape(t *testing.T) {
+       for _, buffer := range []string{
+               "\x1b]11;rgb:",
+               "\x1bP>|kitty",
+               "\x1b_Gi=1",
+               "\x1b]0;t\x1b",
+               "\x1b]0;title\a",
+               "ab\x1b]11;rgb:",
+       } {
+               if incompleteEscape([]byte(buffer)) {
+                       t.Errorf("incompleteEscape(%q) = true, want false", 
buffer)
+               }
+       }
+}
+
+// SS3 reaches the same parameterized subcases as CSI in the parser, so it has 
to
+// be judged the same way here. Declaring it finished after one byte made the
+// parser give up on a split \eO1;5A and type its tail into the query.
+func TestIncompleteSS3(t *testing.T) {
+       for _, c := range []struct {
+               buffer string
+               want   bool
+       }{
+               {"\x1bO", true},
+               {"\x1bO1", true},
+               {"\x1bO1;", true},
+               {"\x1bO1;5", true},
+               {"\x1bOA", false},
+               {"\x1bOP", false},
+               {"\x1bO1;5A", false},
+               {"\x1b[23$", false}, // rxvt SHIFT-F11 is complete
+               {"\x1b[7$", false},
+               {"\x1b[4;2$", true}, // $ after a ; is intermediate
+       } {
+               if got := incompleteEscape([]byte(c.buffer)); got != c.want {
+                       t.Errorf("incompleteEscape(%q) = %v, want %v", 
c.buffer, got, c.want)
+               }
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/fzf-0.74.4/src/tui/light_query_test.go 
new/fzf-0.74.5/src/tui/light_query_test.go
--- old/fzf-0.74.4/src/tui/light_query_test.go  2026-09-12 02:21:00.000000000 
+0200
+++ new/fzf-0.74.5/src/tui/light_query_test.go  2026-10-11 04:29:16.000000000 
+0200
@@ -6,6 +6,9 @@
        "os"
        "strings"
        "testing"
+       "time"
+
+       "golang.org/x/term"
 )
 
 // Drives queryStartup against a terminal simulated by pipes, with the replies
@@ -126,3 +129,54 @@
                })
        }
 }
+
+// The second-chance read has no time limit, so a key typed long after an
+// unfinished sequence must not be dropped with it.
+func TestLateKeyAfterUnfinishedSequence(t *testing.T) {
+       inR, inW, err := os.Pipe()
+       if err != nil {
+               t.Fatal(err)
+       }
+       t.Cleanup(func() { inR.Close(); inW.Close() })
+       inW.WriteString("\x1b[12")
+       go func() {
+               time.Sleep(2 * defaultEscDelay * time.Millisecond)
+               inW.WriteString("x")
+               time.Sleep(2 * defaultEscDelay * time.Millisecond)
+               inW.WriteString("y") // ends the wait if x was dropped
+       }()
+
+       r := &LightRenderer{ttyin: inR, escDelay: defaultEscDelay}
+       for range 3 {
+               if ev := r.GetChar(false); ev.Type == Rune {
+                       if ev.Char != 'x' {
+                               t.Errorf("got %q, want 'x'", ev.Char)
+                       }
+                       return
+               }
+       }
+       t.Error("the late key was dropped")
+}
+
+// A read error during the second-chance read must end fzf cleanly, not panic.
+func TestSecondChanceReadError(t *testing.T) {
+       inR, inW, err := os.Pipe()
+       if err != nil {
+               t.Fatal(err)
+       }
+       t.Cleanup(func() { inR.Close() })
+       inW.WriteString("\x1b[12")
+       go func() {
+               time.Sleep(2 * defaultEscDelay * time.Millisecond)
+               inW.Close()
+       }()
+
+       out, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
+       if err != nil {
+               t.Fatal(err)
+       }
+       r := &LightRenderer{ttyin: inR, ttyout: out, escDelay: defaultEscDelay, 
origState: &term.State{}}
+       if ev := r.GetChar(false); ev.Type != Fatal {
+               t.Errorf("got %s, want Fatal", ev.Type.String())
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/fzf-0.74.4/src/tui/light_test.go 
new/fzf-0.74.5/src/tui/light_test.go
--- old/fzf-0.74.4/src/tui/light_test.go        2026-09-12 02:21:00.000000000 
+0200
+++ new/fzf-0.74.5/src/tui/light_test.go        2026-10-11 04:29:16.000000000 
+0200
@@ -66,7 +66,6 @@
        assertEscSequence("\x1b[1;1R", "!Invalid")
        assertEscSequence("\x1b[", "!Invalid")
        assertEscSequence("\x1b[1", "!Invalid")
-       assertEscSequence("\x1b[3;3~1", "!Invalid")
        assertEscSequence("\x1b[13", "!Invalid")
        assertEscSequence("\x1b[1;3", "!Invalid")
        assertEscSequence("\x1b[1;10", "!Invalid")
@@ -200,6 +199,7 @@
        assertEscSequence("\x1b[1;3H", "alt-home")
        assertEscSequence("\x1b[1;3F", "alt-end")
        assertEscSequence("\x1b[3;3~", "alt-delete")
+       assertEscSequence("\x1b[3;3~1", "alt-delete") // then 1
        assertEscSequence("\x1b[5;3~", "alt-page-up")
        assertEscSequence("\x1b[6;3~", "alt-page-down")
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/fzf-0.74.4/src/util/util.go 
new/fzf-0.74.5/src/util/util.go
--- old/fzf-0.74.4/src/util/util.go     2026-09-12 02:21:00.000000000 +0200
+++ new/fzf-0.74.5/src/util/util.go     2026-10-11 04:29:16.000000000 +0200
@@ -105,16 +105,18 @@
        rest := limit % length
        output := strings.Repeat(str, times)
        if rest > 0 {
-               for _, r := range str {
-                       rest -= uniseg.StringWidth(string(r))
-                       if rest < 0 {
-                               break
-                       }
-                       output += string(r)
-                       if rest == 0 {
+               // Iterate over grapheme clusters so that we don't cut a 
cluster in half
+               end := 0
+               graphemes := uniseg.NewGraphemes(str)
+               for rest > 0 && graphemes.Next() {
+                       width := graphemes.Width()
+                       if width > rest {
                                break
                        }
+                       rest -= width
+                       _, end = graphemes.Positions()
                }
+               output += str[:end]
        }
        return output
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/fzf-0.74.4/src/util/util_test.go 
new/fzf-0.74.5/src/util/util_test.go
--- old/fzf-0.74.4/src/util/util_test.go        2026-09-12 02:21:00.000000000 
+0200
+++ new/fzf-0.74.5/src/util/util_test.go        2026-10-11 04:29:16.000000000 
+0200
@@ -109,6 +109,27 @@
        if RepeatToFill("abcde", 10, 42) != strings.Repeat("abcde", 
4)+"abcde"[:2] {
                t.Error("Expected:", strings.Repeat("abcde", 4)+"abcde"[:2])
        }
+
+       // Should not cut a grapheme cluster in half
+       for _, test := range []struct {
+               str      string
+               limit    int
+               expected string
+       }{
+               {"a\u0301b", 1, "a\u0301"},
+               {"a\u0301b", 3, "a\u0301ba\u0301"},
+               {"a\u0301b", 4, "a\u0301ba\u0301b"},
+               {"a\u4e00", 2, "a"},
+               {"a\u4e00", 4, "a\u4e00a"},
+               {"-\U0001f468\u200d\U0001f469\u200d\U0001f467", 1, "-"},
+               {"-\U0001f468\u200d\U0001f469\u200d\U0001f467", 2, "-"},
+               {"-\U0001f468\u200d\U0001f469\u200d\U0001f467", 4, 
"-\U0001f468\u200d\U0001f469\u200d\U0001f467-"},
+       } {
+               actual := RepeatToFill(test.str, StringWidth(test.str), 
test.limit)
+               if actual != test.expected {
+                       t.Errorf("Expected: %q, actual: %q", test.expected, 
actual)
+               }
+       }
 }
 
 func TestStringWidth(t *testing.T) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/fzf-0.74.4/test/test_core.rb 
new/fzf-0.74.5/test/test_core.rb
--- old/fzf-0.74.4/test/test_core.rb    2026-09-12 02:21:00.000000000 +0200
+++ new/fzf-0.74.5/test/test_core.rb    2026-10-11 04:29:16.000000000 +0200
@@ -1532,6 +1532,61 @@
     tmux.until { |lines| refute_includes(lines[-1], '[[1]]') }
   end
 
+  def assert_focus_log(expected)
+    wait do
+      assert_path_exists tempname
+      assert_equal expected, File.readlines(tempname, chomp: true)
+    end
+  end
+
+  def test_focus_event_after_reload
+    # Without --id-nth, the item at the same index after reload is a new item
+    tmux.send_keys %(printf 'a\\nb\\n' | #{FZF} --bind 
'focus:execute-silent(echo {} >> #{tempname})' --bind 'ctrl-r:reload(printf 
"c\\nd\\n")'), :Enter
+    tmux.until { |lines| assert_includes lines, '> a' }
+    assert_focus_log(%w[a])
+    tmux.send_keys 'C-r'
+    tmux.until { |lines| assert_includes lines, '> c' }
+    assert_focus_log(%w[a c])
+  end
+
+  def test_focus_event_after_reload_id_nth_same_key
+    # Same --id-nth key after reload: no focus event
+    tmux.send_keys %(printf '1 a\\n2 b\\n' | #{FZF} --track --id-nth 1 --bind 
'focus:execute-silent(echo {} >> #{tempname})' --bind 'ctrl-r:reload(printf "0 
x\\n1 aa\\n2 bb\\n")'), :Enter
+    tmux.until { |lines| assert_includes lines, '> 1 a' }
+    assert_focus_log(['1 a'])
+    tmux.send_keys 'C-r'
+    tmux.until { |lines| assert_includes lines, '> 1 aa' }
+    tmux.send_keys :Up
+    tmux.until { |lines| assert_includes lines, '> 2 bb' }
+    assert_focus_log(['1 a', '2 bb'])
+  end
+
+  def test_focus_event_after_reload_id_nth_key_not_found
+    tmux.send_keys %(printf '1 a\\n2 b\\n' | #{FZF} --track --id-nth 1 --bind 
'focus:execute-silent(echo {} >> #{tempname})' --bind 'ctrl-r:reload(printf "3 
c\\n4 d\\n")'), :Enter
+    tmux.until { |lines| assert_includes lines, '> 1 a' }
+    assert_focus_log(['1 a'])
+    tmux.send_keys 'C-r'
+    tmux.until { |lines| assert_includes lines, '> 3 c' }
+    assert_focus_log(['1 a', '3 c'])
+  end
+
+  def test_focus_event_after_async_reload_id_nth
+    # No focus event for the items shown before the tracked item arrives.
+    # 'result' binding makes fzf redraw the list while blocked.
+    tmux.send_keys %(printf '1 a\\n2 b\\n' | #{FZF} --track --id-nth 1 --bind 
'result:change-footer(R)' --bind 'focus:execute-silent(echo {} >> #{tempname})' 
--bind 'ctrl-r:reload(printf "0 x\\n"; sleep 1; printf "1 aa\\n2 bb\\n")'), 
:Enter
+    tmux.until { |lines| assert_includes lines, '> 1 a' }
+    assert_focus_log(['1 a'])
+    tmux.send_keys 'C-r'
+    tmux.until { |lines| assert_includes lines[-2], '+T*' }
+    tmux.until do |lines|
+      assert_includes lines, '> 1 aa'
+      refute_includes lines[-2], '+T*'
+    end
+    tmux.send_keys :Up
+    tmux.until { |lines| assert_includes lines, '> 2 bb' }
+    assert_focus_log(['1 a', '2 bb'])
+  end
+
   def test_result_event
     tmux.send_keys '(echo 0; seq 10) | fzf --bind "result:pos(2)"', :Enter
     tmux.until { |lines| assert_equal 11, lines.match_count }

++++++ vendor.tar.zst ++++++

Reply via email to