261020.24 · a6323f0c0f46222be3db60c1c691fd94b0fd6b97
fix(cli): detect the tty width on Windows/ConPTY (flex preview flip)
The CLI (`nvim -l scripts/cli.lua`, e.g. the pwsh profile's Ctrl+P/K binds) always showed the flex preview in the vertical position on Windows: `_G.fzf_tty_get_width` probed only `uv.new_tty(0)`, whose `get_winsize()` returns nil under ConPTY (psmux, Windows Terminal) - a redirected stdin tty reports no size - and the fallbacks (`$COLUMNS` unset, `vim.o.columns` = 80 in a UI-less process) pinned the layout below `flip_columns` forever, including on resize (SIGWINCH recomputes the same way).
Measured in a 226x64 psmux pane (`nvim -l`):
o.columns=80, COLUMNS=nil
new_tty(0):get_winsize() -> nil
new_tty(1):get_winsize() -> 226 (stdout/stderr tty reports the real size)
- move the detection to `utils.fzf_tty_get_width` (the CLI keeps
assigning the `_G.fzf_tty_get_width` hook) and probe tty handles in
`1, 2, 0` order, keeping the `$COLUMNS`/`vim.o.columns` fallbacks
(stdout is frequently piped in the shell-widget flow: stderr/stdin
cover that)
- posix branch unchanged (`ioctl(TIOCGWINSZ)` on stdin)
Verified end-to-end in a detached psmux pane (226x64): before the fix the CLI picker opened with the preview below; after, it opens on the right and follows resize 226->120 (down) ->226 (right). Unit test stubs `uv.new_tty` for the fd order (stdout preferred, stdin fallback kept).
Verification: full nightly 316/0 fails/19 notes; full stable 316/0/19; focused {utils} 15/0; emmylua_check clean.
reading declarations
Changes to tests/utils_spec.lua. All files
@@ -84,6 +84,114 @@ describe("Testing utils module", function() eq(utils.has({ __FZF_VERSION = { 2, 5, 5 } }, "fzf", "2.5.6"), false) end) + it("fzf_tty_get_width probe order (ConPTY stdout before stdin)", function()+ helpers.SKIP_IF_NOT_WIN()+ local libuv = require("fzf-lua.libuv")+ local orig_console_width = libuv.win_console_width+ local orig = {}+ local sizes = {} ---@type table<integer, integer|false>+ ---@return function restore+ local function stub_new_tty()+ ---@diagnostic disable-next-line: deprecated+ local t = vim.uv or vim.loop+ if t and not orig[t] then+ orig[t] = t.new_tty+ t.new_tty = function(fd, _)+ if sizes[fd] == false then error("UV_ENOTTY", 0) end+ return {+ get_winsize = function() return sizes[fd] end,+ close = function() end,+ }+ end+ end+ return function()+ for tt, o in pairs(orig) do tt.new_tty = o end+ end+ end+ local restore = stub_new_tty()+ local ok, err = pcall(function()+ -- stdin tty reports no size under ConPTY (psmux): stdout must win+ sizes = { [1] = 226 }+ eq(utils.fzf_tty_get_width(), 226)+ -- stdout/stderr unusable, stdin is the console (previous behavior)+ sizes = { [0] = 120 }+ eq(utils.fzf_tty_get_width(), 120)+ -- all std handles redirected (pwsh PSReadLine key handlers):+ -- `uv.new_tty` fails on both pipes, the console device is queried+ sizes = { [1] = false, [2] = false, [0] = false }+ libuv.win_console_width = function() return 172 end+ eq(utils.fzf_tty_get_width(), 172)+ -- console unavailable: `$COLUMNS` is the last resort before o.columns+ libuv.win_console_width = function() return nil end+ local prev = vim.env.COLUMNS+ vim.env.COLUMNS = "222"+ eq(utils.fzf_tty_get_width(), 222)+ vim.env.COLUMNS = prev+ end)+ libuv.win_console_width = orig_console_width+ restore()+ if not ok then error(err) end+ end)++ it("fzf_tty_get_width ioctl request per platform", function()+ eq(utils._tty_ioctl_request("Linux"), 0x5413)+ eq(utils._tty_ioctl_request("OSX"), 0x40087468)+ eq(utils._tty_ioctl_request("BSD"), 0x40087468)+ eq(utils._tty_ioctl_request("Windows"), nil)+ eq(utils._tty_ioctl_request("POSIX"), nil)+ end)++ it("fzf_tty_get_width fallbacks", function()+ local libuv = require("fzf-lua.libuv")+ local orig_posix = utils._posix_tty_width+ local orig_console = libuv.win_console_width+ ---@diagnostic disable-next-line: deprecated+ local tty = vim.uv or vim.loop+ local orig_new_tty = tty.new_tty+ if helpers.IS_WIN() then+ -- force the std handle probes to fail so the stubs below are used+ tty.new_tty = function() return nil end+ end+ local prev_columns = vim.env.COLUMNS+ local ok, err = pcall(function()+ -- a successful probe wins+ utils._posix_tty_width = function() return 172 end+ libuv.win_console_width = function() return 172 end+ eq(utils.fzf_tty_get_width(), 172)+ -- failed probes fall through to `$COLUMNS`+ utils._posix_tty_width = function() return nil end+ libuv.win_console_width = function() return nil end+ vim.env.COLUMNS = "222"+ eq(utils.fzf_tty_get_width(), 222)+ -- then to `o.columns`+ vim.env.COLUMNS = nil+ eq(utils.fzf_tty_get_width(), vim.o.columns)+ end)+ utils._posix_tty_width = orig_posix+ libuv.win_console_width = orig_console+ tty.new_tty = orig_new_tty+ vim.env.COLUMNS = prev_columns+ if not ok then error(err) end+ end)++ it("fzf_tty_get_width without ffi (PUC Lua)", function()+ helpers.SKIP_IF_WIN()+ local saved_preload = package.preload.ffi+ local saved_loaded = package.loaded.ffi+ package.preload.ffi = nil+ package.loaded.ffi = nil+ local prev_columns = vim.env.COLUMNS+ local ok, err = pcall(function()+ vim.env.COLUMNS = "222"+ -- must not raise and must fall through to `$COLUMNS`+ eq(utils.fzf_tty_get_width(), 222)+ end)+ package.preload.ffi = saved_preload+ package.loaded.ffi = saved_loaded+ vim.env.COLUMNS = prev_columns+ if not ok then error(err) end+ end)+ it("setmetatable with gc", function() local gc_called = nil local _obj = utils.setmetatable({}, { __gc = function() gc_called = true end })