151 lines
4.7 KiB
Lua
151 lines
4.7 KiB
Lua
--[[
|
|
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 |