@ 261105.2

261020.15 · ee1d77b5e7d5ac3b2661ffddb6243a7b477370fa

feat(win): spawn fzf via uv.spawn on a single route (CLI + TUI)

Windows fzf launches ran through `cmd.exe /c` with a single assembled command line, so shell escaping had to survive two layers (nvim -> cmd), user fragments needed caret escaping, and command lines over cmd.exe's 8191-char limit fell back to a PowerShell `ProcessStartInfo` script with its own quoting rules. The fallback also owned the producer pump and the result-file redirect, so quoting and lifecycle bugs only reproduced on one of the two paths.

Replace both with one managed route: `libuv.win_spawn` runs fzf with the raw argv (`_cli_segments`; no shell, no escaping, no command line limit), redirects stdout through the result file descriptor and pumps an optional producer (`cmd.exe /c <cmd>`) into fzf's stdin -- every chunk forwarded as-is, immediately, with backpressure (4MB/1MB watermarks). When fzf exits, the producer's process tree is reaped via `taskkill /PID /T /F`, which also covers producers blocked writing into a dead stdin.

TUI hosts (`utils.termopen`) need fzf to own the terminal buffer's pty: `win_spawn.lua`, a mediator nvim attached to that pty (`-u NONE -l`, like the existing `spawn.lua` producers), reads the serialized spec from a temp file (deleted right away), runs the same spawn route and exits with fzf's code. The CLI host (`scripts/cli.lua`) calls `libuv.win_spawn` in-process with a scheduled `on_exit`, keeping stdio like a bare `fzf`.

User fragments are cmd-ready strings (`shellescape` caret style), so `libuv.cmd_argv` reproduces what `cmd.exe /c` plus the Win32 argv parser would deliver (caret consumption, then quoting rules) and replaces `shellparse` on Windows -- existing configs keep working, including `--bind=load:...` payloads built by `stringify_data` (regression tests).

Removed: the PowerShell fallback and its BOM strip, `libuv.ps_escape`, the `escape_cli` "powershell" variant and the fzf_spec tests for the fallback. Note: fragments are now data, exactly like on posix -- `cmd` operators in user options are no longer interpreted by an outer shell (caret-escaped fragments are unaffected; values are still expanded by any shell fzf itself invokes).

Tests: `win_spawn_spec` (byte-exact pump/argv quoting, exit codes, 4.8MB backpressure, mediator round-trip, producer tree-kill on early exit), `libuv_spec` (`cmd_argv` round-trips, callback payload), `fzf_spec` (route wiring for both hosts), `api_spec` repro case.

reading declarations

lua/fzf-lua/core.lua

@@ -778,7 +778,17 @@ M.build_fzf_cli = function(opts)           table.insert(cli_args, arg)           -- Split raw user argument strings (e.g. fzf_args entries with spaces)           -- into separate argument elements for native quoting.-          local tokens = shellparse(tostring(arg))+          -- On Windows these strings are `cmd.exe` command line fragments+          -- (typically pre-escaped with `libuv.shellescape`): recover the raw+          -- argv values `cmd.exe` + the Win32 argv parser would deliver so+          -- the shell-less spawn route (`libuv.win_spawn`) gets identical+          -- values (`libuv.cmd_argv`); posix uses the shellwords parser.+          local tokens+          if utils.__IS_WINDOWS then+            tokens = libuv.cmd_argv(tostring(arg))+          else+            tokens = shellparse(tostring(arg))+          end           if type(tokens) ~= "table" then tokens = { tostring(arg) } end           ---@cast tokens string[]           for _, tok in ipairs(tokens) do

lua/fzf-lua/fzf.lua

@@ -1,5 +1,7 @@ -- slimmed down version of nvim-fzf's 'raw_fzf', changes include:--- DOES NOT SUPPORT WINDOWS+-- Windows runs a single managed route: `uv.spawn` with raw argv and the+-- result file as stdio, mediated by `libuv.win_spawn` (CLI) or the+-- `win_spawn.lua` script nvim attached to the terminal buffer (TUI). -- does not close the pipe before all writes are complete -- option to not add '\n' on content function callbacks -- https://github.com/vijaymarupudi/nvim-fzf/blob/master/lua/fzf.lua@@ -9,6 +11,11 @@ local libuv = require "fzf-lua.libuv"  local M = {} +-- used to locate `win_spawn.lua` (the TUI mediator) next to this file+local _info = debug.getinfo(1, "S")+---@cast _info {source: string}+local __FILE__ = _info.source:gsub("^@", "")+ -- contents can be either a table with tostring()able items, or a function that -- can be called repeatedly for values. The latter can use coroutines for async -- behavior.@@ -58,8 +65,35 @@ function M.raw_fzf(contents, fzf_cli_args, opts)   -- local readEOL = get_EOL("read0")   local printEOL = get_EOL("print0") -  table.insert(cmd, ">")-  table.insert(cmd, libuv.shellescape(outputtmpname))+  -- Windows direct spawn (CLI + TUI mediator), see `libuv.win_spawn`:+  -- raw argv rebuilt from the CLI segments, fzf's stdout is redirected via+  -- the result file descriptor (no `>`/PowerShell), no shell escaping and+  -- no cmd.exe 8191-char command line limit+  local win_spec = nil ---@type fzf-lua.WinSpawnSpec?+  if utils.__IS_WINDOWS and not opts.is_fzf_tmux+      and opts._cli_segments and #opts._cli_segments > 0 then+    local raw_args = {}+    for _, elem in ipairs(opts._cli_segments) do+      local arg = {}+      for _, seg in ipairs(elem) do+        table.insert(arg, seg.text)+      end+      table.insert(raw_args, table.concat(arg))+    end+    local item = opts.pipe_cmd and FZF_DEFAULT_COMMAND or nil+    if item then FZF_DEFAULT_COMMAND = nil end+    win_spec = {+      bin = cmd[1],+      args = raw_args,+      item = item,+      outfile = outputtmpname,+      cwd = opts.cwd,+    }+  else+    -- legacy shell route: redirect fzf's stdout to the result file+    table.insert(cmd, ">")+    table.insert(cmd, libuv.shellescape(outputtmpname))+  end    if not opts.is_fzf_tmux then     utils.eventignore(function() vim.bo.filetype = "fzf" end)@@ -97,87 +131,14 @@ function M.raw_fzf(contents, fzf_cli_args, opts)     -- })   end -  -- Windows PowerShell fallback when the assembled fzf launcher command line-  -- exceeds cmd.exe's 8191-char limit. Rebuild from raw segments so values are-  -- PowerShell-escaped; nested `nvim -l` action payloads stay on fast cmd.exe-  -- via SHELL=cmd.exe.-  local use_powershell = false-  local ps_command ---@type string?-  local cmd_len = #table.concat(cmd, " ")-  if opts.pipe_cmd and FZF_DEFAULT_COMMAND then-    -- the non-powershell branch below prepends the item command as-    -- `(FZF_DEFAULT_COMMAND) | ` to the same cmd.exe command line-    cmd_len = cmd_len + #FZF_DEFAULT_COMMAND + 8-  end-  if utils.__IS_WINDOWS and not opts.is_fzf_tmux-      and opts._cli_segments and #opts._cli_segments > 0-      and cmd_len > 8000 then-    use_powershell = true-    -- Reconstruct the raw argv strings from the segments, encode each one with-    -- Windows CommandLineToArgvW quoting rules, and pass the joined command-    -- line as a single string to Start-Process. This keeps empty arguments as-    -- "" and correctly escapes embedded quotes/backslashes while avoiding both-    -- cmd.exe's 8191-char limit and Start-Process array-literal edge cases.-    local raw_args = {}-    for _, elem in ipairs(opts._cli_segments) do-      local arg = {}-      for _, seg in ipairs(elem) do-        table.insert(arg, seg.text)-      end-      table.insert(raw_args, table.concat(arg))-    end-    local ps_fzf_bin = libuv.ps_escape(cmd[1])-    local ps_outfile = libuv.ps_escape(outputtmpname)-    local argline = table.concat(vim.tbl_map(function(a)-      return libuv.shellescape(a, 1)-    end, raw_args), " ")-    local ps_parts = {}-    if opts.pipe_cmd and FZF_DEFAULT_COMMAND then-      -- `(item) | fzf` can't be used in pipe mode: the item command would be-      -- prepended to the same command line, which cmd.exe rejects past 8191-      -- chars. Start-Process can't chain two processes either, so launch both-      -- through ProcessStartInfo (its `Arguments` string is passed verbatim,-      -- no CommandLineToArgvW re-quoting) and pump the item command's stdout-      -- into fzf's stdin from this script. fzf reads stdin in the background,-      -- so the picker starts immediately while items stream in, matching the-      -- concurrency of the `(item) | fzf` cmd.exe branch.-      table.insert(ps_parts, string.format(-        "$ipsi = New-Object System.Diagnostics.ProcessStartInfo; $ipsi.FileName = 'cmd.exe'; $ipsi.Arguments = %s; $ipsi.UseShellExecute = $false; $ipsi.RedirectStandardOutput = $true; $iproc = [System.Diagnostics.Process]::Start($ipsi);",-        libuv.ps_escape("/d /e:off /f:off /v:off /c (" .. FZF_DEFAULT_COMMAND .. ")")))-      table.insert(ps_parts, string.format(-        "$fpsi = New-Object System.Diagnostics.ProcessStartInfo; $fpsi.FileName = %s; $fpsi.Arguments = %s; $fpsi.UseShellExecute = $false; $fpsi.RedirectStandardInput = $true; $fpsi.RedirectStandardOutput = $true; $fproc = [System.Diagnostics.Process]::Start($fpsi);",-        ps_fzf_bin, libuv.ps_escape(argline)))-      -- drain fzf's stdout into the result file asynchronously while the pump-      -- runs: the selection is written at exit and can exceed the pipe buffer,-      -- and fzf stops reading stdin by then, so draining only after the pump-      -- would block fzf on stdout and the pump on fzf's stdin at the same time-      table.insert(ps_parts, string.format(-        "$ofs = [System.IO.File]::Create(%s); $drain = $fproc.StandardOutput.BaseStream.CopyToAsync($ofs);",-        ps_outfile))-      -- the pump runs async so fzf's exit is noticed while it still streams;-      -- the producer is killed as soon as fzf is gone, even when it is-      -- blocked without producing output (a sync pump would sit in Read-      -- until the producer yields); the kill must take the whole tree:-      -- `$iproc` is only the wrapper `cmd.exe`, a plain `Kill()` orphans-      -- the actual producer, whose stdout handle keeps the pump reading-      -- until it exits on its own; stdin is closed once the pump finishes-      -- (producer EOF), since fzf may need that EOF to finish (--select-1)-      table.insert(ps_parts,-        "$pump = $iproc.StandardOutput.BaseStream.CopyToAsync($fproc.StandardInput.BaseStream); $pumpDone = $false; while (-not $fproc.WaitForExit(200)) { if (-not $pumpDone -and $pump.IsCompleted) { $pumpDone = $true; try { $fproc.StandardInput.Close() } catch {} } }; $code = $fproc.ExitCode; if (-not $iproc.HasExited) { & taskkill.exe /PID $iproc.Id /T /F 2>$null | Out-Null; $iproc.WaitForExit() }; try { $null = $pump.Wait(2000) } catch {}; if (-not $pumpDone) { try { $fproc.StandardInput.Close() } catch {} }; $drain.Wait(); $ofs.Close(); exit $code")-      FZF_DEFAULT_COMMAND = nil-    else-      table.insert(ps_parts, string.format(-        "$proc = Start-Process -FilePath %s -ArgumentList %s -RedirectStandardOutput %s -Wait -NoNewWindow -PassThru -ErrorAction Stop;",-        ps_fzf_bin, libuv.ps_escape(argline), ps_outfile))-      table.insert(ps_parts, "if ($null -eq $proc) { exit 1 }; exit $proc.ExitCode")-    end-    ps_command = table.concat(ps_parts, " ")-  end-   if opts.debug and type(opts.debug) ~= "number" then     utils.info("FZF_DEFAULT_COMMAND: %s", tostring(FZF_DEFAULT_COMMAND))-    if use_powershell then-      utils.info("fzf cmd [powershell]: %s", ps_command)+    if win_spec then+      utils.info("fzf cmd [nvim/uv]: %s %s",+        win_spec.bin, table.concat(win_spec.args, " "))+      if win_spec.item then+        utils.info("fzf item [cmd.exe]: %s", win_spec.item)+      end     else       utils.info("fzf cmd [%s]: %s",         utils.__IS_WINDOWS and "cmd.exe" or "sh", table.concat(cmd, " "))@@ -186,11 +147,31 @@ function M.raw_fzf(contents, fzf_cli_args, opts)    local co = coroutine.running()   ---@cast co thread-  local jobstart = _G.fzf_jobstart or opts.is_fzf_tmux and vim.fn.jobstart or utils.termopen-  local shell_cmd-  if use_powershell then-    shell_cmd = { "powershell.exe", "-NoProfile", "-NonInteractive", "-Command", ps_command }+  local jobstart ---@type fun(cmd: any, opts: table): any+  local shell_cmd ---@type table+  -- TUI mediator spec file path (written below, once the environment is+  -- assembled, and deleted by `win_spawn.lua` right after reading)+  local win_spec_path ---@type string?+  if win_spec then+    if _G.fzf_jobstart then+      -- CLI: the current process hosts the spawn (`libuv.win_spawn`)+      shell_cmd = { __win_spawn = win_spec }+      jobstart = _G.fzf_jobstart+    else+      -- TUI: fzf must own this terminal buffer's pty, run the same spawn+      -- route from a mediator script nvim attached to the pty (`-u NONE -l`,+      -- like `spawn.lua` producers); the spec travels via a temp file+      local nvim_bin = os.getenv("FZF_LUA_NVIM_BIN") or vim.v.progpath+      win_spec_path = utils.tempname()+      shell_cmd = {+        nvim_bin, "-u", "NONE", "-l",+        vim.fn.fnamemodify(__FILE__, ":h") .. "/win_spawn.lua",+        win_spec_path,+      }+      jobstart = utils.termopen+    end   else+    jobstart = _G.fzf_jobstart or opts.is_fzf_tmux and vim.fn.jobstart or utils.termopen     shell_cmd = utils.__IS_WINDOWS         -- MSYS2 comes with "/usr/bin/cmd" that precedes "cmd.exe" (#1396)         and { "cmd.exe", "/d", "/e:off", "/f:off", "/v:off", "/c" }@@ -209,57 +190,64 @@ function M.raw_fzf(contents, fzf_cli_args, opts)   end   -- This obscure option makes jobstart fail with: "The syntax of the command is incorrect"   -- temporarily set to `false`, for more info see `:help shellslash` (#1055)-  local nvim_opt_shellslash = utils.__WIN_HAS_SHELLSLASH and vim.o.shellslash+  -- (only affects the legacy cmd.exe launcher, not the direct route)+  local nvim_opt_shellslash = not win_spec+      and utils.__WIN_HAS_SHELLSLASH and vim.o.shellslash   if nvim_opt_shellslash then vim.o.shellslash = false end+  local job_env = {+    ["SHELL"] = win_spec and "cmd.exe" or shell_cmd[1],+    -- Nullify "NVIM_LISTEN_ADDRESS", will cause issues if already in use (#2253)+    ["NVIM_LISTEN_ADDRESS"] = "",+    ["FZF_DEFAULT_COMMAND"] = FZF_DEFAULT_COMMAND,+    ["SKIM_DEFAULT_COMMAND"] = FZF_DEFAULT_COMMAND,+    ["FZF_LUA_SERVER"] = vim.g.fzf_lua_server,+    -- sk --tmux didn't pass all environemnt variable (https://github.com/skim-rs/skim/issues/732)+    ["SKIM_FZF_LUA_SERVER"] = vim.g.fzf_lua_server,+    ["VIMRUNTIME"] = vim.env.VIMRUNTIME,+    ["FZF_DEFAULT_OPTS"] = (function()+      -- Newer style `--preview-window` options in FZF_DEFAULT_OPTS such as:+      --   --preview-window "right,50%,hidden,<60(up,70%,hidden)"+      -- prevents our previewer from working properly, since there is never+      -- a reason to inherit `preview-window` options it can be safely stripped+      -- from FZF_DEFAULT_OPTS (#1107)+      local default_opts = os.getenv("FZF_DEFAULT_OPTS")+      if not default_opts then return end+      local patterns = { "--preview-window" }+      for _, p in ipairs(patterns) do+        -- remove flag end of string+        default_opts = default_opts:gsub(utils.lua_regex_escape(p) .. "[=%s]+[^%-]+%s-$", "")+        -- remove flag start/mid of string+        default_opts = default_opts:gsub(utils.lua_regex_escape(p) .. "[=%s]+.-%s+%-%-", " --")+      end+      return default_opts+    end)(),+    -- Nullify user's RG config as this can cause conflicts+    -- with fzf-lua's rg opts (#1266)+    ["RIPGREP_CONFIG_PATH"] = type(opts.RIPGREP_CONFIG_PATH) == "string"+        and libuv.expand(opts.RIPGREP_CONFIG_PATH) or "",+    -- Prevents spamming rust logs with skim (#1959)+    ["RUST_LOG"] = "",+  }+  -- the direct route (`win_spawn`) receives the same environment in its spec+  -- (used in-process by the CLI host and serialized for the TUI mediator)+  if win_spec then win_spec.env = job_env end+  -- write the mediator spec file now that it carries the full environment+  if win_spec and win_spec_path then+    local payload = libuv.serialize(win_spec, true)+    local f = assert(io.open(win_spec_path, "w"))+    f:write(payload)+    f:close()+  end   jobstart(shell_cmd, {     cwd = opts.cwd,     pty = true,-    env = {-      ["SHELL"] = use_powershell and "cmd.exe" or shell_cmd[1],-      -- Nullify "NVIM_LISTEN_ADDRESS", will cause issues if already in use (#2253)-      ["NVIM_LISTEN_ADDRESS"] = "",-      ["FZF_DEFAULT_COMMAND"] = FZF_DEFAULT_COMMAND,-      ["SKIM_DEFAULT_COMMAND"] = FZF_DEFAULT_COMMAND,-      ["FZF_LUA_SERVER"] = vim.g.fzf_lua_server,-      -- sk --tmux didn't pass all environemnt variable (https://github.com/skim-rs/skim/issues/732)-      ["SKIM_FZF_LUA_SERVER"] = vim.g.fzf_lua_server,-      ["VIMRUNTIME"] = vim.env.VIMRUNTIME,-      ["FZF_DEFAULT_OPTS"] = (function()-        -- Newer style `--preview-window` options in FZF_DEFAULT_OPTS such as:-        --   --preview-window "right,50%,hidden,<60(up,70%,hidden)"-        -- prevents our previewer from working properly, since there is never-        -- a reason to inherit `preview-window` options it can be safely stripped-        -- from FZF_DEFAULT_OPTS (#1107)-        local default_opts = os.getenv("FZF_DEFAULT_OPTS")-        if not default_opts then return end-        local patterns = { "--preview-window" }-        for _, p in ipairs(patterns) do-          -- remove flag end of string-          default_opts = default_opts:gsub(utils.lua_regex_escape(p) .. "[=%s]+[^%-]+%s-$", "")-          -- remove flag start/mid of string-          default_opts = default_opts:gsub(utils.lua_regex_escape(p) .. "[=%s]+.-%s+%-%-", " --")-        end-        return default_opts-      end)(),-      -- Nullify user's RG config as this can cause conflicts-      -- with fzf-lua's rg opts (#1266)-      ["RIPGREP_CONFIG_PATH"] = type(opts.RIPGREP_CONFIG_PATH) == "string"-          and libuv.expand(opts.RIPGREP_CONFIG_PATH) or "",-      -- Prevents spamming rust logs with skim (#1959)-      ["RUST_LOG"] = "",-    },+    env = job_env,     on_exit = function(_, rc, _)       local output = nil ---@type string[]?       local f = io.open(outputtmpname)       if f then         local text = f:read("*a")         f:close()-        -- Redirected stdout from the PowerShell fallback may begin with an-        -- optional leading UTF-8 BOM in some PowerShell versions; strip it-        -- before splitting so selected lines stay clean.-        if use_powershell then-          text = text:gsub("^\239\187\191", "")-        end         output = vim.split(text, printEOL)         -- `file:read("*a")` appends an empty string on EOL         output[#output] = nil

lua/fzf-lua/libuv.lua

@@ -205,6 +205,181 @@ M.win_stdio_no_inherit = function(stdio)   end end +---@class fzf-lua.WinSpawnSpec+---@field bin string fzf executable (`fzf`/`sk` or full path)+---@field args string[] raw fzf argv (`uv.spawn` prepends `bin` as argv[0])+---@field item? string producer command (cmd syntax), piped into fzf stdin+---@field outfile string fzf stdout is redirected to this file+---@field env? table<string,any>+---@field cwd? string+---@field stdin? integer fzf stdin fd when there is no `item` (default 0)+---@field stderr? integer fzf stderr fd (default `win_stderr_fd()`)++-- Windows ("one route") fzf spawn for `fzf.raw_fzf`: no shell, no escaping.+-- `uv.spawn` runs fzf with the raw argv (`_cli_segments`), stderr stays on+-- the console/pty (fzf's light renderer requires a console, see+-- `IsLightRendererSupported` in fzf's `src/tui/light_windows.go`), stdout is+-- redirected to `spec.outfile` (replaces the `> outfile` shell redirect) and+-- the optional `spec.item` producer is spawned via `cmd.exe /c` with its+-- stdout pumped into fzf's stdin, every chunk forwarded as-is, immediately.+-- This replaces the caret-escaped cmd.exe launcher, the `(item) | fzf` cmd+-- pipeline and the >8191-char PowerShell fallback. Callers: `scripts/cli.lua`+-- (CLI nvim) and `win_spawn.lua` (mediator for TUI terminal buffers).+---@param spec fzf-lua.WinSpawnSpec+---@param opts { on_exit: fun(rc: integer) } called once fzf and the producer+---  tree exited (the `outfile` is flushed before that)+---@return boolean ok false when fzf failed to spawn (on_exit(1) still called)+M.win_spawn = function(spec, opts)+  assert(_is_win, "win_spawn is Windows only")+  -- pause the producer read when this many bytes are queued for fzf+  local HIGH, LOW = 4 * 1024 * 1024, 1024 * 1024+  local item = spec.item and #spec.item > 0 and spec.item or nil+  local stderr_fd = spec.stderr or M.win_stderr_fd()+  ---@type integer?+  local out_fd = assert(uv.fs_open(spec.outfile, "w", 420))+  local prod_out = item and assert(uv.new_pipe(false)) or nil+  local fzf_in = item and assert(uv.new_pipe(false)) or nil+  local fzf_handle, prod_handle, kill_handle, kill_null+  local prod_pid+  local fzf_done, prod_done, kill_done = false, item == nil, true+  local fzf_rc, finished+  local writes, pending, paused, eof = 0, 0, false, false++  ---@param h uv.uv_pipe_t|uv.uv_process_t|nil+  local function close_handle(h)+    if h and not h:is_closing() then h:close() end+  end++  local function finish(rc)+    if finished then return end+    finished = true+    close_handle(fzf_in)+    close_handle(prod_out)+    close_handle(kill_handle)+    close_handle(kill_null)+    close_handle(fzf_handle)+    close_handle(prod_handle)+    if out_fd then+      uv.fs_close(out_fd)+      out_fd = nil+    end+    opts.on_exit(rc)+  end++  local function maybe_finish()+    if fzf_done and prod_done and kill_done then+      finish(fzf_rc == nil and 1 or fzf_rc)+    end+  end++  -- producer EOF: flush the pending writes, then close our end of fzf's+  -- stdin so fzf sees EOF (`--select-1` and `reload` actions rely on it)+  local function close_write_end()+    if eof and writes == 0 and fzf_in and not fzf_in:is_closing() then+      fzf_in:close()+    end+  end++  -- byte pump: forward each chunk as-is, immediately, no processing+  local function pump(err, data)+    if err or data == nil then+      eof = true+      close_handle(prod_out)+      close_write_end()+      return+    end+    if not fzf_in or fzf_in:is_closing() then return end+    local len = #data+    writes, pending = writes + 1, pending + len+    fzf_in:write(data, function(werr)+      writes, pending = writes - 1, pending - len+      -- after fzf exits the queued writes error (EPIPE/ECANCELED)+      if not werr and fzf_in and not fzf_in:is_closing() then+        if paused and pending < LOW and prod_out and not prod_out:is_closing() then+          paused = false+          prod_out:read_start(pump)+        end+        close_write_end()+      end+    end)+    if not paused and pending > HIGH and prod_out and not prod_out:is_closing() then+      paused = true+      prod_out:read_stop()+    end+  end++  -- fzf first: a spawn failure aborts before the producer is started+  local ok, spawn_err = pcall(function()+    fzf_handle = M.uv_spawn(spec.bin, {+      cwd = spec.cwd,+      args = spec.args,+      stdio = { fzf_in or spec.stdin or 0, out_fd, stderr_fd },+      env = spec.env,+    }, function(rc, _)+      fzf_done, fzf_rc = true, rc+      close_handle(fzf_handle)+      if prod_out and not prod_out:is_closing() then+        prod_out:read_stop()+      end+      -- fzf exited while the producer may still be running (e.g. the user+      -- aborted a `ping`-like producer): luv's `kill` is not a tree kill+      -- on Windows, `taskkill /T /F` reaps the launcher and its children+      if not prod_done and prod_pid then+        kill_done = false+        local kok = pcall(function()+          if not kill_null then kill_null = assert(uv.new_pipe(false)) end+          kill_handle = M.uv_spawn("taskkill.exe", {+            args = { "/PID", tostring(prod_pid), "/T", "/F" },+            stdio = { nil, kill_null, kill_null },+          }, function()+            kill_done = true+            close_handle(kill_handle)+            maybe_finish()+          end)+          close_handle(kill_null)+          kill_null = nil+        end)+        if not kok then kill_done = true end+      end+      maybe_finish()+    end)+  end)+  if not ok or not fzf_handle then+    io.stderr:write(+      ("[fzf-lua] failed to spawn '%s': %s\n"):format(spec.bin, tostring(spawn_err)))+    finish(1)+    return false+  end++  if item then+    local pok = pcall(function()+      prod_handle, prod_pid = M.uv_spawn("cmd.exe", {+        -- pass the producer command verbatim: it is a raw `cmd /c` line,+        -- libuv's quoting would corrupt it (same as the old launcher)+        args = { "/d", "/e:off", "/f:off", "/v:off", "/c", item },+        verbatim = true,+        cwd = spec.cwd,+        env = spec.env,+        stdio = { nil, prod_out, stderr_fd },+      }, function(_, _)+        prod_done = true+        close_handle(prod_handle)+        maybe_finish()+      end)+    end)+    if pok and prod_handle and prod_out then+      prod_out:read_start(pump)+    else+      -- producer failed to spawn (e.g. malformed command): close the write+      -- end so fzf still terminates (empty list, `shell_nop` semantics)+      prod_done, eof = true, true+      close_write_end()+      maybe_finish()+    end+  end+  return true+end+  ---@class fzf-lua.SpawnOpts ---@field cwd? string@@ -821,7 +996,9 @@ end -- this function is a better fit for utils but we're -- trying to avoid having any 'require' in this file ---@param s string----@param win_style integer|string? 1=classic, 2=caret+---@param win_style integer|string|boolean? force Windows escaping on posix:+---  1 = classic CommandLineToArgvW backslash escape, 2 = caret escape with+---  delayed expansion double-escape, `true`/nil (on Windows) = caret escape ---@return string M.shellescape = function(s, win_style)   if _is_win or win_style then@@ -947,14 +1124,80 @@ M.shellescape = function(s, win_style)   end end --- PowerShell escaping for the fzf launcher on Windows when the command line--- exceeds cmd.exe's 8191-char limit (see `fzf.raw_fzf`). A single-quoted--- PowerShell string treats every character literally except a single quote,--- which is escaped by doubling it (''), no caret/backslash/metachar handling--- needed (our internal `nvim -l` payloads are already base64-encoded).----@param s string----@return string-M.ps_escape = function(s) return "'" .. s:gsub("'", "''") .. "'" end+-- Parse a `cmd.exe` command line fragment into argv values, exactly as+-- `cmd.exe` + the Win32 argv parser (Go runtime; fzf is a Go binary) would+-- deliver them. User provided argument strings (`fzf_args`, `fzf_cli_args`,+-- etc.) are written for the platform shell: on Windows they are cmd.exe+-- fragments, typically pre-escaped with `shellescape` (caret style). The+-- Windows direct spawn route (`libuv.win_spawn`) passes the raw argv to+-- `uv.spawn` with no shell, so this reverses the two parsing passes:+--   1. cmd.exe caret consumption: `^X` -> `X` (and thus `^^` -> `^`)+--   2. Win32 argv parsing: quotes toggle and are dropped, backslash runs+--      before quotes collapse per CommandLineToArgvW rules (`2n` backslashes+--      followed by a quote -> `n` backslashes + toggle quote mode, `2n+1` ->+--      `n` backslashes + literal quote), unquoted whitespace splits args+---@param s string cmd.exe command line fragment+---@return string[] argv+M.cmd_argv = function(s)+  if not s or s == "" then return {} end+  -- (1) consume cmd.exe caret escapes+  local parts = {}+  local i, len = 1, #s+  while i <= len do+    local c = s:sub(i, i)+    if c == "^" then+      if i == len then break end -- dangling caret is dropped by cmd.exe+      parts[#parts + 1] = s:sub(i + 1, i + 1)+      i = i + 2+    else+      parts[#parts + 1] = c+      i = i + 1+    end+  end+  s = table.concat(parts)+  -- (2) parse the unescaped command line into argv values+  local argv = {}+  local j = 1+  len = #s+  while j <= len do+    local sp = s:sub(j, j)+    if sp == " " or sp == "\t" then+      j = j + 1+    else+      local buf, in_quotes = {}, false+      while j <= len do+        local c = s:sub(j, j)+        if c == "\\" then+          local k = j+          while k <= len and s:sub(k, k) == "\\" do k = k + 1 end+          local nback = k - j+          if k <= len and s:sub(k, k) == '"' then+            buf[#buf + 1] = string.rep("\\", math.floor(nback / 2))+            if nback % 2 == 1 then+              buf[#buf + 1] = '"'+            else+              in_quotes = not in_quotes+            end+            j = k + 1+          else+            buf[#buf + 1] = string.rep("\\", nback)+            j = k+          end+        elseif c == '"' then+          in_quotes = not in_quotes+          j = j + 1+        elseif not in_quotes and (c == " " or c == "\t") then+          break+        else+          buf[#buf + 1] = c+          j = j + 1+        end+      end+      argv[#argv + 1] = table.concat(buf)+    end+  end+  return argv+end  ---@class fzf-lua.CliSegment ---@field text string    -- raw (unescaped) text@@ -967,16 +1210,17 @@ M.ps_escape = function(s) return "'" .. s:gsub("'", "''") .. "'" end ---@alias fzf-lua.CliElement fzf-lua.CliSegment[]  -- Escape a list of CLI elements for the given shell, re-fusing each element's--- segments into a single string. "posix"/"cmd" reuse `shellescape` (cmd uses--- the caret/win_style variant), "powershell" uses `ps_escape`. Literal segments--- (`escape=false`) are emitted verbatim in all shells.+-- segments into a single string. Both shells reuse `shellescape` (`cmd` uses+-- the caret/win_style variant). Literal segments (`escape=false`) are emitted+-- verbatim. ---@param elements fzf-lua.CliElement[]----@param shell "posix"|"cmd"|"powershell"----@param win_style integer|string? forwarded to shellescape for "cmd"+---@param shell "posix"|"cmd"+---@param win_style integer|string|boolean? forwarded to shellescape for "cmd" ---@return string[] M.escape_cli = function(elements, shell, win_style)-  local escapefn = shell == "powershell" and M.ps_escape-    or function(s) return M.shellescape(s, shell == "cmd" and win_style or nil) end+  local escapefn = function(s)+    return M.shellescape(s, shell == "cmd" and win_style or nil)+  end   local argv = {}   for _, elem in ipairs(elements) do     local buf = {}@@ -991,9 +1235,10 @@ end -- Marker for `_fzf_cli_args` entries that carry a raw (unescaped) value -- alongside the escaped string form. `build_fzf_cli` consumes these to emit -- BOTH the legacy escaped string (for cmd/posix, byte-identical) and a raw--- CliElement segment list (used to rebuild the command for the PowerShell--- fallback when the cmd line exceeds 8191 chars). `prefix` is the literal,--- never-escaped flag text (e.g. "--bind=" or "--interactive --cmd ").+-- CliElement segment list -- `fzf.raw_fzf` rebuilds the raw argv from the+-- segments for the Windows direct spawn (`libuv.win_spawn`, no shell and no+-- escaping; shares `fzf-tmux` and the CLI launcher equally). `prefix` is the+-- literal, never-escaped flag text (e.g. "--bind=" or "--interactive --cmd "). ---@param prefix string literal flag prefix (e.g. "--bind=") ---@param value string raw (unescaped) value ---@return table marker

lua/fzf-lua/test/helpers.lua

@@ -187,7 +187,8 @@ M.new_child_neovim = function()     -- timeout+retries (5x on Windows) make the suite appear hung. Piping via     -- `cmd.exe /c "(cmd) | fzf"` uses a stdin pipe (which closes) and works     -- regardless of tty detection, so default to pipe for headless (tests, cli)-    -- on Windows. GUI sessions keep env (better exit kill + powershell fallback).+    -- on Windows. GUI sessions keep env (fzf kills its own default command on+    -- exit; `pipe_cmd` routes the command through the `win_spawn` item pump).     if M.IS_WIN() then defaults.pipe_cmd = true end     child.lua(function(...)       require("fzf-lua").setup(vim.tbl_deep_extend("keep", ..., {

lua/fzf-lua/win_spawn.lua

new file mode 100644@@ -0,0 +1,40 @@+---@diagnostic disable-next-line: deprecated+-- This file should only be loaded from the mediator headless instance,+-- spawned by `fzf.raw_fzf` via `termopen` inside a terminal buffer (TUI).+-- It executes a serialized `fzf-lua.WinSpawnSpec`: `uv.spawn` runs fzf with+-- raw argv (no shell, no escaping), stderr inherits this pty (fzf's light+-- renderer needs a console), stdout is redirected to the result file and the+-- optional item producer is pumped into fzf's stdin. The exit code becomes+-- the terminal job's exit code (see `fzf.raw_fzf`).+assert(#vim.api.nvim_list_uis() == 0)++-- path to this file+local _info = debug.getinfo(1, "S")+---@cast _info {source: string}+local __FILE__ = _info.source:gsub("^@", "")++-- add the current folder to package.path so we can 'require'+-- prepend this folder first, so our modules always get first+-- priority over some unknown random module with the same name+package.path = ("%s/?.lua;"):format(vim.fn.fnamemodify(__FILE__, ":h:h")) .. package.path++local libuv = require("fzf-lua.libuv")++local spec_path = assert(_G.arg[1], "win_spawn.lua: missing spec path")+local f = assert(io.open(spec_path, "r"), "win_spawn.lua: cannot open " .. spec_path)+local spec_str = f:read("*a") --[[@as string]]+f:close()+vim.fn.delete(spec_path)++local rc ---@type integer?+local spec = libuv.deserialize(spec_str)+---@cast spec fzf-lua.WinSpawnSpec+libuv.win_spawn(spec, {+  on_exit = function(code) rc = code end,+})++-- keep the event loop running until fzf and the producer tree exited,+-- then exit with fzf's code (`cquit` keeps the temp dir cleanup intact,+-- unlike `os.exit`)+vim.wait(2147483647, function() return rc ~= nil end)+vim.cmd.cquit({ count = rc })

scripts/cli.lua

@@ -54,12 +54,26 @@ end  _G.fzf_jobstart = function(cmd, opts)   if fn.has("win32") == 1 or fn.has("win64") == 1 then-    -- Inherit stdio like bare `fzf` (works on any terminal: conhost,-    -- Windows Terminal, tmux pane). Join argv verbatim for the `cmd.exe`-    -- launcher: the caret-escaped args from `fzf.raw_fzf` are built for-    -- raw `cmd /c` parsing and libuv quoting corrupts them ("...syntax-    -- is incorrect"). The PowerShell fallback (>8000 chars) keeps-    -- default quoting.+    -- One managed Windows route: `fzf.raw_fzf` hands us a `win_spawn` spec+    -- (raw argv, result file, optional item command); `libuv.win_spawn`+    -- runs fzf directly via `uv.spawn` -- no shell, no escaping, no+    -- PowerShell fallback, results via file descriptor and the item+    -- command piped into fzf's stdin (see `libuv.win_spawn`).+    if cmd.__win_spawn then+      FzfLua.libuv.win_spawn(cmd.__win_spawn, {+        on_exit = vim.schedule_wrap(function(rc)+          opts.on_exit(nil, rc, nil)+          if not opts.no_quit and #api.nvim_get_proc_children(fn.getpid()) == 0 then+            vim.cmd.cquit { count = rc, bang = true }+          end+        end),+      })+      return+    end+    -- Legacy `cmd.exe` launcher for callers without raw segments (direct+    -- `raw_fzf` invocations). Join argv verbatim for `cmd.exe`: the+    -- caret-escaped args from `fzf.raw_fzf` are built for raw `cmd /c`+    -- parsing and libuv quoting corrupts them ("...syntax is incorrect").     -- stderr: fzf's light renderer (`--height`) requires a console on     -- stderr (it draws the UI there and falls back to fullscreen without     -- one, see `IsLightRendererSupported` in src/tui/light_windows.go).

tests/fzf_spec.lua

@@ -7,38 +7,27 @@ local eq = assert.are.equal local same = assert.are.same  local _orig_is_windows = utils.__IS_WINDOWS+local _orig_termopen = utils.termopen  describe("Testing fzf launcher", function()   after_each(function()     utils.__IS_WINDOWS = _orig_is_windows+    utils.termopen = _orig_termopen     _G.fzf_jobstart = nil   end)    ---@param fzf_cli_args string[]   ---@param opts table-  ---@param bom_content string?   ---@return table captured   ---@return table? output   ---@return integer? rc-  local function mock_run(fzf_cli_args, opts, bom_content)+  local function mock_run(fzf_cli_args, opts)     local captured = {}     local output, rc     local old_jobstart = _G.fzf_jobstart     _G.fzf_jobstart = function(shell_cmd, job_opts)       captured.shell_cmd = shell_cmd       captured.env = job_opts.env-      if bom_content and shell_cmd[1] == "powershell.exe" then-        -- outputtmpname is ps_escaped inside the single fused -Command string-        local qpath = shell_cmd[5]:match("%-RedirectStandardOutput%s+('[^']+')")-        if qpath then-          local path = qpath:sub(2, -2):gsub("''", "'")-          local f = io.open(path, "w")-          if f then-            f:write(bom_content)-            f:close()-          end-        end-      end       -- Real jobstart calls on_exit asynchronously; mimic that so the       -- coroutine is suspended before it is resumed.       vim.schedule(function() job_opts.on_exit({}, 1, {}) end)@@ -57,160 +46,165 @@ describe("Testing fzf launcher", function()     return captured, output, rc   end -  it("uses cmd.exe below threshold on Windows", function()+  --- Same as `mock_run` but for the TUI route (no `_G.fzf_jobstart`, the+  --- `termopen` mediator is intercepted instead of being executed)+  ---@return table captured+  ---@return table? output+  ---@return integer? rc+  local function mock_run_tui(fzf_cli_args, opts)+    local captured = {}+    local output, rc+    local ok, err = pcall(function()+      utils.termopen = function(shell_cmd, job_opts)+        captured.shell_cmd = shell_cmd+        captured.env = job_opts.env+        vim.schedule(function() job_opts.on_exit({}, 1, {}) end)+        return 1+      end+      coroutine.wrap(function()+        local fzf = require("fzf-lua.fzf")+        output, rc = fzf.raw_fzf("echo foo", fzf_cli_args, opts)+      end)()+      vim.wait(2000, function() return rc ~= nil end)+    end)+    if not ok then error(err) end+    return captured, output, rc+  end++  it("uses the legacy cmd.exe launcher without raw segments", function()     utils.__IS_WINDOWS = true     local fzf_cli_args = { "--bind=" .. libuv.shellescape("ctrl-t:execute(echo hi)") }     local captured = mock_run(fzf_cli_args, {})     eq(captured.shell_cmd[1], "cmd.exe")     eq(captured.env.SHELL, "cmd.exe")+    eq(captured.env.FZF_DEFAULT_COMMAND, "echo foo || break")   end) -  it("uses powershell.exe above 8000 chars on Windows", function()+  it("builds a win_spawn spec with raw argv (no shell escaping)", function()     utils.__IS_WINDOWS = true-    local val = string.rep("a", 8100)-    local raw_bind = "ctrl-t:" .. val+    local raw_bind = [[ctrl-t:execute(echo "hi" && echo bye)]]     local fzf_cli_args = { "--bind=" .. libuv.shellescape(raw_bind) }     local segments = {       { { text = "--bind=", escape = false }, { text = raw_bind, escape = true } },     }-    local captured = mock_run(fzf_cli_args, { _cli_segments = segments })-    eq(captured.shell_cmd[1], "powershell.exe")-    eq(captured.shell_cmd[2], "-NoProfile")-    eq(captured.shell_cmd[3], "-NonInteractive")-    eq(captured.shell_cmd[4], "-Command")+    local captured = mock_run(fzf_cli_args,+      { _cli_segments = segments, cwd = "C:/some/dir" })+    -- no shell command line, just the direct spawn spec+    eq(captured.shell_cmd[1], nil)+    local spec = captured.shell_cmd.__win_spawn+    eq(spec.bin, "fzf")+    -- raw unescaped argument, uv quoting handles the rest+    eq(spec.args[1], "--bind=" .. raw_bind)+    eq(spec.cwd, "C:/some/dir")+    eq(spec.item, nil)+    eq(type(spec.outfile), "string")+    assert.is.True(#spec.outfile > 0)     eq(captured.env.SHELL, "cmd.exe")-    local fused = captured.shell_cmd[5]-    assert.is.True(fused:find("Start%-Process") ~= nil)-    assert.is.True(fused:find("%-FilePath%s+'fzf'") ~= nil)-    assert.is.True(fused:find("%-ArgumentList%s+'.-'") ~= nil)-    assert.is.True(fused:find("%-RedirectStandardOutput%s+'.-'") ~= nil)-    assert.is.True(fused:find("exit $proc%.ExitCode$") ~= nil)-    -- The argument line uses CommandLineToArgvW-style double-quote quoting.-    local expected = libuv.shellescape("--bind=" .. raw_bind, 1)-    assert.is.True(fused:find(expected, 1, true) ~= nil)+    eq(captured.env.FZF_DEFAULT_COMMAND, "echo foo || break")   end) -  it("empty --query remains a distinct argument for PowerShell", function()+  it("has no cmd.exe/PowerShell length limit for long arguments", function()     utils.__IS_WINDOWS = true-    local val = string.rep("a", 8100)-    local raw_bind = "ctrl-t:" .. val-    local fzf_cli_args = {-      "--query", libuv.shellescape(""),-      "--bind=" .. libuv.shellescape(raw_bind),+    local raw_bind = "ctrl-t:" .. string.rep("a", 8100)+    local fzf_cli_args = { "--bind=" .. libuv.shellescape(raw_bind) }+    local segments = {+      { { text = "--bind=", escape = false }, { text = raw_bind, escape = true } },     }+    local captured = mock_run(fzf_cli_args, { _cli_segments = segments })+    local spec = captured.shell_cmd.__win_spawn+    same(spec.args, { "--bind=" .. raw_bind })+  end)++  it("keeps empty arguments as distinct raw argv entries", function()+    utils.__IS_WINDOWS = true     local segments = {       { { text = "--query", escape = false } },       { { text = "", escape = true } },-      { { text = "--bind=", escape = false }, { text = raw_bind, escape = true } },     }-    local captured = mock_run(fzf_cli_args, { _cli_segments = segments })-    eq(captured.shell_cmd[1], "powershell.exe")-    local fused = captured.shell_cmd[5]-    local expected = libuv.shellescape("--query", 1) .. " " .. libuv.shellescape("", 1)-    assert.is.True(fused:find("-ArgumentList '" .. expected, 1, true) ~= nil)+    local captured = mock_run({ "--query", libuv.shellescape("") },+      { _cli_segments = segments })+    local spec = captured.shell_cmd.__win_spawn+    same(spec.args, { "--query", "" })   end) -  it("Windows-quotes arguments with embedded quotes and backslashes", function()+  it("keeps embedded quotes and backslashes raw in the spec argv", function()     utils.__IS_WINDOWS = true-    local val = string.rep("a", 8100)-    local raw_bind = "ctrl-t:" .. val     local tricky = [[foo"bar\]]-    local fzf_cli_args = {-      "--query", libuv.shellescape(tricky),-      "--bind=" .. libuv.shellescape(raw_bind),-    }     local segments = {       { { text = "--query", escape = false } },       { { text = tricky, escape = true } },-      { { text = "--bind=", escape = false }, { text = raw_bind, escape = true } },     }-    local captured = mock_run(fzf_cli_args, { _cli_segments = segments })-    eq(captured.shell_cmd[1], "powershell.exe")-    local fused = captured.shell_cmd[5]-    local expected = libuv.shellescape("--query", 1) .. " " .. libuv.shellescape(tricky, 1)-    assert.is.True(fused:find(expected, 1, true) ~= nil)+    local captured = mock_run({ "--query", libuv.shellescape(tricky) },+      { _cli_segments = segments })+    local spec = captured.shell_cmd.__win_spawn+    same(spec.args, { "--query", tricky })   end) -  it("does not wrap fzf-tmux in powershell even when over threshold", function()+  it("streams pipe_cmd items through the spec item command", function()     utils.__IS_WINDOWS = true-    local val = string.rep("a", 8100)-    local raw_bind = "ctrl-t:" .. val-    local fzf_cli_args = { "--bind=" .. libuv.shellescape(raw_bind) }     local segments = {-      { { text = "--bind=", escape = false }, { text = raw_bind, escape = true } },+      { { text = "--bind=", escape = false }, { text = "ctrl-t:execute(echo hi)", escape = true } },     }-    local captured = mock_run(fzf_cli_args, { is_fzf_tmux = true, _cli_segments = segments })-    eq(captured.shell_cmd[1], "cmd.exe")+    local captured = mock_run({ "--bind=" .. libuv.shellescape("ctrl-t:execute(echo hi)") },+      { pipe_cmd = true, _cli_segments = segments })+    local spec = captured.shell_cmd.__win_spawn+    eq(spec.item, "echo foo || break")+    eq(spec.args[1], "--bind=ctrl-t:execute(echo hi)")+    -- the item command is no longer exported via the environment+    eq(captured.env.FZF_DEFAULT_COMMAND, nil)     eq(captured.env.SHELL, "cmd.exe")   end) -  it("streams pipe_cmd items into fzf above threshold (win)", function()+  it("wires the TUI route through the win_spawn.lua mediator", function()     utils.__IS_WINDOWS = true-    local val = string.rep("a", 8100)-    local raw_bind = "ctrl-t:" .. val-    local fzf_cli_args = { "--bind=" .. libuv.shellescape(raw_bind) }+    local raw_bind = "ctrl-t:execute(echo hi)"     local segments = {       { { text = "--bind=", escape = false }, { text = raw_bind, escape = true } },     }-    local captured = mock_run(fzf_cli_args, { pipe_cmd = true, _cli_segments = segments })-    eq(captured.shell_cmd[1], "powershell.exe")-    eq(captured.env.SHELL, "cmd.exe")-    eq(captured.env.FZF_DEFAULT_COMMAND, nil)-    local fused = captured.shell_cmd[5]-    -- both processes start through ProcessStartInfo (verbatim `Arguments`),-    -- no Start-Process and no blocking wait on the item producer-    assert.is.True(fused:find("New%-Object System%.Diagnostics%.ProcessStartInfo") ~= nil)-    assert.is.False(fused:find("Start%-Process") ~= nil)-    -- item command runs through cmd.exe without a redirect; its stdout is-    -- pumped into fzf's stdin asynchronously so items stream in while fzf-    -- is already up and fzf's exit is observed while the pump still runs-    assert.is.True(fused:find(-      "$ipsi.Arguments = '/d /e:off /f:off /v:off /c (echo foo || break)'", 1, true) ~= nil)-    assert.is.True(fused:find(-      "$pump = $iproc.StandardOutput.BaseStream.CopyToAsync($fproc.StandardInput.BaseStream)", 1, true) ~= nil)-    assert.is.True(fused:find("$fproc.WaitForExit(200)", 1, true) ~= nil)-    -- the producer is killed as a whole process tree: `$iproc` is only the-    -- wrapper cmd.exe, a plain `Process.Kill()` would orphan the actual-    -- producer still holding the stdout pipe (the pump would stay blocked-    -- until its natural exit); `$iproc` is then reaped before the pump is-    -- awaited and its exit code is propagated-    assert.is.True(fused:find(-      "& taskkill.exe /PID $iproc.Id /T /F 2>$null | Out-Null; $iproc.WaitForExit()", 1, true) ~= nil)-    assert.is.False(fused:find("$iproc.Kill()", 1, true) ~= nil)-    -- fzf receives the long argument line through the verbatim Arguments string-    assert.is.True(fused:find("$fpsi.Arguments = ", 1, true) ~= nil)-    local expected = libuv.shellescape("--bind=" .. raw_bind, 1)-    assert.is.True(fused:find(expected, 1, true) ~= nil)-    -- fzf's stdout is drained into the output temp file for on_exit-    -- asynchronously (before the pump, so a large selection can't dead-lock-    -- against the still-running item producer) and the fzf exit code is-    -- propagated after both finish-    assert.is.True(fused:find("$drain = $fproc.StandardOutput.BaseStream.CopyToAsync($ofs)", 1, true) ~= nil)-    assert.is.True(fused:find("$drain.Wait(); $ofs.Close()", 1, true) ~= nil)-    assert.is.True(fused:find("exit $code", 1, true) ~= nil)+    local captured = mock_run_tui({ "--bind=" .. libuv.shellescape(raw_bind) },+      { _cli_segments = segments })+    local shell_cmd = captured.shell_cmd+    ---@cast shell_cmd string[]+    eq(shell_cmd[1], os.getenv("FZF_LUA_NVIM_BIN") or vim.v.progpath)+    eq(shell_cmd[2], "-u")+    eq(shell_cmd[3], "NONE")+    eq(shell_cmd[4], "-l")+    local prog = shell_cmd[5] or error("missing win_spawn.lua argument")+    assert.is.True(prog:find("win_spawn%.lua") ~= nil)+    -- the spec file exists and round-trips the spawn spec+    local spec_path = shell_cmd[6] or error("missing spec path argument")+    eq(vim.fn.filereadable(spec_path), 1)+    local f = io.open(spec_path, "r")+    if not f then error("cannot open " .. spec_path) end+    local payload = f:read("*a")+    f:close()+    local spec = libuv.deserialize(payload)+    eq(spec.bin, "fzf")+    same(spec.args, { "--bind=" .. raw_bind })+    eq(spec.env.SHELL, "cmd.exe")+    vim.fn.delete(spec_path)   end) -  it("uses cmd.exe for pipe_cmd below threshold", function()+  it("keeps fzf-tmux on the shell route", function()     utils.__IS_WINDOWS = true     local fzf_cli_args = { "--bind=" .. libuv.shellescape("ctrl-t:execute(echo hi)") }-    local captured = mock_run(fzf_cli_args, { pipe_cmd = true })+    local segments = {+      { { text = "--bind=", escape = false }, { text = "ctrl-t:execute(echo hi)", escape = true } },+    }+    local captured = mock_run(fzf_cli_args, { is_fzf_tmux = true, _cli_segments = segments })     eq(captured.shell_cmd[1], "cmd.exe")-    eq(captured.env.FZF_DEFAULT_COMMAND, nil)-    assert.is.True(captured.shell_cmd[7]:find("echo foo", 1, true) ~= nil)+    eq(captured.env.SHELL, "cmd.exe")+    -- result file redirect via the legacy shell route+    assert.is.True(table.concat(captured.shell_cmd, " "):find(">", 1, true) ~= nil)   end) -  it("strips UTF-8 BOM from powershell output", function()+  it("uses the legacy cmd.exe pipe for pipe_cmd without raw segments", function()     utils.__IS_WINDOWS = true-    local val = string.rep("a", 8100)-    local raw_bind = "ctrl-t:" .. val-    local fzf_cli_args = { "--bind=" .. libuv.shellescape(raw_bind) }-    local segments = {-      { { text = "--bind=", escape = false }, { text = raw_bind, escape = true } },-    }-    local captured, output = mock_run(fzf_cli_args, { _cli_segments = segments },-      "\239\187\191line1\nline2\n")-    eq(captured.shell_cmd[1], "powershell.exe")-    same(output, { "line1", "line2" })+    local captured = mock_run({}, { pipe_cmd = true })+    eq(captured.shell_cmd[1], "cmd.exe")+    eq(captured.env.FZF_DEFAULT_COMMAND, nil)+    assert.is.True(captured.shell_cmd[7]+      :find("(echo foo || break) | ", 1, true) ~= nil)   end)    it("does not emit --with-shell for skim", function()

tests/libuv_spec.lua

@@ -95,15 +95,43 @@ describe("Testing libuv module", function()     eq(libuv.shellescape([[foo\\\^^^"]], 2), [[^"foo\\\^^^^^^\^"^"]])   end) -  it("ps_escape", function()-    -- PowerShell single-quoted strings: everything literal except '' (escaped ')-    eq(libuv.ps_escape([[plain]]), [['plain']])-    eq(libuv.ps_escape([[it's]]), [['it''s']])-    eq(libuv.ps_escape([['quoted']]), [['''quoted''']])-    -- metacharacters ($, backtick, ", (), &, |) stay literal inside single quotes-    eq(libuv.ps_escape([[$(x) `" & | ; ( )]]), [['$(x) `" & | ; ( )']])-    eq(libuv.ps_escape([[]]), [['']])-    eq(libuv.ps_escape([[a b]]), [['a b']])+  it("cmd_argv round-trips shellescape (cmd style)", function()+    -- `win_style = true` forces the `cmd.exe` caret escaping (the default+    -- on Windows), also runs on posix CI+    local values = {+      "foo", "a b", "x^y", "a^^b", "p%q", "a&b", "it's", [["q"]], [[E\f]],+      "汉 é", "bang!x", "eq=a", "semi;x", "lt<x", "pipe|x", "paren(x)",+      "amp&pipe|semi;", "trailback\\", " caret^ leading", "^caret",+      "trailcaret^", [[a"b]], [[a\"b]], "two  spaces", "10%%", "a!b",+      "%TEMP%", "a(b)c", "cur|pipe", "d>e", "a<b",+      "load:+execute-silent:echo hi",+    }+    for _, v in ipairs(values) do+      same(libuv.cmd_argv(libuv.shellescape(v, true)), { v })+    end+    -- the pre-escaped user fragment pattern, e.g. test harness+    -- `"--bind=" .. shellescape("load:+execute-silent:...")`+    same(libuv.cmd_argv("--bind=" .. libuv.shellescape("load:+execute-silent:echo hi", true)),+      { "--bind=load:+execute-silent:echo hi" })+  end)++  it("cmd_argv parses cmd fragments", function()+    same(libuv.cmd_argv(""), {})+    same(libuv.cmd_argv("   "), {})+    same(libuv.cmd_argv("--no-multi --layout=reverse"), { "--no-multi", "--layout=reverse" })+    same(libuv.cmd_argv([[--header="a b" --no-multi]]), { "--header=a b", "--no-multi" })+    same(libuv.cmd_argv("--bind=ctrl-a:first --expect=ctrl-e"),+      { "--bind=ctrl-a:first", "--expect=ctrl-e" })+    -- caret escaping quotes/spaces (user args are cmd fragments)+    same(libuv.cmd_argv([[--header=^"a b^"]]), { "--header=a b" })+    same(libuv.cmd_argv([[--bind=^"load:+execute-silent:echo^ hi^"]]),+      { "--bind=load:+execute-silent:echo hi" })+    same(libuv.cmd_argv("^^"), { "^" })+    -- backslashes: literal unless collapsing before a quote (2n/2n+1 rules)+    same(libuv.cmd_argv([[C:\dir\sub]]), { [[C:\dir\sub]] })+    same(libuv.cmd_argv([[a\"b]]), { [[a"b]] })+    same(libuv.cmd_argv([[trailback\\]]), { [[trailback\\]] })+    same(libuv.cmd_argv([[\^^]]), { [[\^]] })   end)    it("escape_cli escapes value segments per shell, keeps literal segments", function()@@ -117,11 +145,6 @@ describe("Testing libuv module", function()     eq(cmd[1], "--bind=" .. libuv.shellescape("ctrl-r:reload(a b)"))     eq(cmd[2], "--query")     eq(cmd[3], libuv.shellescape("it's"))-    -- powershell: value segments via ps_escape-    local ps = libuv.escape_cli(elems, "powershell")-    eq(ps[1], "--bind='ctrl-r:reload(a b)'")-    eq(ps[2], "--query")-    eq(ps[3], "'it''s'")     -- posix: value segments via posix shellescape     local posix = libuv.escape_cli(elems, "posix")     eq(posix[1], "--bind=" .. libuv.shellescape("ctrl-r:reload(a b)"))

tests/win_spawn_spec.lua

new file mode 100644@@ -0,0 +1,194 @@+local helpers = require("fzf-lua.test.helpers")+local assert = helpers.assert++---@diagnostic disable-next-line: deprecated+local uv = vim.uv or vim.loop+local libuv = require("fzf-lua.libuv")+local utils = require("fzf-lua.utils")+local eq = assert.are.equal++local fzf_bin = vim.fn.exepath("fzf")++---@param path string+---@param content string+local function write_file(path, content)+  local f = io.open(path, "w")+  if not f then error("cannot open " .. path .. " for writing") end+  f:write(content)+  f:close()+end++---@param path string+---@return string+local function read_file(path)+  local f = io.open(path, "r")+  if not f then return "" end+  local content = f:read("*a") or ""+  f:close()+  return content+end++describe("Testing win_spawn (Windows one route)", function()+  --- Run one full `libuv.win_spawn` cycle (item producer -> pump -> result+  --- file) and return fzf's exit code and the result file contents+  ---@param item string producer command (cmd syntax)+  ---@param args string[] raw fzf argv (without the executable)+  ---@return integer? rc+  ---@return string out+  local function win_run(item, args)+    local outfile = utils.tempname()+    local rc ---@type integer?+    local ok, err = pcall(libuv.win_spawn, {+      bin = fzf_bin,+      args = args,+      item = item,+      outfile = outfile,+      env = { SHELL = "cmd.exe" },+    }, { on_exit = function(code) rc = code end })+    if not ok then error(err) end+    vim.wait(30000, function() return rc ~= nil end)+    local out = read_file(outfile)+    vim.fn.delete(outfile)+    return rc, out+  end++  it("pump preserves bytes: argv quoting, streaming and result file", function()+    helpers.SKIP_IF_NOT_WIN()+    local infile = utils.tempname()+    local lines = { "ab c", [[E"f G]], [[H\I]], "汉 é" }+    write_file(infile, table.concat(lines, "\n") .. "\n")+    local item = string.format('type "%s"', infile)+    -- filter arguments with a space (single argv token), quotes, a trailing+    -- backslash and multi-byte characters survive the uv quoting + pump+    for _, case in ipairs({+      { filter = "ab c",  match = "ab c" },+      { filter = [[E"f G]], match = [[E"f G]] },+      { filter = [[H\I]], match = [[H\I]] },+      { filter = "汉 é", match = "汉 é" },+    }) do+      local rc, out = win_run(item, { "--filter=" .. case.filter })+      eq(rc, 0)+      eq(out, case.match .. "\n")+    end+    -- empty filter: match everything, the full stream is byte-identical+    local rc, out = win_run(item, { "--filter=", "--no-sort" })+    eq(rc, 0)+    eq(out, table.concat(lines, "\n") .. "\n")+    vim.fn.delete(infile)+  end)++  it("propagates fzf's exit code when nothing matches", function()+    helpers.SKIP_IF_NOT_WIN()+    local rc, out = win_run("echo abc", { "--filter=zzz" })+    eq(rc, 1)+    eq(out, "")+  end)++  it("streams a large input (backpressure) and matches the last line", function()+    helpers.SKIP_IF_NOT_WIN()+    local infile = utils.tempname()+    local f = io.open(infile, "w")+    if not f then error("cannot open " .. infile .. " for writing") end+    local buf = {}+    for i = 0, 399999 do+      buf[#buf + 1] = ("line %06d\n"):format(i)+      if #buf >= 4096 then+        f:write(table.concat(buf))+        buf = {}+      end+    end+    f:write(table.concat(buf))+    f:close()+    -- ~4.8MB (> the 4MB high watermark): exercises the pump across many+    -- chunks with the producer paused by the write queue backpressure+    local rc, out = win_run(string.format('type "%s"', infile),+      { "--filter=line 399999" })+    eq(rc, 0)+    eq(out, "line 399999\n")+    vim.fn.delete(infile)+  end)++  it("kills the producer's process tree when fzf exits early", function()+    helpers.SKIP_IF_NOT_WIN()+    -- use a uniquely named copy of PING.EXE so the tasklist check below cannot+    -- match an unrelated `ping` running on the machine+    local exe_name = "fzflua_spawn_ping.exe"+    local tmpdir = utils.tempname() .. "_treekill"+    vim.fn.mkdir(tmpdir, "p")+    assert.is.True(vim.fn.isdirectory(tmpdir) == 1)+    local exe = tmpdir .. "/" .. exe_name+    uv.fs_copyfile(vim.fn.exepath("ping"), exe)+    assert.is.True(vim.fn.filereadable(exe) == 1)+    ---@return boolean+    local function ping_running()+      local out = vim.fn.system({ "tasklist", "/FI", "IMAGENAME eq " .. exe_name, "/NH" })+      return out:lower():find(exe_name) ~= nil+    end+    -- control: the unique binary is detected by `ping_running` while running+    local job = vim.fn.jobstart({ exe, "-n", "60", "127.0.0.1" })+    assert.is.True(job > 0)+    vim.wait(5000, ping_running)+    assert.is.True(ping_running())+    vim.fn.jobstop(job)+    vim.wait(5000, function() return not ping_running() end)+    assert.is.False(ping_running())+    -- fzf exits immediately with an invalid option while the producer is still+    -- alive (output to NUL so only the tree-kill can stop it)+    local rc = win_run(string.format('"%s" -n 60 127.0.0.1 >NUL', exe),+      { "--fzflua-bad-option" })+    eq(rc, 2)+    assert.is.False(ping_running())+    vim.fn.delete(tmpdir, "rf")+  end)++  it("runs the spec through the win_spawn.lua mediator script", function()+    helpers.SKIP_IF_NOT_WIN()+    ---@diagnostic disable-next-line: deprecated+    local _info = debug.getinfo(1, "S")+    ---@cast _info {source: string}+    local root = vim.fn.fnamemodify(_info.source:gsub("^@", ""), ":h:h")+    local script = (root .. "/lua/fzf-lua/win_spawn.lua"):gsub("\\", "/")++    ---@param item string+    ---@param filter string+    ---@return integer? rc+    ---@return string out+    local function run_mediator(item, filter)+      local outfile = utils.tempname()+      local spec_path = utils.tempname()+      write_file(spec_path, libuv.serialize({+        bin = fzf_bin,+        args = { "--filter=" .. filter },+        item = item,+        outfile = outfile,+        env = { SHELL = "cmd.exe" },+      }, true))+      local rc ---@type integer?+      local handle = uv.spawn((vim.v.progpath):gsub("\\", "/"), {+        args = { "-u", "NONE", "-l", script, (spec_path:gsub("\\", "/")) },+        stdio = { nil, nil, nil },+      }, function(code, _)+        rc = code+      end)+      assert.is.True(handle ~= nil)+      vim.wait(30000, function() return rc ~= nil end)+      local out = read_file(outfile)+      vim.fn.delete(outfile)+      -- the mediator removes the spec file after reading it+      eq(vim.fn.filereadable(spec_path), 0)+      return rc, out+    end++    local infile = utils.tempname()+    write_file(infile, "abc\n")+    local item = string.format('type "%s"', infile)+    -- fzf's exit code becomes the mediator script's exit code+    local rc, out = run_mediator(item, "abc")+    eq(rc, 0)+    eq(out, "abc\n")+    rc, out = run_mediator(item, "zzz")+    eq(rc, 1)+    eq(out, "")+    vim.fn.delete(infile)+  end)+end)