Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Persistence of shell aliases/completions, settings definitions. #2005

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -723,8 +723,11 @@ if _CC_DEFAULT_SETTINGS then
end

-- Load user settings
if fs.exists(".settings") then
settings.load(".settings")
if fs.exists(".cc/settings") then
settings.load(".cc/settings")
end
if fs.exists(".cc/settings.def") then
settings.loadDefinitions(".cc/settings.def")
end

-- Run the shell
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ end
local valid_types = { "number", "string", "boolean", "table" }
for _, v in ipairs(valid_types) do valid_types[v] = true end

--- Define a new setting, optional specifying various properties about it.
--- Define a new setting, optionally specifying various properties about it.
--
-- While settings do not have to be added before being used, doing so allows
-- you to provide defaults and additional metadata.
Expand Down Expand Up @@ -208,21 +208,64 @@ function getNames()
return result
end

--- Load settings definitions from the given file.
--
-- Existing definitions will be merged with any pre-existing ones. Conflicting
-- entries will be overwritten, but any others will be preserved.
--
-- @tparam[opt=".cc/settings.def"] string path The file to load from.
-- @treturn boolean Whether definitions were successfully read from this
-- file. Reasons for failure may include the file not existing or being
-- corrupted.
--
-- @see settings.saveDefinitions
-- @since x.xx.x
function loadDefinitions(path)
expect(1, path, "string", "nil")
path = path or ".cc/settings.def"

local file = fs.open(path, "r")
if not file then
return false
end

local sText = file.readAll()
file.close()

local tFile = textutils.unserialize(sText)
if type(tFile) ~= "table" then
return false
end

for k, v in pairs(tFile) do
if type(k) == "string" and type(v) == "table" then
local ok, v = pcall(reserialize, v)
if ok then details[k] = v end
end
end

return true
end

--- Load settings from the given file.
--
-- Existing settings will be merged with any pre-existing ones. Conflicting
-- entries will be overwritten, but any others will be preserved.
--
-- @tparam[opt=".settings"] string path The file to load from.
-- @tparam[opt=".cc/settings"] string path The file to load from.
-- @treturn boolean Whether settings were successfully read from this
-- file. Reasons for failure may include the file not existing or being
-- corrupted.
--
-- @see settings.save
-- @changed 1.87.0 `path` is now optional.
-- @changed x.xx.x Default path is now `.cc/settings`.
function load(path)
expect(1, path, "string", "nil")
local file = fs.open(path or ".settings", "r")
path = path or ".cc/settings"

-- Load the current values from the main file.
local file = fs.open(path, "r")
if not file then
return false
end
Expand Down Expand Up @@ -250,25 +293,48 @@ function load(path)
return true
end

--- Save settings to the given file.
local function writeFile(path, data)
local file = fs.open(path, "w")
if not file then
return false
end

file.write(data)
file.close()

return true
end

--- Save current settings to the given file.
--
-- This will entirely overwrite the pre-existing file. Settings defined in the
-- file, but not currently loaded will be removed.
-- This will entirely overwrite the pre-existing file. This only saves
-- the current values, without the definitions.
--
-- @tparam[opt=".settings"] string path The path to save settings to.
-- @tparam[opt=".cc/settings"] string path The path to save settings to.
-- @treturn boolean If the settings were successfully saved.
--
-- @see settings.load
-- @changed 1.87.0 `path` is now optional.
-- @changed x.xx.x Default path is now `.cc/settings`.
function save(path)
expect(1, path, "string", "nil")
local file = fs.open(path or ".settings", "w")
if not file then
return false
end
path = path or ".cc/settings"

file.write(textutils.serialize(values))
file.close()
return writeFile(path, textutils.serialize(values))
end

return true
--- Save settings definitions to the given file.
--
-- This will entirely overwrite the pre-existing file.
--
-- @tparam[opt=".cc/settings.def"] string path The path to save settings to.
-- @treturn boolean If the definitions were successfully saved.
--
-- @see settings.loadDefinitions
-- @since x.xx.x
function saveDefinitions(path)
expect(1, path, "string", "nil")
path = path or ".cc/settings.def"

return writeFile(path, textutils.serialize(details))
end
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,48 @@ function shell.completeProgram(program)
return completeProgram(program)
end

local function loadCompletion(file, data)
--[[
local env = setmetatable(createShellEnv(dir), { __index = _G })
env.arg = args
]]

local env = setmetatable(
createShellEnv("/rom/programs"),
{ __index = _G }
)

env.completion = require("cc.shell.completion")

local fn, err = load(
"return " .. data,
"@/" .. file,
"t",
env
)
if not fn then
-- Attempt 2: load without the return statement
fn, err = load(
data,
"@/" .. file,
"t",
env
)

-- If we still failed to load, return the error
if not fn then
error(err, 3)
end
end

local ok, result = pcall(fn)
if not ok then
error(result, 0)
end

return result
end

--- Set the completion function for a program. When the program is entered on
-- the command line, this program will be called to provide auto-complete
-- information.
Expand All @@ -595,22 +637,113 @@ end
-- You completion entries may also be followed by a space, if you wish to
-- indicate another argument is expected.
--
-- Optionally, you may pass a string containing the function you wish to use as
-- the completion function. This string will be stored on disk, and loaded when
-- the computer starts up. This allows you to write completion functions that
-- persist between reboots. The shell `cc.shell.completion` module is injected
-- into the environment of this function, allowing you to use its utilities.
--
-- @usage
-- ```lua
-- local completion = require("cc.shell.completion")
-- shell.setCompletionFunction(
-- "yourprogram.lua",
-- completion.build(completion.program)
-- )
-- ```
--
-- @usage
-- ```lua
-- shell.setCompletionFunction("yourprogram.lua", [[
-- completion.build(completion.program) -- The shell completion library is injected into the environment
-- ]])
-- ```
--
-- @tparam string program The path to the program. This should be an absolute path
-- _without_ the leading `/`.
-- @tparam function(shell: table, index: number, argument: string, previous: { string }):({ string }|nil) complete
-- The completion function.
-- @tparam string|function(shell: table, index: number, argument: string, previous: { string }):({ string }|nil) complete
-- The completion function, or a `loadstring`able version of it.
-- @see cc.shell.completion Various utilities to help with writing completion functions.
-- @see shell.complete
-- @see _G.read For more information about completion.
-- @since 1.74
-- @changed x.xx.x Accepts a loadable string as a completion function, and saves it to disk.
function shell.setCompletionFunction(program, complete)
expect(1, program, "string")
expect(2, complete, "function")
expect(2, complete, "function", "string")

if type(complete) == "string" then
-- Load the completion function
local result = loadCompletion(".cc/completion/" .. program, complete)

if not result then
error("Bad completion function: nil value returned", 2)
end

-- Save the loadable version of the function
local file = fs.open(".cc/completion/" .. program, "w")
if not file then
error("Failed to save completion function.", 2)
end
file.write(complete)
file.close()

complete = result
end

tCompletionInfo[program] = {
fnComplete = complete,
}
end

--- Clear a completion function for a program.
--
-- @tparam string program The path to the program.
-- @since x.xx.x
function shell.clearCompletionFunction(program)
expect(1, program, "string")
tCompletionInfo[program] = nil

-- Delete the completion function file
fs.delete(".cc/completion/" .. program)
end

--- Load all completion functions from disk.
--
-- This is called automatically when the shell is loaded, and should not be
-- needed in normal usage.
--
-- @since x.xx.x
function shell.loadCompletionFunctions()
if not fs.exists(".cc/completion") or not fs.isDir(".cc/completion") then
return
end

local files = fs.list(".cc/completion")

for _, file in ipairs(files) do
local path = fs.combine(".cc/completion", file)
local handle = fs.open(path, "r")
if handle then
local data = handle.readAll()
handle.close()

-- Problem: This runs at startup, so if someone enters an invalid
-- completion function to be loaded, we'll brick the computer.
-- We will fix this by only printing the error, and not throwing it.
local ok, result = pcall(loadCompletion, path, data)

if ok and result then
tCompletionInfo[file] = {
fnComplete = result,
}
else
printError(result and result or ("%s:Bad completion function: nil value returned"):format(path))
end
end
end
end

--- Get a table containing all completion functions.
--
-- This should only be needed when building custom shells. Use
Expand All @@ -637,22 +770,71 @@ end
--
-- @tparam string command The name of the alias to add.
-- @tparam string program The name or path to the program.
-- @tparam boolean|nil dont_save If true, will not save the alias to disk.
-- This is mainly used internally for aliases that are registered in the shell's
-- startup script.
-- Used mostly internally for aliases that are registered in the shell's
-- startup script.
-- @since 1.2
-- @usage Alias `vim` to the `edit` program
--
-- shell.setAlias("vim", "edit")
function shell.setAlias(command, program)
-- @changed x.xx.x Aliases are now saved to disk.
function shell.setAlias(command, program, dont_save)
expect(1, command, "string")
expect(2, program, "string")
expect(3, dont_save, "boolean", "nil")
tAliases[command] = program

if dont_save then
return
end
-- Save the new alias
local file = fs.open(".cc/aliases/" .. command, "w")
if not file then
printError(("Failed to save alias '%s'"):format(command))
return
end

file.write(program)
file.close()
end

--- Remove an alias.
--
-- @tparam string command The alias name to remove.
-- @since x.xx.x
function shell.clearAlias(command)
expect(1, command, "string")
tAliases[command] = nil

-- Delete the alias file
fs.delete(".cc/aliases/" .. command)
end

--- Load aliases from disk.
--
-- This is called automatically when the shell is loaded, and should not be
-- needed in normal usage.
--
-- @since x.xx.x
function shell.loadAliases()
if not fs.exists(".cc/aliases") or not fs.isDir(".cc/aliases") then
return
end

local files = fs.list(".cc/aliases")

for _, file in ipairs(files) do
local path = fs.combine(".cc/aliases", file)
local handle = fs.open(path, "r")
if handle then
local program = handle.readAll()
handle.close()

tAliases[file] = program
end
end
end

--- Get the current aliases for this shell.
Expand Down
Loading
Loading