From e3c518f486bac95aae0dbc46e2b299171a352ec3 Mon Sep 17 00:00:00 2001 From: Stack Date: Thu, 24 Feb 2022 10:04:45 -0800 Subject: [PATCH] build: :heavy_plus_sign: add MDK dependencies --- MDK/LICENSE-MDK.lua | 80 ++ MDK/demontools.lua | 1317 ++++++++++++++++++++++++ MDK/emco.lua | 2173 ++++++++++++++++++++++++++++++++++++++++ MDK/loggingconsole.lua | 459 +++++++++ MDK/sug.lua | 255 +++++ 5 files changed, 4284 insertions(+) create mode 100644 MDK/LICENSE-MDK.lua create mode 100644 MDK/demontools.lua create mode 100644 MDK/emco.lua create mode 100644 MDK/loggingconsole.lua create mode 100644 MDK/sug.lua diff --git a/MDK/LICENSE-MDK.lua b/MDK/LICENSE-MDK.lua new file mode 100644 index 0000000..bbe0cef --- /dev/null +++ b/MDK/LICENSE-MDK.lua @@ -0,0 +1,80 @@ +--[===[ +The MIT License (MIT) + +Copyright (c) 2020 Damian Monogue + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +--]===] + +-- schema validation provided by schema.lua, license below +--[[ +The MIT License (MIT) + +Copyright (c) 2014 Sebastian Schoener + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +]] + + +-- Testing uses luaunit. License below +--[=[ +This software is distributed under the BSD License. + +Copyright (c) 2005-2018, Philippe Fremy + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or other +materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +© 2020 GitHub, Inc. +--]=] diff --git a/MDK/demontools.lua b/MDK/demontools.lua new file mode 100644 index 0000000..6199335 --- /dev/null +++ b/MDK/demontools.lua @@ -0,0 +1,1317 @@ +--- Collection of miscellaneous functions and tools which don't necessarily warrant their own module/class +-- @module demontools +-- @author Damian Monogue +-- @copyright 2020 Damian Monogue +-- @license MIT, see LICENSE.lua +local DemonTools = {} +local cheatConsole = Geyser.MiniConsole:new({name = "DemonnicCheatConsole", width = 4000, wrapWidth = 10000, color = "black"}) +cheatConsole:hide() +local function exists(path) + path = path:gsub([[\]], "/") + if path:ends("/") then + path = path:sub(1,-2) + end + local ok, err, code = lfs.attributes(path) + if ok then + return true + end + if err:lower():find("no such file or directory") then + return false + end + return ok, err, code +end + +local function isWindows() + return package.config:sub(1, 1) == [[\]] +end + +local function isDir(path) + if not exists(path) then return false end + path = path:gsub([[\]], "/") + if path:ends("/") then + path = path:sub(1,-2) + end + local ok, err, code = lfs.attributes(path, "mode") + if ok then + if ok == "directory" then + return true + else + return false + end + end + return ok, err, code +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 htmlHeader = [=[ + + + + + + + +]=] + +local htmlHeaderPattern = [=[ + + + + + + + +]=] + +-- internal function, recursively digs for a value within subtables if possible +local function digForValue(dataFrom, tableTo) + if dataFrom == nil or table.size(tableTo) == 0 then + return dataFrom + else + local newData = dataFrom[tableTo[1]] + table.remove(tableTo, 1) + return digForValue(newData, tableTo) + end +end + +-- Internal function, used to turn a string variable name into a value +local function getValueAt(accessString) + if accessString == "" then + return nil + end + local tempTable = accessString:split("%.") + local accessTable = {} + for i, v in ipairs(tempTable) do + if tonumber(v) then + accessTable[i] = tonumber(v) + else + accessTable[i] = v + end + end + return digForValue(_G, accessTable) +end + +-- internal sorting function, sorts first by hue, then luminosity, then value +local function sortColorsByHue(lhs, rhs) + local lh, ll, lv = unpack(lhs.sort) + local rh, rl, rv = unpack(rhs.sort) + if lh < rh then + return true + elseif lh > rh then + return false + elseif ll < rl then + return true + elseif ll > rl then + return false + else + return lv < rv + end +end + +-- internal sorting function, removes _ from snake_case and compares to camelCase +local function sortColorsByName(a, b) + local aname = string.gsub(string.lower(a.name), "_", "") + local bname = string.gsub(string.lower(b.name), "_", "") + return aname < bname +end + +-- internal function used to turn sorted colors table into columns +local function chunkify(tbl, num_chunks) + local pop = function(t) + return table.remove(t, 1) + end + tbl = table.deepcopy(tbl) + local tblsize = #tbl + local base_chunk_size = tblsize / num_chunks + local chunky_chunks = tblsize % num_chunks + local chunks = {} + for i = 1, num_chunks do + local chunk_size = base_chunk_size + if i <= chunky_chunks then + chunk_size = chunk_size + 1 + end + local chunk = {} + for j = 1, chunk_size do + chunk[j] = pop(tbl) + end + chunks[i] = chunk + end + return chunks +end + +-- internal function, converts rgb to hsv +-- found at https://github.com/EmmanuelOga/columns/blob/master/utils/color.lua#L89 +local function rgbToHsv(r, g, b) + r, g, b = r / 255, g / 255, b / 255 + local max, min = math.max(r, g, b), math.min(r, g, b) + local h, s, v + v = max + local d = max - min + if max == 0 then + s = 0 + else + s = d / max + end + if max == min then + h = 0 + -- achromatic + else + if max == r then + h = (g - b) / d + if g < b then + h = h + 6 + end + elseif max == g then + h = (b - r) / d + 2 + elseif max == b then + h = (r - g) / d + 4 + end + h = h / 6 + end + return h, s, v +end + +-- internal stepping function, removes some of the noise for a more pleasing sort +-- cribbed from the python on https://www.alanzucconi.com/2015/09/30/colour-sorting/ +local function step(r, g, b) + local lum = math.sqrt(.241 * r + .691 * g + .068 * b) + local reps = 8 + local h, s, v = rgbToHsv(r, g, b) + local h2 = math.floor(h * reps) + local lum2 = math.floor(lum * reps) + local v2 = math.floor(v * reps) + if h2 % 2 == 1 then + v2 = reps - v2 + lum2 = reps - lum2 + end + return h2, lum2, v2 +end + +local function calc_luminosity(r, g, b) + r = r < 11 and r / (255 * 12.92) or ((0.055 + r / 255) / 1.055) ^ 2.4 + g = g < 11 and g / (255 * 12.92) or ((0.055 + g / 255) / 1.055) ^ 2.4 + b = b < 11 and b / (255 * 12.92) or ((0.055 + b / 255) / 1.055) ^ 2.4 + return (0.2126 * r) + (0.7152 * g) + (0.0722 * b) +end + +local function include(color, options) + if options.removeDupes and (string.find(color, "_") and not color:starts("ansi")) or string.find(color:lower(), 'gray') then + return false + end + if options.removeAnsi255 and string.find(color, "ansi_%d%d%d") then + return false + end +end + +local function echoColor(color, options) + local rgb = color.rgb + local fgc = "white" + if calc_luminosity(unpack(rgb)) > 0.5 then + fgc = "black" + end + local colorString + if options.justText then + colorString = string.format('<%s:%s> %-23s ', color.name, 'black', color.name) + else + colorString = string.format('<%s:%s> %-23s ', fgc, color.name, color.name) + end + if options.window == "main" then + if options.echoOnly then + cecho(colorString) + else + cechoLink(colorString, [[appendCmdLine("]] .. color.name .. [[")]], table.concat(rgb, ", "), true) + end + else + if options.echoOnly then + cecho(options.window, colorString) + else + cechoLink(options.window, colorString, [[appendCmdLine("]] .. color.name .. [[")]], table.concat(rgb, ", "), true) + end + end +end + +local cnames = {} + +local function _color_name(rgb) + if cnames[rgb] then + return cnames[rgb] + end + local least_distance = math.huge + local cname = "" + for name, color in pairs(color_table) do + local color_distance = math.sqrt((color[1] - rgb[1]) ^ 2 + (color[2] - rgb[2]) ^ 2 + (color[3] - rgb[3]) ^ 2) + if color_distance < least_distance then + least_distance = color_distance + cname = name + end + end + cnames[rgb] = cname + return cname +end + +-- converts decho color information to ansi escape sequences +local function rgbToAnsi(rgb) + local result = "" + local cols = rgb:split(":") + local fore = cols[1] + local back = cols[2] + if fore ~= "" then + local components = fore:split(",") + result = string.format("%s\27[38:2::%s:%s:%sm", result, components[1] or "0", components[2] or "0", components[3] or "0") + end + if back then + local components = back:split(",") + result = string.format("%s\27[48:2::%s:%s:%sm", result, components[1] or "0", components[2] or "0", components[3] or "0") + end + return result +end + +-- converts a 6 digit hex color code to ansi escape sequence +local function hexToAnsi(hexcode) + local result = "" + local cols = hexcode:split(",") + local fore = cols[1] + local back = cols[2] + if fore ~= "" then + local components = {tonumber(fore:sub(1, 2), 16), tonumber(fore:sub(3, 4), 16), tonumber(fore:sub(5, 6), 16)} + result = string.format("%s\27[38:2::%s:%s:%sm", result, components[1] or "0", components[2] or "0", components[3] or "0") + end + if back then + local components = {tonumber(back:sub(1, 2), 16), tonumber(back:sub(3, 4), 16), tonumber(back:sub(5, 6), 16)} + result = string.format("%s\27[48:2::%s:%s:%sm", result, components[1] or "0", components[2] or "0", components[3] or "0") + end + return result +end + +local function hexToRgb(hexcode) + local result = "<" + local cols = hexcode:split(",") + local fore = cols[1] + local back = cols[2] + if fore ~= "" then + local r, g, b = Geyser.Color.parse("#" .. fore) + result = string.format("%s%s,%s,%s", result, r, g, b) + end + if back then + local r, g, b = Geyser.Color.parse("#" .. back) + result = string.format("%s:%s,%s,%s", result, r, g, b) + end + return string.format("%s>", result) +end + +local function rgbToHex(rgb) + local result = "#" + local cols = rgb:split(":") + local fore = cols[1] + local back = cols[2] + if fore ~= "" then + local r, g, b = unpack(string.split(fore, ",")) + result = string.format("%s%02x%02x%02x", result, r, g, b) + end + if back then + local r, g, b = unpack(string.split(back, ",")) + result = string.format("%s,%02x%02x%02x", result, r, g, b) + end + return result +end + +local function rgbToCname(rgb) + local result = "<" + local cols = rgb:split(":") + local fore = cols[1] + local back = cols[2] + if fore ~= "" then + result = string.format("%s%s", result, _color_name(fore:split(","))) + end + if back then + result = string.format("%s:%s", result, _color_name(back:split(","))) + end + return string.format("%s>", result) +end + +local function cnameToRgb(cname) + local result = "<" + local cols = cname:split(":") + local fore = cols[1] + local back = cols[2] + if fore ~= "" then + local rgb = color_table[fore] or {0, 0, 0} + result = string.format("%s%s", result, table.concat(rgb, ",")) + end + if back then + local rgb = color_table[back] or {0, 0, 0} + result = string.format("%s:%s", result, table.concat(rgb, ",")) + end + return string.format("%s>", result) +end + +local function toFromDecho(from, to, text) + local patterns = {d = _Echos.Patterns.Decimal[1], c = _Echos.Patterns.Color[1], h = _Echos.Patterns.Hex[1]} + local funcs = {d = {c = rgbToCname, h = rgbToHex, a = rgbToAnsi}, c = {d = cnameToRgb}, h = {d = hexToRgb}} + local resetCodes = {d = "", h = "#r", c = "", a = "\27[39;49m"} + + local colorPattern = patterns[from] + local func = funcs[from][to] + local reset = resetCodes[to] + local result = "" + for str, color, res in rex.split(text, colorPattern) do + result = result .. str + if color then + if color:sub(1, 1) == "|" then + color = color:gsub("|c", "#") + end + if from == "h" then + result = result .. func(color:sub(2, -1)) + else + result = result .. func(color:match("<(.+)>")) + end + end + if res then + result = result .. reset + end + end + return result +end + +local function decho2cecho(text) + return toFromDecho("d", "c", text) +end + +local function cecho2decho(text) + return toFromDecho("c", "d", text) +end + +local function decho2hecho(text) + return toFromDecho("d", "h", text) +end + +local function hecho2decho(text) + return toFromDecho("h", "d", text) +end + +local function cecho2ansi(text) + local dtext = cecho2decho(text) + return decho2ansi(dtext) +end + +local function cecho2hecho(text) + local dtext = cecho2decho(text) + return decho2hecho(dtext) +end + +local function hecho2cecho(text) + local dtext = hecho2decho(text) + return decho2cecho(dtext) +end + +local function ansi2decho(tstring) + local cpattern = [=[\e\[([0-9;:]+)m]=] + local result = "" + local resets = {"39;49", "00", "0"} + local colours = { + [0] = color_table.ansiBlack, + [1] = color_table.ansiRed, + [2] = color_table.ansiGreen, + [3] = color_table.ansiYellow, + [4] = color_table.ansiBlue, + [5] = color_table.ansiMagenta, + [6] = color_table.ansiCyan, + [7] = color_table.ansiWhite, + } + local lightColours = { + [0] = color_table.ansiLightBlack, + [1] = color_table.ansiLightRed, + [2] = color_table.ansiLightGreen, + [3] = color_table.ansiLightYellow, + [4] = color_table.ansiLightBlue, + [5] = color_table.ansiLightMagenta, + [6] = color_table.ansiLightCyan, + [7] = color_table.ansiLightWhite, + } + + local function colorCodeToRGB(color, parts) + local rgb + if color ~= 8 then + rgb = colours[color] + else + if parts[2] == "5" then + local color_number = tonumber(parts[3]) + if color_number < 8 then + rgb = colours[color_number] + elseif color_number > 7 and color_number < 16 then + rgb = lightColours[color_number - 8] + else + rgb = color_table["ansi_" .. color_number] + end + elseif parts[2] == "2" then + local r = parts[4] or 0 + local g = parts[5] or 0 + local b = parts[6] or 0 + if r == "" then + r = 0 + end + if g == "" then + g = 0 + end + if b == "" then + b = 0 + end + rgb = {r, g, b} + end + end + return rgb + end + + for str, color in rex.split(tstring, cpattern) do + result = result .. str + if color then + if table.contains(resets, color) then + result = result .. "" + else + local parts + if color:find(";") then + parts = color:split(";") + else + parts = color:split(":") + end + local code = parts[1] + if code:starts("3") then + color = tonumber(code:sub(2, 2)) + local rgb = colorCodeToRGB(color, parts) + result = string.format("%s<%s,%s,%s>", result, rgb[1], rgb[2], rgb[3]) + elseif code:starts("4") then + color = tonumber(code:sub(2, 2)) + local rgb = colorCodeToRGB(color, parts) + result = string.format("%s<:%s,%s,%s>", result, rgb[1], rgb[2], rgb[3]) + elseif tonumber(code) >= 90 and tonumber(code) <= 97 then + local rgb = colours[tonumber(code) - 90] + result = string.format("%s<%s,%s,%s>", result, rgb[1], rgb[2], rgb[3]) + elseif tonumber(code) >= 100 and tonumber(code) <= 107 then + local rgb = colours[tonumber(code) - 100] + result = string.format("%s<:%s,%s,%s>", result, rgb[1], rgb[2], rgb[3]) + end + end + end + end + return result +end + +local function decho2ansi(text) + local colorPattern = _Echos.Patterns.Decimal[1] + local result = "" + for str, color, res in rex.split(text, colorPattern) do + result = result .. str + if color then + result = result .. rgbToAnsi(color:match("<(.+)>")) + end + if res then + result = result .. "\27[39;49m" + end + end + return result +end + +local function hecho2ansi(text) + local colorPattern = _Echos.Patterns.Hex[1] + local result = "" + for str, color, res in rex.split(text, colorPattern) do + result = result .. str + if color then + if color:sub(1, 1) == "|" then + color = color:gsub("|c", "#") + end + result = result .. hexToAnsi(color:sub(2, -1)) + end + if res then + result = result .. "\27[39;49m" + end + end + return result +end + +local function ansi2hecho(text) + local dtext = ansi2decho(text) + return decho2hecho(dtext) +end + +local function displayColors(options) + options = options or {} + local optionsType = type(options) + assert(optionsType == "table", "displayColors(options) argument error: options as table expects, got " .. optionsType) + options.cols = options.cols or 4 + options.search = options.search or "" + options.sort = options.sort or false + if options.removeDupes == nil then + options.removeDupes = true + end + if options.removeAnsi255 == nil then + options.removeAnsi255 = true + end + if options.columnSort == nil then + options.columnSort = true + end + if type(options.window) == "table" then + options.window = options.window.name + end + options.window = options.window or "main" + local color_table = options.color_table or color_table + local cols, search, sort = options.cols, options.search, options.sort + local colors = {} + for k, v in pairs(color_table) do + local color = {} + color.rgb = v + color.name = k + color.sort = {step(unpack(v))} + if include(k, options) and k:lower():find(search) then + table.insert(colors, color) + end + end + if sort then + table.sort(colors, sortColorsByName) + else + table.sort(colors, sortColorsByHue) + end + if options.columnSort then + local columns_table = chunkify(colors, cols) + local lines = #columns_table[1] + for i = 1, lines do + for j = 1, cols do + local color = columns_table[j][i] + if color then + echoColor(color, options) + end + end + echo(options.window, "\n") + end + else + local i = 1 + for _, k in ipairs(colors) do + echoColor(k, options) + if i == cols then + echo(options.window, "\n") + i = 1 + else + i = i + 1 + end + end + if i ~= 1 then + echo(options.window, "\n") + end + end +end + +local function cecho2string(text) + local pattern = _Echos.Patterns.Color[2] + local result = rex.gsub(text, pattern, "") + return result +end + +local function decho2string(text) + local pattern = _Echos.Patterns.Decimal[2] + local result = rex.gsub(text, pattern, "") + return result +end + +local function hecho2string(text) + local pattern = _Echos.Patterns.Hex[2] + local result = rex.gsub(text, pattern, "") + return result +end + +local function append2decho() + cheatConsole:clear() + cheatConsole:appendBuffer() + local str = copy2decho(cheatConsole.name) + cheatConsole:clear() + return str +end + +local function html2decho(text) + text = text:gsub(htmlHeaderPattern, "") + text = text:gsub("", "<%1:%2>") + text = text:gsub("
", "\n") + text = text:gsub("
", "") + return text +end + +local function html2cecho(text) + local dtext = html2decho(text) + return decho2cecho(dtext) +end + +local function html2hecho(text) + local dtext = html2decho(text) + return decho2hecho(dtext) +end + +local function html2ansi(text) + local dtext = html2decho(text) + return decho2ansi(dtext) +end + +local function html2string(text) + local dtext = html2decho(text) + return decho2string(text) +end + +local function consoleToString(options) + options = options or {} + options.win = options.win or "main" + options.format = options.format or "d" + options.start_line = options.start_line or 0 + if options.includeHtmlWrapper == nil then + options.includeHtmlWrapper = true + end + local console_line_count = options.win == "main" and getLineCount() or getLineCount(options.win) + if not options.end_line then + options.end_line = console_line_count + end + if options.end_line > console_line_count then + options.end_line = console_line_count + end + local start, finish, format = options.start_line, options.end_line, options.format + local current_x, current_y + if options.win == "main" then + current_x = getColumnNumber() + current_y = getLineNumber() + else + current_x = getColumnNumber(options.win) + current_y = getLineNumber(options.win) + end + + local function move(x, y) + if options.win == "main" then + return moveCursor(x, y) + else + return moveCursor(options.win, x, y) + end + end + local function gcl() + local win, raw + if options.win ~= "main" then + win = options.win + raw = getCurrentLine(win) + else + win = nil + raw = getCurrentLine() + end + if raw == "" then + return "" + end + if format == "h" then + return copy2html(win) + elseif format == "d" then + return copy2decho(win) + elseif format == "a" then + return decho2ansi(copy2decho(win)) + elseif format == "c" then + return decho2cecho(copy2decho(win)) + elseif format == "x" then + return decho2hecho(copy2decho(win)) + elseif format == "r" then + return raw + end + end + local lines = {} + if format == "h" and options.includeHtmlWrapper then + lines[#lines + 1] = htmlHeader + end + for line_number = start, finish do + move(0, line_number) + lines[#lines + 1] = gcl() + end + if format == "h" and options.includeHtmlWrapper then + lines[#lines + 1] = "
" + end + moveCursor(current_x, current_y) + return table.concat(lines, "\n") +end + +local function decho2html(text) + cheatConsole:clear() + text = text:gsub("\n", "
") + cheatConsole:decho(text) + local html = copy2html(cheatConsole.name) + cheatConsole:clear() + return html +end + +local function cecho2html(text) + local dtext = cecho2decho(text) + return decho2html(dtext) +end + +local function hecho2html(text) + local dtext = hecho2decho(text) + return decho2html(dtext) +end + +local function ansi2html(text) + local dtext = ansi2decho(text) + return decho2html(dtext) +end + +local function scientific_round(number, sigDigits) + local decimalPlace = string.find(number, "%.") + if not decimalPlace or (sigDigits < decimalPlace) then + local numberTable = {} + local count = 1 + for digit in string.gmatch(number, "%d") do + table.insert(numberTable, digit) + end + local endNumber = "" + for i, digit in ipairs(numberTable) do + if i < sigDigits then + endNumber = endNumber .. digit + end + if i == sigDigits then + if tonumber(numberTable[i + 1]) >= 5 then + endNumber = endNumber .. digit + 1 + else + endNumber = endNumber .. digit + end + end + if i > sigDigits and (not decimalPlace or (i < decimalPlace)) then + endNumber = endNumber .. "0" + end + end + return tonumber(endNumber) + else + local decimalDigits = sigDigits - decimalPlace + 1 + return tonumber(string.format("%" .. decimalPlace - 1 .. "." .. decimalDigits .. "f", number)) + end +end + +local function roundInt(number) + return math.floor(number + 0.5) +end + +function string.tobyte(self) + return (self:gsub('.', function(c) + return string.byte(c) + end)) +end + +function string.tocolor(self) + -- This next bit takes the string and 'unshuffles' it, breaking it into odds and evens + -- reverses the evens, then adds the odds to the new even set. So demonnic becomes cnoedmni + -- this makes sure that names which are similar in the beginning don't color the same + -- especially since we have to cut the number for the random seed due to OSX using a default + -- randomseed if you feed it something too large, which made every name longer than 7 characters + -- always the same color, no matter what it was. + local strTable = {} + local part1 = {} + local part2 = {} + self:gsub(".", function(c) + table.insert(strTable, c) + end) + for index, value in ipairs(strTable) do + if (index % 2 == 0) then + table.insert(part1, value) + else + table.insert(part2, value) + end + end + local newStr = string.reverse(table.concat(part1)) .. table.concat(part2) + -- end munging of the original string to get more uniqueness + math.randomseed(string.cut(newStr:tobyte(), 18)) + local r = math.random(0, 255) + local g = math.random(0, 255) + local b = math.random(0, 255) + math.randomseed(os.time()) + return {r, g, b} +end + +local function colorMunge(strForColor, strToEcho, format) + format = format or 'd' + local rgb = strForColor:tocolor() + local color + if format == "d" then + color = string.format("<%s>", table.concat(rgb, ",")) + elseif format == "c" then + color = string.format("<%s>", _color_name(rgb)) + elseif format == "h" then + color = string.format("#%02x%02x%02x", rgb[1], rgb[2], rgb[3]) + end + return color .. strToEcho +end + +local function colorMungeEcho(strForColor, strToEcho, format, win) + format = format or "d" + win = win or "main" + local str = colorMunge(strForColor, strToEcho, format) + local func + if format == "d" then + func = decho + end + if format == "c" then + func = cecho + end + if format == "h" then + func = hecho + end + if win == "main" then + func(str) + else + func(win, str) + end +end + +local function milliToHuman(milliseconds) + local totalseconds = math.floor(milliseconds / 1000) + milliseconds = milliseconds % 1000 + local seconds = totalseconds % 60 + local minutes = math.floor(totalseconds / 60) + local hours = math.floor(minutes / 60) + minutes = minutes % 60 + return string.format("%02d:%02d:%02d:%03d", hours, minutes, seconds, milliseconds) +end + +--- Takes a list table and returns it as a table of 'chunks'. If the table has 12 items and you ask for 3 chunks, each chunk will have 4 items in it +-- @tparam table tbl The table you want to turn into chunks. Must be traversable using ipairs() +-- @tparam number num_chunks The number of chunks to turn the table into +-- @usage local dt = require("MDK.demontools") +-- testTable = { "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten" } +-- display(dt.chunkify(testTable, 3)) +-- --displays the following +-- { +-- { +-- "one", +-- "two", +-- "three", +-- "four" +-- }, +-- { +-- "five", +-- "six", +-- "seven" +-- }, +-- { +-- "eight", +-- "nine", +-- "ten" +-- } +-- } + +function DemonTools.chunkify(tbl, num_chunks) + return chunkify(tbl, num_chunks) +end + +--- Takes an ansi colored text string and returns a cecho colored one +-- @tparam string text the text to convert +-- @usage dt.ansi2cecho("Test") +-- --returns "Test" +function DemonTools.ansi2cecho(text) + local dtext = ansi2decho(text) + return decho2cecho(dtext) +end + +--- Takes an ansi colored text string and returns a decho colored one. Handles 256 color SGR codes better than Mudlet's ansi2decho +-- @tparam string text the text to convert +-- @usage dt.ansi2decho("Test") --returns "<128,0,0>Test" +-- @usage dt.ansi2decho("[38:2::127:0:0mTest") --returns "<127,0,0>Test" +-- @usage ansi2decho("[38:2::127:0:0mTest") -- doesn't parse this format of colors and so returns "[38:2::127:0:0mTest" +function DemonTools.ansi2decho(text) + return ansi2decho(text) +end + +--- Takes an ansi colored text string and returns a hecho colored one +-- @tparam string text the text to convert +-- @usage dt.ansi2hecho("Test") +-- --returns "#800000Test" +function DemonTools.ansi2hecho(text) + return ansi2hecho(text) +end + +--- Takes an cecho colored text string and returns a decho colored one +-- @tparam string text the text to convert +-- @usage dt.cecho2decho("Test") --returns "<0,255,0>Test" +function DemonTools.cecho2decho(text) + return cecho2decho(text) +end + +--- Takes an cecho colored text string and returns an ansi colored one +-- @tparam string text the text to convert +-- @usage dt.cecho2ansi("Test") --returns "[38:2::0:255:0mTest" +function DemonTools.cecho2ansi(text) + return cecho2ansi(text) +end + +--- Takes an cecho colored text string and returns a hecho colored one +-- @tparam string text the text to convert +-- @usage dt.cecho2hecho("Test") --returns "#00ff00Test" +function DemonTools.cecho2hecho(text) + return cecho2hecho(text) +end + +--- Takes an decho colored text string and returns a cecho colored one +-- @tparam string text the text to convert +-- @usage dt.decho2cecho("<127,0,0:0,0,127>Test") --returns "Test" +function DemonTools.decho2cecho(text) + return decho2cecho(text) +end + +--- Takes an decho colored text string and returns an ansi colored one +-- @tparam string text the text to convert +-- @usage dt.decho2ansi("<127,0,0:0,0,127>Test") --returns "[38:2::127:0:0m[48:2::0:0:127mTest" +function DemonTools.decho2ansi(text) + return decho2ansi(text) +end + +--- Takes an decho colored text string and returns an hecho colored one +-- @tparam string text the text to convert +-- @usage dt.decho2hecho("<127,0,0:0,0,127>Test") --returns "#7f0000,00007fTest" +function DemonTools.decho2hecho(text) + return decho2hecho(text) +end + +--- Takes a decho colored text string and returns html. +-- @tparam string text the text to convert +function DemonTools.decho2html(text) + return decho2html(text) +end + +--- Takes a cecho colored text string and returns html. +-- @tparam string text the text to convert +function DemonTools.cecho2html(text) + return cecho2html(text) +end + +--- Takes a hecho colored text string and returns html. +-- @tparam string text the text to convert +function DemonTools.hecho2html(text) + return hecho2html(text) +end + +--- Takes an ansi colored text string and returns html. +-- @tparam string text the text to convert +function DemonTools.ansi2html(text) + return ansi2html(text) +end + +--- Takes an html colored string of the sort turned out by the DemonTools *2html functions and returns a cecho string +-- @tparam string text the text to convert +function DemonTools.html2cecho(text) + return html2cecho(text) +end + +--- Takes an html colored string of the sort turned out by the DemonTools *2html functions and returns a decho string +-- @tparam string text the text to convert +function DemonTools.html2decho(text) + return html2decho(text) +end + +--- Takes an html colored string of the sort turned out by the DemonTools *2html functions and returns an ansi string +-- @tparam string text the text to convert +function DemonTools.html2ansi(text) + return html2ansi(text) +end + +--- Takes an html colored string of the sort turned out by the DemonTools *2html functions and returns an hecho string +-- @tparam string text the text to convert +function DemonTools.html2hecho(text) + return html2hecho(text) +end + +--- Takes a cecho string and returns it without the formatting +-- @param text the text to transform +function DemonTools.cecho2string(text) + return cecho2string(text) +end + +--- Takes a decho string and returns it without the formatting +-- @param text the text to transform +function DemonTools.decho2string(text) + return decho2string(text) +end + +--- Takes a hecho string and returns it without the formatting +-- @param text the text to transform +function DemonTools.hecho2string(text) + return hecho2string(text) +end + +--- Takes an html colored string of the sort turned out by the DemonTools *2html functions and returns a clean string +function DemonTools.html2string(text) + return html2string(text) +end + +--- Takes an hecho colored text string and returns a ansi colored one +-- @tparam string text the text to convert +-- @usage dt.hecho2ansi("#7f0000,00007fTest") --returns "[38:2::127:0:0m[48:2::0:0:127mTest" +function DemonTools.hecho2ansi(text) + return hecho2ansi(text) +end + +--- Takes an hecho colored text string and returns a cecho colored one +-- @tparam string text the text to convert +-- @usage dt.hecho2cecho("#7f0000,00007fTest") --returns "Test" +function DemonTools.hecho2cecho(text) + return hecho2cecho(text) +end + +--- Takes an hecho colored text string and returns a decho colored one +-- @tparam string text the text to convert +-- @usage dt.hecho2decho("#7f0000,00007fTest") --returns "<127,0,0:0,0,127>Test" +function DemonTools.hecho2decho(text) + return hecho2decho(text) +end + +--- Takes the currently copy()ed item and returns it as a decho string +function DemonTools.append2decho() + return append2decho() +end + +--- Dump the contents of a miniconsole, user window, or the main window in one of several formats, as determined by a table of options +-- @tparam table options Table of options which controls which console and how it returns the data. +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
option namedescriptiondefault
formatWhat format to return the text as? 'h' for html, 'c' for cecho, 'a' for ansi, 'd' for decho, and 'x' for hecho"d"
winwhat console/window to dump the buffer of?"main"
start_lineWhat line to start dumping the buffer from?0
end_lineWhat line to stop dumping the buffer on?Last line of the console
includeHtmlWrapperIf the format is html, should it include the front and back portions required to make it a functioning html page?true
+function DemonTools.consoleToString(options) + return consoleToString(options) +end + +--- Alternative to Mudlet's showColors(), this one has additional options. +-- @tparam table options table of options which control the output of displayColors +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
option namedescriptiondefault
colsNumber of columsn wide to display the colors in4
searchIf not the empty string, will check colors against string.find using this property.
IE if set to "blue" only colors which include the word 'blue' would be listed
""
sortIf true, sorts alphabetically. Otherwise sorts based on the color valuefalse
echoOnlyIf true, colors will not be clickable linksfalse
windowWhat window/console to echo the colors out to."main"
removeDupesIf true, will remove snake_case entries and 'gray' in favor of 'grey'true
columnSortIf true, will print top-to-bottom, then left-to-right. false is like showColorstrue
justTextIf true, will echo the text in the color and leave the background black.
If false, the background will be the colour(like showColors).
false
color_tableTable of colors to display. If you provide your own table, it must be in the same format as Mudlet's own color_tablecolor_table
+function DemonTools.displayColors(options) + return displayColors(options) +end + +--- Rounds a number to the nearest whole integer +-- @param number the number to round off +-- @usage dt.roundInt(8.3) -- returns 8 +-- @usage dt.roundInt(10.7) -- returns 11 +function DemonTools.roundInt(number) + local num = tonumber(number) + local numType = type(num) + assert(numType == "number", string.format("DemonTools.roundInt(number): number as number expected, got %s", type(number))) + return roundInt(num) +end + +--- Rounds a number to a specified number of significant digits +-- @tparam number number the number to round +-- @tparam number sig_digits the number of significant digits to keep +-- @usage dt.scientific_round(1348290, 3) -- will return 1350000 +-- @usage dt.scientific_found(123.3452, 5) -- will return 123.34 +function DemonTools.scientific_round(number, sig_digits) + return scientific_round(number, sig_digits) +end + +--- Returns a color table {r,g,b} derived from str. Will return the same color every time for the same string. +-- @tparam string str the string to turn into a color. +-- @usage dt.string2color("Demonnic") --returns { 131, 122, 209 } +function DemonTools.string2color(str) + return string.tocolor(str) +end + +--- Returns a colored string where strForColor is run through DemonTools.string2color and applied to strToColor based on format. +-- @tparam string strForColor the string to turn into a color using DemonTools.string2color +-- @tparam string strToColor the string you want to color based on strForColor +-- @param format What format to use for the color portion. "d" for decho, "c" for cecho, or "h" for hecho. Defaults to "d" +-- @usage dt.colorMunge("Demonnic", "Test") --returns "<131,122,209>Test" +function DemonTools.colorMunge(strForColor, strToColor, format) + return colorMunge(strForColor, strToColor, format) +end + +--- Like colorMunge but also echos the result to win. +-- @tparam string strForColor the string to turn into a color using DemonTools.string2color +-- @tparam string strToEcho the string you want to color and echo based on strForColor +-- @param format What format to use for the color portion. "d" for decho, "c" for cecho, or "h" for hecho. Defaults to "d" +-- @param win the window to echo to. You must provide the format if you want to change the window. Defaults to "main" +function DemonTools.colorMungeEcho(strForColor, strToEcho, format, win) + colorMungeEcho(strForColor, strToEcho, format, win) +end + +--- Converts milliseconds to hours:minutes:seconds:milliseconds +-- @tparam number milliseconds the number of milliseconds to convert +-- @tparam boolean tbl if true, returns the time as a key/value table instead +-- @usage dt.milliToHuman(37194572) --returns "10:19:54:572" +-- @usage display(dt.milliToHuman(37194572, true)) +-- { +-- minutes = 19, +-- original = 37194572, +-- hours = 10, +-- milliseconds = 572, +-- seconds = 54 +-- } +function DemonTools.milliToHuman(milliseconds, tbl) + local human = milliToHuman(milliseconds) + local output + if tbl then + local timetbl = human:split(":") + output = { + hours = tonumber(timetbl[1]), + minutes = tonumber(timetbl[2]), + seconds = tonumber(timetbl[3]), + milliseconds = tonumber(timetbl[4]), + original = milliseconds, + } + else + output = human + end + return output +end + +--- Takes the name of a variable as a string and returns the value. "health" will return the value in varable health, "gmcp.Char.Vitals" will return the table at gmcp.Char.Vitals, etc +-- @tparam string variableString the string name of the variable you want the value of +-- @usage currentHP = 50 +-- dt.getValueAt("currentHP") -- returns 50 +function DemonTools.getValueAt(variableString) + return getValueAt(variableString) +end + +--- Returns if a file or directory exists on the filesystem +-- @tparam string path the path to the file or directory to check +function DemonTools.exists(path) + return exists(path) +end + +--- Returns if a path is a directory or not +-- @tparam string path the path to check +function DemonTools.isDir(path) + return isDir(path) +end + +--- Returns true if running on windows, false otherwise +function DemonTools.isWindows() + return isWindows() +end + +--- Creates a directory, creating each directory as necessary along the way. +-- @tparam string path the path to create +function DemonTools.mkdir_p(path) + return mkdir_p(path) +end + +DemonTools.htmlHeader = htmlHeader +DemonTools.htmlHeaderPattern = htmlHeaderPattern + +return DemonTools diff --git a/MDK/emco.lua b/MDK/emco.lua new file mode 100644 index 0000000..b26cc64 --- /dev/null +++ b/MDK/emco.lua @@ -0,0 +1,2173 @@ +--- Embeddable Multi Console Object. +-- This is essentially YATCO, but with some tweaks, updates, and it returns an object +-- similar to Geyser so that you can a.) have multiple of them and b.) easily embed it +-- into your existing UI as you would any other Geyser element. +-- @classmod EMCO +-- @author Damian Monogue +-- @copyright 2020 Damian Monogue +-- @copyright 2021 Damian Monogue +-- @license MIT, see LICENSE.lua +local EMCO = Geyser.Container:new({ + name = "TabbedConsoleClass", + timestampExceptions = {}, + path = "|h/log/|E/|y/|m/|d/", + fileName = "|N.|e", + bufferSize = "100000", + deleteLines = "1000", + blinkTime = 3, + tabFontSize = 8, + tabAlignment = "c", + fontSize = 9, + activeTabCSS = "", + inactiveTabCSS = "", + activeTabFGColor = "purple", + inactiveTabFGColor = "white", + activeTabBGColor = "<0,180,0>", + inactiveTabBGColor = "<60,60,60>", + consoleColor = "black", + tabBoxCSS = "", + tabBoxColor = "black", + consoleContainerCSS = "", + consoleContainerColor = "black", + tabHeight = 25, + leftMargin = 0, + rightMargin = 0, + topMargin = 0, + bottomMargin = 0, + gap = 1, + wrapAt = 300, + autoWrap = true, + logExclusions = {}, + gags = {}, +}) + +-- patch Geyser.MiniConsole if it does not have its own display method defined +if Geyser.MiniConsole.display == Geyser.display then + function Geyser.MiniConsole:display(...) + local arg = {...} + arg.n = table.maxn(arg) + if arg.n > 1 then + for i = 1, arg.n do + self:display(arg[i]) + end + else + self:echo((prettywrite(arg[1], ' ') or 'nil') .. '\n') + end + end +end + +local pathOfThisFile = (...):match("(.-)[^%.]+$") +local ok, content = pcall(require, pathOfThisFile .. "loggingconsole") +local LC +if ok then + LC = content +else + debugc("EMCO tried to require loggingconsole but could not because: " .. content) +end +--- Creates a new Embeddable Multi Console Object. +--
see https://github.com/demonnic/EMCO/wiki for information on valid constraints and defaults +-- @tparam table cons table of constraints which configures the EMCO. +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
option namedescriptiondefault
timestampdisplay timestamps on the miniconsoles?false
blankLineput a blank line between appends/echos?false
scrollbarsenable scrollbars for the miniconsoles?false
customTimestampColorif showing timestamps, use a custom color?false
mapTabshould we attach the Mudlet Mapper to this EMCO?false
mapTabNameWhich tab should we attach the map to? +--
If mapTab is true and you do not set this, it will throw an error
blinkFromAllshould tabs still blink, even if you're on the 'all' tab?false
preserveBackgroundpreserve the miniconsole background color during append()?false
gagwhen running :append(), should we also gag the line?false
timestampFormatFormat string for the timestamp. Uses getTime()"HH:mm:ss"
timestampBGColorCustom BG color to use for timestamps. Any valid Geyser.Color works."blue"
timestampFGColorCustom FG color to use for timestamps. Any valid Geyser.Color works"red"
allTabShould we send everything to an 'all' tab?false
allTabNameAnd which tab should we use for the 'all' tab?"All"
blinkShould we blink tabs that have been written to since you looked at them?false
blinkTimeHow long to wait between blinks, in seconds?3
fontSizeWhat font size to use for the miniconsoles?9
fontWhat font to use for the miniconsoles?
tabFontWhat font to use for the tabs?
activeTabCssWhat css to use for the active tab?""
inactiveTabCSSWhat css to use for the inactive tabs?""
activeTabFGColorWhat color to use for the text on the active tab. Any Geyser.Color works."purple"
inactiveTabFGColorWhat color to use for the text on the inactive tabs. Any Geyser.Color works."white"
activeTabBGColorWhat BG color to use for the active tab? Any Geyser.Color works. Overriden by activeTabCSS"<0,180,0>"
inactiveTabBGColorWhat BG color to use for the inactavie tabs? Any Geyser.Color works. Overridden by inactiveTabCSS"<60,60,60>"
consoleColorDefault background color for the miniconsoles. Any Geyser.Color works"black"
tabBoxCSStss for the entire tabBox (not individual tabs)""
tabBoxColorWhat color to use for the tabBox? Any Geyser.Color works. Overridden by tabBoxCSS"black"
consoleContainerCSSCSS to use for the container holding the miniconsoles""
consoleContainerColorColor to use for the container holding the miniconsole. Any Geyser.Color works. Overridden by consoleContainerCSS"black"
gapHow many pixels to place between the tabs and the miniconsoles?1
consolesList of the tabs for this EMCO in table format{ "All" }
allTabExclusionsList of the tabs which should never echo to the 'all' tab in table format{}
tabHeightHow many pixels high should the tabs be?25
autoWrapUse autoWrap for the miniconsoles?true
wrapAtHow many characters to wrap it, if autoWrap is turned off?300
leftMarginNumber of pixels to put between the left edge of the EMCO and the miniconsole?0
rightMarginNumber of pixels to put between the right edge of the EMCO and the miniconsole?0
bottomMarginNumber of pixels to put between the bottom edge of the EMCO and the miniconsole?0
topMarginNumber of pixels to put between the top edge of the miniconsole container, and the miniconsole? This is in addition to gap0
timestampExceptionsTable of tabnames which should not get timestamps even if timestamps are turned on{}
tabFontSizeFont size for the tabs8
tabBoldShould the tab text be bold? Boolean valuefalse
tabItalicsShould the tab text be italicized?false
tabUnderlineShould the tab text be underlined?false
tabAlignmentValid alignments are 'c', 'center', 'l', 'left', 'r', 'right', or '' to not include the alignment as part of the echo (to allow the stylesheet to handle it)'c'
commandLineShould we enable commandlines for the miniconsoles?false
cmdActionsA table with console names as keys, and values which are templates for the command to send. see the setCustomCommandline function for more{}
backgroundImagesA table containing definitions for the background images. Each entry should have a key the same name as the tab it applies to, with entries "image" which is the path to the image file,
and "mode" which determines how it is displayed. "border" stretches, "center" center, "tile" tiles, and "style". See Mudletwikilink for details.
{}
bufferSizeNumber of lines of scrollback to keep for the miniconsoles100000
deleteLinesNumber of lines to delete if a console's buffer fills up.1000
gagsA table of Lua patterns you wish to gag from being added to the EMCO. Useful for removing mob says and such example: {[[^A green leprechaun says, ".*"$]], "^Bob The Dark Lord of the Keep mutters darkly to himself.$"} see this tutorial on Lua patterns for more information.{}
+-- @tparam GeyserObject container The container to use as the parent for the EMCO +-- @return the newly created EMCO +function EMCO:new(cons, container) + local funcName = "EMCO:new(cons, container)" + cons = cons or {} + cons.type = cons.type or "tabbedConsole" + cons.consoles = cons.consoles or {"All"} + if cons.mapTab then + if not type(cons.mapTabName) == "string" then + self:ce(funcName, [["mapTab" is true, thus constraint "mapTabName" as string expected, got ]] .. type(cons.mapTabName)) + elseif not table.contains(cons.consoles, cons.mapTabName) then + self:ce(funcName, [["mapTabName" must be one of the consoles contained within constraint "consoles". Valid option for tha mapTab are: ]] .. + table.concat(cons.consoles, ",")) + end + end + cons.allTabExclusions = cons.allTabExclusions or {} + if not type(cons.allTabExclusions) == "table" then + self:se(funcName, "allTabExclusions must be a table if it is provided") + end + local me = self.parent:new(cons, container) + setmetatable(me, self) + self.__index = self + -- set some defaults. Almost all the defaults we had for YATCO, plus a few new ones + me.cmdActions = cons.cmdActions or {} + if not type(me.cmdActions) == "table" then + self:se(funcName, "cmdActions must be a table if it is provided") + end + me.backgroundImages = cons.backgroundImages or {} + if not type(me.backgroundImages) == "table" then + self:se(funcName, "backgroundImages must be a table if provided.") + end + if me:fuzzyBoolean(cons.timestamp) then + me:enableTimestamp() + else + me:disableTimestamp() + end + if me:fuzzyBoolean(cons.customTimestampColor) then + me:enableCustomTimestampColor() + else + me:disableCustomTimestampColor() + end + if me:fuzzyBoolean(cons.mapTab) then + me.mapTab = true + else + me.mapTab = false + end + if me:fuzzyBoolean(cons.blinkFromAll) then + me:enableBlinkFromAll() + else + me:disableBlinkFromAll() + end + if me:fuzzyBoolean(cons.preserveBackground) then + me:enablePreserveBackground() + else + me:disablePreserveBackground() + end + if me:fuzzyBoolean(cons.gag) then + me:enableGag() + else + me:disableGag() + end + me:setTimestampFormat(cons.timestampFormat or "HH:mm:ss") + me:setTimestampBGColor(cons.timestampBGColor or "blue") + me:setTimestampFGColor(cons.timestampFGColor or "red") + if me:fuzzyBoolean(cons.allTab) then + me:enableAllTab(cons.allTab) + else + me:disableAllTab() + end + if me:fuzzyBoolean(cons.blink) then + me:enableBlink() + else + me:disableBlink() + end + if me:fuzzyBoolean(cons.blankLine) then + me:enableBlankLine() + else + me:disableBlankLine() + end + if me:fuzzyBoolean(cons.scrollbars) then + me.scrollbars = true + else + me.scrollbars = false + end + me.tabUnderline = me:fuzzyBoolean(cons.tabUnderline) and true or false + me.tabBold = me:fuzzyBoolean(cons.tabBold) and true or false + me.tabItalics = me:fuzzyBoolean(cons.tabItalics) and true or false + me.commandLine = me:fuzzyBoolean(cons.commandLine) and true or false + me.consoles = cons.consoles + me.font = cons.font + me.tabFont = cons.tabFont + me.currentTab = "" + me.tabs = {} + me.tabsToBlink = {} + me.mc = {} + if me.blink then + me:enableBlink() + end + me.gags = {} + for _,pattern in ipairs(cons.gags or {}) do + me:addGag(pattern) + end + me:reset() + if me.allTab then + me:setAllTabName(me.allTabName or me.consoles[1]) + end + table.insert(EMCOHelper.items, me) + return me +end + +function EMCO:readYATCO() + local config + if demonnic and demonnic.chat and demonnic.chat.config then + config = demonnic.chat.config + else + cecho("(EMCO) Could not find demonnic.chat.config, nothing to convert\n") + return + end + local constraints = "EMCO:new({\n" + constraints = string.format("%s x = %d,\n", constraints, demonnic.chat.container.get_x()) + constraints = string.format("%s y = %d,\n", constraints, demonnic.chat.container.get_y()) + constraints = string.format("%s width = %d,\n", constraints, demonnic.chat.container.get_width()) + constraints = string.format("%s height = %d,\n", constraints, demonnic.chat.container.get_height()) + if config.timestamp then + constraints = string.format("%s timestamp = true,\n timestampFormat = \"%s\",\n", constraints, config.timestamp) + else + constraints = string.format("%s timestamp = false,\n", constraints) + end + if config.timestampColor then + constraints = string.format("%s customTimestampColor = true,\n", constraints) + else + constraints = string.format("%s customTimestampColor = false,\n", constraints) + end + if config.timestampFG then + constraints = string.format("%s timestampFGColor = \"%s\",\n", constraints, config.timestampFG) + end + if config.timestampBG then + constraints = string.format("%s timestampBGColor = \"%s\",\n", constraints, config.timestampBG) + end + if config.channels then + local channels = "consoles = {\n" + for _, channel in ipairs(config.channels) do + if _ == #config.channels then + channels = string.format("%s \"%s\"", channels, channel) + else + channels = string.format("%s \"%s\",\n", channels, channel) + end + end + channels = string.format("%s\n },\n", channels) + constraints = string.format([[%s %s]], constraints, channels) + end + if config.Alltab then + constraints = string.format("%s allTab = true,\n", constraints) + constraints = string.format("%s allTabName = \"%s\",\n", constraints, config.Alltab) + else + constraints = string.format("%s allTab = false,\n", constraints) + end + if config.Maptab and config.Maptab ~= "" then + constraints = string.format("%s mapTab = true,\n", constraints) + constraints = string.format("%s mapTabName = \"%s\",\n", constraints, config.Maptab) + else + constraints = string.format("%s mapTab = false,\n", constraints) + end + constraints = string.format("%s blink = %s,\n", constraints, tostring(config.blink)) + constraints = string.format("%s blinkFromAll = %s,\n", constraints, tostring(config.blinkFromAll)) + if config.fontSize then + constraints = string.format("%s fontSize = %d,\n", constraints, config.fontSize) + end + constraints = string.format("%s preserveBackground = %s,\n", constraints, tostring(config.preserveBackground)) + constraints = string.format("%s gag = %s,\n", constraints, tostring(config.gag)) + constraints = string.format("%s activeTabBGColor = \"<%s,%s,%s>\",\n", constraints, config.activeColors.r, config.activeColors.g, + config.activeColors.b) + constraints = string.format("%s inactiveTabBGColor = \"<%s,%s,%s>\",\n", constraints, config.inactiveColors.r, config.inactiveColors.g, + config.inactiveColors.b) + constraints = + string.format("%s consoleColor = \"<%s,%s,%s>\",\n", constraints, config.windowColors.r, config.windowColors.g, config.windowColors.b) + constraints = string.format("%s activeTabFGColor = \"%s\",\n", constraints, config.activeTabText) + constraints = string.format("%s inactiveTabFGColor = \"%s\"", constraints, config.inactiveTabText) + constraints = string.format("%s\n})", constraints) + return constraints +end + +--- Scans for the old YATCO configuration values and prints out a set of constraints to use. +-- with EMCO to achieve the same effect. Is just the invocation +function EMCO:miniConvertYATCO() + local constraints = self:readYATCO() + cecho( + "(EMCO) Found a YATCO config. Here are the constraints to use with EMCO(x,y,width, and height have been converted to their absolute values):\n\n") + echo(constraints .. "\n") +end + +--- Echos to the main console a script object you can add which will fully convert YATCO to EMCO. +-- This replaces the demonnic.chat variable with a newly created EMCO object, so that the main +-- functions used to place information on the consoles (append(), cecho(), etc) should continue to +-- work in the user's triggers and events. +function EMCO:convertYATCO() + local invocation = self:readYATCO() + local header = [[ + (EMCO) Found a YATCO config. Make a new script, then copy and paste the following output into it. + (EMCO) Afterward, uninstall YATCO (you can leave YATCOConfig until you're sure everything is right) and restart Mudlet + (EMCO) If everything looks right, you can uninstall YATCOConfig. + + +-- Copy everything below this line until the next line starting with -- +demonnic = demonnic or {} +demonnic.chat = ]] + cecho(string.format("%s%s\n--- End script\n", header, invocation)) +end + +function EMCO:checkTabPosition(position) + if position == nil then + return 0 + end + return tonumber(position) or type(position) +end + +function EMCO:checkTabName(tabName) + if not tostring(tabName) then + return "tabName as string expected, got" .. type(tabName) + end + tabName = tostring(tabName) + if table.contains(self.consoles, tabName) then + return "tabName must be unique, and we already have a tab named " .. tabName + else + return "clear" + end +end + +function EMCO.ae(funcName, message) + error(string.format("%s: Argument Error: %s", funcName, message)) +end + +function EMCO:ce(funcName, message) + error(string.format("%s:gg Constraint Error: %s", funcName, message)) +end + +--- Display the contents of one or more variables to an EMCO tab. like display() but targets the miniconsole +-- @tparam string tabName the name of the tab you want to display to +-- @param tabName string the tab to displayu to +-- @param item any The thing to display() +-- @param[opt] any item2 another thing to display() +function EMCO:display(tabName, ...) + local funcName = "EMCO:display(tabName, item)" + if not table.contains(self.consoles, tabName) then + self.ae(funcName, "tabName must be a tab which exists in this EMCO. valid options are: " .. table.concat(self.consoles, ",")) + end + self.mc[tabName]:display(...) +end + +--- Remove a tab from the EMCO +-- @param tabName string the name of the tab you want to remove from the EMCO +function EMCO:removeTab(tabName) + local funcName = "EMCO:removeTab(tabName)" + if not table.contains(self.consoles, tabName) then + self.ae(funcName, "tabName must be a tab which exists in this EMCO. valid options are: " .. table.concat(self.consoles, ",")) + end + table.remove(self.consoles, table.index_of(self.consoles, tabName)) + local window = self.mc[tabName] + local tab = self.tabs[tabName] + window:hide() + tab:hide() + self.tabBox:remove(tab) + self.tabBox:organize() + self.consoleContainer:remove(window) + self.mc[tabName] = nil + self.tabs[tabName] = nil +end + +--- Adds a tab to the EMCO object +-- @tparam string tabName the name of the tab to add +-- @tparam[opt] number position position in the tab switcher to put this tab +function EMCO:addTab(tabName, position) + local funcName = "EMCO:addTab(tabName, position)" + position = self:checkTabPosition(position) + if type(position) == "string" then + self.ae(funcName, "position as number expected, got " .. position) + end + local tabCheck = self:checkTabName(tabName) + if tabCheck ~= "clear" then + self.ae(funcName, tabCheck) + end + if position == 0 then + table.insert(self.consoles, tabName) + self:createComponentsForTab(tabName) + else + table.insert(self.consoles, position, tabName) + self:reset() + end +end + +--- Switches the active, visible tab of the EMCO to tabName +-- @param tabName string the name of the tab to show +function EMCO:switchTab(tabName) + local oldTab = self.currentTab + if oldTab ~= tabName and oldTab ~= "" then + self.mc[oldTab]:hide() + self.tabs[oldTab]:setStyleSheet(self.inactiveTabCSS) + self.tabs[oldTab]:setColor(self.inactiveTabBGColor) + self.tabs[oldTab]:echo(oldTab, self.inactiveTabFGColor) + if self.blink then + if self.allTab and tabName == self.allTabName then + self.tabsToBlink = {} + elseif self.tabsToBlink[tabName] then + self.tabsToBlink[tabName] = nil + end + end + end + self.tabs[tabName]:setStyleSheet(self.activeTabCSS) + self.tabs[tabName]:setColor(self.activeTabBGColor) + self.tabs[tabName]:echo(tabName, self.activeTabFGColor) + if oldTab and self.mc[oldTab] then + self.mc[oldTab]:hide() + end + self.mc[tabName]:show() + self.currentTab = tabName + if oldTab ~= tabName then + raiseEvent("EMCO tab change", self.name, oldTab, tabName) + end +end + +function EMCO:createComponentsForTab(tabName) + local tab = Geyser.Label:new({name = string.format("%sTab%s", self.name, tabName)}, self.tabBox) + if self.tabFont then + tab:setFont(self.tabFont) + end + tab:echo(tabName, self.inactiveTabFGColor) + tab:setAlignment(self.tabAlignment) + tab:setFontSize(self.tabFontSize) + tab:setItalics(self.tabItalics) + tab:setBold(self.tabBold) + tab:setUnderline(self.tabUnderline) + -- use the inactive CSS. It's "" if unset, which is ugly, but + tab:setStyleSheet(self.inactiveTabCSS) + -- set the BGColor if set. if the CSS is set it overrides the setColor, but if it's "" then the setColor actually covers that. + -- and we set a default for the inactiveBGColor + tab:setColor(self.inactiveTabBGColor) + tab:setClickCallback("EMCOHelper.switchTab", nil, string.format("%s+%s", self.name, tabName)) + self.tabs[tabName] = tab + local window + local windowConstraints = { + x = self.leftMargin, + y = self.topMargin, + height = string.format("-%dpx", self.bottomMargin), + width = string.format("-%dpx", self.rightMargin), + name = string.format("%sWindow%s", self.name, tabName), + commandLine = self.commandLine, + path = self:processTemplate(self.path, tabName), + fileName = self:processTemplate(self.fileName, tabName), + } + if table.contains(self.logExclusions, tabName) then + windowConstraints.log = false + end + local parent = self.consoleContainer + local mapTab = self.mapTab and tabName == self.mapTabName + if mapTab then + window = Geyser.Mapper:new(windowConstraints, parent) + else + if LC then + window = LC:new(windowConstraints, parent) + else + window = Geyser.MiniConsole:new(windowConstraints, parent) + end + if self.font then + window:setFont(self.font) + end + window:setFontSize(self.fontSize) + window:setColor(self.consoleColor) + if self.autoWrap then + window:enableAutoWrap() + else + window:setWrap(self.wrapAt) + end + if self.scrollbars then + window:enableScrollBar() + else + window:disableScrollBar() + end + window:setBufferSize(self.bufferSize, self.deleteLines) + end + self.mc[tabName] = window + if not mapTab then + self:setCmdAction(tabName) + end + window:hide() + self:processImage(tabName) +end + +--- Sets the buffer size and number of lines to delete for all managed miniconsoles. +--- @tparam number bufferSize number of lines of scrollback to maintain in the miniconsoles. Uses current value if nil is passed +--- @tparam number deleteLines number of line to delete if the buffer filles up. Uses current value if nil is passed +function EMCO:setBufferSize(bufferSize, deleteLines) + bufferSize = bufferSize or self.bufferSize + deleteLines = deleteLines or self.deleteLines + self.bufferSize = bufferSize + self.deleteLines = deleteLines + for tabName, window in pairs(self.mc) do + local mapTab = self.mapTab and tabName == self.mapTabName + if not mapTab then + window:setBufferSize(bufferSize, deleteLines) + end + end +end + +--- Sets the background image for a tab's console. use EMCO:resetBackgroundImage(tabName) to remove an image. +--- @tparam string tabName the tab to change the background image for. +--- @tparam string imagePath the path to the image file to use. +--- @tparam string mode the mode to use. Will default to "center" if not provided. +function EMCO:setBackgroundImage(tabName, imagePath, mode) + mode = mode or "center" + local tabNameType = type(tabName) + local imagePathType = type(imagePath) + local modeType = type(mode) + local funcName = "EMCO:setBackgroundImage(tabName, imagePath, mode)" + if tabNameType ~= "string" or not table.contains(self.consoles, tabName) then + self.ae(funcName, "tabName must be a string and an existing tab") + end + if imagePathType ~= "string" or not io.exists(imagePath) then + self.ae(funcName, "imagePath must be a string and point to an existing image file") + end + if modeType ~= "string" or not table.contains({"border", "center", "tile", "style"}, mode) then + self.ae(funcName, "mode must be one of 'border', 'center', 'tile', or 'style'") + end + local image = {image = imagePath, mode = mode} + self.backgroundImages[tabName] = image + self:processImage(tabName) +end + +--- Resets the background image on a tab's console, returning it to the background color +--- @tparam string tabName the tab to change the background image for. +function EMCO:resetBackgroundImage(tabName) + local tabNameType = type(tabName) + local funcName = "EMCO:resetBackgroundImage(tabName)" + if tabNameType ~= "string" or not table.contains(self.consoles, tabName) then + self.ae(funcName, "tabName must be a string and an existing tab") + end + self.backgroundImages[tabName] = nil + self:processImage(tabName) +end + +--- Does the work of actually setting/resetting the background image on a tab +--- @tparam string tabName the name of the tab to process the image for. +--- @local +function EMCO:processImage(tabName) + if self.mapTab and tabName == self.mapTabName then + return + end + local image = self.backgroundImages[tabName] + local window = self.mc[tabName] + if image then + if image.image and io.exists(image.image) then + window:setBackgroundImage(image.image, image.mode) + end + else + window:resetBackgroundImage() + end +end + +--- Replays the last numLines lines from the log for tabName +-- @param tabName the name of the tab to replay +-- @param numLines the number of lines to replay +function EMCO:replay(tabName, numLines) + if not LC then + return + end + if self.mapTab and tabName == self.mapTabName then + return + end + numLines = numLines or 10 + self.mc[tabName]:replay(numLines) +end + +--- Replays the last numLines in all miniconsoles +-- @param numLines +function EMCO:replayAll(numLines) + if not LC then + return + end + numLines = numLines or 10 + for _, tabName in ipairs(self.consoles) do + self:replay(tabName, numLines) + end +end + +--- Formats the string through EMCO's template. |E is replaced with the EMCO's name. |N is replaced with the tab's name. +-- @param str the string to replace tokens in +-- @param tabName optional, if included will be used for |N in the templated string. +function EMCO:processTemplate(str, tabName) + str = str:gsub("|E", self.name) + str = str:gsub("|N", tabName or "") + return str +end + +--- Sets the path for the EMCO for logging +-- @param path the template for the path. @see EMCO:new() +function EMCO:setPath(path) + if not LC then + return + end + path = path or self.path + self.path = path + path = self:processTemplate(path) + for name, window in pairs(self.mc) do + if not (self.mapTab and self.mapTabName == name) then + window:setPath(path) + end + end +end + +--- Sets the fileName for the EMCO for logging +-- @param fileName the template for the path. @see EMCO:new() +function EMCO:setFileName(fileName) + if not LC then + return + end + fileName = fileName or self.fileName + self.fileName = fileName + fileName = self:processTemplate(fileName) + for name, window in pairs(self.mc) do + if not (self.mapTab and self.mapTabName == name) then + window:setFileName(fileName) + end + end +end + +--- Sets the command action for a tab's command line. Can either be a template string to send where '|t' is replaced by the text sent, or a funnction which takes the text +--- @tparam string tabName the name of the tab to set the command action on +--- @param template the template for the commandline to use, or the function to run when enter is hit. +--- @usage myEMCO:setCmdAction("CT", "ct |t") -- will send everything in the CT tab's command line to CT by doing "ct Hi there!" if you type "Hi there!" in CT's command line +--- @usage myEMCO:setCmdAction("CT", function(txt) send("ct " .. txt) end) -- functionally the same as the above +function EMCO:setCmdAction(tabName, template) + template = template or self.cmdActions[tabName] + if template == "" then + template = nil + end + self.cmdActions[tabName] = template + local window = self.mc[tabName] + if template then + if type(template) == "string" then + window:setCmdAction(function(txt) + txt = template:gsub("|t", txt) + send(txt) + end) + elseif type(template) == "function" then + window:setCmdAction(template) + else + debugc(string.format( + "EMCO:setCmdAction(tabName, template): template must be a string or function if provided. Leaving CmdAction for tab %s be. Template type was: %s", + tabName, type(template))) + end + else + window:resetCmdAction() + end +end + +--- Resets the command action for tabName's miniconsole, which makes it work like the normal commandline +--- @tparam string tabName the name of the tab to reset the cmdAction for +function EMCO:resetCmdAction(tabName) + self.cmdActions[tabName] = nil + self.mc[tabName]:resetCmdAction() +end + +--- Gets the contents of tabName's cmdLine +--- @param tabName the name of the tab to get the commandline of +function EMCO:getCmdLine(tabName) + return self.mc[tabName]:getCmdLine() +end + +--- Prints to tabName's command line +--- @param tabName the tab whose command line you want to print to +--- @param txt the text to print to the command line +function EMCO:printCmd(tabName, txt) + return self.mc[tabName]:printCmd(txt) +end + +--- Clears tabName's command line +--- @tparam string tabName the tab whose command line you want to clear +function EMCO:clearCmd(tabName) + return self.mc[tabName]:clearCmd() +end + +--- Appends text to tabName's command line +--- @tparam string tabName the tab whose command line you want to append to +--- @tparam string txt the text to append to the command line +function EMCO:appendCmd(tabName, txt) + return self.mc[tabName]:appendCmd(txt) +end + +--- resets the object, redrawing everything +function EMCO:reset() + self:createContainers() + for _, tabName in ipairs(self.consoles) do + self:createComponentsForTab(tabName) + end + local default + if self.currentTab == "" then + default = self.allTabName or self.consoles[1] + else + default = self.currentTab + end + self:switchTab(default) +end + +function EMCO:createContainers() + self.tabBoxLabel = Geyser.Label:new({ + x = 0, + y = 0, + width = "100%", + height = tostring(tonumber(self.tabHeight) + 2) .. "px", + name = self.name .. "TabBoxLabel", + }, self) + self.tabBox = Geyser.HBox:new({x = 0, y = 0, width = "100%", height = "100%", name = self.name .. "TabBox"}, self.tabBoxLabel) + self.tabBoxLabel:setStyleSheet(self.tabBoxCSS) + self.tabBoxLabel:setColor(self.tabBoxColor) + + local heightPlusGap = tonumber(self.tabHeight) + tonumber(self.gap) + self.consoleContainer = Geyser.Label:new({ + x = 0, + y = tostring(heightPlusGap) .. "px", + width = "100%", + height = "-0px", + name = self.name .. "ConsoleContainer", + }, self) + self.consoleContainer:setStyleSheet(self.consoleContainerCSS) + self.consoleContainer:setColor(self.consoleContainerColor) +end + +function EMCO:stripTimeChars(str) + return string.gsub(string.trim(str), '[hHmMszZaApPdy:. ]', '') +end + +--- Expands boolean definitions to be more flexible. +--
True values are "true", "yes", "0", 0, and true +--
False values are "false", "no", "1", 1, false, and nil +-- @param bool item to test for truthiness +function EMCO:fuzzyBoolean(bool) + if type(bool) == "boolean" or bool == nil then + return bool + elseif tostring(bool) then + local truth = {"yes", "true", "0"} + local untruth = {"no", "false", "1"} + local boolstr = tostring(bool) + if table.contains(truth, boolstr) then + return true + elseif table.contains(untruth, boolstr) then + return false + else + return nil + end + else + return nil + end +end + +--- clears a specific tab +--- @tparam string tabName the name of the tab to clear +function EMCO:clear(tabName) + local funcName = "EMCO:clear(tabName)" + if not table.contains(self.consoles, tabName) then + self.ae(funcName, "tabName must be an existing tab") + end + if self.mapTab and self.mapTabName == tabName then + self.ae(funcName, "Cannot clear the map tab") + end + self.mc[tabName]:clear() +end + +--- clears all the tabs +function EMCO:clearAll() + for _, tabName in ipairs(self.consoles) do + if not self.mapTab or (tabName ~= self.mapTabName) then + self:clear(tabName) + end + end +end + +--- sets the font for all tabs +--- @tparam string font the font to use. +function EMCO:setTabFont(font) + self.tabFont = font + for _, tab in pairs(self.tabs) do + tab:setFont(font) + end +end + +--- sets the font for a single tab. If you use setTabFont this will be overridden +--- @tparam string tabName the tab to change the font of +--- @tparam string font the font to use for that tab +function EMCO:setSingleTabFont(tabName, font) + local funcName = "EMCO:setSingleTabFont(tabName, font)" + if not table.contains(self.consoles, tabName) then + self.ae(funcName, "tabName must be an existing tab") + end + self.tabs[tabName]:setFont(font) +end + +--- sets the font for all the miniconsoles +--- @tparam string font the name of the font to use +function EMCO:setFont(font) + local af = getAvailableFonts() + if not (af[font] or font == "") then + local err = "EMCO:setFont(font): attempt to call setFont with font '" .. font .. + "' which is not available, see getAvailableFonts() for valid options\n" + err = err .. "In the meantime, we will use a similar font which isn't the one you asked for but we hope is close enough" + debugc(err) + end + self.font = font + for _, tabName in pairs(self.consoles) do + if not self.mapTab or tabName ~= self.mapTabName then + self.mc[tabName]:setFont(font) + end + end +end + +--- sets the font for a specific miniconsole. If setFont is called this will be overridden +--- @tparam string tabName the name of window to set the font for +--- @tparam string font the name of the font to use +function EMCO:setSingleWindowFont(tabName, font) + local funcName = "EMCO:setSingleWindowFont(tabName, font)" + if not table.contains(self.consoles, tabName) then + self.ae(funcName, "tabName must be an existing tab") + end + local af = getAvailableFonts() + if not (af[font] or font == "") then + local err = "EMCO:setSingleWindowFont(tabName, font): attempt to call setFont with font '" .. font .. + "' which is not available, see getAvailableFonts() for valid options\n" + err = err .. "In the meantime, we will use a similar font which isn't the one you asked for but we hope is close enough" + debugc(err) + end + self.mc[tabName]:setFont(font) +end + +--- sets the font size for all tabs +--- @tparam number fontSize the font size to use for the tabs +function EMCO:setTabFontSize(fontSize) + self.tabFontSize = fontSize + for _, tab in pairs(self.tabs) do + tab:setFontSize(fontSize) + end +end + +--- Sets the alignment for all the tabs +-- @param alignment Valid alignments are 'c', 'center', 'l', 'left', 'r', 'right', or '' to not include the alignment as part of the echo +function EMCO:setTabAlignment(alignment) + self.tabAlignment = alignment + for _, tab in pairs(self.tabs) do + tab:setAlignment(self.tabAlignment) + end +end + +--- enables underline on all tabs +function EMCO:enableTabUnderline() + self.tabUnderline = true + for _, tab in pairs(self.tabs) do + tab:setUnderline(self.tabUnderline) + end +end + +--- disables underline on all tabs +function EMCO:disableTabUnderline() + self.tabUnderline = false + for _, tab in pairs(self.tabs) do + tab:setUnderline(self.tabUnderline) + end +end + +--- enables italics on all tabs +function EMCO:enableTabItalics() + self.tabItalics = true + for _, tab in pairs(self.tabs) do + tab:setItalics(self.tabItalics) + end +end + +--- enables italics on all tabs +function EMCO:disableTabItalics() + self.tabItalics = false + for _, tab in pairs(self.tabs) do + tab:setItalics(self.tabItalics) + end +end + +--- enables bold on all tabs +function EMCO:enableTabBold() + self.tabBold = true + for _, tab in pairs(self.tabs) do + tab:setBold(self.tabBold) + end +end + +--- disables bold on all tabs +function EMCO:disableTabBold() + self.tabBold = false + for _, tab in pairs(self.tabs) do + tab:setBold(self.tabBold) + end +end + +--- enables custom colors for the timestamp, if displayed +function EMCO:enableCustomTimestampColor() + self.customTimestampColor = true +end + +--- disables custom colors for the timestamp, if displayed +function EMCO:disableCustomTimestampColor() + self.customTimestampColor = false +end + +--- enables the display of timestamps +function EMCO:enableTimestamp() + self.timestamp = true +end + +--- disables the display of timestamps +function EMCO:disableTimestamp() + self.timestamp = false +end + +--- Sets the formatting for the timestamp, if enabled +-- @tparam string format Format string which describes the display of the timestamp. See: https://wiki.mudlet.org/w/Manual:Lua_Functions#getTime +function EMCO:setTimestampFormat(format) + local funcName = "EMCO:setTimestampFormat(format)" + local strippedFormat = self:stripTimeChars(format) + if strippedFormat ~= "" then + self.ae(funcName, + "format contains invalid time format characters. Please see https://wiki.mudlet.org/w/Manual:Lua_Functions#getTime for formatting information") + else + self.timestampFormat = format + end +end + +--- Sets the background color for the timestamp, if customTimestampColor is enabled. +-- @param color Color string suitable for decho or hecho, or color name eg "purple", or table of colors {r,g,b} +function EMCO:setTimestampBGColor(color) + self.timestampBGColor = color +end +--- Sets the foreground color for the timestamp, if customTimestampColor is enabled. +-- @param color Color string suitable for decho or hecho, or color name eg "purple", or table of colors {r,g,b} +function EMCO:setTimestampFGColor(color) + self.timestampFGColor = color +end + +--- Sets the 'all' tab name. +--
This is the name of the tab itself +-- @tparam string allTabName name of the tab to use as the all tab. Must be a tab which exists in the object. +function EMCO:setAllTabName(allTabName) + local funcName = "EMCO:setAllTabName(allTabName)" + local allTabNameType = type(allTabName) + if allTabNameType ~= "string" then + self.ae(funcName, "allTabName expected as string, got" .. allTabNameType) + end + if not table.contains(self.consoles, allTabName) then + self.ae(funcName, "allTabName must be the name of one of the console tabs. Valid options are: " .. table.concat(self.containers, ",")) + end + self.allTabName = allTabName +end + +--- Enables use of the 'all' tab +function EMCO:enableAllTab() + self.allTab = true +end + +--- Disables use of the 'all' tab +function EMCO:disableAllTab() + self.allTab = false +end + +--- Enables tying the Mudlet Mapper to one of the tabs. +--
mapTabName must be set, or this will error. Forces a redraw of the entire object +function EMCO:enableMapTab() + local funcName = "EMCO:enableMapTab()" + if not self.mapTabName then + error(funcName .. + ": cannot enable the map tab, mapTabName not set. try running :setMapTabName(mapTabName) first with the name of the tab you want to bind the map to") + end + self.mapTab = true + self:reset() +end + +--- disables binding the Mudlet Mapper to one of the tabs. +--
CAUTION: this may have unexpected behaviour, as you can only open one Mapper console per profile +-- so you can't really unbind it. Binding of the Mudlet Mapper is best decided at instantiation. +function EMCO:disableMapTab() + self.mapTab = false +end + +--- sets the name of the tab to bind the Mudlet Map. +--
Forces a redraw of the object +--
CAUTION: Mudlet only allows one Map object to be open at one time, so if you are going to attach the map to an object +-- you should probably do it at instantiation. +-- @tparam string mapTabName name of the tab to connect the Mudlet Map to. +function EMCO:setMapTabName(mapTabName) + local funcName = "EMCO:setMapTabName(mapTabName)" + local mapTabNameType = type(mapTabName) + if mapTabNameType ~= "string" then + self.ae(funcName, "mapTabName as string expected, got" .. mapTabNameType) + end + if not table.contains(self.consoles, mapTabName) and mapTabName ~= "" then + self.ae(funcName, "mapTabName must be one of the existing console tabs. Current tabs are: " .. table.concat(self.consoles, ",")) + end + self.mapTabName = mapTabName +end + +--- Enables tab blinking even if you're on the 'all' tab +function EMCO:enableBlinkFromAll() + self.enableBlinkFromAll = true +end + +--- Disables tab blinking when you're on the 'all' tab +function EMCO:disableBlinkFromAll() + self.enableBlinkFromAll = false +end + +--- Enables gagging of the line passed in to :append(tabName) +function EMCO:enableGag() + self.gag = true +end + +--- Disables gagging of the line passed in to :append(tabName) +function EMCO:disableGag() + self.gag = false +end + +--- Enables tab blinking when new information comes in to an inactive tab +function EMCO:enableBlink() + self.blink = true + if not self.blinkTimerID then + self.blinkTimerID = tempTimer(self.blinkTime, function() + self:doBlink() + end, true) + end +end + +--- Disables tab blinking when new information comes in to an inactive tab +function EMCO:disableBlink() + self.blink = false + if self.blinkTimerID then + killTimer(self.blinkTimerID) + self.blinkTimerID = nil + end +end + +--- Enables preserving the chat's background over the background of an incoming :append() +function EMCO:enablePreserveBackground() + self.preserveBackground = true +end + +--- Enables preserving the chat's background over the background of an incoming :append() +function EMCO:disablePreserveBackground() + self.preserveBackground = false +end + +--- Sets how long in seconds to wait between blinks +-- @tparam number blinkTime time in seconds to wait between blinks +function EMCO:setBlinkTime(blinkTime) + local funcName = "EMCO:setBlinkTime(blinkTime)" + local blinkTimeNumber = tonumber(blinkTime) + if not blinkTimeNumber then + self.ae(funcName, "blinkTime as number expected, got " .. type(blinkTime)) + else + self.blinkTime = blinkTimeNumber + if self.blinkTimerID then + killTimer(self.blinkTimerID) + end + self.blinkTimerID = tempTimer(blinkTimeNumber, function() + self:blink() + end, true) + end +end + +function EMCO:doBlink() + if self.hidden or self.auto_hidden or not self.blink then + return + end + for tab, _ in pairs(self.tabsToBlink) do + self.tabs[tab]:flash() + end +end + +--- Sets the font size of the attached consoles +-- @tparam number fontSize font size for attached consoles +function EMCO:setFontSize(fontSize) + local funcName = "EMCO:setFontSize(fontSize)" + local fontSizeNumber = tonumber(fontSize) + local fontSizeType = type(fontSize) + if not fontSizeNumber then + self.ae(funcName, "fontSize as number expected, got " .. fontSizeType) + else + self.fontSize = fontSizeNumber + for _, tabName in ipairs(self.consoles) do + if self.mapTab and tabName == self.mapTabName then + -- skip this one + else + local window = self.mc[tabName] + window:setFontSize(fontSizeNumber) + end + end + end +end + +function EMCO:adjustTabNames() + for _, console in ipairs(self.consoles) do + if console == self.currentTab then + self.tabs[console]:echo(console, self.activTabFGColor, 'c') + else + self.tabs[console]:echo(console, self.inactiveTabFGColor, 'c') + end + end +end + +function EMCO:adjustTabBackgrounds() + for _, console in ipairs(self.consoles) do + local tab = self.tabs[console] + if console == self.currentTab then + tab:setStyleSheet(self.activeTabCSS) + tab:setColor(self.activeBGColor) + else + tab:setStyleSheet(self.inactiveTabCSS) + tab:setColor(self.inactiveBGColor) + end + end +end + +--- Sets the FG color for the active tab +-- @param color Color string suitable for decho or hecho, or color name eg "purple", or table of colors {r,g,b} +function EMCO:setActiveTabFGColor(color) + self.activeTabFGColor = color + self:adjustTabNames() +end + +--- Sets the FG color for the inactive tab +-- @param color Color string suitable for decho or hecho, or color name eg "purple", or table of colors {r,g,b} +function EMCO:setInactiveTabFGColor(color) + self.inactiveTabFGColor = color + self:adjustTabNames() +end + +--- Sets the BG color for the active tab. +--
NOTE: If you set CSS for the active tab, it will override this setting. +-- @param color Color string suitable for decho or hecho, or color name eg "purple", or table of colors {r,g,b} +function EMCO:setActiveTabBGColor(color) + self.activeTabBGColor = color + self:adjustTabBackgrounds() +end + +--- Sets the BG color for the inactive tab. +--
NOTE: If you set CSS for the inactive tab, it will override this setting. +-- @param color Color string suitable for decho or hecho, or color name eg "purple", or table of colors {r,g,b} +function EMCO:setInactiveTabBGColor(color) + self.inactiveTabBGColor = color + self:adjustTabBackgrounds() +end + +--- Sets the BG color for the consoles attached to this object +-- @param color Color string suitable for decho or hecho, or color name eg "purple", or table of colors {r,g,b} +function EMCO:setConsoleColor(color) + self.consoleColor = color + self:adjustConsoleColors() +end + +function EMCO:adjustConsoleColors() + for _, console in ipairs(self.consoles) do + if self.mapTab and self.mapTabName == console then + -- skip Map + else + self.mc[console]:setColor(self.consoleColor) + end + end +end + +--- Sets the CSS to use for the tab box which contains the tabs for the object +-- @tparam string css The css styling to use for the tab box +function EMCO:setTabBoxCSS(css) + local funcName = "EMCHO:setTabBoxCSS(css)" + local cssType = type(css) + if cssType ~= "string" then + self.ae(funcName, "css as string expected, got " .. cssType) + else + self.tabBoxCSS = css + self:adjustTabBoxBackground() + end +end + +--- Sets the color to use for the tab box background +-- @param color Color string suitable for decho or hecho, or color name eg "purple", or table of colors {r,g,b} +function EMCO:setTabBoxColor(color) + self.tabBoxColor = color + self:adjustTabBoxBackground() +end + +function EMCO:adjustTabBoxBackground() + self.tabBoxLabel:setStyleSheet(self.tabBoxCSS) + self.tabBoxLabel:setColor(self.tabBoxColor) +end + +--- Sets the color for the container which holds the consoles attached to this object. +-- @param color Color string suitable for decho or hecho, or color name eg "purple", or table of colors {r,g,b} +function EMCO:setConsoleContainerColor(color) + self.consoleContainerColor = color + self:adjustConsoleContainerBackground() +end + +--- Sets the CSS to use for the container which holds the consoles attached to this object +-- @tparam string css CSS to use for the container +function EMCO:setConsoleContainerCSS(css) + self.consoleContainerCSS = css + self:adjustConsoleContainerBackground() +end + +function EMCO:adjustConsoleContainerBackground() + self.consoleContainer:setStyleSheet(self.consoleContainerCSS) + self.consoleContainer:setColor(self.consoleContainerColor) +end + +--- Sets the amount of space to use between the tabs and the consoles +-- @tparam number gap Number of pixels to keep between the tabs and consoles +function EMCO:setGap(gap) + local gapNumber = tonumber(gap) + local funcName = "EMCO:setGap(gap)" + local gapType = type(gap) + if not gapNumber then + self.ae(funcName, "gap expected as number, got " .. gapType) + else + self.gap = gapNumber + self:reset() + end +end + +--- Sets the height of the tabs in pixels +-- @tparam number tabHeight the height of the tabs for the object, in pixels +function EMCO:setTabHeight(tabHeight) + local tabHeightNumber = tonumber(tabHeight) + local funcName = "EMCO:setTabHeight(tabHeight)" + local tabHeightType = type(tabHeight) + if not tabHeightNumber then + self.ae(funcName, "tabHeight as number expected, got " .. tabHeightType) + else + self.tabHeight = tabHeightNumber + self:reset() + end +end + +--- Enables autowrap for the object, and by extension all attached consoles. +--
To enable autoWrap for a specific miniconsole only, call myEMCO.windows[tabName]:enableAutoWrap() +-- but be warned if you do this it may be overwritten by future calls to EMCO:enableAutoWrap() or :disableAutoWrap() +function EMCO:enableAutoWrap() + self.autoWrap = true + for _, console in ipairs(self.consoles) do + if self.mapTab and console == self.mapTabName then + -- skip the map + else + self.mc[console]:enableAutoWrap() + end + end +end + +--- Disables autowrap for the object, and by extension all attached consoles. +--
To disable autoWrap for a specific miniconsole only, call myEMCO.windows[tabName]:disableAutoWrap() +-- but be warned if you do this it may be overwritten by future calls to EMCO:enableAutoWrap() or :disableAutoWrap() +function EMCO:disableAutoWrap() + self.autoWrap = false + for _, console in ipairs(self.consoles) do + if self.mapTab and self.mapTabName == console then + -- skip Map + else + self.mc[console]:disableAutoWrap() + end + end +end + +--- Sets the number of characters to wordwrap the attached consoles at. +--
it is generally recommended to make use of autoWrap unless you need +-- a specific width for some reason +function EMCO:setWrap(wrapAt) + local funcName = "EMCO:setWrap(wrapAt)" + local wrapAtNumber = tonumber(wrapAt) + local wrapAtType = type(wrapAt) + if not wrapAtNumber then + self.ae(funcName, "wrapAt as number expect, got " .. wrapAtType) + else + self.wrapAt = wrapAtNumber + for _, console in ipairs(self.consoles) do + if self.mapTab and self.mapTabName == console then + -- skip the Map + else + self.mc[console]:setWrap(wrapAtNumber) + end + end + end +end + +--- Appends the current line from the MUD to a tab. +--
depending on this object's configuration, may gag the line +--
depending on this object's configuration, may gag the next prompt +-- @tparam string tabName The name of the tab to append the line to +-- @tparam boolean excludeAll if true, will exclude this from being mirrored to the allTab +function EMCO:append(tabName, excludeAll) + local funcName = "EMCO:append(tabName, excludeAll)" + local tabNameType = type(tabName) + local validTab = table.contains(self.consoles, tabName) + if tabNameType ~= "string" then + self.ae(funcName, "tabName as string expected, got " .. tabNameType) + elseif not validTab then + self.ae(funcName, "tabName must be a tab which is contained in this object. Valid tabnames are: " .. table.concat(self.consoles, ",")) + end + self:xEcho(tabName, nil, 'a', excludeAll) +end + +function EMCO:checkEchoArgs(funcName, tabName, message, excludeAll) + local tabNameType = type(tabName) + local messageType = type(message) + local validTabName = table.contains(self.consoles, tabName) + local excludeAllType = type(excludeAll) + local ae = self.ae + if tabNameType ~= "string" then + ae(funcName, "tabName as string expected, got " .. tabNameType) + elseif messageType ~= "string" then + ae(funcName, "message as string expected, got " .. messageType) + elseif not validTabName then + ae(funcName, "tabName must be the name of a tab attached to this object. Valid names are: " .. table.concat(self.consoles, ",")) + elseif excludeAllType ~= "nil" and excludeAllType ~= "boolean" then + ae(funcName, "optional argument excludeAll expected as boolean, got " .. excludeAllType) + end +end + +--- Adds a pattern to the gag list for the EMCO +--@tparam pattern string a Lua pattern to gag. http://lua-users.org/wiki/PatternsTutorial +--@return true if it was added, false if it was already included. +function EMCO:addGag(pattern) + if self.gags[pattern] then + return false + end + self.gags[pattern] = true + return true +end + +--- Removes a pattern from the gag list for the EMCO +--@tparam pattern string a Lua pattern to no longer gag. http://lua-users.org/wiki/PatternsTutorial +--@return true if it was removed, false if it was not there to remove. +function EMCO:removeGag(pattern) + if self.gags[pattern] then + self.gags[pattern] = nil + return true + end + return false +end + +--- Checks if a string matches any of the EMCO's gag patterns +--@tparam str string The text you're testing against the gag patterns +--@return false if it does not match any gag patterns. true and the matching pattern if it does match. +function EMCO:matchesGag(str) + for pattern,_ in pairs(self.gags) do + if str:match(pattern) then + return true, pattern + end + end + return false +end + +function EMCO:xEcho(tabName, message, xtype, excludeAll) + if self.mapTab and self.mapTabName == tabName then + error("You cannot send text to the Map tab") + end + local console = self.mc[tabName] + local allTab = (self.allTab and not excludeAll and not table.contains(self.allTabExclusions, tabName) and tabName ~= self.allTabName) and + self.mc[self.allTabName] or false + local ofr, ofg, ofb, obr, obg, obb + if xtype == "a" then + local line = getCurrentLine() + local mute, reason = self:matchesGag(line) + if mute then + debugc(f"{self.name}:append(tabName) denied because current line matches the pattern '{reason}'") + return + end + selectCurrentLine() + ofr, ofg, ofb = getFgColor() + obr, obg, obb = getBgColor() + if self.preserveBackground then + local r, g, b = Geyser.Color.parse(self.consoleColor) + setBgColor(r, g, b) + end + copy() + if self.preserveBackground then + setBgColor(obr, obg, obb) + end + deselect() + resetFormat() + else + local mute, reason = self:matchesGag(message) + if mute then + debugc(f"{self.name}:{xtype}(tabName, msg, excludeAll) denied because msg matches '{reason}'") + return + end + ofr, ofg, ofb = Geyser.Color.parse("white") + obr, obg, obb = Geyser.Color.parse(self.consoleColor) + end + if self.timestamp then + local colorString = "" + if self.customTimestampColor then + local tfr, tfg, tfb = Geyser.Color.parse(self.timestampFGColor) + local tbr, tbg, tbb = Geyser.Color.parse(self.timestampBGColor) + colorString = string.format("<%s,%s,%s:%s,%s,%s>", tfr, tfg, tfb, tbr, tbg, tbb) + else + colorString = string.format("<%s,%s,%s:%s,%s,%s>", ofr, ofg, ofb, obr, obg, obb) + end + local timestamp = getTime(true, self.timestampFormat) + local fullTimestamp = string.format("%s%s ", colorString, timestamp) + if not table.contains(self.timestampExceptions, tabName) then + console:decho(fullTimestamp) + end + if allTab and tabName ~= self.allTabName and not table.contains(self.timestampExceptions, self.allTabName) then + allTab:decho(fullTimestamp) + end + end + if self.blink and tabName ~= self.currentTab then + if not (self.allTabName == self.currentTab and not self.blinkFromAll) then + self.tabsToBlink[tabName] = true + end + end + if xtype == "a" then + console:appendBuffer() + if allTab then + allTab:appendBuffer() + end + if self.gag then + deleteLine() + if self.gagPrompt then + tempPromptTrigger(function() + deleteLine() + end, 1) + end + end + else + console[xtype](console, message) + if allTab then + allTab[xtype](allTab, message) + end + end + if self.blankLine then + console:echo("\n") + if allTab then + allTab:echo("\n") + end + end +end + +--- cecho to a tab, maintaining functionality +-- @tparam string tabName the name of the tab to cecho to +-- @tparam string message the message to cecho to that tab's console +-- @tparam boolean excludeAll if true, will exclude this from being mirrored to the allTab +function EMCO:cecho(tabName, message, excludeAll) + local funcName = "EMCO:cecho(tabName, message, excludeAll)" + self:checkEchoArgs(funcName, tabName, message, excludeAll) + self:xEcho(tabName, message, 'cecho', excludeAll) +end + +--- decho to a tab, maintaining functionality +-- @tparam string tabName the name of the tab to decho to +-- @tparam string message the message to decho to that tab's console +-- @tparam boolean excludeAll if true, will exclude this from being mirrored to the allTab +function EMCO:decho(tabName, message, excludeAll) + local funcName = "EMCO:decho(console, message, excludeAll)" + self:checkEchoArgs(funcName, tabName, message, excludeAll) + self:xEcho(tabName, message, 'decho', excludeAll) +end + +--- hecho to a tab, maintaining functionality +-- @tparam string tabName the name of the tab to hecho to +-- @tparam string message the message to hecho to that tab's console +-- @tparam boolean excludeAll if true, will exclude this from being mirrored to the allTab +function EMCO:hecho(tabName, message, excludeAll) + local funcName = "EMCO:hecho(console, message, excludeAll)" + self:checkEchoArgs(funcName, tabName, message, excludeAll) + self:xEcho(tabName, message, 'hecho', excludeAll) +end + +--- echo to a tab, maintaining functionality +-- @tparam string tabName the name of the tab to echo to +-- @tparam string message the message to echo to that tab's console +-- @tparam boolean excludeAll if true, will exclude this from being mirrored to the allTab +function EMCO:echo(tabName, message, excludeAll) + local funcName = "EMCO:echo(console, message, excludeAll)" + self:checkEchoArgs(funcName, tabName, message, excludeAll) + self:xEcho(tabName, message, 'echo', excludeAll) +end + +-- internal function used for type checking echoLink/Popup arguments +function EMCO:checkLinkArgs(funcName, tabName, text, commands, hints, excludeAll, popup) + local expectedType = popup and "table" or "string" + local textType = type(text) + local commandsType = type(commands) + local hintsType = type(hints) + local tabNameType = type(tabName) + local validTabName = table.contains(self.consoles, tabName) + local excludeAllType = type(excludeAll) + local sf = string.format + local ae = self.ae + if textType ~= "string" then + ae(funcName, "text as string expected, got " .. textType) + elseif commandsType ~= expectedType then + ae(funcName, sf("commands as %s expected, got %s", expectedType, commandsType)) + elseif hintsType ~= expectedType then + ae(funcName, sf("hints as %s expected, got %s", expectedType, hintsType)) + elseif tabNameType ~= "string" then + ae(funcName, "tabName as string expected, got " .. tabNameType) + elseif not validTabName then + ae(funcName, sf("tabName must be a tab which exists, tab %s could not be found", tabName)) + elseif self.mapTab and tabName == self.mapTabName then + ae(funcName, sf("You cannot echo to the map tab, and %s is configured as the mapTabName", tabName)) + elseif excludeAllType ~= "nil" and excludeAllType ~= "boolean" then + ae(funcName, "Optional argument excludeAll expected as boolean, got " .. excludeAllType) + end +end + +-- internal function used for handling echoLink/popup +function EMCO:xLink(tabName, linkType, text, commands, hints, useCurrentFormat, excludeAll) + local gag, reason = self:matchesGag(text) + if gag then + debugc(f"{self.name}:{linkType}(tabName, text, command, hint, excludeAll) denied because text matches '{reason}'") + return + end + local console = self.mc[tabName] + local allTab = (self.allTab and not excludeAll and not table.contains(self.allTabExclusions, tabName) and tabName ~= self.allTabName) and + self.mc[self.allTabName] or false + local arguments = {text, commands, hints, useCurrentFormat} + if self.timestamp then + local colorString = "" + if self.customTimestampColor then + local tfr, tfg, tfb = Geyser.Color.parse(self.timestampFGColor) + local tbr, tbg, tbb = Geyser.Color.parse(self.timestampBGColor) + colorString = string.format("<%s,%s,%s:%s,%s,%s>", tfr, tfg, tfb, tbr, tbg, tbb) + else + local ofr, ofg, ofb = Geyser.Color.parse("white") + local obr, obg, obb = Geyser.Color.parse(self.consoleColor) + colorString = string.format("<%s,%s,%s:%s,%s,%s>", ofr, ofg, ofb, obr, obg, obb) + end + local timestamp = getTime(true, self.timestampFormat) + local fullTimestamp = string.format("%s%s ", colorString, timestamp) + if not table.contains(self.timestampExceptions, tabName) then + console:decho(fullTimestamp) + end + if allTab and tabName ~= self.allTabName and not table.contains(self.timestampExceptions, self.allTabName) then + allTab:decho(fullTimestamp) + end + end + console[linkType](console, unpack(arguments)) + if allTab then + allTab[linkType](allTab, unpack(arguments)) + end +end + +--- cechoLink to a tab +-- @tparam string tabName the name of the tab to cechoLink to +-- @tparam string text the text underlying the link +-- @tparam string command the lua code to run in string format +-- @tparam string hint the tooltip hint to use for the link +-- @tparam boolean excludeAll if true, will exclude this from being mirrored to the allTab +function EMCO:cechoLink(tabName, text, command, hint, excludeAll) + local funcName = "EMCO:cechoLink(tabName, text, command, hint)" + self:checkLinkArgs(funcName, tabName, text, command, hint, excludeAll) + self:xLink(tabName, "cechoLink", text, command, hint, true, excludeAll) +end + +--- dechoLink to a tab +-- @tparam string tabName the name of the tab to dechoLink to +-- @tparam string text the text underlying the link +-- @tparam string command the lua code to run in string format +-- @tparam string hint the tooltip hint to use for the link +-- @tparam boolean excludeAll if true, will exclude this from being mirrored to the allTab +function EMCO:dechoLink(tabName, text, command, hint, excludeAll) + local funcName = "EMCO:dechoLink(tabName, text, command, hint)" + self:checkLinkArgs(funcName, tabName, text, command, hint, excludeAll) + self:xLink(tabName, "dechoLink", text, command, hint, true, excludeAll) +end + +--- hechoLink to a tab +-- @tparam string tabName the name of the tab to hechoLink to +-- @tparam string text the text underlying the link +-- @tparam string command the lua code to run in string format +-- @tparam string hint the tooltip hint to use for the link +-- @tparam boolean excludeAll if true, will exclude this from being mirrored to the allTab +function EMCO:hechoLink(tabName, text, command, hint, excludeAll) + local funcName = "EMCO:hechoLink(tabName, text, command, hint)" + self:checkLinkArgs(funcName, tabName, text, command, hint, excludeAll) + self:xLink(tabName, "hechoLink", text, command, hint, true, excludeAll) +end + +--- echoLink to a tab +-- @tparam string tabName the name of the tab to echoLink to +-- @tparam string text the text underlying the link +-- @tparam string command the lua code to run in string format +-- @tparam string hint the tooltip hint to use for the link +-- @tparam boolean useCurrentFormat use the format for the window or blue on black (hyperlink colors) +-- @tparam boolean excludeAll if true, will exclude this from being mirrored to the allTab +function EMCO:echoLink(tabName, text, command, hint, useCurrentFormat, excludeAll) + local funcName = "EMCO:echoLink(tabName, text, command, hint, useCurrentFormat)" + self:checkLinkArgs(funcName, tabName, text, command, hint, excludeAll) + self:xLink(tabName, "echoLink", text, command, hint, useCurrentFormat, excludeAll) +end + +--- cechoPopup to a tab +-- @tparam string tabName the name of the tab to cechoPopup to +-- @tparam string text the text underlying the link +-- @tparam table commands the lua code to run in string format +-- @tparam table hints the tooltip hint to use for the link +-- @tparam boolean excludeAll if true, will exclude this from being mirrored to the allTab +function EMCO:cechoPopup(tabName, text, commands, hints, excludeAll) + local funcName = "EMCO:cechoPopup(tabName, text, commands, hints)" + self:checkLinkArgs(funcName, tabName, text, commands, hints, excludeAll, true) + self:xLink(tabName, "cechoPopup", text, commands, hints, true, excludeAll) +end + +--- dechoPopup to a tab +-- @tparam string tabName the name of the tab to dechoPopup to +-- @tparam string text the text underlying the link +-- @tparam table commands the lua code to run in string format +-- @tparam table hints the tooltip hint to use for the link +-- @tparam boolean excludeAll if true, will exclude this from being mirrored to the allTab +function EMCO:dechoPopup(tabName, text, commands, hints, excludeAll) + local funcName = "EMCO:dechoPopup(tabName, text, commands, hints)" + self:checkLinkArgs(funcName, tabName, text, commands, hints, excludeAll, true) + self:xLink(tabName, "dechoPopup", text, commands, hints, true, excludeAll) +end + +--- hechoPopup to a tab +-- @tparam string tabName the name of the tab to hechoPopup to +-- @tparam string text the text underlying the link +-- @tparam table commands the lua code to run in string format +-- @tparam table hints the tooltip hint to use for the link +-- @tparam boolean excludeAll if true, will exclude this from being mirrored to the allTab +function EMCO:hechoPopup(tabName, text, commands, hints, excludeAll) + local funcName = "EMCO:hechoPopup(tabName, text, commands, hints)" + self:checkLinkArgs(funcName, tabName, text, commands, hints, excludeAll, true) + self:xLink(tabName, "hechoPopup", text, commands, hints, true, excludeAll) +end + +--- echoPopup to a tab +-- @tparam string tabName the name of the tab to echoPopup to +-- @tparam string text the text underlying the link +-- @tparam table commands the lua code to run in string format +-- @tparam table hints the tooltip hint to use for the link +-- @tparam boolean useCurrentFormat use the format for the window or blue on black (hyperlink colors) +-- @tparam boolean excludeAll if true, will exclude this from being mirrored to the allTab +function EMCO:echoPopup(tabName, text, commands, hints, useCurrentFormat, excludeAll) + local funcName = "EMCO:echoPopup(tabName, text, commands, hints, useCurrentFormat)" + self:checkLinkArgs(funcName, tabName, text, commands, hints, excludeAll, true) + self:xLink(tabName, "echoPopup", text, commands, hints, useCurrentFormat, excludeAll) +end + +--- adds a tab to the exclusion list for echoing to the allTab +-- @tparam string tabName the name of the tab to add to the exclusion list +function EMCO:addAllTabExclusion(tabName) + local funcName = "EMCO:addAllTabExclusion(tabName)" + self:validTabNameOrError(tabName, funcName) + if not table.contains(self.allTabExclusions, tabName) then + table.insert(self.allTabExclusions, tabName) + end +end + +--- removess a tab from the exclusion list for echoing to the allTab +-- @tparam string tabName the name of the tab to remove from the exclusion list +function EMCO:removeAllTabExclusion(tabName) + local funcName = "EMCO:removeAllTabExclusion(tabName)" + self:validTabNameOrError(tabName, funcName) + local index = table.index_of(self.allTabExclusions, tabName) + if index then + table.remove(self.allTabExclusions, index) + end +end + +function EMCO:validTabNameOrError(tabName, funcName) + local ae = self.ae + local tabNameType = type(tabName) + local validTabName = table.contains(self.consoles, tabName) + if tabNameType ~= "string" then + ae(funcName, "tabName as string expected, got " .. tabNameType) + elseif not validTabName then + ae(funcName, string.format("tabName %s does not exist in this EMCO. valid tabs: " .. table.concat(self.consoles, ","))) + end +end + +function EMCO:addTimestampException(tabName) + local funcName = "EMCO:addTimestampException(tabName)" + self:validTabNameOrError(tabName, funcName) + if not table.contains(self.timestampExceptions, tabName) then + table.insert(self.timestampExceptions, tabName) + end +end + +function EMCO:removeTimestampException(tabName) + local funcName = "EMCO:removeTimestampTabException(tabName)" + self:validTabNameOrError(tabName, funcName) + local index = table.index_of(self.timestampExceptions, tabName) + if index then + table.remove(self.timestampExceptions, index) + end +end + +--- Enable placing a blank line between all messages. +function EMCO:enableBlankLine() + self.blankLine = true +end + +--- Enable placing a blank line between all messages. +function EMCO:disableBlankLine() + self.blankLine = false +end + +--- Enable scrollbars for the miniconsoles +function EMCO:enableScrollbars() + self.scrollbars = true + self:adjustScrollbars() +end + +--- Disable scrollbars for the miniconsoles +function EMCO:disableScrollbars() + self.scrollbars = false + self:adjustScrollbars() +end + +function EMCO:adjustScrollbars() + for _, console in ipairs(self.consoles) do + if self.mapTab and self.mapTabName == console then + -- skip the Map tab + else + if self.scrollbars then + self.mc[console]:enableScrollBar() + else + self.mc[console]:disableScrollBar() + end + end + end +end + +EMCOHelper = EMCOHelper or {} +EMCOHelper.items = EMCOHelper.items or {} +function EMCOHelper:switchTab(designator) + local args = string.split(designator, "+") + local emcoName = args[1] + local tabName = args[2] + for _, emco in ipairs(EMCOHelper.items) do + if emco.name == emcoName then + emco:switchTab(tabName) + return + end + end +end + +--- Save an EMCO's configuration for reloading later. Filename is based on the EMCO's name property. +function EMCO:save() + local configtable = { + timestamp = self.timestamp, + blankLine = self.blankLine, + scrollbars = self.scrollbars, + customTimestampColor = self.customTimestampColor, + mapTab = self.mapTab, + mapTabName = self.mapTabName, + blinkFromAll = self.blinkFromAll, + preserveBackground = self.preserveBackground, + gag = self.gag, + timestampFormat = self.timestampFormat, + timestampBGColor = self.timestampBGColor, + allTab = self.allTab, + allTabName = self.allTabName, + blink = self.blink, + blinkTime = self.blinkTime, + fontSize = self.fontSize, + font = self.font, + tabFont = self.tabFont, + activeTabCSS = self.activeTabCSS, + inactiveTabCSS = self.inactiveTabCSS, + activeTabFGColor = self.activeTabFGColor, + activeTabBGColor = self.activeTabBGColor, + inactiveTabFGColor = self.inactiveTabFGColor, + inactiveTabBGColor = self.inactiveTabBGColor, + consoleColor = self.consoleColor, + tabBoxCSS = self.tabBoxCSS, + tabBoxColor = self.tabBoxColor, + consoleContainerCSS = self.consoleContainerCSS, + consoleContainerColor = self.consoleContainerColor, + gap = self.gap, + consoles = self.consoles, + allTabExclusions = self.allTabExclusions, + timestampExceptions = self.timestampExceptions, + tabHeight = self.tabHeight, + autoWrap = self.autoWrap, + wrapAt = self.wrapAt, + leftMargin = self.leftMargin, + rightMargin = self.rightMargin, + bottomMargin = self.bottomMargin, + topMargin = self.topMargin, + x = self.x, + y = self.y, + height = self.height, + width = self.width, + tabFontSize = self.tabFontSize, + tabBold = self.tabBold, + tabItalics = self.tabItalics, + tabUnderline = self.tabUnderline, + tabAlignment = self.tabAlignment, + bufferSize = self.bufferSize, + deleteLines = self.deleteLines, + logExclusions = self.logExclusions, + gags = self.gags, + } + local dirname = getMudletHomeDir() .. "/EMCO/" + local filename = dirname .. self.name .. ".lua" + if not (io.exists(dirname)) then + lfs.mkdir(dirname) + end + table.save(filename, configtable) +end + +--- Load and apply a saved config for this EMCO +function EMCO:load() + local dirname = getMudletHomeDir() .. "/EMCO/" + local filename = dirname .. self.name .. ".lua" + local configTable = {} + if io.exists(filename) then + table.load(filename, configTable) + else + debugc(string.format("Attempted to load config for EMCO named %s but the file could not be found. Filename: %s", self.name, filename)) + end + self.timestamp = configTable.timestamp + self.blankLine = configTable.blankLine + self.scrollbars = configTable.scrollbars + self.customTimestampColor = configTable.customTimestampColor + self.mapTab = configTable.mapTab + self.mapTabName = configTable.mapTabName + self.blinkFromAll = configTable.blinkFromAll + self.preserveBackground = configTable.preserveBackground + self.gag = configTable.gag + self.timestampFormat = configTable.timestampFormat + self.timestampBGColor = configTable.timestampBGColor + self.allTab = configTable.allTab + self.allTabName = configTable.allTabName + self.blink = configTable.blink + self.blinkTime = configTable.blinkTime + self.activeTabCSS = configTable.activeTabCSS + self.inactiveTabCSS = configTable.inactiveTabCSS + self.activeTabFGColor = configTable.activeTabFGColor + self.activeTabBGColor = configTable.activeTabBGColor + self.inactiveTabFGColor = configTable.inactiveTabFGColor + self.inactiveTabBGColor = configTable.inactiveTabBGColor + self.consoleColor = configTable.consoleColor + self.tabBoxCSS = configTable.tabBoxCSS + self.tabBoxColor = configTable.tabBoxColor + self.consoleContainerCSS = configTable.consoleContainerCSS + self.consoleContainerColor = configTable.consoleContainerColor + self.gap = configTable.gap + self.consoles = configTable.consoles + self.allTabExclusions = configTable.allTabExclusions + self.timestampExceptions = configTable.timestampExceptions + self.tabHeight = configTable.tabHeight + self.wrapAt = configTable.wrapAt + self.leftMargin = configTable.leftMargin + self.rightMargin = configTable.rightMargin + self.bottomMargin = configTable.bottomMargin + self.topMargin = configTable.topMargin + self.tabFontSize = configTable.tabFontSize + self.tabBold = configTable.tabBold + self.tabItalics = configTable.tabItalics + self.tabUnderline = configTable.tabUnderline + self.tabAlignment = configTable.tabAlignment + self.bufferSize = configTable.bufferSize + self.deleteLines = configTable.deleteLines + self.logExclusions = configTable.logExclusions + self.gags = configTable.gags + self:move(configTable.x, configTable.y) + self:resize(configTable.width, configTable.height) + self:reset() + if configTable.fontSize then + self:setFontSize(configTable.fontSize) + end + if configTable.font then + self:setFont(configTable.font) + end + if configTable.tabFont then + self:setTabFont(configTable.tabFont) + end + if configTable.autoWrap then + self:enableAutoWrap() + else + self:disableAutoWrap() + end +end + +--- Enables logging for tabName +--@tparam string tabName the name of the tab you want to enable logging for +function EMCO:enableTabLogging(tabName) + local console = self.mc[tabName] + if not console then + debugc(f"EMCO:enableTabLogging(tabName): tabName {tabName} not found.") + return + end + console.log = true + local logDisabled = table.index_of(self.logExclusions, tabName) + if logDisabled then table.remove(self.logExclusions, logDisabled) end +end + +--- Disables logging for tabName +--@tparam string tabName the name of the tab you want to disable logging for +function EMCO:disableTabLogging(tabName) + local console = self.mc[tabName] + if not console then + debugc(f"EMCO:disableTabLogging(tabName): tabName {tabName} not found.") + return + end + console.log = false + local logDisabled = table.index_of(self.logExclusions, tabName) + if not logDisabled then table.insert(self.logExclusions, tabName) end +end + +--- Enables logging on all EMCO managed consoles +function EMCO:enableAllLogging() + for _,console in pairs(self.mc) do + console.log = true + end + self.logExclusions = {} +end + +--- Disables logging on all EMCO managed consoles +function EMCO:disableAllLogging() + self.logExclusions = {} + for tabName,console in pairs(self.mc) do + console.log = false + self.logExclusions[#self.logExclusions+1] = tabName + end +end + +EMCO.parent = Geyser.Container + +return EMCO diff --git a/MDK/loggingconsole.lua b/MDK/loggingconsole.lua new file mode 100644 index 0000000..3efd8d5 --- /dev/null +++ b/MDK/loggingconsole.lua @@ -0,0 +1,459 @@ +--- MiniConsole with logging capabilities +-- @classmod LoggingConsole +-- @author Damian Monogue +-- @copyright 2020 Damian Monogue +-- @license MIT, see LICENSE.lua +local homedir = getMudletHomeDir():gsub("\\", "/") +local pathOfThisFile = (...):match("(.-)[^%.]+$") +local dt = require(pathOfThisFile .. "demontools") +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"} + +--- Creates and returns a new LoggingConsole. +-- @param cons table of constraints. Includes all the valid Geyser.MiniConsole constraints, plus +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
option namedescriptiondefault
logShould the miniconsole be logging?true
logFormat"h" for html, "t" for plaintext, "l" for log (with ansi)h
pathThe path the file lives in. It is templated.
|h is replaced by the profile homedir.
|y by 4 digit year.
|m by 2 digit month
|d by 2 digit day
|n by the name constraint
|e by the file extension (html for h logType, log for others)
"|h/log/consoleLogs/|y/|m/|d/"
fileNameThe name of the log file. It is templated, same as path above"|n.|e"
+-- @param container the container for the console +-- @usage +-- local LoggingConsole = require("MDK.loggingconsole") +-- myLoggingConsole = LoggingConsole:new({ +-- name = "my logging console", +-- x = 0, +-- y = 0, +-- height = 200, +-- width = 400, +-- }) -- just like making a miniconsole, really +function LoggingConsole:new(cons, container) + cons = cons or {} + local consType = type(cons) + assert(consType == "table", "LoggingConsole:new(cons, container): cons must be a valid table of constraints. Got: " .. consType) + local me = Geyser.MiniConsole:new(cons, container) + setmetatable(me, self) + self.__index = self + return me +end + +--- Returns the file extension of the logfile this console will log to +function LoggingConsole:getExtension() + local extension = "log" + if table.contains({"h", "html"}, self.logFormat) then + extension = "html" + end + return extension +end + +--- Returns a string with all templated items replaced +---@tparam string str The templated string to transform +---@local +function LoggingConsole:transformTemplate(str) + local ttbl = getTime() + local year = ttbl.year + local month = string.format("%02d", ttbl.month) + local day = string.format("%02d", ttbl.day) + local name = self.name + local extension = self:getExtension() + str = str:gsub("|h", homedir) + str = str:gsub("|y", year) + str = str:gsub("|m", month) + str = str:gsub("|d", day) + str = str:gsub("|n", name) + str = str:gsub("|e", extension) + return str +end + +--- Returns the path to the logfile for this console +function LoggingConsole:getPath() + local path = self:transformTemplate(self.path) + if not path:ends("/") then + path = path .. "/" + end + return path +end + +--- Sets the path to use for the log file. +-- @param path the path to put the log file in. It is templated.
|h is replaced by the profile homedir.
|y by 4 digit year.
|m by 2 digit month
|d by 2 digit day
|n by the name constraint
|e by the file extension (html for h logType, log for others) +function LoggingConsole:setPath(path) + self.path = path +end + +--- Returns the filename for the logfile for this console +function LoggingConsole:getFileName() + local fileName = self:transformTemplate(self.fileName) + return fileName +end + +--- Sets the fileName to use for the log file. +-- @param fileName the fileName to use for the logfile. It is templated.
|h is replaced by the profile homedir.
|y by 4 digit year.
|m by 2 digit month
|d by 2 digit day
|n by the name constraint
|e by the file extension (html for h logType, log for others) +function LoggingConsole:setFileName(fileName) + self.fileName = fileName +end + +--- Returns the pull path and filename for the logfile for this console +function LoggingConsole:getFullFilename() + local path = self:getPath() + local fileName = self:getFileName() + local fullPath = path .. fileName + return fullPath +end + +--- Turns logging for this console on +function LoggingConsole:enableLogging() + self.log = true +end + +--- Turns logging for this console off +function LoggingConsole:disableLogging() + self.log = false +end + +--- Creates the path for the logfile for this console if necessary +---@local +function LoggingConsole:createPathIfNotExists() + local path = self:transformTemplate(self.path) + if not path:ends("/") then + path = path .. "/" + end + if not exists(path) then + local ok, err = dt.mkdir_p(path) + if not ok then + assert(false, "Could not create directory for log files:" .. path .. "\n Reason was: " .. err) + end + end + return true +end + +--- Handles actually writing to the log file +---@local +function LoggingConsole:writeToLog(str) + local fileName = self:getFullFilename() + self:createPathIfNotExists() + if self:getExtension() == "html" then + if not io.exists(fileName) then + str = htmlHeader .. str + end + str = str + end + local file, err = io.open(fileName, "a") + if not file then + echo(err .. "\n") + return + end + file:write(str) + file:close() +end + +local parent = Geyser.MiniConsole +--- Handler function which does the lifting for c/d/h/echo and appendBuffer to provide the logfile writing functionality +---@param str the string to echo. Use "" for appends +---@param etype the type of echo. Valid are "c", "d", "h", "e", and "a" +---@param log Allows you to override the default behaviour defined by the .log property. Pass true to definitely log, false to skip logging. +---@local +function LoggingConsole:xEcho(str, etype, log) + if log == nil then + log = self.log + end + local logStr + local logType = self.logFormat + if logType:find("h") then + logType = "h" + else + logType = "l" + end + if etype == "d" then -- decho + if logType == "h" then + logStr = dt.decho2html(str) + elseif logType == "t" then + logStr = dt.decho2string(str) + else + logStr = dt.decho2ansi(str) + end + parent.decho(self, str) + elseif etype == "c" then -- cecho + if logType == "h" then + logStr = dt.cecho2html(str) + elseif logType == "t" then + logStr = dt.cecho2string(str) + else + logStr = dt.cecho2ansi(str) + end + parent.cecho(self, str) + elseif etype == "h" then -- hecho + if logType == "h" then + logStr = dt.hecho2html(str) + elseif logType == "t" then + logStr = dt.hecho2string(str) + else + logStr = dt.hecho2ansi(str) + end + parent.hecho(self, str) + elseif etype == "a" then -- append + str = dt.append2decho() + str = str .. "\n" + if logType == "h" then + logStr = dt.decho2html(str) + elseif logType == "t" then + logStr = dt.decho2string(str) + else + logStr = dt.decho2ansi(str) + end + parent.appendBuffer(self) + elseif etype == "e" then -- echo + if logType == "h" then + logStr = dt.decho2html(str) + else + logStr = str + end + parent.echo(self, str) + end + if log then + self:writeToLog(logStr) + end +end + +--- Does the actual lifting of echoing links/popups +-- @local +function LoggingConsole:xEchoLink(text, lType, command, hint, useFormat, log) + if log == nil then + log = self.log + end + local logStr = "" + if lType:starts("c") then + if self.logFormat == "h" then + logStr = dt.cecho2html(text) + elseif self.logFormat == "l" then + logStr = dt.cecho2ansi(text) + elseif self.logFormat == "t" then + logStr = dt.cecho2string(text) + end + if lType:ends("p") then + parent.cechoPopup(self, text, command, hint, useFormat) + else + parent.cechoLink(self, text, command, hint, useFormat) + end + elseif lType:starts("d") then + if self.logFormat == "h" then + logStr = dt.decho2html(text) + elseif self.logFormat == "l" then + logStr = dt.decho2ansi(text) + elseif self.logFormat == "t" then + logStr = dt.decho2string(text) + end + if lType:ends("p") then + parent.dechoPopup(self, text, command, hint, useFormat) + else + parent.dechoLink(self, text, command, hint, useFormat) + end + elseif lType:starts("h") then + if self.logFormat == "h" then + logStr = dt.hecho2html(text) + elseif self.logFormat == "l" then + logStr = dt.hecho2ansi(text) + elseif self.logFormat == "t" then + logStr = dt.hecho2string(text) + end + if lType:ends("p") then + parent.hechoPopup(self, text, command, hint, useFormat) + else + parent.hechoLink(self, text, command, hint, useFormat) + end + elseif lType:starts("e") then + logStr = text + if lType:ends("p") then + parent.echoPopup(self, text, command, hint, useFormat) + else + parent.echoLink(self, text, command, hint, useFormat) + end + end + if log then + self:writeToLog(logStr) + end +end + +--- cechoLink for LoggingConsole +-- @param text the text to use for the link +-- @param command the command to send when the link is clicked, as text. IE [[send("sleep")]] +-- @param hint A tooltip which is displayed when the mouse is over the link +-- @param log Should we log this line? Defaults to self.log if not passed. +function LoggingConsole:cechoLink(text, command, hint, log) + self:xEchoLink(text, "c", command, hint, true, log) +end + +--- dechoLink for LoggingConsole +-- @param text the text to use for the link +-- @param command the command to send when the link is clicked, as text. IE [[send("sleep")]] +-- @param hint A tooltip which is displayed when the mouse is over the link +-- @param log Should we log this line? Defaults to self.log if not passed. +function LoggingConsole:dechoLink(text, command, hint, log) + self:xEchoLink(text, "d", command, hint, true, log) +end + +--- hechoLink for LoggingConsole +-- @param text the text to use for the link +-- @param command the command to send when the link is clicked, as text. IE [[send("sleep")]] +-- @param hint A tooltip which is displayed when the mouse is over the link +-- @param log Should we log this line? Defaults to self.log if not passed. +function LoggingConsole:hechoLink(text, command, hint, log) + self:xEchoLink(text, "h", command, hint, true, log) +end + +--- echoLink for LoggingConsole +-- @param text the text to use for the link +-- @param command the command to send when the link is clicked, as text. IE [[send("sleep")]] +-- @param hint A tooltip which is displayed when the mouse is over the link +-- @param useCurrentFormat If set to true, will look like the text around it. If false it will be blue and underline. +-- @param log Should we log this line? Defaults to self.log if not passed. If you want to pass this you must pass in useCurrentFormat +-- @usage myLoggingConsole:echoLink("This is a link!", [[send("sleep")]], "sleep") -- text "This is a link" will send("sleep") when clicked and be blue w/ underline. Defaut log behaviour (self.log) +-- @usage myLoggingConsole:echoLink("This is a link!", [[send("sleep")]], "sleep", false, false) -- same as above, but forces it not to log regardless of self.log setting +-- @usage myLoggingConsole:echoLink("This is a link!", [[send("sleep")]], "sleep", true, true) -- same as above, but forces it to log regardless of self.log setting and the text will look like anything else echoed to the console. +function LoggingConsole:echoLink(text, command, hint, useCurrentFormat, log) + self:xEchoLink(text, "e", command, hint, useCurrentFormat, log) +end + +--- cechoPopup for LoggingConsole +-- @param text the text to use for the link +-- @param commands the commands to send when the popup is activated, as table. IE {[[send("sleep")]], [[send("stand")]]} +-- @param hints A tooltip which is displayed when the mouse is over the link. IE {{"sleep", "stand"}} +-- @param log Should we log this line? Defaults to self.log if not passed. +function LoggingConsole:cechoPopup(text, commands, hints, log) + self:xEchoLink(text, "cp", commands, hints, true, log) +end + +--- dechoPopup for LoggingConsole +-- @param text the text to use for the link +-- @param commands the commands to send when the popup is activated, as table. IE {[[send("sleep")]], [[send("stand")]]} +-- @param hints A tooltip which is displayed when the mouse is over the link. IE {{"sleep", "stand"}} +-- @param log Should we log this line? Defaults to self.log if not passed. +function LoggingConsole:dechoPopup(text, commands, hints, log) + self:xEchoLink(text, "dp", commands, hints, true, log) +end + +--- hechoPopup for LoggingConsole +-- @param text the text to use for the link +-- @param commands the commands to send when the popup is activated, as table. IE {[[send("sleep")]], [[send("stand")]]} +-- @param hints A tooltip which is displayed when the mouse is over the link. IE {{"sleep", "stand"}} +-- @param log Should we log this line? Defaults to self.log if not passed. +function LoggingConsole:hechoPopup(text, commands, hints, log) + self:xEchoLink(text, "hp", commands, hints, true, log) +end + +--- echoPopup for LoggingConsole +-- @param text the text to use for the link +-- @param commands the commands to send when the popup is activated, as table. IE {[[send("sleep")]], [[send("stand")]]} +-- @param hints A tooltip which is displayed when the mouse is over the link. IE {{"sleep", "stand"}} +-- @param useCurrentFormat If set to true, will look like the text around it. If false it will be blue and underline. +-- @param log Should we log this line? Defaults to self.log if not passed. If you want to pass this you must pass in useCurrentFormat +-- @usage myLoggingConsole:echoPopup("This is a link!", {[[send("sleep")]], [[send("stand")]], {"sleep", "stand"}) -- text "This is a link" will send("sleep") when clicked and be blue w/ underline. Defaut log behaviour (self.log) +-- @usage myLoggingConsole:echoPopup("This is a link!", {[[send("sleep")]], [[send("stand")]], {"sleep", "stand"}, false, false) -- same as above, but forces it not to log regardless of self.log setting +-- @usage myLoggingConsole:echoPopup("This is a link!", {[[send("sleep")]], [[send("stand")]], {"sleep", "stand"}, true, true) -- same as above, but forces it to log regardless of self.log setting and the text will look like anything else echoed to the console. +function LoggingConsole:echoPopup(text, commands, hints, useCurrentFormat, log) + self:xEchoLink(text, "ep", commands, hints, useCurrentFormat, log) +end + +--- Append copy()ed text to the console +-- @param log should we log this? +function LoggingConsole:appendBuffer(log) + self:xEcho("", "a", log) +end + +--- Append copy()ed text to the console +-- @param log should we log this? +function LoggingConsole:append(log) + self:xEcho("", "a", log) +end + +--- echo's a string to the console. +-- @param str the string to echo +-- @param log should this be logged? Used to override the .log constraint +function LoggingConsole:echo(str, log) + self:xEcho(str, "e", log) +end + +--- hecho's a string to the console. +-- @param str the string to hecho +-- @param log should this be logged? Used to override the .log constraint +function LoggingConsole:hecho(str, log) + self:xEcho(str, "h", log) +end + +--- decho's a string to the console. +-- @param str the string to decho +-- @param log should this be logged? Used to override the .log constraint +function LoggingConsole:decho(str, log) + self:xEcho(str, "d", log) +end + +--- cecho's a string to the console. +-- @param str the string to cecho +-- @param log should this be logged? Used to override the .log constraint +function LoggingConsole:cecho(str, log) + self:xEcho(str, "c", log) +end + +--- Replays the last X lines from the console's log file, if it exists +-- @param numberOfLines The number of lines to replay from the end of the file +function LoggingConsole:replay(numberOfLines) + local fileName = self:getFullFilename() + if not exists(fileName) then + return + end + 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) + end + local linesTbl = lines:split("\n") + local result + if #linesTbl <= numberOfLines then + result = lines + else + result = "" + local start = #linesTbl - numberOfLines + for index, str in ipairs(linesTbl) do + if index >= start then + result = string.format("%s\n%s", result, str) + end + end + end + self:decho(result, false) +end + +setmetatable(LoggingConsole, parent) + +return LoggingConsole diff --git a/MDK/sug.lua b/MDK/sug.lua new file mode 100644 index 0000000..3db8a73 --- /dev/null +++ b/MDK/sug.lua @@ -0,0 +1,255 @@ +--- Self Updating Gauge, extends Geyser.Gauge +-- @classmod SUG +-- @author Damian Monogue +-- @copyright 2020 Damian Monogue +-- @license MIT, see LICENSE.lua +local SUG = { + name = "SelfUpdatingGaugeClass", + active = true, + updateTime = 333, + currentVariable = "", + maxVariable = "", + defaultCurrent = 50, + defaultMax = 100, + textTemplate = " |c/|m |p%", + strict = true, +} + +-- ========== Copied from demontools.lua in order to cut the dependency for just this small functionality ========== +-- internal function, recursively digs for a value within subtables if possible +local function digForValue(dataFrom, tableTo) + if dataFrom == nil or table.size(tableTo) == 0 then + return dataFrom + else + local newData = dataFrom[tableTo[1]] + table.remove(tableTo, 1) + return digForValue(newData, tableTo) + end +end + +-- Internal function, used to turn a string variable name into a value +local function getValueAt(accessString) + if accessString == "" then + return nil + end + local tempTable = accessString:split("%.") + local accessTable = {} + for i, v in ipairs(tempTable) do + if tonumber(v) then + accessTable[i] = tonumber(v) + else + accessTable[i] = v + end + end + return digForValue(_G, accessTable) +end + +-- ========== End section copied from demontools.lua + +--- Creates a new Self Updating Gauge. +-- @tparam table cons table of options which control the Gauge's behaviour. In addition to all valid contraints for Geyser.Gauge, SUG adds: +--
+-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
namedescriptiondefault
activeboolean, if true starts the timer updatingtrue
updateTimeHow often should the gauge autoupdate? Milliseconds. 0 to disable the timer but still allow event updates333
currentVariableWhat variable will hold the 'current' value of the gauge? Pass the name as a string, IE "currentHP" or "gmcp.Char.Vitals.hp"""
maxVariableWhat variable will hold the 'current' value of the gauge? Pass the name as a string, IE "maxHP" or "gmcp.Char.Vitals.maxhp"""
textTemplateTemplate to use for the text on the gauge. "|c" replaced with current value, "|m" replaced with max value, "|p" replaced with the % full the gauge should be" |c/|m |p%"
defaultCurrentWhat value to use if the currentVariable points to nil or something which cannot be made a number?50
defaultMaxWhat value to use if the maxVariable points to nil or something which cannot be made a number?100
updateEventThe name of an event to listen for to perform an update. Can be run alongside or instead of the timer updates. Empty string to turn off""
+-- @param container The Geyser container for this gauge +-- @usage +-- local SUG = require("MDK.sug") --the following will watch "gmcp.Char.Vitals.hp" and "gmcp.Char.Vitals.maxhp" and update itself every 333 milliseconds +-- myGauge = SUG:new({ +-- name = "myGauge", +-- currentVariable = "gmcp.Char.Vitals.hp", --if this is nil, it will use the defaultCurrent of 50 +-- maxVariable = "gmcp.Char.Vitals.maxhp", --if this is nil, it will use the defaultMax of 100. +-- height = 50, +-- }) +function SUG:new(cons, container) + local funcName = "SUG:new(cons, container)" + cons = cons or {} + local consType = type(cons) + assert(consType == "table", string.format("%s: cons as table expected, got %s", funcName, consType)) + local me = SUG.parent:new(cons, container) + setmetatable(me, self) + self.__index = self + -- apply any styling requested + if me.cssFront then + if not me.cssBack then + me.cssBack = me.cssFront .. "background-color: black;" + end + me:setStyleSheet(me.cssFront, me.cssBack, me.cssText) + end + if me.active then + me:start() + end + me:update() + return me +end + +--- Set how often to update the gauge on a timer +-- @tparam number time time in milliseconds. 0 to disable the timer +function SUG:setUpdateTime(time) + if type(time) ~= "number" then + debugc("SUG:setUpdateTime(time) time as number expected, got " .. type(time)) + return + end + self.updateTime = time + if self.active then self:start() end +end + +--- Set the event to listen for to update the gauge +-- @tparam string event the name of the event to listen for, use "" to disable events without stopping any existing timers +function SUG:setUpdateEvent(event) + if type(event) ~= string then + debugc("SUG:setUpdateEvent(event) event name as string expected, got " .. type(event)) + return + end + self.updateEvent = event + if self.active then self:start() end +end + +--- Set the name of the variable the Self Updating Gauge watches for the 'current' value of the gauge +-- @tparam string variableName The name of the variable to get the current value for the gauge. For instance "currentHP", "gmcp.Char.Vitals.hp" etc +function SUG:setCurrentVariable(variableName) + local nameType = type(variableName) + local funcName = "SUG:setCurrentVariable(variableName)" + assert(nameType == "string", string.format("%s: variableName as string expected, got: %s", funcName, nameType)) + local val = getValueAt(variableName) + local valType = type(tonumber(val)) + assert(valType == "number", + string.format("%s: variableName must point to a variable which is a number or coercable into one. %s points to a %s", funcName, variableName, + type(val))) + self.currentVariable = variableName + self:update() +end + +--- Set the name of the variable the Self Updating Gauge watches for the 'max' value of the gauge +-- @tparam string variableName The name of the variable to get the max value for the gauge. For instance "maxHP", "gmcp.Char.Vitals.maxhp" etc. Set to "" to only check the current value +function SUG:setMaxVariable(variableName) + if variableName == "" then + self.maxVariable = variableName + self:update() + return + end + local nameType = type(variableName) + local funcName = "SUG:setMaxVariable(variableName)" + assert(nameType == "string", string.format("%s: variableName as string expected, got: %s", funcName, nameType)) + local val = getValueAt(variableName) + local valType = type(tonumber(val)) + assert(valType == "number", + string.format("%s: variableName must point to a variable which is a number or coercable into one. %s points to a %s", funcName, variableName, + type(val))) + self.maxVariable = variableName + self:update() +end + +--- Set the template for the Self Updating Gauge to set the text with. "|c" is replaced by the current value, "|m" is replaced by the max value, and "|p" is replaced by the percentage current/max +-- @tparam string template The template to use for the text on the gauge. If the max value is 200 and current is 68, then |c will be replace by 68, |m replaced by 200, and |p replaced by 34. +function SUG:setTextTemplate(template) + local templateType = type(template) + local funcName = "SUG:setTextTemplate(template)" + assert(templateType == "string", string.format("%s: template as string expected, got %s", funcName, templateType)) + self.textTemplate = template + self:update() +end + +--- Stops the Self Updating Gauge from updating +function SUG:stop() + self.active = false + if self.timer then + killTimer(self.timer) + self.timer = nil + end + if self.eventHandler then + killAnonymousEventHandler(self.eventHandler) + self.eventHandler = nil + end +end + +--- Starts the Self Updating Gauge updating. If it is already updating, it will restart it. +function SUG:start() + self:stop() + self.active = true + local update = function() self:update() end + if self.updateTime > 0 then + self.timer = tempTimer(self.updateTime / 1000, update, true) + end + local updateEvent = self.updateEvent + if updateEvent and updateEvent ~= "" and updateEvent ~= "*" then + self.eventHandler = registerAnonymousEventHandler(self.updateEvent, update) + end +end + +--- Reads the values from currentVariable and maxVariable, and updates the gauge's value and text. +function SUG:update() + local current = getValueAt(self.currentVariable) + local max = getValueAt(self.maxVariable) + current = tonumber(current) + max = tonumber(max) + if current == nil then + current = self.defaultCurrent + debugc(string.format( + "Self Updating Gauge named %s is trying to update with an invalid current value. Using the defaultCurrent instead. currentVariable: '%s' maxVariable: '%s'", + self.name, self.currentVariable, self.maxVariable)) + end + if max == nil then + max = self.defaultMax + if self.maxVariable ~= "" then + debugc(string.format( + "Self Updating Gauge named %s is trying to update with an invalid max value. Using the defaultCurrent instead. currentVariable: '%s' maxVariable: '%s'", + self.name, self.currentVariable, self.maxVariable)) + end + end + local text = self.textTemplate + local percent = math.floor((current / max * 100) + 0.5) + text = text:gsub("|c", current) + text = text:gsub("|m", max) + text = text:gsub("|p", percent) + self:setValue(current, max, text) +end + +SUG.parent = Geyser.Gauge +setmetatable(SUG, Geyser.Gauge) + +return SUG