It is reported that the built-in lua function os.remove(path) does not removes empty directories on windows. To fix this a system.rmdir function is introduced that calls a native win32 function. Also common.rm(path, recursively) was added which wraps system.rmdir() to easily delete an entire folder with all its contents.
373 lines
8.6 KiB
Lua
373 lines
8.6 KiB
Lua
local common = {}
|
|
|
|
|
|
function common.is_utf8_cont(char)
|
|
local byte = char:byte()
|
|
return byte >= 0x80 and byte < 0xc0
|
|
end
|
|
|
|
|
|
function common.utf8_chars(text)
|
|
return text:gmatch("[\0-\x7f\xc2-\xf4][\x80-\xbf]*")
|
|
end
|
|
|
|
|
|
function common.clamp(n, lo, hi)
|
|
return math.max(math.min(n, hi), lo)
|
|
end
|
|
|
|
|
|
function common.round(n)
|
|
return n >= 0 and math.floor(n + 0.5) or math.ceil(n - 0.5)
|
|
end
|
|
|
|
|
|
function common.find_index(tbl, prop)
|
|
for i, o in ipairs(tbl) do
|
|
if o[prop] then return i end
|
|
end
|
|
end
|
|
|
|
|
|
function common.lerp(a, b, t)
|
|
if type(a) ~= "table" then
|
|
return a + (b - a) * t
|
|
end
|
|
local res = {}
|
|
for k, v in pairs(b) do
|
|
res[k] = common.lerp(a[k], v, t)
|
|
end
|
|
return res
|
|
end
|
|
|
|
|
|
function common.color(str)
|
|
local r, g, b, a = str:match("#(%x%x)(%x%x)(%x%x)")
|
|
if r then
|
|
r = tonumber(r, 16)
|
|
g = tonumber(g, 16)
|
|
b = tonumber(b, 16)
|
|
a = 1
|
|
elseif str:match("rgba?%s*%([%d%s%.,]+%)") then
|
|
local f = str:gmatch("[%d.]+")
|
|
r = (f() or 0)
|
|
g = (f() or 0)
|
|
b = (f() or 0)
|
|
a = f() or 1
|
|
else
|
|
error(string.format("bad color string '%s'", str))
|
|
end
|
|
return r, g, b, a * 0xff
|
|
end
|
|
|
|
|
|
function common.splice(t, at, remove, insert)
|
|
insert = insert or {}
|
|
local offset = #insert - remove
|
|
local old_len = #t
|
|
if offset < 0 then
|
|
for i = at - offset, old_len - offset do
|
|
t[i + offset] = t[i]
|
|
end
|
|
elseif offset > 0 then
|
|
for i = old_len, at, -1 do
|
|
t[i + offset] = t[i]
|
|
end
|
|
end
|
|
for i, item in ipairs(insert) do
|
|
t[at + i - 1] = item
|
|
end
|
|
end
|
|
|
|
|
|
|
|
local function compare_score(a, b)
|
|
return a.score > b.score
|
|
end
|
|
|
|
local function fuzzy_match_items(items, needle, files)
|
|
local res = {}
|
|
for _, item in ipairs(items) do
|
|
local score = system.fuzzy_match(tostring(item), needle, files)
|
|
if score then
|
|
table.insert(res, { text = item, score = score })
|
|
end
|
|
end
|
|
table.sort(res, compare_score)
|
|
for i, item in ipairs(res) do
|
|
res[i] = item.text
|
|
end
|
|
return res
|
|
end
|
|
|
|
|
|
function common.fuzzy_match(haystack, needle, files)
|
|
if type(haystack) == "table" then
|
|
return fuzzy_match_items(haystack, needle, files)
|
|
end
|
|
return system.fuzzy_match(haystack, needle, files)
|
|
end
|
|
|
|
|
|
function common.fuzzy_match_with_recents(haystack, recents, needle)
|
|
if needle == "" then
|
|
local recents_ext = {}
|
|
for i = 2, #recents do
|
|
table.insert(recents_ext, recents[i])
|
|
end
|
|
table.insert(recents_ext, recents[1])
|
|
local others = common.fuzzy_match(haystack, "", true)
|
|
for i = 1, #others do
|
|
table.insert(recents_ext, others[i])
|
|
end
|
|
return recents_ext
|
|
else
|
|
return fuzzy_match_items(haystack, needle, true)
|
|
end
|
|
end
|
|
|
|
|
|
function common.path_suggest(text)
|
|
local path, name = text:match("^(.-)([^/\\]*)$")
|
|
local files = system.list_dir(path == "" and "." or path) or {}
|
|
local res = {}
|
|
for _, file in ipairs(files) do
|
|
file = path .. file
|
|
local info = system.get_file_info(file)
|
|
if info then
|
|
if info.type == "dir" then
|
|
file = file .. PATHSEP
|
|
end
|
|
if file:lower():find(text:lower(), nil, true) == 1 then
|
|
table.insert(res, file)
|
|
end
|
|
end
|
|
end
|
|
return res
|
|
end
|
|
|
|
|
|
function common.dir_path_suggest(text)
|
|
local path, name = text:match("^(.-)([^/\\]*)$")
|
|
local files = system.list_dir(path == "" and "." or path) or {}
|
|
local res = {}
|
|
for _, file in ipairs(files) do
|
|
file = path .. file
|
|
local info = system.get_file_info(file)
|
|
if info and info.type == "dir" and file:lower():find(text:lower(), nil, true) == 1 then
|
|
table.insert(res, file)
|
|
end
|
|
end
|
|
return res
|
|
end
|
|
|
|
|
|
function common.dir_list_suggest(text, dir_list)
|
|
local path, name = text:match("^(.-)([^/\\]*)$")
|
|
local res = {}
|
|
for _, dir_path in ipairs(dir_list) do
|
|
if dir_path:lower():find(text:lower(), nil, true) == 1 then
|
|
table.insert(res, dir_path)
|
|
end
|
|
end
|
|
return res
|
|
end
|
|
|
|
|
|
function common.match_pattern(text, pattern, ...)
|
|
if type(pattern) == "string" then
|
|
return text:find(pattern, ...)
|
|
end
|
|
for _, p in ipairs(pattern) do
|
|
local s, e = common.match_pattern(text, p, ...)
|
|
if s then return s, e end
|
|
end
|
|
return false
|
|
end
|
|
|
|
|
|
function common.draw_text(font, color, text, align, x,y,w,h)
|
|
local tw, th = font:get_width(text), font:get_height(text)
|
|
if align == "center" then
|
|
x = x + (w - tw) / 2
|
|
elseif align == "right" then
|
|
x = x + (w - tw)
|
|
end
|
|
y = common.round(y + (h - th) / 2)
|
|
return renderer.draw_text(font, text, x, y, color), y + th
|
|
end
|
|
|
|
|
|
function common.bench(name, fn, ...)
|
|
local start = system.get_time()
|
|
local res = fn(...)
|
|
local t = system.get_time() - start
|
|
local ms = t * 1000
|
|
local per = (t / (1 / 60)) * 100
|
|
print(string.format("*** %-16s : %8.3fms %6.2f%%", name, ms, per))
|
|
return res
|
|
end
|
|
|
|
|
|
function common.serialize(val)
|
|
if type(val) == "string" then
|
|
return string.format("%q", val)
|
|
elseif type(val) == "table" then
|
|
local t = {}
|
|
for k, v in pairs(val) do
|
|
table.insert(t, "[" .. common.serialize(k) .. "]=" .. common.serialize(v))
|
|
end
|
|
return "{" .. table.concat(t, ",") .. "}"
|
|
end
|
|
return tostring(val)
|
|
end
|
|
|
|
|
|
function common.basename(path)
|
|
-- a path should never end by / or \ except if it is '/' (unix root) or
|
|
-- 'X:\' (windows drive)
|
|
return path:match("[^\\/]+$") or path
|
|
end
|
|
|
|
|
|
function common.home_encode(text)
|
|
if HOME and string.find(text, HOME, 1, true) == 1 then
|
|
local dir_pos = #HOME + 1
|
|
-- ensure we don't replace if the text is just "$HOME" or "$HOME/" so
|
|
-- it must have a "/" following the $HOME and some characters following.
|
|
if string.find(text, PATHSEP, dir_pos, true) == dir_pos and #text > dir_pos then
|
|
return "~" .. text:sub(dir_pos)
|
|
end
|
|
end
|
|
return text
|
|
end
|
|
|
|
|
|
function common.home_encode_list(paths)
|
|
local t = {}
|
|
for i = 1, #paths do
|
|
t[i] = common.home_encode(paths[i])
|
|
end
|
|
return t
|
|
end
|
|
|
|
|
|
function common.home_expand(text)
|
|
return HOME and text:gsub("^~", HOME) or text
|
|
end
|
|
|
|
|
|
function common.normalize_path(filename)
|
|
if filename and PATHSEP == '\\' then
|
|
filename = filename:gsub('[/\\]', '\\')
|
|
local drive, rem = filename:match('^([a-zA-Z])(:.*)')
|
|
return drive and drive:upper() .. rem or filename
|
|
end
|
|
return filename
|
|
end
|
|
|
|
|
|
local function split_on_slash(s, sep_pattern)
|
|
local t = {}
|
|
for fragment in string.gmatch(s, "([^/\\]+)") do
|
|
t[#t + 1] = fragment
|
|
end
|
|
return t
|
|
end
|
|
|
|
|
|
function common.path_belongs_to(filename, path)
|
|
return filename and string.find(filename, path .. PATHSEP, 1, true) == 1
|
|
end
|
|
|
|
|
|
function common.relative_path(ref_dir, dir)
|
|
local ref_ls = split_on_slash(ref_dir)
|
|
local dir_ls = split_on_slash(dir)
|
|
local i = 1
|
|
while i <= #ref_ls do
|
|
if dir_ls[i] ~= ref_ls[i] then
|
|
break
|
|
end
|
|
i = i + 1
|
|
end
|
|
local ups = ""
|
|
for k = i, #ref_ls do
|
|
ups = ups .. ".." .. PATHSEP
|
|
end
|
|
local rel_path = ups .. table.concat(dir_ls, PATHSEP, i)
|
|
return rel_path ~= "" and rel_path or "."
|
|
end
|
|
|
|
|
|
function common.mkdirp(path)
|
|
local stat = system.get_file_info(path)
|
|
if stat and stat.type then
|
|
return false, "path exists", path
|
|
end
|
|
local subdirs = {}
|
|
while path and path ~= "" do
|
|
local success_mkdir = system.mkdir(path)
|
|
if success_mkdir then break end
|
|
local updir, basedir = path:match("(.*)[/\\](.+)$")
|
|
table.insert(subdirs, 1, basedir or path)
|
|
path = updir
|
|
end
|
|
for _, dirname in ipairs(subdirs) do
|
|
path = path and path .. PATHSEP .. dirname or dirname
|
|
if not system.mkdir(path) then
|
|
return false, "cannot create directory", path
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
function common.rm(path, recursively)
|
|
local stat = system.get_file_info(path)
|
|
if not stat or (stat.type ~= "file" and stat.type ~= "dir") then
|
|
return false, "invalid path given", path
|
|
end
|
|
|
|
if stat.type == "file" then
|
|
local removed, error = os.remove(path)
|
|
if not removed then
|
|
return false, error, path
|
|
end
|
|
else
|
|
local contents = system.list_dir(path)
|
|
if #contents > 0 and not recursively then
|
|
return false, "directory is not empty", path
|
|
end
|
|
|
|
for _, item in pairs(contents) do
|
|
local item_path = path .. PATHSEP .. item
|
|
local item_stat = system.get_file_info(item_path)
|
|
|
|
if not item_stat then
|
|
return false, "invalid file encountered", item_path
|
|
end
|
|
|
|
if item_stat.type == "dir" then
|
|
local deleted, error, ipath = common.rm(item_path, recursively)
|
|
if not deleted then
|
|
return false, error, ipath
|
|
end
|
|
elseif item_stat.type == "file" then
|
|
local removed, error = os.remove(item_path)
|
|
if not removed then
|
|
return false, error, item_path
|
|
end
|
|
end
|
|
end
|
|
|
|
local removed, error = system.rmdir(path)
|
|
if not removed then
|
|
return false, error, path
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
return common
|