--[[ file_picker.lua Cross-platform native file picker for LuaJIT (Windows + Linux). Windows: uses the native Win32 comdlg32.dll GetOpenFileNameA API via FFI. No external dependencies required. Linux: there is no single native "file picker" API — shells out to whichever dialog tool is installed (zenity, kdialog, or yad). Install one of these: sudo apt install zenity # GNOME/GTK sudo apt install kdialog # KDE sudo apt install yad # lightweight alternative Usage: local picker = require("file_picker") local path = picker.open_file{ title = "Select a file", filter = "*.txt" } ]] local ffi = require("ffi") local M = {} -- --------------------------------------------------------------------- -- Windows implementation -- --------------------------------------------------------------------- local function win_open_file(opts) opts = opts or {} ffi.cdef[[ typedef struct { unsigned long lStructSize; void* hwndOwner; void* hInstance; const char* lpstrFilter; char* lpstrCustomFilter; unsigned long nMaxCustFilter; unsigned long nFilterIndex; char* lpstrFile; unsigned long nMaxFile; char* lpstrFileTitle; unsigned long nMaxFileTitle; const char* lpstrInitialDir; const char* lpstrTitle; unsigned long Flags; unsigned short nFileOffset; unsigned short nFileExtension; const char* lpstrDefExt; long lCustData; void* lpfnHook; const char* lpTemplateName; void* pvReserved; unsigned long dwReserved; unsigned long FlagsEx; } OPENFILENAMEA; int GetOpenFileNameA(OPENFILENAMEA* lpofn); int GetSaveFileNameA(OPENFILENAMEA* lpofn); ]] local comdlg32 = ffi.load("comdlg32") local OFN_EXPLORER = 0x00080000 local OFN_FILEMUSTEXIST = 0x00001000 local OFN_PATHMUSTEXIST = 0x00000800 local MAX_PATH = 32768 -- generous buffer for long paths local file_buf = ffi.new("char[?]", MAX_PATH) -- Filter must be a "\0"-delimited, double-"\0"-terminated string, -- e.g. "Text Files\0*.txt\0All Files\0*.*\0\0" local filter = opts.filter or "All Files\0*.*\0\0" local ofn = ffi.new("OPENFILENAMEA") ofn.lStructSize = ffi.sizeof("OPENFILENAMEA") ofn.lpstrFile = file_buf ofn.nMaxFile = MAX_PATH ofn.lpstrFilter = filter ofn.lpstrTitle = opts.title or "Select a file" ofn.Flags = OFN_EXPLORER + OFN_FILEMUSTEXIST + OFN_PATHMUSTEXIST local picker_fn = opts.save and comdlg32.GetSaveFileNameA or comdlg32.GetOpenFileNameA local ok = picker_fn(ofn) if ok ~= 0 then return ffi.string(file_buf) end return nil -- user cancelled or an error occurred end -- --------------------------------------------------------------------- -- Linux implementation -- --------------------------------------------------------------------- local function shell_escape(s) return "'" .. s:gsub("'", "'\\''") .. "'" end local function try_command(cmd) local handle = io.popen(cmd) if not handle then return nil end local result = handle:read("*l") local ok = handle:close() if ok and result and #result > 0 then return result end return nil end local function linux_open_file(opts) opts = opts or {} local title = shell_escape(opts.title or "Select a file") local save = opts.save local candidates = { save and ("zenity --file-selection --save --confirm-overwrite --title=%s 2>/dev/null"):format(title) or ("zenity --file-selection --title=%s 2>/dev/null"):format(title), save and ("kdialog --getsavefilename . --title %s 2>/dev/null"):format(title) or ("kdialog --getopenfilename . --title %s 2>/dev/null"):format(title), save and ("yad --file-selection --save --title=%s 2>/dev/null"):format(title) or ("yad --file-selection --title=%s 2>/dev/null"):format(title), } for _, cmd in ipairs(candidates) do local result = try_command(cmd) if result then return result end end return nil, "No supported dialog tool found (install zenity, kdialog, or yad)" end -- --------------------------------------------------------------------- -- Public API -- --------------------------------------------------------------------- --- Opens a native file picker dialog. -- @param opts table: { title = string, filter = string (Windows only), save = bool } -- @return path (string) or nil, err function M.open_file(opts) if ffi.os == "Windows" then return win_open_file(opts) elseif ffi.os == "Linux" then return linux_open_file(opts) else return nil, "Unsupported OS: " .. ffi.os end end return M