glava-config widget generation (incomplete)
This commit is contained in:
307
glava-config/config.lua
Normal file
307
glava-config/config.lua
Normal file
@@ -0,0 +1,307 @@
|
||||
local lfs = require "lfs"
|
||||
local mappings = require "glava-config.mappings"
|
||||
|
||||
local config = {
|
||||
Profile = { mt = {} }
|
||||
}
|
||||
|
||||
config.Profile.__index = config.Profile
|
||||
setmetatable(config.Profile, config.Profile.mt)
|
||||
|
||||
-- Split path into entries, such that `table.concat` can be used to
|
||||
-- reconstruct the path. Prepends the result with an empty string so
|
||||
-- root (absolute) paths are preserved
|
||||
local function path_split(str, sep)
|
||||
local sep, fields = sep or ":", (str:sub(1, sep:len()) == sep and {""} or {})
|
||||
local pattern = string.format("([^%s]+)", sep)
|
||||
str:gsub(pattern, function(c) fields[#fields + 1] = c end)
|
||||
return fields
|
||||
end
|
||||
|
||||
-- Concatenates paths such that duplicate path separators are removed.
|
||||
-- Can be used on non-split arguments
|
||||
local function path_concat(...)
|
||||
local ret = {}
|
||||
for _, v in ipairs({...}) do
|
||||
for _, e in ipairs(path_split(v, "/")) do
|
||||
if e ~= "" or #ret == 0 then
|
||||
ret[#ret + 1] = e
|
||||
end
|
||||
end
|
||||
end
|
||||
return table.concat(ret, "/")
|
||||
end
|
||||
|
||||
-- To parse data from GLSL configs we use some complex pattern matching.
|
||||
--
|
||||
-- Because Lua's patterns operate on a per-character basis and do not offer
|
||||
-- any read-ahead functionality, we use a pattern 'replacement' functionality
|
||||
-- such that the match of an input pattern is passed to a function to produce
|
||||
-- an output pattern.
|
||||
--
|
||||
-- This effectively means we have some fairly powerful parsing which allows us
|
||||
-- to handle things like quoted strings with escaped characters.
|
||||
local function none(...) return ... end
|
||||
local MATCH_ENTRY_PATTERN = "^%s*%#(%a+)%s+(%a+)"
|
||||
local MATCH_DATA_PREFIX = "^%s*%#%a+%s+%a+"
|
||||
local MATCH_TYPES = {
|
||||
["float"] = {
|
||||
pattern = "(%d+.?%d*)",
|
||||
cast = tonumber,
|
||||
serialize = tostring
|
||||
},
|
||||
["int"] = {
|
||||
pattern = "(%d+)",
|
||||
cast = tonumber,
|
||||
serialize = function(x) tostring(math.floor(x)) end
|
||||
},
|
||||
["string"] = {
|
||||
pattern = "(.+)",
|
||||
-- Strip away string quotation and escape syntax
|
||||
cast = function(match)
|
||||
local ret = {}
|
||||
local escaped = false
|
||||
for c in match:gmatch(".") do
|
||||
if c == "\"" then
|
||||
if escaped then ret[#ret + 1] = c end
|
||||
elseif c ~= "\\" then ret[#ret + 1] = c end
|
||||
if c == "\\" then
|
||||
if escaped then ret[#ret + 1] = c end
|
||||
escaped = not escaped
|
||||
else escaped = false end
|
||||
end
|
||||
return table.concat(ret, "")
|
||||
end,
|
||||
-- Read-ahead function to generate a fixed-width pattern
|
||||
-- to match the next (possibly quoted) string
|
||||
transform = function(match)
|
||||
local quoted = false
|
||||
local start = true
|
||||
local escaped = false
|
||||
local count = 0
|
||||
local skip = 0
|
||||
for c in match:gmatch(".") do
|
||||
count = count + 1
|
||||
if c == "\"" then
|
||||
if start then
|
||||
start = false
|
||||
quoted = true
|
||||
elseif not escaped then
|
||||
if quoted then
|
||||
-- End-quote; end of string
|
||||
break
|
||||
else
|
||||
-- Formatting error: non-escaped quote after string start: `foo"bar`
|
||||
-- We attempt to resolve this by halting parsing and skipping the
|
||||
-- out-of-context quotation
|
||||
count = count - 1
|
||||
skip = skip + 1
|
||||
break
|
||||
end
|
||||
end
|
||||
elseif c == " " then
|
||||
if not start and not quoted then
|
||||
-- Un-escaped space; end of string
|
||||
-- skip the space itself
|
||||
count = count - 1
|
||||
break
|
||||
end
|
||||
else start = false end
|
||||
if c == "\\" then
|
||||
escaped = not escaped
|
||||
else escaped = false end
|
||||
end
|
||||
-- Strings without an ending quote will simply take up the remainder of
|
||||
-- the request, causing the following arguments to be overwritten. This
|
||||
-- is intended to ensure we can save valid options after stripping out
|
||||
-- the errornous quotes and using defaults for the subsequent arguments.
|
||||
local ret = { "(" }
|
||||
for t = 1, count do
|
||||
ret[1 + t] = "."
|
||||
end
|
||||
ret[2 + count] = ")"
|
||||
for t = 1, skip do
|
||||
ret[2 + count + t] = "."
|
||||
end
|
||||
return table.concat(ret, "")
|
||||
end,
|
||||
serialize = function(x)
|
||||
return string.format("\"%s\"", x)
|
||||
end
|
||||
}
|
||||
}
|
||||
|
||||
config.path_concat = path_concat
|
||||
config.path_split = path_split
|
||||
|
||||
local function create_p(parts, mode, silent)
|
||||
local function errfmt(err)
|
||||
return string.format("Failed to create '%s' in '%s': %s",
|
||||
path_concat(parts, "/"), path_concat(arr, "/"), err)
|
||||
end
|
||||
for i, v in ipairs(arr) do
|
||||
parts[#parts + 1] = v
|
||||
local failret = false
|
||||
if silent then failret = #parts == #arr end
|
||||
local path = path_concat(parts, "/")
|
||||
local m = (i == #arr and mode or "directory")
|
||||
local attr, err = lfs.attributes(path, "mode")
|
||||
if attr == nil then
|
||||
({
|
||||
file = function()
|
||||
local ret, err = lfs.touch(path)
|
||||
if ret ~= true then return false, errfmt(err) end
|
||||
end,
|
||||
directory = function()
|
||||
local ret, err = lfs.mkdir(path)
|
||||
if ret ~= true then return false, errfmt(err) end
|
||||
end,
|
||||
})[m]()
|
||||
elseif attr ~= m then
|
||||
if not (silent and #parts == #arr) then
|
||||
return false, string.format("'%s' is not a %s", path, m)
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function create_p(path, ...) create_pf(path_split(path, "/"), ...) end
|
||||
local function unwrap(ret, err)
|
||||
if ret == nil or ret == false then
|
||||
glava.fail(err)
|
||||
else return ret end
|
||||
end
|
||||
|
||||
function config.Profile:__call(args)
|
||||
local self = { name = args.name }
|
||||
self:rebuild()
|
||||
return setmetatable(self, config.Profile)
|
||||
end
|
||||
|
||||
function config.Profile:rename(new)
|
||||
|
||||
end
|
||||
|
||||
function config.Profile:rebuild()
|
||||
self.store = {}
|
||||
self.path = path_concat(glava.config_path, "profiles", self.name)
|
||||
unwrap(create_p(self.path, "directory", true))
|
||||
local unbuilt = {}
|
||||
for k, _ in pairs(mappings) do
|
||||
unbuilt[k] = true
|
||||
end
|
||||
for file in lfs.dir(self.path) do
|
||||
if file ~= "." and file ~= ".." and mappings[file] ~= nil then
|
||||
self:rebuild_file(file, path_concat(path, file))
|
||||
unbuilt[file] = nil
|
||||
end
|
||||
end
|
||||
for file, _ in pairs(unbuilt) do
|
||||
self:rebuild_file(file, path_concat(path, file), true)
|
||||
end
|
||||
end
|
||||
|
||||
function config.Profile:rebuild_file(file, path, phony)
|
||||
local fstore = {}
|
||||
local fmap = mappings[file]
|
||||
self.store[file] = fstore
|
||||
|
||||
for k, _ in pairs(fmap) do
|
||||
if type(k) == "string" and k ~= "name" then
|
||||
unbuilt[k] = true
|
||||
end
|
||||
end
|
||||
|
||||
function parse_line(line, idx, key, default)
|
||||
local map = fmap[key]
|
||||
if map == nil then return end
|
||||
local tt = type(map.field_type) == "table" and map.field_type or { map.field_type }
|
||||
local _,e = string.find(line, MATCH_DATA_PREFIX)
|
||||
local at = string.sub(line, 1, e)
|
||||
if default == nil or fstore[key] == nil then
|
||||
fstore[key] = {}
|
||||
end
|
||||
if default == nil then fstore[key].line = idx end
|
||||
for t, v in ipairs(tt) do
|
||||
local r, i, match = string.find(at, "%s*" .. MATCH_TYPES[v].pattern)
|
||||
if r ~= nil then
|
||||
-- Handle read-ahead pattern transforms
|
||||
if MATCH_TYPES[v].transform ~= nil then
|
||||
_, i, match = string.find(at, "%s*" .. MATCH_TYPES[v].transform(match))
|
||||
end
|
||||
if default == nil or fstore[key][t] == nil then
|
||||
fstore[key][t] = MATCH_TYPES[v].cast(match)
|
||||
end
|
||||
at = string.sub(at, 1, i)
|
||||
else break end
|
||||
end
|
||||
end
|
||||
|
||||
local idx = 1
|
||||
if phony ~= true then
|
||||
for line in io.lines(path) do
|
||||
local mtype, arg = string.match(line, MATCH_ENTRY_PATTERN)
|
||||
if mtype ~= nil then
|
||||
parse_line(line, idx, string.format("%s:%s", mtype, arg))
|
||||
end
|
||||
idx = idx + 1
|
||||
end
|
||||
end
|
||||
|
||||
idx = 1
|
||||
for line in io.lines(path_concat(glava.system_shader_path, file)) do
|
||||
local mtype, arg = string.match(line, MATCH_ENTRY_PATTERN)
|
||||
if mtype ~= nil then
|
||||
parse_line(line, idx, string.format("%s:%s", mtype, arg), true)
|
||||
end
|
||||
idx = idx + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Sync all
|
||||
function config.Profile:sync()
|
||||
for k, v in pairs(self.store) do self:sync_file(k) end
|
||||
end
|
||||
|
||||
-- Sync filename relative to profile root
|
||||
function config.Profile:sync_file(fname)
|
||||
local fstore = self.store[fname]
|
||||
local fmap = mappings[file]
|
||||
local fpath = path_concat(self.path, fname)
|
||||
local buf = {}
|
||||
local extra = {}
|
||||
local idx = 1
|
||||
for k, v in fstore do
|
||||
local parts = { string.format("#%s", string.gsub(k, ":", " ")) }
|
||||
local field = fmap[k].field_type
|
||||
for i, e in ipairs(type(field) == "table" and field or { field }) do
|
||||
parts[#parts + 1] = MATCH_TYPES[e].serialize(v[i])
|
||||
end
|
||||
local serialized = table.concat(parts, " ")
|
||||
if v.line then buf[line] = serialized
|
||||
else extra[#extra + 1] = serialized end
|
||||
end
|
||||
if lfs.attributes(fpath, "mode") == "file" then
|
||||
for line in io.lines(path) do
|
||||
if not buf[idx] then
|
||||
buf[idx] = line
|
||||
end
|
||||
idx = idx + 1
|
||||
end
|
||||
for _, v in ipairs(extra) do
|
||||
buf[#buf + 1] = v
|
||||
end
|
||||
end
|
||||
local handle, err = io.open(fpath, "w+")
|
||||
if handle then
|
||||
handle:write(table.concat(buf, "\n"))
|
||||
handle:close()
|
||||
else
|
||||
glava.fail(string.format("Could not open file handle to \"%s\": %s", handle, err))
|
||||
end
|
||||
end
|
||||
|
||||
return config
|
||||
Reference in New Issue
Block a user