diff --git a/README.md b/README.md index ceb0136..7910fb6 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,9 @@ You should maybe also include demontools.lua, as it notes below several other of * loggingconsole.lua * Self logging extension to the mini console. Works just like a Geyser.MiniConsole but adds a templated path and fileName constraint, as well as logFormat so it can log what is echod or appended to it. Requires demontools.lua in order to work. +* loginator.lua + * Creates objects for logging messages to disk. + * mastermindsolver.lua * A class which will help you solve Master Mind puzzles. diff --git a/src/resources/demontools.lua b/src/resources/demontools.lua index 4c41507..d303e34 100644 --- a/src/resources/demontools.lua +++ b/src/resources/demontools.lua @@ -4,7 +4,7 @@ -- @copyright 2020 Damian Monogue -- @license MIT, see LICENSE.lua local DemonTools = {} -local cheatConsole = Geyser.MiniConsole:new({name = "DemonnicCheatConsole", width = 4000, wrapWidth = 10000}) +local cheatConsole = Geyser.MiniConsole:new({name = "DemonnicCheatConsole", width = 4000, wrapWidth = 10000, color = "black"}) cheatConsole:hide() local function exists(path) local ok, err, code = os.rename(path, path) @@ -19,6 +19,7 @@ local function isWindows() end local function isDir(path) + path = path:gsub("\\", "/") if not path:ends("/") then path = path .. "/" end @@ -64,7 +65,8 @@ local htmlHeader = [=[ -]=] + +]=] local htmlHeaderPattern = [=[ @@ -81,7 +83,8 @@ local htmlHeaderPattern = [=[ -]=] + +]=] -- internal function, recursively digs for a value within subtables if possible local function digForValue(dataFrom, tableTo) @@ -1292,4 +1295,7 @@ function DemonTools.mkdir_p(path) return mkdir_p(path) end +DemonTools.htmlHeader = htmlHeader +DemonTools.htmlHeaderPattern = htmlHeaderPattern + return DemonTools diff --git a/src/resources/loggingconsole.lua b/src/resources/loggingconsole.lua index 58b00a5..88277cb 100644 --- a/src/resources/loggingconsole.lua +++ b/src/resources/loggingconsole.lua @@ -6,23 +6,7 @@ local homedir = getMudletHomeDir():gsub("\\", "/") local pathOfThisFile = (...):match("(.-)[^%.]+$") local dt = require(pathOfThisFile .. "demontools") -local exists, isDir = dt.exists, dt.isDir -local htmlHeader = [=[ - - - - - - -]=] +local exists, htmlHeader, htmlHeaderPattern = dt.exists, dt.htmlHeader, dt.htmlHeaderPattern local LoggingConsole = {log = true, logFormat = "h", path = "|h/log/consoleLogs/|y/|m/|d/", fileName = "|n.|e"} @@ -442,6 +426,11 @@ function LoggingConsole:replay(numberOfLines) local file = io.open(fileName, "r") local lines = file:read("*a") if self:getExtension() == "html" then + for _, line in ipairs(htmlHeaderPattern:split("\n")) do + if line ~= "" then + lines = lines:gsub(line .. "\n", "") + end + end lines = dt.html2decho(lines) else lines = ansi2decho(lines) diff --git a/src/resources/loginator.lua b/src/resources/loginator.lua new file mode 100644 index 0000000..adb6c5f --- /dev/null +++ b/src/resources/loginator.lua @@ -0,0 +1,456 @@ +--- Loginator creates an object which allows you to log things to file at +-- various severity levels, with the ability to only log items above a specific +-- severity to file. +-- @classmod Loginator +-- @author Damian Monogue +-- @copyright 2021 Damian Monogue +-- @license MIT, see LICENSE.lua +local Loginator = { + format = "h", + name = "logname", + fileNameTemplate = "|p/log/Loginator/|y-|M-|d-|n.|e", + entryTemplate = "|y-|M-|d |h:|m:|s.|x [|c|l|r] |t", + level = "warn", + bgColor = "black", + fontSize = 12, + fgColor = "white", +} + +local levelColors = {error = "red", warn = "DarkOrange", info = "ForestGreen", debug = "ansi_yellow"} +local loggerLevels = {error = 1, warn = 2, info = 3, debug = 4} + +local function exists(path) + local ok, err, code = os.rename(path, path) + if not ok and code == 13 then + return true + end + return ok, err +end + +local function isWindows() + return package.config:sub(1, 1) == [[\]] +end + +local function mkdir_p(path) + path = path:gsub("\\", "/") + local pathTbl = path:split("/") + local cwd = "/" + if isWindows() then + cwd = "" + end + for index, dirName in ipairs(pathTbl) do + if index == 1 then + cwd = cwd .. dirName + else + cwd = cwd .. "/" .. dirName + cwd = cwd:gsub("//", "/") + end + if not table.contains({"/", "C:"}, cwd) and not exists(cwd) then + local ok, err = lfs.mkdir(cwd) + if not ok then + return ok, err + end + end + end + return true +end + +local htmlHeaderTemplate = [=[ + + + + + + + +]=] + +--- Creates a new Loginator object +--@tparam table options table of options for the logger +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
option namedescriptiondefault
formatWhat format to log in? "h" for html, "a" for ansi, anything else for plaintext."h"
nameWhat is the name of the logger? Will replace |n in templateslogname
levelWhat level should the logger operate at? This will control what level the log function defaults to, as well as what logs will actually be written
+-- Only items of an equal or higher severity to this will be written to the log file.
"info"
bgColorWhat background color to use for html logs"black"
fgColorWhat color to use for the main text in html logs"white"
fontSizeWhat font size to use in html logs12
levelColorsTable with the log level as the key, and the color which corresponds to it as the value{ error = "red", warn = "DarkOrange", info = "ForestGreen", debug = "ansi_yellow" }
fileNameTemplateA template which will be transformed into the full filename, with path. See template options below for replacements"|p/log/Loginator/|y-|M-|d-|n.|e"
entryTemplateThe template which controls the look of each log entry. See template options below for replacements"|y-|M-|d |h:|m:|s.|x [|c|l|r] |t"

+-- Table of template options +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
template codewhat it is replaced withexample
|ythe year in 4 digits2021
|pgetMudletHomeDir()/home/demonnic/.config/mudlet/profiles/testprofile
|MMonth as 2 digits05
|dday, as 2 digits23
|hhour in 24hr time format, 2 digits03
|mminute as 2 digits42
|sseconds as 2 digits34
|xmilliseconds as 3 digits194
|eFilename extension expected. "html" for html format, "log" for everything elsehtml
|lThe logging level of the entry, in ALLCAPSWARN
|cThe color which corresponds with the logging level. Set via the levelColors table in the options. Example not included.
|rReset back to standard color. Used to close |c. Example not included
|nThe name of the logger, set via the options when you have Loginator create it.CoolPackageLog
+--@return newly created logger object +function Loginator:new(options) + options = options or {} + local optionsType = type(options) + if optionsType ~= "table" then + return nil, f "Loginator:new(options) options as table expected, got {optionsType}" + end + local me = table.deepcopy(options) + me.levelColors = me.levelColors or {} + local lcType = type(me.levelColors) + if lcType ~= "table" then + return nil, f "Loginator:new(options) provided options.levelColors must be a table, but you provided a {lcType}" + end + for lvl,clr in pairs(levelColors) do + me.levelColors[lvl] = me.levelColors[lvl] or clr + end + setmetatable(me, self) + self.__index = self + return me +end + +---@local +function Loginator:processTemplate(str, level) + local lvl = level or self.level + local timeTable = getTime() + for what, with in pairs({ + ["|y"] = function() + return timeTable.year + end, + ["|p"] = getMudletHomeDir, + ["|M"] = function() + return string.format("%02d", timeTable.month) + end, + ["|d"] = function() + return string.format("%02d", timeTable.day) + end, + ["|h"] = function() + return string.format("%02d", timeTable.hour) + end, + ["|m"] = function() + return string.format("%02d", timeTable.min) + end, + ["|s"] = function() + return string.format("%02d", timeTable.sec) + end, + ["|x"] = function() + return string.format("%03d", timeTable.msec) + end, + ["|e"] = function() + return (self.format:starts("h") and "html" or "log") + end, + ["|l"] = function() + return lvl:upper() + end, + ["|c"] = function() + return self:getColor(lvl) + end, + ["|r"] = function() + return self:getReset() + end, + ["|n"] = function() + return self.name + end, + }) do + if str:find(what) then + str = str:gsub(what, with()) + end + end + return str +end + +--- Set the color to associate with a logging level post-creation +--@param color The color to set for the level, as a string. Can be any valid color string for cecho, decho, or hecho. +--@param level The level to set the color for. Must be one of 'error', 'warn', 'info', or 'debug' +--@returns true if the color is updated, or nil+error if it could not be updated for some reason. +function Loginator:setColorForLevel(color, level) + if not color then + return nil, "You must provide a color to set" + end + if not level then + return nil, "You must provide a level to set the color for" + end + if not loggerLevels[level] then + return nil, "Invalid level. Valid levels are 'error', 'warn', 'info', or 'debug'" + end + if not Geyser.Color.parse(color) then + return nil, "You must provide a color which can be parsed by Geyser.Color.parse. Examples are 'blue' (cecho), '<128,0,0>' (decho), '#aa3388' (hecho), or {128,0,0} (table of r,g,b values)" + end + self.levelColors[level] = color + return true +end + +---@local +function Loginator:getColor(level) + if self.format == "t" then + return "" + end + local r, g, b = Geyser.Color.parse((self.levelColors[level] or {128, 128, 128})) + if self.format == "h" then + return string.format("", r, g, b) + elseif self.format == "a" then + return string.format("\27[38:2::%d:%d:%dm", r, g, b) + end + return "" +end + +---@local +function Loginator:getReset() + if self.format == "t" then + return "" + elseif self.format == "h" then + return "" + elseif self.format == "a" then + return "\27[39;49m" + end + return "" +end + +--- Returns the full path and filename to the logfile +function Loginator:getFullFilename() + return self:processTemplate(self.fileNameTemplate) +end + +--- Write an error level message to the logfile. Error level messages are always written. +--@param msg the message to log +--@return true if msg written, nil+error if error +function Loginator:error(msg) + return self:log(msg, "error") +end + +--- Write a warn level message to the logfile. +-- Msg is only written if the logger level is <= warn +-- From most to least severe the levels are: +-- error > warn > info > debug +--@param msg the message to log +--@return true if msg written, false if skipped due to level, nil+error if error +function Loginator:warn(msg) + return self:log(msg, "warn") +end + +--- Write an info level message to the logfile. +-- Msg is only written if the logger level is <= info +-- From most to least severe the levels are: +-- error > warn > info > debug +--@param msg the message to log +--@return true if msg written, false if skipped due to level, nil+error if error +function Loginator:info(msg) + return self:log(msg, "info") +end + +--- Write a debug level message to the logfile. +-- Msg is only written if the logger level is debug +-- From most to least severe the levels are: +-- error > warn > info > debug +--@param msg the message to log +--@return true if msg written, false if skipped due to level, nil+error if error +function Loginator:debug(msg) + return self:log(msg, "debug") +end + +--- Write a message to the log file and optionally specify the level +--@param msg the message to log +--@param level the level to log the message at. Defaults to the level of the logger itself if not provided. +--@return true if msg written, false if skipped due to level, nil+error if error +function Loginator:log(msg, level) + level = level or self.level + local levelNumber = loggerLevels[level] + if not levelNumber then + return nil, f"Unknown logging level: {level}. Valid levels are 'error', 'warn', 'info', and 'debug'" + end + local displayLevelNumber = loggerLevels[self.level] + if levelNumber > displayLevelNumber then + return false + end + local filename = self:getFullFilename() + local filteredMsg = self:processTemplate(self.entryTemplate, level):gsub("|t", msg) + local ok, err = self:createPathIfNotExists(filename) + if err then + debugc(err) + return ok, err + end + if self.format == "h" and not io.exists(filename) then + filteredMsg = self:getHtmlHeader() .. filteredMsg + end + local file, err = io.open(filename, "a") + if not file then + err = string.format("Logger %s failed to open %s because: %s\n", self.name, filename, err) + debugc(err) + return nil, err + end + file:write(filteredMsg .. "\n") + file:close() + return true +end + +--- Uses openUrl() to request your OS open the logfile in the appropriate application. Usually your web browser for html and text editor for all others. +function Loginator:open() + openUrl(self:getFullFilename()) +end + +--- Uses openUrl() to request your OS open the directory the logfile resides in. This allows for easier browsing if you have more than one file. +function Loginator:openDir() + openUrl(self:getPath()) +end + +--- Returns the path to the log file (directory in which the file resides) as a string +--@param filename optional filename to return the path of. If not supplied, with use the logger's current filename +function Loginator:getPath(filename) + filename = filename or self:getFullFilename() + filename = filename:gsub([[\]], "/") + local filenameTable = filename:split("/") + filenameTable[#filenameTable] = nil + local path = table.concat(filenameTable, "/") + return path +end + +---@local +function Loginator:createPathIfNotExists(filename) + if exists(filename) then + return false + end + filename = filename:gsub([[\]], "/") + local path = self:getPath(filename) + if exists(path) then + return false + end + local ok, err = mkdir_p(path) + if not ok then + err = string.format("Could not create directory for log files: %s\n Reason was: %s", path, err) + return nil, err + end + return true +end + +---@local +function Loginator:getHtmlHeader() + local header = htmlHeaderTemplate + header = header:gsub("|b", self.bgColor) + header = header:gsub("|c", self.fgColor) + header = header:gsub("|f", self.fontSize) + return header +end + +return Loginator diff --git a/src/resources/mdkversion.txt b/src/resources/mdkversion.txt index 7ec1d6d..ccbccc3 100644 --- a/src/resources/mdkversion.txt +++ b/src/resources/mdkversion.txt @@ -1 +1 @@ -2.1.0 +2.2.0 diff --git a/src/resources/sug.lua b/src/resources/sug.lua index 8daf14c..85e091a 100644 --- a/src/resources/sug.lua +++ b/src/resources/sug.lua @@ -177,7 +177,7 @@ end --- Starts the Self Updating Gauge updating. If it is already updating, it will restart it. function SUG:start() - SUG:stop() + self:stop() self.active = true self.timer = tempTimer(self.updateTime / 1000, function() self:update()