feat: more cosmetics

This commit is contained in:
2026-09-05 10:16:22 -07:00
parent ebb8325e98
commit 63f36816ef
6 changed files with 171 additions and 22 deletions
+151
View File
@@ -0,0 +1,151 @@
--[[
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