* feat: add keybind cheatsheet * docs: refresh keybind cheatsheet preview * docs: use official generated keybind thumbnail * feat(keybind-cheatsheet): preload cached snapshots Move parser and cache ownership into an event-driven data service so the panel opens from a prepared last-known-good snapshot. Document the plugin's inspiration and independent v5 implementation.
221 lines
7.0 KiB
Luau
221 lines
7.0 KiB
Luau
--!nonstrict
|
|
-- Minimal Noctalia host used by the standalone Luau parser tests.
|
|
|
|
local files = {}
|
|
local stateValues = {}
|
|
local stateWatchers = {}
|
|
local fileExistsOverrides = {}
|
|
local configValues = {}
|
|
local availableCommands = {}
|
|
local asyncRequests = {}
|
|
local renderCount = 0
|
|
local renderedTree = nil
|
|
local readCounts = {}
|
|
local writeCounts = {}
|
|
local wantsSecondTicks = false
|
|
local encodedValues = {}
|
|
local encodedIndex = 0
|
|
local errorNotifications = 0
|
|
local updateInterval = nil
|
|
|
|
local function copy(value)
|
|
if type(value) ~= "table" then return value end
|
|
local result = {}
|
|
for key, item in pairs(value) do result[copy(key)] = copy(item) end
|
|
return result
|
|
end
|
|
|
|
local function normalize(path)
|
|
local absolute = path:sub(1, 1) == "/"
|
|
local parts = {}
|
|
for part in path:gmatch("[^/]+") do
|
|
if part == ".." then
|
|
if #parts > 0 then table.remove(parts) end
|
|
elseif part ~= "." and part ~= "" then
|
|
table.insert(parts, part)
|
|
end
|
|
end
|
|
return (absolute and "/" or "") .. table.concat(parts, "/")
|
|
end
|
|
|
|
local function directory(path)
|
|
local prefix = path:gsub("/+$", "") .. "/"
|
|
local names = {}
|
|
for file in pairs(files) do
|
|
if file:sub(1, #prefix) == prefix then
|
|
local rest = file:sub(#prefix + 1)
|
|
local name = rest:match("^([^/]+)")
|
|
if name ~= nil then names[name] = true end
|
|
end
|
|
end
|
|
local result = {}
|
|
for name in pairs(names) do table.insert(result, name) end
|
|
table.sort(result)
|
|
return result
|
|
end
|
|
|
|
local function translation(key, substitutions)
|
|
local value = key
|
|
for name, replacement in pairs(substitutions or {}) do
|
|
value = value:gsub("{" .. name .. "}", tostring(replacement))
|
|
end
|
|
return value
|
|
end
|
|
|
|
local noctalia = {
|
|
tr = translation,
|
|
getenv = function(name) return name == "HOME" and "/home/test" or nil end,
|
|
expandPath = function(path) return normalize(path:gsub("^~", "/home/test")) end,
|
|
fileExists = function(path)
|
|
path = normalize(path)
|
|
if fileExistsOverrides[path] ~= nil then return fileExistsOverrides[path] end
|
|
if files[path] ~= nil then return true end
|
|
return #directory(path) > 0
|
|
end,
|
|
fileInfo = function(path)
|
|
path = normalize(path)
|
|
if files[path] ~= nil then return { size = #files[path], mtime = 0, isDir = false } end
|
|
if #directory(path) > 0 then return { size = 0, mtime = 0, isDir = true } end
|
|
return nil, "missing"
|
|
end,
|
|
listDir = function(path)
|
|
local values = directory(normalize(path))
|
|
if #values > 0 then return values, nil end
|
|
return nil, "missing"
|
|
end,
|
|
readFile = function(path)
|
|
path = normalize(path)
|
|
readCounts[path] = (readCounts[path] or 0) + 1
|
|
local content = files[path]
|
|
if content ~= nil then return content, nil end
|
|
return nil, "missing"
|
|
end,
|
|
writeFile = function(path, content)
|
|
path = normalize(path)
|
|
files[path] = content
|
|
writeCounts[path] = (writeCounts[path] or 0) + 1
|
|
return true
|
|
end,
|
|
renameFile = function(from, to)
|
|
from = normalize(from)
|
|
to = normalize(to)
|
|
if files[from] == nil then return false, "missing" end
|
|
files[to] = files[from]
|
|
files[from] = nil
|
|
return true
|
|
end,
|
|
removeFile = function(path)
|
|
files[normalize(path)] = nil
|
|
return true
|
|
end,
|
|
pluginDir = function() return "/plugin" end,
|
|
pluginDataDir = function() return "/data" end,
|
|
commandExists = function(name) return availableCommands[name] == true end,
|
|
getConfig = function(key) return configValues[key] end,
|
|
outputs = function() return {} end,
|
|
focusedOutputName = function() return nil end,
|
|
clipboardText = function() return nil end,
|
|
openColorPicker = function() return false end,
|
|
notify = function() end,
|
|
notifyError = function() errorNotifications += 1 end,
|
|
log = function() end,
|
|
togglePanel = function() end,
|
|
setUpdateInterval = function(value) updateInterval = value end,
|
|
runAsync = function(command, callback, timeoutMs)
|
|
table.insert(asyncRequests, { command = command, callback = callback, timeoutMs = timeoutMs, completed = false })
|
|
return true
|
|
end,
|
|
string = { trim = function(value) return (value:gsub("^%s+", ""):gsub("%s+$", "")) end },
|
|
json = {
|
|
decode = function(raw)
|
|
local value = encodedValues[raw]
|
|
if value == nil then return nil, "invalid mock JSON" end
|
|
return copy(value), nil
|
|
end,
|
|
encode = function(value)
|
|
encodedIndex += 1
|
|
local token = "__mock_json_" .. tostring(encodedIndex)
|
|
encodedValues[token] = copy(value)
|
|
return token, nil
|
|
end,
|
|
},
|
|
state = {
|
|
get = function(key) return copy(stateValues[key]) end,
|
|
set = function(key, value)
|
|
stateValues[key] = copy(value)
|
|
for _, watcher in ipairs(stateWatchers[key] or {}) do watcher(copy(value)) end
|
|
end,
|
|
watch = function(key, callback)
|
|
stateWatchers[key] = stateWatchers[key] or {}
|
|
table.insert(stateWatchers[key], callback)
|
|
end,
|
|
},
|
|
}
|
|
|
|
local function node(kind)
|
|
return function(props, children) return { type = kind, props = props or {}, children = children or {} } end
|
|
end
|
|
|
|
local ui = {}
|
|
for _, kind in ipairs({ "column", "row", "box", "label", "glyph", "image", "separator", "spacer", "progress", "button", "graph", "input", "select", "slider", "toggle", "scroll" }) do
|
|
ui[kind] = node(kind)
|
|
end
|
|
|
|
return {
|
|
noctalia = noctalia,
|
|
ui = ui,
|
|
panel = {
|
|
render = function(_tree)
|
|
renderCount += 1
|
|
renderedTree = _tree
|
|
end,
|
|
close = function() end,
|
|
setWantsSecondTicks = function(value)
|
|
wantsSecondTicks = value == true
|
|
end,
|
|
},
|
|
setFiles = function(value)
|
|
files = {}
|
|
fileExistsOverrides = {}
|
|
for path, content in pairs(value) do files[normalize(path)] = content end
|
|
end,
|
|
setFileExists = function(path, value)
|
|
fileExistsOverrides[normalize(path)] = value
|
|
end,
|
|
resetRuntime = function()
|
|
stateValues = {}
|
|
configValues = {}
|
|
availableCommands = {}
|
|
asyncRequests = {}
|
|
renderCount = 0
|
|
renderedTree = nil
|
|
readCounts = {}
|
|
writeCounts = {}
|
|
wantsSecondTicks = false
|
|
errorNotifications = 0
|
|
updateInterval = nil
|
|
end,
|
|
setFile = function(path, content)
|
|
files[normalize(path)] = content
|
|
end,
|
|
setConfig = function(values) configValues = values or {} end,
|
|
setAvailableCommands = function(values) availableCommands = values or {} end,
|
|
asyncLaunchCount = function() return #asyncRequests end,
|
|
completeAsync = function(index, result)
|
|
local request = asyncRequests[index]
|
|
if request == nil or request.completed then return false end
|
|
request.completed = true
|
|
if request.callback ~= nil then request.callback(result) end
|
|
return true
|
|
end,
|
|
renderCount = function() return renderCount end,
|
|
renderedTree = function() return renderedTree end,
|
|
readCount = function(path) return readCounts[normalize(path)] or 0 end,
|
|
writeCount = function(path) return writeCounts[normalize(path)] or 0 end,
|
|
fileContent = function(path) return files[normalize(path)] end,
|
|
wantsSecondTicks = function() return wantsSecondTicks end,
|
|
errorNotificationCount = function() return errorNotifications end,
|
|
stateValue = function(key) return copy(stateValues[key]) end,
|
|
updateInterval = function() return updateInterval end,
|
|
}
|