From 954dc3c6079947d7b5a60dbf167eb289cc26548b Mon Sep 17 00:00:00 2001 From: Charles Click Date: Wed, 9 Oct 2024 23:11:30 +0000 Subject: [PATCH] * Add Demonic MDK. * Inject autostudy plugin into the UI directly. * Add a TLC automator for study. [SKIP CI] Do not run CI for this. --- src/aliases/autostudy/aliases.json | 30 + src/aliases/autostudy/study.add.lua | 3 + src/aliases/autostudy/study.auto.lua | 25 + src/aliases/autostudy/study.clear.lua | 5 + src/aliases/autostudy/study.help.lua | 0 src/aliases/autostudy/study.list.lua | 32 + src/aliases/autostudy/study.resume.lua | 5 + src/aliases/autostudy/study.start.lua | 8 + src/resources/MDK/Big.flf | 2204 +++++++++++ src/resources/MDK/LICENSE.lua | 48 + src/resources/MDK/README.md | 91 + src/resources/MDK/aliasmgr.lua | 164 + src/resources/MDK/chyron.lua | 235 ++ src/resources/MDK/computer.png | Bin 0 -> 351335 bytes src/resources/MDK/demontools.lua | 1486 ++++++++ src/resources/MDK/doc/classes/Chyron.html | 402 ++ src/resources/MDK/doc/classes/EMCO.html | 3347 +++++++++++++++++ .../MDK/doc/classes/LoggingConsole.html | 813 ++++ src/resources/MDK/doc/classes/Loginator.html | 546 +++ .../MDK/doc/classes/MasterMindSolver.html | 277 ++ src/resources/MDK/doc/classes/SUG.html | 407 ++ src/resources/MDK/doc/classes/SortBox.html | 593 +++ src/resources/MDK/doc/classes/TextGauge.html | 622 +++ src/resources/MDK/doc/classes/TimerGauge.html | 631 ++++ src/resources/MDK/doc/classes/aliasmgr.html | 415 ++ .../MDK/doc/classes/revisionator.html | 252 ++ src/resources/MDK/doc/classes/spinbox.html | 337 ++ src/resources/MDK/doc/index.html | 237 ++ src/resources/MDK/doc/ldoc.css | 315 ++ .../MDK/doc/modules/GradientMaker.html | 422 +++ src/resources/MDK/doc/modules/demontools.html | 1330 +++++++ src/resources/MDK/doc/modules/echofile.html | 552 +++ src/resources/MDK/doc/modules/figlet.html | 260 ++ src/resources/MDK/doc/modules/ftext.html | 1780 +++++++++ src/resources/MDK/doc/source/LICENSE.lua.html | 147 + .../MDK/doc/source/aliasmgr.lua.html | 263 ++ src/resources/MDK/doc/source/chyron.lua.html | 334 ++ .../MDK/doc/source/demontools.lua.html | 1585 ++++++++ .../MDK/doc/source/echofile.lua.html | 395 ++ src/resources/MDK/doc/source/emco.lua.html | 2452 ++++++++++++ src/resources/MDK/doc/source/figlet.lua.html | 366 ++ src/resources/MDK/doc/source/ftext.lua.html | 1796 +++++++++ .../MDK/doc/source/ftext_spec.lua.html | 546 +++ .../MDK/doc/source/gradientmaker.lua.html | 441 +++ .../MDK/doc/source/loggingconsole.lua.html | 560 +++ .../MDK/doc/source/loginator.lua.html | 555 +++ .../MDK/doc/source/mastermindsolver.lua.html | 353 ++ .../MDK/doc/source/revisionator.lua.html | 240 ++ src/resources/MDK/doc/source/schema.lua.html | 743 ++++ src/resources/MDK/doc/source/sortbox.lua.html | 613 +++ src/resources/MDK/doc/source/spinbox.lua.html | 580 +++ src/resources/MDK/doc/source/sug.lua.html | 354 ++ .../MDK/doc/source/textgauge.lua.html | 434 +++ .../MDK/doc/source/timergauge.lua.html | 628 ++++ src/resources/MDK/echofile.lua | 296 ++ src/resources/MDK/emco.lua | 2353 ++++++++++++ src/resources/MDK/figlet.lua | 267 ++ src/resources/MDK/ftext.lua | 1697 +++++++++ src/resources/MDK/ftext_spec.lua | 447 +++ src/resources/MDK/gradientmaker.lua | 342 ++ src/resources/MDK/loggingconsole.lua | 461 +++ src/resources/MDK/loginator.lua | 456 +++ src/resources/MDK/mastermindsolver.lua | 254 ++ src/resources/MDK/mdkversion.txt | 1 + src/resources/MDK/revisionator.lua | 141 + src/resources/MDK/schema.lua | 644 ++++ src/resources/MDK/sortbox.lua | 514 +++ src/resources/MDK/spinbox.lua | 481 +++ src/resources/MDK/sug.lua | 255 ++ src/resources/MDK/textgauge.lua | 335 ++ src/resources/MDK/timergauge.lua | 529 +++ src/scripts/autostudy/autostudy.lua | 33 + src/scripts/autostudy/scripts.json | 5 + src/triggers/autoresearch/triggers.json | 4 +- src/triggers/autostudy/study.botStart.lua | 3 + src/triggers/autostudy/study.copyOver.lua | 1 + src/triggers/autostudy/study.lua | 1 + src/triggers/autostudy/study.next.lua | 4 + src/triggers/autostudy/study.nextMove.lua | 1 + src/triggers/autostudy/triggers.json | 56 + 80 files changed, 40838 insertions(+), 2 deletions(-) create mode 100644 src/aliases/autostudy/aliases.json create mode 100644 src/aliases/autostudy/study.add.lua create mode 100644 src/aliases/autostudy/study.auto.lua create mode 100644 src/aliases/autostudy/study.clear.lua create mode 100644 src/aliases/autostudy/study.help.lua create mode 100644 src/aliases/autostudy/study.list.lua create mode 100644 src/aliases/autostudy/study.resume.lua create mode 100644 src/aliases/autostudy/study.start.lua create mode 100755 src/resources/MDK/Big.flf create mode 100755 src/resources/MDK/LICENSE.lua create mode 100755 src/resources/MDK/README.md create mode 100755 src/resources/MDK/aliasmgr.lua create mode 100755 src/resources/MDK/chyron.lua create mode 100755 src/resources/MDK/computer.png create mode 100755 src/resources/MDK/demontools.lua create mode 100755 src/resources/MDK/doc/classes/Chyron.html create mode 100755 src/resources/MDK/doc/classes/EMCO.html create mode 100755 src/resources/MDK/doc/classes/LoggingConsole.html create mode 100755 src/resources/MDK/doc/classes/Loginator.html create mode 100755 src/resources/MDK/doc/classes/MasterMindSolver.html create mode 100755 src/resources/MDK/doc/classes/SUG.html create mode 100755 src/resources/MDK/doc/classes/SortBox.html create mode 100755 src/resources/MDK/doc/classes/TextGauge.html create mode 100755 src/resources/MDK/doc/classes/TimerGauge.html create mode 100755 src/resources/MDK/doc/classes/aliasmgr.html create mode 100755 src/resources/MDK/doc/classes/revisionator.html create mode 100755 src/resources/MDK/doc/classes/spinbox.html create mode 100755 src/resources/MDK/doc/index.html create mode 100755 src/resources/MDK/doc/ldoc.css create mode 100755 src/resources/MDK/doc/modules/GradientMaker.html create mode 100755 src/resources/MDK/doc/modules/demontools.html create mode 100755 src/resources/MDK/doc/modules/echofile.html create mode 100755 src/resources/MDK/doc/modules/figlet.html create mode 100755 src/resources/MDK/doc/modules/ftext.html create mode 100755 src/resources/MDK/doc/source/LICENSE.lua.html create mode 100755 src/resources/MDK/doc/source/aliasmgr.lua.html create mode 100755 src/resources/MDK/doc/source/chyron.lua.html create mode 100755 src/resources/MDK/doc/source/demontools.lua.html create mode 100755 src/resources/MDK/doc/source/echofile.lua.html create mode 100755 src/resources/MDK/doc/source/emco.lua.html create mode 100755 src/resources/MDK/doc/source/figlet.lua.html create mode 100755 src/resources/MDK/doc/source/ftext.lua.html create mode 100755 src/resources/MDK/doc/source/ftext_spec.lua.html create mode 100755 src/resources/MDK/doc/source/gradientmaker.lua.html create mode 100755 src/resources/MDK/doc/source/loggingconsole.lua.html create mode 100755 src/resources/MDK/doc/source/loginator.lua.html create mode 100755 src/resources/MDK/doc/source/mastermindsolver.lua.html create mode 100755 src/resources/MDK/doc/source/revisionator.lua.html create mode 100755 src/resources/MDK/doc/source/schema.lua.html create mode 100755 src/resources/MDK/doc/source/sortbox.lua.html create mode 100755 src/resources/MDK/doc/source/spinbox.lua.html create mode 100755 src/resources/MDK/doc/source/sug.lua.html create mode 100755 src/resources/MDK/doc/source/textgauge.lua.html create mode 100755 src/resources/MDK/doc/source/timergauge.lua.html create mode 100755 src/resources/MDK/echofile.lua create mode 100755 src/resources/MDK/emco.lua create mode 100755 src/resources/MDK/figlet.lua create mode 100755 src/resources/MDK/ftext.lua create mode 100755 src/resources/MDK/ftext_spec.lua create mode 100755 src/resources/MDK/gradientmaker.lua create mode 100755 src/resources/MDK/loggingconsole.lua create mode 100755 src/resources/MDK/loginator.lua create mode 100755 src/resources/MDK/mastermindsolver.lua create mode 100755 src/resources/MDK/mdkversion.txt create mode 100755 src/resources/MDK/revisionator.lua create mode 100755 src/resources/MDK/schema.lua create mode 100755 src/resources/MDK/sortbox.lua create mode 100755 src/resources/MDK/spinbox.lua create mode 100755 src/resources/MDK/sug.lua create mode 100755 src/resources/MDK/textgauge.lua create mode 100755 src/resources/MDK/timergauge.lua create mode 100644 src/scripts/autostudy/autostudy.lua create mode 100644 src/scripts/autostudy/scripts.json create mode 100644 src/triggers/autostudy/study.botStart.lua create mode 100644 src/triggers/autostudy/study.copyOver.lua create mode 100644 src/triggers/autostudy/study.lua create mode 100644 src/triggers/autostudy/study.next.lua create mode 100644 src/triggers/autostudy/study.nextMove.lua create mode 100644 src/triggers/autostudy/triggers.json diff --git a/src/aliases/autostudy/aliases.json b/src/aliases/autostudy/aliases.json new file mode 100644 index 0000000..673d569 --- /dev/null +++ b/src/aliases/autostudy/aliases.json @@ -0,0 +1,30 @@ +[ + { + "name": "study.add", + "regex": "^studyadd (.*)$" + }, + { + "name": "study.start", + "regex": "^studystart$" + }, + { + "name": "study.auto", + "regex": "^studyauto$" + }, + { + "name": "study.list", + "regex": "^studylist$" + }, + { + "name": "study.clear", + "regex": "^studyclear$" + }, + { + "name": "study.resume", + "regex": "^studyresume$" + }, + { + "name": "study.help", + "regex": "^studyhelp$" + } +] \ No newline at end of file diff --git a/src/aliases/autostudy/study.add.lua b/src/aliases/autostudy/study.add.lua new file mode 100644 index 0000000..c9b9343 --- /dev/null +++ b/src/aliases/autostudy/study.add.lua @@ -0,0 +1,3 @@ +studyList = studyList or {} +table.insert(studyList, matches[2]) +cecho(matches[2] .. " added.") \ No newline at end of file diff --git a/src/aliases/autostudy/study.auto.lua b/src/aliases/autostudy/study.auto.lua new file mode 100644 index 0000000..c437650 --- /dev/null +++ b/src/aliases/autostudy/study.auto.lua @@ -0,0 +1,25 @@ +studyIndex = 1 +studyList = { + "camera", + "set", + "construction", + "data", + "sword", + "buffet", + "tome", + "datpad", + "bioplug", + "gloves", + "dna", + "beacon", + "terraform", + "support", + "formation", + "spice", + "models", + "bag" +} +cecho("Starting Ithor TLC Automator.") +send("bot start") +send("study " .. studyList[studyIndex]) +enableTrigger("autostudy") \ No newline at end of file diff --git a/src/aliases/autostudy/study.clear.lua b/src/aliases/autostudy/study.clear.lua new file mode 100644 index 0000000..ff1f496 --- /dev/null +++ b/src/aliases/autostudy/study.clear.lua @@ -0,0 +1,5 @@ +studyList = [] +studyIndex = 0 +disableTrigger("autostudy") + +cecho("Cleared list of study items.") \ No newline at end of file diff --git a/src/aliases/autostudy/study.help.lua b/src/aliases/autostudy/study.help.lua new file mode 100644 index 0000000..e69de29 diff --git a/src/aliases/autostudy/study.list.lua b/src/aliases/autostudy/study.list.lua new file mode 100644 index 0000000..261087e --- /dev/null +++ b/src/aliases/autostudy/study.list.lua @@ -0,0 +1,32 @@ +local TableMaker = require("lotj-ui.MDK.ftext").TableMaker + +if not studyList then + cecho("Studylist Empty.") +else + studyTable = TableMaker:new({ + title = "Studylist", + printTitle = true, + printHeaders = false, + separateRows = false, + frameColor = "" + }) + + studyTable:addColumn({ + name = "Index", + width = "5", + textColor = "" + }) + studyTable:addColumn({ + name = "Study Item", + width = "25", + textColor = "<>"green + }) + + for index, value in ipairs(studyList) do + studyTable:addRow({ + index, + value + }) + end +end +cecho(studyTable:assemble()) \ No newline at end of file diff --git a/src/aliases/autostudy/study.resume.lua b/src/aliases/autostudy/study.resume.lua new file mode 100644 index 0000000..4085a98 --- /dev/null +++ b/src/aliases/autostudy/study.resume.lua @@ -0,0 +1,5 @@ +if studyList and studyIndex then + send("study " .. studyList[studyIndex]) +else + send("You need to add items to your study list first. See studyhelp for commands.") +end \ No newline at end of file diff --git a/src/aliases/autostudy/study.start.lua b/src/aliases/autostudy/study.start.lua new file mode 100644 index 0000000..4a8fd67 --- /dev/null +++ b/src/aliases/autostudy/study.start.lua @@ -0,0 +1,8 @@ +if #studyList > 0 then + studyIndex = studyIndex or 1 + if studyIndex == 0 then studyIndex = 1 end + send("study " .. studyList[studyIndex]) + enableTrigger("autostudy") +else + cecho("No study list. See studyhelp for commands.") +end \ No newline at end of file diff --git a/src/resources/MDK/Big.flf b/src/resources/MDK/Big.flf new file mode 100755 index 0000000..b130c1b --- /dev/null +++ b/src/resources/MDK/Big.flf @@ -0,0 +1,2204 @@ +flf2a$ 8 6 59 15 10 0 24463 +Big by Glenn Chappell 4/93 -- based on Standard +Includes ISO Latin-1 +Greek characters by Bruce Jakeway +figlet release 2.2 -- November 1996 +Permission is hereby given to modify this font, as long as the +modifier's name is placed on a comment line. + +Modified by Paul Burton 12/96 to include new parameter +supported by FIGlet and FIGWin. May also be slightly modified for better use +of new full-width/kern/smush alternatives, but default output is NOT changed. + $@ + $@ + $@ + $@ + $@ + $@ + $@ + $@@ + _ @ + | |@ + | |@ + | |@ + |_|@ + (_)@ + @ + @@ + _ _ @ + ( | )@ + V V @ + $ @ + $ @ + $ @ + @ + @@ + _ _ @ + _| || |_ @ + |_ __ _|@ + _| || |_ @ + |_ __ _|@ + |_||_| @ + @ + @@ + _ @ + | | @ + / __)@ + \__ \@ + ( /@ + |_| @ + @ + @@ + _ __@ + (_) / /@ + / / @ + / / @ + / / _ @ + /_/ (_)@ + @ + @@ + @ + ___ @ + ( _ ) @ + / _ \/\@ + | (_> <@ + \___/\/@ + @ + @@ + _ @ + ( )@ + |/ @ + $ @ + $ @ + $ @ + @ + @@ + __@ + / /@ + | | @ + | | @ + | | @ + | | @ + \_\@ + @@ + __ @ + \ \ @ + | |@ + | |@ + | |@ + | |@ + /_/ @ + @@ + _ @ + /\| |/\ @ + \ ` ' / @ + |_ _|@ + / , . \ @ + \/|_|\/ @ + @ + @@ + @ + _ @ + _| |_ @ + |_ _|@ + |_| @ + $ @ + @ + @@ + @ + @ + @ + @ + _ @ + ( )@ + |/ @ + @@ + @ + @ + ______ @ + |______|@ + $ @ + $ @ + @ + @@ + @ + @ + @ + @ + _ @ + (_)@ + @ + @@ + __@ + / /@ + / / @ + / / @ + / / @ + /_/ @ + @ + @@ + ___ @ + / _ \ @ + | | | |@ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ + __ @ + /_ |@ + | |@ + | |@ + | |@ + |_|@ + @ + @@ + ___ @ + |__ \ @ + $) |@ + / / @ + / /_ @ + |____|@ + @ + @@ + ____ @ + |___ \ @ + __) |@ + |__ < @ + ___) |@ + |____/ @ + @ + @@ + _ _ @ + | || | @ + | || |_ @ + |__ _|@ + | | @ + |_| @ + @ + @@ + _____ @ + | ____|@ + | |__ @ + |___ \ @ + ___) |@ + |____/ @ + @ + @@ + __ @ + / / @ + / /_ @ + | '_ \ @ + | (_) |@ + \___/ @ + @ + @@ + ______ @ + |____ |@ + $/ / @ + / / @ + / / @ + /_/ @ + @ + @@ + ___ @ + / _ \ @ + | (_) |@ + > _ < @ + | (_) |@ + \___/ @ + @ + @@ + ___ @ + / _ \ @ + | (_) |@ + \__, |@ + / / @ + /_/ @ + @ + @@ + @ + _ @ + (_)@ + $ @ + _ @ + (_)@ + @ + @@ + @ + _ @ + (_)@ + $ @ + _ @ + ( )@ + |/ @ + @@ + __@ + / /@ + / / @ + < < @ + \ \ @ + \_\@ + @ + @@ + @ + ______ @ + |______|@ + ______ @ + |______|@ + @ + @ + @@ + __ @ + \ \ @ + \ \ @ + > >@ + / / @ + /_/ @ + @ + @@ + ___ @ + |__ \ @ + ) |@ + / / @ + |_| @ + (_) @ + @ + @@ + @ + ____ @ + / __ \ @ + / / _` |@ + | | (_| |@ + \ \__,_|@ + \____/ @ + @@ + @ + /\ @ + / \ @ + / /\ \ @ + / ____ \ @ + /_/ \_\@ + @ + @@ + ____ @ + | _ \ @ + | |_) |@ + | _ < @ + | |_) |@ + |____/ @ + @ + @@ + _____ @ + / ____|@ + | | $ @ + | | $ @ + | |____ @ + \_____|@ + @ + @@ + _____ @ + | __ \ @ + | | | |@ + | | | |@ + | |__| |@ + |_____/ @ + @ + @@ + ______ @ + | ____|@ + | |__ @ + | __| @ + | |____ @ + |______|@ + @ + @@ + ______ @ + | ____|@ + | |__ @ + | __| @ + | | @ + |_| @ + @ + @@ + _____ @ + / ____|@ + | | __ @ + | | |_ |@ + | |__| |@ + \_____|@ + @ + @@ + _ _ @ + | | | |@ + | |__| |@ + | __ |@ + | | | |@ + |_| |_|@ + @ + @@ + _____ @ + |_ _|@ + | | @ + | | @ + _| |_ @ + |_____|@ + @ + @@ + _ @ + | |@ + | |@ + _ | |@ + | |__| |@ + \____/ @ + @ + @@ + _ __@ + | |/ /@ + | ' / @ + | < @ + | . \ @ + |_|\_\@ + @ + @@ + _ @ + | | @ + | | @ + | | @ + | |____ @ + |______|@ + @ + @@ + __ __ @ + | \/ |@ + | \ / |@ + | |\/| |@ + | | | |@ + |_| |_|@ + @ + @@ + _ _ @ + | \ | |@ + | \| |@ + | . ` |@ + | |\ |@ + |_| \_|@ + @ + @@ + ____ @ + / __ \ @ + | | | |@ + | | | |@ + | |__| |@ + \____/ @ + @ + @@ + _____ @ + | __ \ @ + | |__) |@ + | ___/ @ + | | @ + |_| @ + @ + @@ + ____ @ + / __ \ @ + | | | |@ + | | | |@ + | |__| |@ + \___\_\@ + @ + @@ + _____ @ + | __ \ @ + | |__) |@ + | _ / @ + | | \ \ @ + |_| \_\@ + @ + @@ + _____ @ + / ____|@ + | (___ @ + \___ \ @ + ____) |@ + |_____/ @ + @ + @@ + _______ @ + |__ __|@ + | | @ + | | @ + | | @ + |_| @ + @ + @@ + _ _ @ + | | | |@ + | | | |@ + | | | |@ + | |__| |@ + \____/ @ + @ + @@ + __ __@ + \ \ / /@ + \ \ / / @ + \ \/ / @ + \ / @ + \/ @ + @ + @@ + __ __@ + \ \ / /@ + \ \ /\ / / @ + \ \/ \/ / @ + \ /\ / @ + \/ \/ @ + @ + @@ + __ __@ + \ \ / /@ + \ V / @ + > < @ + / . \ @ + /_/ \_\@ + @ + @@ + __ __@ + \ \ / /@ + \ \_/ / @ + \ / @ + | | @ + |_| @ + @ + @@ + ______@ + |___ /@ + $/ / @ + / / @ + / /__ @ + /_____|@ + @ + @@ + ___ @ + | _|@ + | | @ + | | @ + | | @ + | |_ @ + |___|@ + @@ + __ @ + \ \ @ + \ \ @ + \ \ @ + \ \ @ + \_\@ + @ + @@ + ___ @ + |_ |@ + | |@ + | |@ + | |@ + _| |@ + |___|@ + @@ + /\ @ + |/\|@ + $ @ + $ @ + $ @ + $ @ + @ + @@ + @ + @ + @ + @ + @ + $ @ + ______ @ + |______|@@ + _ @ + ( )@ + \|@ + $ @ + $ @ + $ @ + @ + @@ + @ + @ + __ _ @ + / _` |@ + | (_| |@ + \__,_|@ + @ + @@ + _ @ + | | @ + | |__ @ + | '_ \ @ + | |_) |@ + |_.__/ @ + @ + @@ + @ + @ + ___ @ + / __|@ + | (__ @ + \___|@ + @ + @@ + _ @ + | |@ + __| |@ + / _` |@ + | (_| |@ + \__,_|@ + @ + @@ + @ + @ + ___ @ + / _ \@ + | __/@ + \___|@ + @ + @@ + __ @ + / _|@ + | |_ @ + | _|@ + | | @ + |_| @ + @ + @@ + @ + @ + __ _ @ + / _` |@ + | (_| |@ + \__, |@ + __/ |@ + |___/ @@ + _ @ + | | @ + | |__ @ + | '_ \ @ + | | | |@ + |_| |_|@ + @ + @@ + _ @ + (_)@ + _ @ + | |@ + | |@ + |_|@ + @ + @@ + _ @ + (_)@ + _ @ + | |@ + | |@ + | |@ + _/ |@ + |__/ @@ + _ @ + | | @ + | | __@ + | |/ /@ + | < @ + |_|\_\@ + @ + @@ + _ @ + | |@ + | |@ + | |@ + | |@ + |_|@ + @ + @@ + @ + @ + _ __ ___ @ + | '_ ` _ \ @ + | | | | | |@ + |_| |_| |_|@ + @ + @@ + @ + @ + _ __ @ + | '_ \ @ + | | | |@ + |_| |_|@ + @ + @@ + @ + @ + ___ @ + / _ \ @ + | (_) |@ + \___/ @ + @ + @@ + @ + @ + _ __ @ + | '_ \ @ + | |_) |@ + | .__/ @ + | | @ + |_| @@ + @ + @ + __ _ @ + / _` |@ + | (_| |@ + \__, |@ + | |@ + |_|@@ + @ + @ + _ __ @ + | '__|@ + | | @ + |_| @ + @ + @@ + @ + @ + ___ @ + / __|@ + \__ \@ + |___/@ + @ + @@ + _ @ + | | @ + | |_ @ + | __|@ + | |_ @ + \__|@ + @ + @@ + @ + @ + _ _ @ + | | | |@ + | |_| |@ + \__,_|@ + @ + @@ + @ + @ + __ __@ + \ \ / /@ + \ V / @ + \_/ @ + @ + @@ + @ + @ + __ __@ + \ \ /\ / /@ + \ V V / @ + \_/\_/ @ + @ + @@ + @ + @ + __ __@ + \ \/ /@ + > < @ + /_/\_\@ + @ + @@ + @ + @ + _ _ @ + | | | |@ + | |_| |@ + \__, |@ + __/ |@ + |___/ @@ + @ + @ + ____@ + |_ /@ + / / @ + /___|@ + @ + @@ + __@ + / /@ + | | @ + / / @ + \ \ @ + | | @ + \_\@ + @@ + _ @ + | |@ + | |@ + | |@ + | |@ + | |@ + | |@ + |_|@@ + __ @ + \ \ @ + | | @ + \ \@ + / /@ + | | @ + /_/ @ + @@ + /\/|@ + |/\/ @ + $ @ + $ @ + $ @ + $ @ + @ + @@ + _ _ @ + (_)_(_) @ + / \ @ + / _ \ @ + / ___ \ @ + /_/ \_\@ + @ + @@ + _ _ @ + (_)_(_)@ + / _ \ @ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ + _ _ @ + (_) (_)@ + | | | |@ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ + _ _ @ + (_) (_)@ + __ _ @ + / _` |@ + | (_| |@ + \__,_|@ + @ + @@ + _ _ @ + (_) (_)@ + ___ @ + / _ \ @ + | (_) |@ + \___/ @ + @ + @@ + _ _ @ + (_) (_)@ + _ _ @ + | | | |@ + | |_| |@ + \__,_|@ + @ + @@ + ___ @ + / _ \ @ + | | ) |@ + | |< < @ + | | ) |@ + | ||_/ @ + |_| @ + @@ +160 NO-BREAK SPACE + $@ + $@ + $@ + $@ + $@ + $@ + $@ + $@@ +161 INVERTED EXCLAMATION MARK + _ @ + (_)@ + | |@ + | |@ + | |@ + |_|@ + @ + @@ +162 CENT SIGN + @ + _ @ + | | @ + / __)@ + | (__ @ + \ )@ + |_| @ + @@ +163 POUND SIGN + ___ @ + / ,_\ @ + _| |_ @ + |__ __| @ + | |____ @ + (_,_____|@ + @ + @@ +164 CURRENCY SIGN + @ + /\___/\@ + \ _ /@ + | (_) |@ + / ___ \@ + \/ \/@ + @ + @@ +165 YEN SIGN + __ __ @ + \ \ / / @ + _\ V /_ @ + |___ ___|@ + |___ ___|@ + |_| @ + @ + @@ +166 BROKEN BAR + _ @ + | |@ + | |@ + |_|@ + _ @ + | |@ + | |@ + |_|@@ +167 SECTION SIGN + __ @ + _/ _)@ + / \ \ @ + \ \\ \@ + \ \_/@ + (__/ @ + @ + @@ +168 DIAERESIS + _ _ @ + (_) (_)@ + $ $ @ + $ $ @ + $ $ @ + $ $ @ + @ + @@ +169 COPYRIGHT SIGN + ________ @ + / ____ \ @ + / / ___| \ @ + | | | |@ + | | |___ |@ + \ \____| / @ + \________/ @ + @@ +170 FEMININE ORDINAL INDICATOR + __ _ @ + / _` |@ + | (_| |@ + \__,_|@ + |_____|@ + $ @ + @ + @@ +171 LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + ____@ + / / /@ + / / / @ + < < < @ + \ \ \ @ + \_\_\@ + @ + @@ +172 NOT SIGN + @ + @ + ______ @ + |____ |@ + |_|@ + $ @ + @ + @@ +173 SOFT HYPHEN + @ + @ + _____ @ + |_____|@ + $ @ + $ @ + @ + @@ +174 REGISTERED SIGN + ________ @ + / ____ \ @ + / | _ \ \ @ + | | |_) | |@ + | | _ < |@ + \ |_| \_\ / @ + \________/ @ + @@ +175 MACRON + ______ @ + |______|@ + $ @ + $ @ + $ @ + $ @ + @ + @@ +176 DEGREE SIGN + __ @ + / \ @ + | () |@ + \__/ @ + $ @ + $ @ + @ + @@ +177 PLUS-MINUS SIGN + _ @ + _| |_ @ + |_ _|@ + |_| @ + _____ @ + |_____|@ + @ + @@ +178 SUPERSCRIPT TWO + ___ @ + |_ )@ + / / @ + /___|@ + $ @ + $ @ + @ + @@ +179 SUPERSCRIPT THREE + ____@ + |__ /@ + |_ \@ + |___/@ + $ @ + $ @ + @ + @@ +180 ACUTE ACCENT + __@ + /_/@ + $ @ + $ @ + $ @ + $ @ + @ + @@ +181 MICRO SIGN + @ + @ + _ _ @ + | | | |@ + | |_| |@ + | ._,_|@ + | | @ + |_| @@ +182 PILCROW SIGN + ______ @ + / |@ + | (| || |@ + \__ || |@ + | || |@ + |_||_|@ + @ + @@ +183 MIDDLE DOT + @ + @ + _ @ + (_)@ + $ @ + $ @ + @ + @@ +184 CEDILLA + @ + @ + @ + @ + @ + _ @ + )_)@ + @@ +185 SUPERSCRIPT ONE + _ @ + / |@ + | |@ + |_|@ + $ @ + $ @ + @ + @@ +186 MASCULINE ORDINAL INDICATOR + ___ @ + / _ \ @ + | (_) |@ + \___/ @ + |_____|@ + $ @ + @ + @@ +187 RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + ____ @ + \ \ \ @ + \ \ \ @ + > > >@ + / / / @ + /_/_/ @ + @ + @@ +188 VULGAR FRACTION ONE QUARTER + _ __ @ + / | / / @ + | |/ / _ @ + |_/ / | | @ + / /|_ _|@ + /_/ |_| @ + @ + @@ +189 VULGAR FRACTION ONE HALF + _ __ @ + / | / / @ + | |/ /__ @ + |_/ /_ )@ + / / / / @ + /_/ /___|@ + @ + @@ +190 VULGAR FRACTION THREE QUARTERS + ____ __ @ + |__ / / / @ + |_ \/ / _ @ + |___/ / | | @ + / /|_ _|@ + /_/ |_| @ + @ + @@ +191 INVERTED QUESTION MARK + _ @ + (_) @ + | | @ + / / @ + | (__ @ + \___|@ + @ + @@ +192 LATIN CAPITAL LETTER A WITH GRAVE + __ @ + \_\ @ + / \ @ + / _ \ @ + / ___ \ @ + /_/ \_\@ + @ + @@ +193 LATIN CAPITAL LETTER A WITH ACUTE + __ @ + /_/ @ + / \ @ + / _ \ @ + / ___ \ @ + /_/ \_\@ + @ + @@ +194 LATIN CAPITAL LETTER A WITH CIRCUMFLEX + //\ @ + |/_\| @ + / \ @ + / _ \ @ + / ___ \ @ + /_/ \_\@ + @ + @@ +195 LATIN CAPITAL LETTER A WITH TILDE + /\/| @ + |/\/ @ + / \ @ + / _ \ @ + / ___ \ @ + /_/ \_\@ + @ + @@ +196 LATIN CAPITAL LETTER A WITH DIAERESIS + _ _ @ + (_)_(_) @ + / \ @ + / _ \ @ + / ___ \ @ + /_/ \_\@ + @ + @@ +197 LATIN CAPITAL LETTER A WITH RING ABOVE + _ @ + (o) @ + / \ @ + / _ \ @ + / ___ \ @ + /_/ \_\@ + @ + @@ +198 LATIN CAPITAL LETTER AE + _______ @ + / ____|@ + / |__ @ + / /| __| @ + / ___ |____ @ + /_/ |______|@ + @ + @@ +199 LATIN CAPITAL LETTER C WITH CEDILLA + _____ @ + / ____|@ + | | $ @ + | | $ @ + | |____ @ + \_____|@ + )_) @ + @@ +200 LATIN CAPITAL LETTER E WITH GRAVE + __ @ + _\_\_ @ + | ____|@ + | _| @ + | |___ @ + |_____|@ + @ + @@ +201 LATIN CAPITAL LETTER E WITH ACUTE + __ @ + _/_/_ @ + | ____|@ + | _| @ + | |___ @ + |_____|@ + @ + @@ +202 LATIN CAPITAL LETTER E WITH CIRCUMFLEX + //\ @ + |/ \| @ + | ____|@ + | _| @ + | |___ @ + |_____|@ + @ + @@ +203 LATIN CAPITAL LETTER E WITH DIAERESIS + _ _ @ + (_) (_)@ + | ____|@ + | _| @ + | |___ @ + |_____|@ + @ + @@ +204 LATIN CAPITAL LETTER I WITH GRAVE + __ @ + \_\ @ + |_ _|@ + | | @ + | | @ + |___|@ + @ + @@ +205 LATIN CAPITAL LETTER I WITH ACUTE + __ @ + /_/ @ + |_ _|@ + | | @ + | | @ + |___|@ + @ + @@ +206 LATIN CAPITAL LETTER I WITH CIRCUMFLEX + //\ @ + |/_\|@ + |_ _|@ + | | @ + | | @ + |___|@ + @ + @@ +207 LATIN CAPITAL LETTER I WITH DIAERESIS + _ _ @ + (_)_(_)@ + |_ _| @ + | | @ + | | @ + |___| @ + @ + @@ +208 LATIN CAPITAL LETTER ETH + _____ @ + | __ \ @ + _| |_ | |@ + |__ __|| |@ + | |__| |@ + |_____/ @ + @ + @@ +209 LATIN CAPITAL LETTER N WITH TILDE + /\/| @ + |/\/_ @ + | \ | |@ + | \| |@ + | |\ |@ + |_| \_|@ + @ + @@ +210 LATIN CAPITAL LETTER O WITH GRAVE + __ @ + \_\ @ + / _ \ @ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ +211 LATIN CAPITAL LETTER O WITH ACUTE + __ @ + /_/ @ + / _ \ @ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ +212 LATIN CAPITAL LETTER O WITH CIRCUMFLEX + //\ @ + |/_\| @ + / _ \ @ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ +213 LATIN CAPITAL LETTER O WITH TILDE + /\/| @ + |/\/ @ + / _ \ @ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ +214 LATIN CAPITAL LETTER O WITH DIAERESIS + _ _ @ + (_)_(_)@ + / _ \ @ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ +215 MULTIPLICATION SIGN + @ + @ + /\/\@ + > <@ + \/\/@ + $ @ + @ + @@ +216 LATIN CAPITAL LETTER O WITH STROKE + _____ @ + / __// @ + | | // |@ + | |//| |@ + | //_| |@ + //___/ @ + @ + @@ +217 LATIN CAPITAL LETTER U WITH GRAVE + __ @ + _\_\_ @ + | | | |@ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ +218 LATIN CAPITAL LETTER U WITH ACUTE + __ @ + _/_/_ @ + | | | |@ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ +219 LATIN CAPITAL LETTER U WITH CIRCUMFLEX + //\ @ + |/ \| @ + | | | |@ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ +220 LATIN CAPITAL LETTER U WITH DIAERESIS + _ _ @ + (_) (_)@ + | | | |@ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ +221 LATIN CAPITAL LETTER Y WITH ACUTE + __ @ + __/_/__@ + \ \ / /@ + \ V / @ + | | @ + |_| @ + @ + @@ +222 LATIN CAPITAL LETTER THORN + _ @ + | |___ @ + | __ \ @ + | |__) |@ + | ___/ @ + |_| @ + @ + @@ +223 LATIN SMALL LETTER SHARP S + ___ @ + / _ \ @ + | | ) |@ + | |< < @ + | | ) |@ + | ||_/ @ + |_| @ + @@ +224 LATIN SMALL LETTER A WITH GRAVE + __ @ + \_\ @ + __ _ @ + / _` |@ + | (_| |@ + \__,_|@ + @ + @@ +225 LATIN SMALL LETTER A WITH ACUTE + __ @ + /_/ @ + __ _ @ + / _` |@ + | (_| |@ + \__,_|@ + @ + @@ +226 LATIN SMALL LETTER A WITH CIRCUMFLEX + //\ @ + |/ \| @ + __ _ @ + / _` |@ + | (_| |@ + \__,_|@ + @ + @@ +227 LATIN SMALL LETTER A WITH TILDE + /\/| @ + |/\/ @ + __ _ @ + / _` |@ + | (_| |@ + \__,_|@ + @ + @@ +228 LATIN SMALL LETTER A WITH DIAERESIS + _ _ @ + (_) (_)@ + __ _ @ + / _` |@ + | (_| |@ + \__,_|@ + @ + @@ +229 LATIN SMALL LETTER A WITH RING ABOVE + __ @ + (()) @ + __ _ @ + / _` |@ + | (_| |@ + \__,_|@ + @ + @@ +230 LATIN SMALL LETTER AE + @ + @ + __ ____ @ + / _` _ \@ + | (_| __/@ + \__,____|@ + @ + @@ +231 LATIN SMALL LETTER C WITH CEDILLA + @ + @ + ___ @ + / __|@ + | (__ @ + \___|@ + )_) @ + @@ +232 LATIN SMALL LETTER E WITH GRAVE + __ @ + \_\ @ + ___ @ + / _ \@ + | __/@ + \___|@ + @ + @@ +233 LATIN SMALL LETTER E WITH ACUTE + __ @ + /_/ @ + ___ @ + / _ \@ + | __/@ + \___|@ + @ + @@ +234 LATIN SMALL LETTER E WITH CIRCUMFLEX + //\ @ + |/ \|@ + ___ @ + / _ \@ + | __/@ + \___|@ + @ + @@ +235 LATIN SMALL LETTER E WITH DIAERESIS + _ _ @ + (_) (_)@ + ___ @ + / _ \ @ + | __/ @ + \___| @ + @ + @@ +236 LATIN SMALL LETTER I WITH GRAVE + __ @ + \_\@ + _ @ + | |@ + | |@ + |_|@ + @ + @@ +237 LATIN SMALL LETTER I WITH ACUTE + __@ + /_/@ + _ @ + | |@ + | |@ + |_|@ + @ + @@ +238 LATIN SMALL LETTER I WITH CIRCUMFLEX + //\ @ + |/ \|@ + _ @ + | | @ + | | @ + |_| @ + @ + @@ +239 LATIN SMALL LETTER I WITH DIAERESIS + _ _ @ + (_) (_)@ + _ @ + | | @ + | | @ + |_| @ + @ + @@ +240 LATIN SMALL LETTER ETH + /\/\ @ + > < @ + \/\ \ @ + / _` |@ + | (_) |@ + \___/ @ + @ + @@ +241 LATIN SMALL LETTER N WITH TILDE + /\/| @ + |/\/ @ + _ __ @ + | '_ \ @ + | | | |@ + |_| |_|@ + @ + @@ +242 LATIN SMALL LETTER O WITH GRAVE + __ @ + \_\ @ + ___ @ + / _ \ @ + | (_) |@ + \___/ @ + @ + @@ +243 LATIN SMALL LETTER O WITH ACUTE + __ @ + /_/ @ + ___ @ + / _ \ @ + | (_) |@ + \___/ @ + @ + @@ +244 LATIN SMALL LETTER O WITH CIRCUMFLEX + //\ @ + |/ \| @ + ___ @ + / _ \ @ + | (_) |@ + \___/ @ + @ + @@ +245 LATIN SMALL LETTER O WITH TILDE + /\/| @ + |/\/ @ + ___ @ + / _ \ @ + | (_) |@ + \___/ @ + @ + @@ +246 LATIN SMALL LETTER O WITH DIAERESIS + _ _ @ + (_) (_)@ + ___ @ + / _ \ @ + | (_) |@ + \___/ @ + @ + @@ +247 DIVISION SIGN + _ @ + (_) @ + _______ @ + |_______|@ + _ @ + (_) @ + @ + @@ +248 LATIN SMALL LETTER O WITH STROKE + @ + @ + ____ @ + / _//\ @ + | (//) |@ + \//__/ @ + @ + @@ +249 LATIN SMALL LETTER U WITH GRAVE + __ @ + \_\ @ + _ _ @ + | | | |@ + | |_| |@ + \__,_|@ + @ + @@ +250 LATIN SMALL LETTER U WITH ACUTE + __ @ + /_/ @ + _ _ @ + | | | |@ + | |_| |@ + \__,_|@ + @ + @@ +251 LATIN SMALL LETTER U WITH CIRCUMFLEX + //\ @ + |/ \| @ + _ _ @ + | | | |@ + | |_| |@ + \__,_|@ + @ + @@ +252 LATIN SMALL LETTER U WITH DIAERESIS + _ _ @ + (_) (_)@ + _ _ @ + | | | |@ + | |_| |@ + \__,_|@ + @ + @@ +253 LATIN SMALL LETTER Y WITH ACUTE + __ @ + /_/ @ + _ _ @ + | | | |@ + | |_| |@ + \__, |@ + __/ |@ + |___/ @@ +254 LATIN SMALL LETTER THORN + _ @ + | | @ + | |__ @ + | '_ \ @ + | |_) |@ + | .__/ @ + | | @ + |_| @@ +255 LATIN SMALL LETTER Y WITH DIAERESIS + _ _ @ + (_) (_)@ + _ _ @ + | | | |@ + | |_| |@ + \__, |@ + __/ |@ + |___/ @@ +0x02BC MODIFIER LETTER APOSTROPHE + @ + @ + ))@ + @ + @ + @ + @ + @@ +0x02BD MODIFIER LETTER REVERSED COMMA + @ + @ + ((@ + @ + @ + @ + @ + @@ +0x037A GREEK YPOGEGRAMMENI + @ + @ + @ + @ + @ + @ + @ + ||@@ +0x0387 GREEK ANO TELEIA + @ + $ @ + _ @ + (_)@ + @ + $ @ + @ + @@ +0x0391 GREEK CAPITAL LETTER ALPHA + ___ @ + / _ \ @ + | |_| |@ + | _ |@ + | | | |@ + |_| |_|@ + @ + @@ +0x0392 GREEK CAPITAL LETTER BETA + ____ @ + | _ \ @ + | |_) )@ + | _ ( @ + | |_) )@ + |____/ @ + @ + @@ +0x0393 GREEK CAPITAL LETTER GAMMA + _____ @ + | ___)@ + | |$ @ + | |$ @ + | | @ + |_| @ + @ + @@ +0x0394 GREEK CAPITAL LETTER DELTA + @ + /\ @ + / \ @ + / /\ \ @ + / /__\ \ @ + /________\@ + @ + @@ +0x0395 GREEK CAPITAL LETTER EPSILON + _____ @ + | ___)@ + | |_ @ + | _) @ + | |___ @ + |_____)@ + @ + @@ +0x0396 GREEK CAPITAL LETTER ZETA + ______@ + (___ /@ + / / @ + / / @ + / /__ @ + /_____)@ + @ + @@ +0x0397 GREEK CAPITAL LETTER ETA + _ _ @ + | | | |@ + | |_| |@ + | _ |@ + | | | |@ + |_| |_|@ + @ + @@ +0x0398 GREEK CAPITAL LETTER THETA + ____ @ + / __ \ @ + | |__| |@ + | __ |@ + | |__| |@ + \____/ @ + @ + @@ +0x0399 GREEK CAPITAL LETTER IOTA + ___ @ + ( )@ + | | @ + | | @ + | | @ + (___)@ + @ + @@ +0x039A GREEK CAPITAL LETTER KAPPA + _ __@ + | | / /@ + | |/ / @ + | < @ + | |\ \ @ + |_| \_\@ + @ + @@ +0x039B GREEK CAPITAL LETTER LAMDA + @ + /\ @ + / \ @ + / /\ \ @ + / / \ \ @ + /_/ \_\@ + @ + @@ +0x039C GREEK CAPITAL LETTER MU + __ __ @ + | \ / |@ + | v |@ + | |\_/| |@ + | | | |@ + |_| |_|@ + @ + @@ +0x039D GREEK CAPITAL LETTER NU + _ _ @ + | \ | |@ + | \| |@ + | |@ + | |\ |@ + |_| \_|@ + @ + @@ +0x039E GREEK CAPITAL LETTER XI + _____ @ + (_____)@ + ___ @ + (___) @ + _____ @ + (_____)@ + @ + @@ +0x039F GREEK CAPITAL LETTER OMICRON + ___ @ + / _ \ @ + | | | |@ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ +0x03A0 GREEK CAPITAL LETTER PI + _______ @ + ( _ )@ + | | | | @ + | | | | @ + | | | | @ + |_| |_| @ + @ + @@ +0x03A1 GREEK CAPITAL LETTER RHO + ____ @ + | _ \ @ + | |_) )@ + | __/ @ + | | @ + |_| @ + @ + @@ +0x03A3 GREEK CAPITAL LETTER SIGMA + ______ @ + \ ___)@ + \ \ @ + > > @ + / /__ @ + /_____)@ + @ + @@ +0x03A4 GREEK CAPITAL LETTER TAU + _____ @ + (_ _)@ + | | @ + | | @ + | | @ + |_| @ + @ + @@ +0x03A5 GREEK CAPITAL LETTER UPSILON + __ __ @ + (_ \ / _)@ + \ v / @ + | | @ + | | @ + |_| @ + @ + @@ +0x03A6 GREEK CAPITAL LETTER PHI + _ @ + _| |_ @ + / \ @ + ( (| |) )@ + \_ _/ @ + |_| @ + @ + @@ +0x03A7 GREEK CAPITAL LETTER CHI + __ __@ + \ \ / /@ + \ v / @ + > < @ + / ^ \ @ + /_/ \_\@ + @ + @@ +0x03A8 GREEK CAPITAL LETTER PSI + _ _ _ @ + | || || |@ + | \| |/ |@ + \_ _/ @ + | | @ + |_| @ + @ + @@ +0x03A9 GREEK CAPITAL LETTER OMEGA + ____ @ + / __ \ @ + | | | | @ + | | | | @ + _\ \/ /_ @ + (___||___)@ + @ + @@ +0x03B1 GREEK SMALL LETTER ALPHA + @ + @ + __ __@ + / \/ /@ + ( () < @ + \__/\_\@ + @ + @@ +0x03B2 GREEK SMALL LETTER BETA + ___ @ + / _ \ @ + | |_) )@ + | _ < @ + | |_) )@ + | __/ @ + | | @ + |_| @@ +0x03B3 GREEK SMALL LETTER GAMMA + @ + @ + _ _ @ + ( \ / )@ + \ v / @ + | | @ + | | @ + |_| @@ +0x03B4 GREEK SMALL LETTER DELTA + __ @ + / _) @ + \ \ @ + / _ \ @ + ( (_) )@ + \___/ @ + @ + @@ +0x03B5 GREEK SMALL LETTER EPSILON + @ + @ + ___ @ + / __)@ + > _) @ + \___)@ + @ + @@ +0x03B6 GREEK SMALL LETTER ZETA + _____ @ + \__ ) @ + / / @ + / / @ + | |__ @ + \__ \ @ + ) )@ + (_/ @@ +0x03B7 GREEK SMALL LETTER ETA + @ + @ + _ __ @ + | '_ \ @ + | | | |@ + |_| | |@ + | |@ + |_|@@ +0x03B8 GREEK SMALL LETTER THETA + ___ @ + / _ \ @ + | |_| |@ + | _ |@ + | |_| |@ + \___/ @ + @ + @@ +0x03B9 GREEK SMALL LETTER IOTA + @ + @ + _ @ + | | @ + | | @ + \_)@ + @ + @@ +0x03BA GREEK SMALL LETTER KAPPA + @ + @ + _ __@ + | |/ /@ + | < @ + |_|\_\@ + @ + @@ +0x03BB GREEK SMALL LETTER LAMDA + __ @ + \ \ @ + \ \ @ + > \ @ + / ^ \ @ + /_/ \_\@ + @ + @@ +0x03BC GREEK SMALL LETTER MU + @ + @ + _ _ @ + | | | |@ + | |_| |@ + | ._,_|@ + | | @ + |_| @@ +0x03BD GREEK SMALL LETTER NU + @ + @ + _ __@ + | |/ /@ + | / / @ + |__/ @ + @ + @@ +0x03BE GREEK SMALL LETTER XI + \=\__ @ + > __) @ + ( (_ @ + > _) @ + ( (__ @ + \__ \ @ + ) )@ + (_/ @@ +0x03BF GREEK SMALL LETTER OMICRON + @ + @ + ___ @ + / _ \ @ + ( (_) )@ + \___/ @ + @ + @@ +0x03C0 GREEK SMALL LETTER PI + @ + @ + ______ @ + ( __ )@ + | || | @ + |_||_| @ + @ + @@ +0x03C1 GREEK SMALL LETTER RHO + @ + @ + ___ @ + / _ \ @ + | |_) )@ + | __/ @ + | | @ + |_| @@ +0x03C2 GREEK SMALL LETTER FINAL SIGMA + @ + @ + ____ @ + / ___)@ + ( (__ @ + \__ \ @ + _) )@ + (__/ @@ +0x03C3 GREEK SMALL LETTER SIGMA + @ + @ + ____ @ + / ._)@ + ( () ) @ + \__/ @ + @ + @@ +0x03C4 GREEK SMALL LETTER TAU + @ + @ + ___ @ + ( )@ + | | @ + \_)@ + @ + @@ +0x03C5 GREEK SMALL LETTER UPSILON + @ + @ + _ _ @ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ +0x03C6 GREEK SMALL LETTER PHI + _ @ + | | @ + _| |_ @ + / \ @ + ( (| |) )@ + \_ _/ @ + | | @ + |_| @@ +0x03C7 GREEK SMALL LETTER CHI + @ + @ + __ __@ + \ \ / /@ + \ v / @ + > < @ + / ^ \ @ + /_/ \_\@@ +0x03C8 GREEK SMALL LETTER PSI + @ + @ + _ _ _ @ + | || || |@ + | \| |/ |@ + \_ _/ @ + | | @ + |_| @@ +0x03C9 GREEK SMALL LETTER OMEGA + @ + @ + __ __ @ + / / _ \ \ @ + | |_/ \_| |@ + \___^___/ @ + @ + @@ +0x03D1 GREEK THETA SYMBOL + ___ @ + / _ \ @ + ( (_| |_ @ + _ \ _ _)@ + | |___| | @ + \_____/ @ + @ + @@ +0x03D5 GREEK PHI SYMBOL + @ + @ + _ __ @ + | | / \ @ + | || || )@ + \_ _/ @ + | | @ + |_| @@ +0x03D6 GREEK PI SYMBOL + @ + @ + _________ @ + ( _____ )@ + | |_/ \_| |@ + \___^___/ @ + @ + @@ +-0x0005 +alpha = a, beta = b, gamma = g, delta = d, epsilon = e @ +zeta = z, eta = h, theta = q, iota = i, lamda = l, mu = m@ +nu = n, xi = x, omicron = o, pi = p, rho = r, sigma = s @ +phi = f, chi = c, psi = y, omega = w, final sigma = V @ + pi symbol = v, theta symbol = J, phi symbol = j @ + middle dot = :, ypogegrammeni = _ @ + rough breathing = (, smooth breathing = ) @ + acute accent = ', grave accent = `, dialytika = ^ @@ diff --git a/src/resources/MDK/LICENSE.lua b/src/resources/MDK/LICENSE.lua new file mode 100755 index 0000000..1812afa --- /dev/null +++ b/src/resources/MDK/LICENSE.lua @@ -0,0 +1,48 @@ +--[===[ +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. +]] diff --git a/src/resources/MDK/README.md b/src/resources/MDK/README.md new file mode 100755 index 0000000..04783af --- /dev/null +++ b/src/resources/MDK/README.md @@ -0,0 +1,91 @@ +# the demonnic MDK and You + +This is a collection of Lua 'classes' and modules I wrote for Mudlet. It is largely targeted at scripters, and comes packaged in two ways depending on how you intend to use/distribute your work. Please see [Installation](#installation) for more details + +## Documentation + +The [MDK wiki](https://github.com/demonnic/MDK/wiki) contains an entry for each module or class, as well as examples. + +Starting with alpha2 of the MDK, the ldocs generated from code are included in the zipped releases. The current release's ldocs can always be viewed at + +## Installation + +How you 'install' the MDK depends on how you intend to use it. + +### I just want to install the MDK for my own personal use + +You just want to get your hands on the goods, and aren't looking to use any MDK items in an exported package for sharing or anything like that. +Well, you are who the mdk mpackage is for! Download the MDK.mpackage from your desired release on the [Releases](https://github.com/demonnic/MDK/releases) page and install it in the package manager. The examples in the [wiki](https://demonnic.github.io/mdk/current/) are written with this in mind, and you would require the items you need as `local EMCO = require("MDK.emco")` + +### I am a package author looking to include/use one of the MDK modules or classes in my package + +You should download the `demonnic-MDK-.zip` file for your desired release on the [Releases](https://github.com/demonnic/MDK/releases) page. +Inside are the individual .lua files for the modules and classes described in the [wiki](https://demonnic.github.io/mdk/current/) and [API docs](https://demonnic.github.io/mdk/current/). +You can include all of them if you wish, or only the ones you actually make use of. I ask that you include the LICENSE.lua or LICENSE-MDK.lua file (depending on the release) file in addition. +They should go in the root of your package, so that when your package is installed the files can be found at `getMudletHomeDir() .. "//emco.lua"`. You would then use `local EMCO = require(".emco")` +So for example if your package name is "MySuperCoolPackage" and it installs to `getMudletHomeDir() .. "/MySuperCoolPackage/"` then you use `local EMCO = require("MySuperCoolPackage.emco")` and the emco.lua file should be at `getMudletHomeDir() .. "/MySuperCoolPackage/emco.lua"` + +## Files (Modules/Classes) + +These files contain the modules in the MDK. You only need to include those files which you intend to use, except as noted in the descriptions below. +If you include any of the modules from the MDK, you should also include LICENSE.lua or LICENSE-MDK.lua. It contains the licenses for my modules and for luaunit and lua-schema which are not my original works. +You should maybe also include demontools.lua, as it notes below several other of the MDK modules make use of items within it. + +* aliasmgr.ua + * Object to manage tempAliases programmatically. + +* chyron.lua + * Label which moves a message across its face from right to left, like a stock ticker or the news chyrons. Documentation at + +* demontools.lua + * Collection of miscellaneous useful functions. You should include this file if you use the MDK, as several other modules make use of it. Include functions for converting c/d/hecho, html, and ansi colored strings between each other, mkdir_p, and some others. + +* emco.lua + * EMCO. Documentation at Will make use of LoggingConsole if loggingconsole.lua and demontools.lua are included + +* figlet.lua + * Creates FIGlets from strings + * Reference package with multiple fonts and color gradients at + +* ftext.lua + * basic fText. Documentation at + * now includes TextFormatter and TableMaker as ftext.TextFormatter and ftext.TableMaker + +* gradientmaker.lua + * Functions for creating color gradients for use with c/d/hecho. Documentation at + +* loggingconsole.lua + * Self logging extension to the mini console. Works just like a Geyser.MiniConsole but adds a templated path and fileName constraint, as well as logFormat so it can log what is echod or appended to it. Requires demontools.lua in order to work. + +* loginator.lua + * Creates objects for logging messages to disk. + +* mastermindsolver.lua + * A class which will help you solve Master Mind puzzles. + +* revisionator.lua + * A class which aims to make upgrading between package versions easier by storing and running patch functions. + +* sortbox.lua + * SortBox, an alternative to H/VBox which can be either, and also provides options for sorting its contents. Overview at + +* spinbox.lua + * SpinBox, a Geyser element for adjusting numbers with your mouse. Overview at + +* sug.lua + * Self Updating Gauges, will watch a set of variables and update itself on a timer based on what values those variables hold. Documentation at + +* textgauge.lua + * TextGauges, what it says on the tin. Documentation at + +* timergauge.lua + * TimerGauge, an extension of Geyser.Gauge which serves as an animated countdown timer. Overview at + +## Others people's work I depend upon + +* schema.lua + * lua-schema, for defining table schema. Documentation at + * will be used by Archon for ensuring configuration tables are as they should be. + +* LICENSE.lua + * Contains the license information for MDK, as well as lua-schema and luaunit which have been included. diff --git a/src/resources/MDK/aliasmgr.lua b/src/resources/MDK/aliasmgr.lua new file mode 100755 index 0000000..7bf788a --- /dev/null +++ b/src/resources/MDK/aliasmgr.lua @@ -0,0 +1,164 @@ +--- Alias Manager +-- @classmod aliasmgr +-- @author Damian Monogue +-- @copyright 2022 Damian Monogue +-- @license MIT, see LICENSE.lua +local aliasmgr = {} +aliasmgr.__index = aliasmgr + +--- Creates a new alias manager +function aliasmgr:new() + local mgr = { + aliases = {} + } + setmetatable(mgr, self) + return mgr +end + +local function argError(funcName, argument, expected, actual) + local msg = string.format("%s: %s as %s expected, got %s", funcName, argument, expected, actual) + printError(msg, true, true) +end + +--- Registers an alias with the alias manager +-- @param name the name for the alias +-- @param regex the regular expression the alias matches against +-- @param func The code to run when the alias matches. Can wrap code in [[ ]] or pass an actual function +function aliasmgr:register(name, regex, func) + local funcName = "aliasmgr:register(name, regex, func)" + if func == nil then + printError(f"{funcName} takes 3 arguments and you have provided less than that", true, true) + end + local nameType = type(name) + if nameType ~= "string" then + argError(funcName, "name", "string", nameType) + end + local regexType = type(regex) + if regexType ~= "string" then + argError(funcName, "regex", "string", regexType) + end + local funcType = type(func) + if funcType ~= "string" and funcType ~= "function" then + argError(funcName, "func", "string or function", funcType) + end + local object = { + regex = regex, + func = func + } + self:kill(name) + local ok, err = pcall(tempAlias, regex, func) + if not ok then + return nil, err + end + object.handlerID = err + self.aliases[name] = object + return true +end + +--- Registers an alias with the alias manager. Alias for register +-- @param name the name for the alias +-- @param regex the regular expression the alias matches against +-- @param func The code to run when the alias matches. Can wrap code in [[ ]] or pass an actual function +-- @see register +function aliasmgr:add(name, regex, func) + self:register(name, regex, func) +end + +--- Disables an alias, but does not delete it so it can be enabled later without being redefined +-- @param name the name of the alias to disable +-- @return true if the alias exists and gets disabled, false if it does not exist or is already disabled +function aliasmgr:disable(name) + local funcName = "aliasmgr:disable(name)" + local nameType = type(name) + if nameType ~= "string" then + argError(funcName, "name", "string", nameType) + end + local object = self.aliases[name] + if not object or object.handlerID == -1 then + return false + end + killAlias(object.handlerID) + object.handlerID = -1 + return true +end + +--- Disables all aliases registered with the manager +function aliasmgr:disableAll() + local aliases = self.aliases + for name, object in pairs(aliases) do + self:disable(name) + end +end + +--- Enables an alias by name +-- @param name the name of the alias to enable +-- @return true if the alias exists and was enabled, false if it does not exist. +function aliasmgr:enable(name) + local funcName = "aliasmgr:enable(name)" + local nameType = type(name) + if nameType ~= "string" then + argError(funcName, "name", "string", nameType) + end + local object = self.aliases[name] + if not object then + return false + end + self:register(name, object.regex, object.func) +end + +--- Enables all aliases registered with the manager +function aliasmgr:enableAll() + local aliases = self.aliases + for name,_ in pairs(aliases) do + self:enable(name) + end + return true +end + +--- Kill an alias, deleting it from the manager +-- @param name the name of the alias to kill +-- @return true if the alias exists and gets deleted, false if the alias does not exist +function aliasmgr:kill(name) + local funcName = "aliasmgr:kill(name)" + local nameType = type(name) + if nameType ~= "string" then + argError(funcName, "name", "string", nameType) + end + local object = self.aliases[name] + if not object then + return false + end + self:disable(name) + self.aliases[name] = nil + return true +end + +--- Kills all aliases registered with the manager, clearing it out +function aliasmgr:killAll() + local aliases = self.aliases + for name, _ in pairs(aliases) do + self:kill(name) + end +end + +--- Kills an alias, deleting it from the manager +-- @param name the name of the alias to delete +-- @return true if the alias exists and gets deleted, false if the alias does not exist +-- @see kill +function aliasmgr:delete(name) + return self:kill(name) +end + +--- Kills all aliases, deleting them from the manager +-- @see killAll +function aliasmgr:deleteAll() + return self:killAll() +end + +--- Returns the list of aliases and the information being tracked for them +-- @return the table of alias information, with names as keys and a table of information as the values. +function aliasmgr:getAliases() + return self.aliases +end + +return aliasmgr \ No newline at end of file diff --git a/src/resources/MDK/chyron.lua b/src/resources/MDK/chyron.lua new file mode 100755 index 0000000..dc77403 --- /dev/null +++ b/src/resources/MDK/chyron.lua @@ -0,0 +1,235 @@ +--- Creates a label with a scrolling text element. It is highly recommended you use a monospace font for this label. +-- @classmod Chyron +-- @author Delra +-- @copyright 2019 +-- @author Damian Monogue +-- @copyright 2020 +local Chyron = { + name = "ChyronClass", + text = "", + displayWidth = 28, + updateTime = 200, + font = "Bitstream Vera Sans Mono", + fontSize = "9", + autoWidth = true, + delimiter = "|", + pos = 1, + enabled = true, + alignment = "center", +} + +--- Creates a new Chyron label +-- @tparam table cons table of constraints which configures the EMCO. +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
option namedescriptiondefault
textThe text to scroll on the label""
updateTimeMilliseconds between movements (one letter shift)200
displayWidthHow many chars wide to display the text28
delimiterThis character will be inserted with a space either side to mark the stop/start of the message"|"
enabledShould the chyron scroll?true
fontWhat font to use for the Chyron? Available in Geyser.Label but we define a default."Bitstream Vera Sans Mono"
fontSizeWhat font size to use for the Chyron? Available in Geyser.Label but we define a default.9
autoWidthShould the Chyron resize to just fit the text?true
alignmentWhat alignment(left/right/center) to use for the Chyron text? Available in Geyser.Label but we define a default."center"
+-- @tparam GeyserObject container The container to use as the parent for the Chyron +function Chyron:new(cons, container) + cons = cons or {} + cons.type = cons.type or "Chyron" + local me = self.parent:new(cons, container) + setmetatable(me, self) + self.__index = self + me.pos = 0 + me:setDisplayWidth(me.displayWidth) + me:setMessage(me.text) + if me.enabled then + me:start() + else + me:stop() + end + return me +end + +--- Sets the numver of characters of the text to display at once +-- @tparam number displayWidth number of characters to show at once +function Chyron:setDisplayWidth(displayWidth) + displayWidth = displayWidth or self.displayWidth + self.displayWidth = displayWidth + if self.autoWidth then + local width = calcFontSize(self.fontSize, self.font) + self:resize(width * (displayWidth + 2), self.height) + end + if not self.enabled then + self.pos = self.pos - 1 + self:doScroll() + end +end + +--- Override setFontSize to call setDisplayWidth in order to resize if necessary +-- @local +function Chyron:setFontSize(fontSize) + Geyser.Label.setFontSize(self, fontSize) + self:setDisplayWidth() +end + +--- Override setFont to call setDisplayWidth in order to resize if necessary +-- @local +function Chyron:setFont(font) + Geyser.Label.setFont(self, font) + self:setDisplayWidth() +end + +--- Returns the proper section of text +-- @local +-- @param start number the character to start at +-- @param length number the length of the text you want to extract +function Chyron:scrollText(start, length) + local t = self.textTable + local s = '' + local e = start + length + for i = start - 1, e - 2 do + local n = (i % #t) + 1 + s = s .. t[n] + end + return s +end + +--- scroll the text +-- @local +function Chyron:doScroll() + self.pos = self.pos + 1 + local displayString = self:scrollText(self.pos, self.displayWidth) + self:echo('<' .. displayString .. '>') + self.message = self.text +end + +--- Sets the Chyron from the first position, without changing enabled status +function Chyron:reset() + self.pos = 0 + if not self.enabled then + self:doScroll() + end +end + +--- Stops the Chyron with its current display +function Chyron:pause() + self.enabled = false + if self.timer then + killTimer(self.timer) + end +end + +--- Start the Chyron back up from wherever it currently is +function Chyron:start() + self.enabled = true + if self.timer then + killTimer(self.timer) + end + self.timer = tempTimer(self.updateTime / 1000, function() + self:doScroll() + end, true) +end + +--- Change the update time for the Chyron +-- @param updateTime number new updateTime in milliseconds +function Chyron:setUpdateTime(updateTime) + self.updateTime = updateTime or self.updateTime + if self.timer then + killTimer(self.timer) + end + if self.enabled then + self:start() + end +end + +--- Enable autoWidth adjustment +function Chyron:enableAutoWidth() + self.autoWidth = true + self:setDisplayWidth() +end + +--- Disable autoWidth adjustment +function Chyron:disableAutoWidth() + self.autoWidth = false +end + +--- Stop the Chyron, and reset it to the original position +function Chyron:stop() + if self.timer then + killTimer(self.timer) + end + self.enabled = false + self.pos = 0 + self:doScroll() +end + +--- Change the text being scrolled on the Chyron +-- @param message string message the text you want to have scroll on the Chyron +function Chyron:setMessage(message) + self.text = message + self.pos = 0 + message = string.format("%s %s ", message, self.delimiter) + local t = {} + for i = 1, #message do + t[i] = message:sub(i, i) + end + self.textTable = t + if not self.enabled then + self:doScroll() + end +end + +--- Change the delimiter used to show the beginning and end of the message +-- @param delimiter string the new delimiter to use. I recommend using one character. +function Chyron:setDelimiter(delimiter) + self.delimiter = delimiter +end + +Chyron.parent = Geyser.Label +setmetatable(Chyron, Geyser.Label) + +return Chyron diff --git a/src/resources/MDK/computer.png b/src/resources/MDK/computer.png new file mode 100755 index 0000000000000000000000000000000000000000..ae15f8fa41eb3833dcc4531c364ff0903a9f9aa9 GIT binary patch literal 351335 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 z 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

bsDvSW@Gk}A}B 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 z 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 &<_Eu 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 +-- @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([[\$]], "") + path = path:gsub([[/$]], "") + return io.exists(path) +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, used to turn a string variable name into a value +local function getValueAt(accessString) + local ok, err = pcall(loadstring("return " .. tostring(accessString))) + if ok then return err end + return nil, err +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 + +local echoOutputs = { + Color = { + ["\27reset"] = "", + ["\27bold"] = "", + ["\27boldoff"] = "", + ["\27italics"] = "", + ["\27italicsoff"] = "", + ["\27underline"] = "", + ["\27underlineoff"] = "", + ["\27strikethrough"] = "", + ["\27strikethroughoff"] = "", + ["\27overline"] = "", + ["\27overlineoff"] = "", + }, + Decimal = { + ["\27reset"] = "", + ["\27bold"] = "", + ["\27boldoff"] = "", + ["\27italics"] = "", + ["\27italicsoff"] = "", + ["\27underline"] = "", + ["\27underlineoff"] = "", + ["\27strikethrough"] = "", + ["\27strikethroughoff"] = "", + ["\27overline"] = "", + ["\27overlineoff"] = "", + }, + Hex = { + ["\27reset"] = "#r", + ["\27bold"] = "#b", + ["\27boldoff"] = "#/b", + ["\27italics"] = "#i", + ["\27italicsoff"] = "#/i", + ["\27underline"] = "#u", + ["\27underlineoff"] = "#/u", + ["\27strikethrough"] = "#s", + ["\27strikethroughoff"] = "#/s", + ["\27overline"] = "#o", + ["\27overlineoff"] = "#/o", + } +} + +local echoPatterns = _Echos.Patterns +local echoProcess = _Echos.Process + +function DemonTools.toHTML(t, reset) + reset = reset or { + background = { 0, 0, 0 }, + bold = false, + foreground = { 255, 255, 255 }, + italic = false, + overline = false, + reverse = false, + strikeout = false, + underline = false + } + local format = table.deepcopy(reset) + local result = getHTMLformat(format) + for _,v in ipairs(t) do + local formatChanged = false + if type(v) == "table" then + if v.fg then + format.foreground = {v.fg[1], v.fg[2], v.fg[3]} + formatChanged = true + end + if v.bg then + format.background = {v.bg[1], v.bg[2], v.bg[3]} + formatChanged = true + end + elseif v == "\27bold" then + format.bold = true + formatChanged = true + elseif v == "\27boldoff" then + format.bold = false + formatChanged = true + elseif v == "\27italics" then + format.italic = true + formatChanged = true + elseif v == "\27italicsoff" then + format.italic = false + formatChanged = true + elseif v == "\27underline" then + format.underline = true + formatChanged = true + elseif v == "\27underlineoff" then + format.underline = false + formatChanged = true + elseif v == "\27strikethrough" then + format.strikeout = true + formatChanged = true + elseif v == "\27strikethroughoff" then + format.strikeout = false + formatChanged = true + elseif v == "\27overline" then + format.overline = true + formatChanged = true + elseif v == "\27overlineoff" then + format.overline = false + formatChanged = true + elseif v == "\27reset" then + format = table.deepcopy(reset) + formatChanged = true + end + v = formatChanged and getHTMLformat(format) or v + result = result .. v + end + return result +end + +local function toEcho(colorType, colors) + colorType = colorType:lower() + local result + if colorType == "hex" then + local fg,bg = "", "" + if colors.fg then + fg = string.format("%02x%02x%02x", unpack(colors.fg)) + end + if colors.bg then + bg = string.format(",%02x%02x%02x", unpack(colors.bg)) + end + result = string.format("#%s%s", fg, bg) + elseif colorType == "color" then + local fg,bg = "","" + if colors.fg then + fg = closestColor(colors.fg) + end + if colors.bg then + bg = ":" .. closestColor(colors.bg[1], colors.bg[2], colors.bg[3]) + end + result = string.format("<%s%s>", fg, bg) + elseif colorType == "decimal" then + local fg,bg = "", "" + if colors.fg then + fg = string.format("%d,%d,%d", unpack(colors.fg)) + end + if colors.bg then + bg = string.format(":%d,%d,%d", unpack(colors.bg)) + end + result = string.format("<%s%s>", fg, bg) + end + return result +end + +function DemonTools.echoConverter(str, from, to, resetFormat) + local strType, fromType, toType, resetType = type(str), type(from), type(to), type(resetFormat) + local errTemplate = "bad argument #{argNum} type ({argName} as string expected, got {argType})" + local argNum, argName, argType + local err = false + if strType ~= "string" then + argNum = 1 + argName = "str" + argType = strType + err = true + elseif fromType ~= "string" then + argNum = 2 + argName = "from" + argType = fromType + err = true + elseif toType ~= "string" then + argNum = 3 + argName = "to" + argType = toType + err = true + elseif resetFormat and resetType ~= "table" then + argType = resetType + errTemplate = "bad argument #4 type (optional resetFormat as table of formatting options expected, got {argType})" + err = true + end + if err then + printError(f(errTemplate), true, true) + end + from = from:title() + local t = echoProcess(str, from) + if not echoPatterns[from] then + local msg = "argument #4 (from) must be a valid echo type. Valid types are: " .. table.concat(table.keys(echoPatterns), ",") + end + local processed = echoProcess(str, from) + if to:lower() == "html" then + return DemonTools.toHTML(processed, resetFormat) + end + local outputs = echoOutputs[to] + if not outputs then + local msg = "argument #3 (to) must be a valid echo type. Valid types are: " .. table.concat(table.keys(echoOutputs), ",") + printError(msg, true, true) + end + local result = "" + for _, token in ipairs(processed) do + local formatter = outputs[token] + if formatter and token:find("\27") then + result = result .. formatter + elseif type(token) == "table" then + result = result .. toEcho(to, token) + else + result = result .. token + end + end + return result +end + +return DemonTools diff --git a/src/resources/MDK/doc/classes/Chyron.html b/src/resources/MDK/doc/classes/Chyron.html new file mode 100755 index 0000000..05cf776 --- /dev/null +++ b/src/resources/MDK/doc/classes/Chyron.html @@ -0,0 +1,402 @@ + + + + + Reference + + + + +

+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class Chyron

+

Creates a label with a scrolling text element.

+

It is highly recommended you use a monospace font for this label.

+

Info:

+
    +
  • Copyright: 2019,2020
  • +
  • Author: Delra,Damian Monogue
  • +
+ + +

Methods

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
chyron:new(cons, container)Creates a new Chyron label
chyron:setDisplayWidth(displayWidth)Sets the numver of characters of the text to display at once
chyron:reset()Sets the Chyron from the first position, without changing enabled status
chyron:pause()Stops the Chyron with its current display
chyron:start()Start the Chyron back up from wherever it currently is
chyron:setUpdateTime(updateTime)Change the update time for the Chyron
chyron:enableAutoWidth()Enable autoWidth adjustment
chyron:disableAutoWidth()Disable autoWidth adjustment
chyron:stop()Stop the Chyron, and reset it to the original position
chyron:setMessage(message)Change the text being scrolled on the Chyron
chyron:setDelimiter(delimiter)Change the delimiter used to show the beginning and end of the message
+ +
+
+ + +

Methods

+ +
+
+ + chyron:new(cons, container) + line 80 +
+
+ Creates a new Chyron label + + +

Parameters:

+
    +
  • cons + table + table of constraints which configures the EMCO. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    option namedescriptiondefault
    textThe text to scroll on the label""
    updateTimeMilliseconds between movements (one letter shift)200
    displayWidthHow many chars wide to display the text28
    delimiterThis character will be inserted with a space either side to mark the stop/start of the message"|"
    enabledShould the chyron scroll?true
    fontWhat font to use for the Chyron? Available in Geyser.Label but we define a default."Bitstream Vera Sans Mono"
    fontSizeWhat font size to use for the Chyron? Available in Geyser.Label but we define a default.9
    autoWidthShould the Chyron resize to just fit the text?true
    alignmentWhat alignment(left/right/center) to use for the Chyron text? Available in Geyser.Label but we define a default."center"
    +
  • +
  • container + GeyserObject + The container to use as the parent for the Chyron +
  • +
+ + + + + +
+
+ + chyron:setDisplayWidth(displayWidth) + line 99 +
+
+ Sets the numver of characters of the text to display at once + + +

Parameters:

+
    +
  • displayWidth + number + number of characters to show at once +
  • +
+ + + + + +
+
+ + chyron:reset() + line 151 +
+
+ Sets the Chyron from the first position, without changing enabled status + + + + + + + +
+
+ + chyron:pause() + line 159 +
+
+ Stops the Chyron with its current display + + + + + + + +
+
+ + chyron:start() + line 167 +
+
+ Start the Chyron back up from wherever it currently is + + + + + + + +
+
+ + chyron:setUpdateTime(updateTime) + line 179 +
+
+ Change the update time for the Chyron + + +

Parameters:

+
    +
  • updateTime + number new updateTime in milliseconds +
  • +
+ + + + + +
+
+ + chyron:enableAutoWidth() + line 190 +
+
+ Enable autoWidth adjustment + + + + + + + +
+
+ + chyron:disableAutoWidth() + line 196 +
+
+ Disable autoWidth adjustment + + + + + + + +
+
+ + chyron:stop() + line 201 +
+
+ Stop the Chyron, and reset it to the original position + + + + + + + +
+
+ + chyron:setMessage(message) + line 212 +
+
+ Change the text being scrolled on the Chyron + + +

Parameters:

+
    +
  • message + string message the text you want to have scroll on the Chyron +
  • +
+ + + + + +
+
+ + chyron:setDelimiter(delimiter) + line 228 +
+
+ Change the delimiter used to show the beginning and end of the message + + +

Parameters:

+
    +
  • delimiter + string the new delimiter to use. I recommend using one character. +
  • +
+ + + + + +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/classes/EMCO.html b/src/resources/MDK/doc/classes/EMCO.html new file mode 100755 index 0000000..621faa7 --- /dev/null +++ b/src/resources/MDK/doc/classes/EMCO.html @@ -0,0 +1,3347 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class EMCO

+

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.

+

Info:

+
    +
  • Copyright: 2020 Damian Monogue,2021 Damian Monogue
  • +
  • License: MIT, see LICENSE.lua
  • +
  • Author: Damian Monogue
  • +
+ + +

Methods

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
emco:new(cons, container)Creates a new Embeddable Multi Console Object.
emco:miniConvertYATCO()Scans for the old YATCO configuration values and prints out a set of constraints to use.
emco:convertYATCO()Echos to the main console a script object you can add which will fully convert YATCO to EMCO.
emco:display(tabName, tabName, item[, any])Display the contents of one or more variables to an EMCO tab.
emco:removeTab(tabName)Remove a tab from the EMCO
emco:addTab(tabName[, position])Adds a tab to the EMCO object
emco:switchTab(tabName)Switches the active, visible tab of the EMCO to tabName
emco:cycleTab(reverse)Cycles between the tabs in order
emco:setBufferSize(bufferSize, deleteLines)Sets the buffer size and number of lines to delete for all managed miniconsoles.
emco:setBackgroundImage(tabName, imagePath, mode)Sets the background image for a tab's console.
emco:resetBackgroundImage(tabName)Resets the background image on a tab's console, returning it to the background color
emco:replay(tabName, numLines)Replays the last numLines lines from the log for tabName
emco:replayAll(numLines)Replays the last numLines in all miniconsoles
emco:processTemplate(str, tabName)Formats the string through EMCO's template.
emco:setPath(path)Sets the path for the EMCO for logging
emco:setFileName(fileName)Sets the fileName for the EMCO for logging
emco:setCmdLineStyleSheet(styleSheet)Sets the stylesheet for command lines in this EMCO
emco:enableCmdLine(tabName, template)Enables the commandLine on the specified tab.
emco:enableAllCmdLines()Enables all command lines, using whatever template they may currently have set
emco:disableAllCmdLines()Disables all commands line, but does not change their template
emco:disableCmdLine(tabName)Disables the command line for a particular tab
emco:setCmdAction(tabName, template)Sets the command action for a tab's command line.
emco:resetCmdAction(tabName)Resets the command action for tabName's miniconsole, which makes it work like the normal commandline
emco:getCmdLine(tabName)Gets the contents of tabName's cmdLine
emco:printCmd(tabName, txt)Prints to tabName's command line
emco:clearCmd(tabName)Clears tabName's command line
emco:appendCmd(tabName, txt)Appends text to tabName's command line
emco:reset()resets the object, redrawing everything
emco:fuzzyBoolean(bool)Expands boolean definitions to be more flexible.
emco:clear(tabName)clears a specific tab
emco:clearAll()clears all the tabs
emco:setTabFont(font)sets the font for all tabs
emco:setSingleTabFont(tabName, font)sets the font for a single tab.
emco:setFont(font)sets the font for all the miniconsoles
emco:setSingleWindowFont(tabName, font)sets the font for a specific miniconsole.
emco:setTabFontSize(fontSize)sets the font size for all tabs
emco:setTabAlignment(alignment)Sets the alignment for all the tabs
emco:enableTabUnderline()enables underline on all tabs
emco:disableTabUnderline()disables underline on all tabs
emco:enableTabItalics()enables italics on all tabs
emco:disableTabItalics()enables italics on all tabs
emco:enableTabBold()enables bold on all tabs
emco:disableTabBold()disables bold on all tabs
emco:enableCustomTimestampColor()enables custom colors for the timestamp, if displayed
emco:disableCustomTimestampColor()disables custom colors for the timestamp, if displayed
emco:enableTimestamp()enables the display of timestamps
emco:disableTimestamp()disables the display of timestamps
emco:setTimestampFormat(format)Sets the formatting for the timestamp, if enabled
emco:setTimestampBGColor(color)Sets the background color for the timestamp, if customTimestampColor is enabled.
emco:setTimestampFGColor(color)Sets the foreground color for the timestamp, if customTimestampColor is enabled.
emco:setAllTabName(allTabName)Sets the 'all' tab name.
emco:enableAllTab()Enables use of the 'all' tab
emco:disableAllTab()Disables use of the 'all' tab
emco:enableMapTab()Enables tying the Mudlet Mapper to one of the tabs.
emco:disableMapTab()disables binding the Mudlet Mapper to one of the tabs.
emco:setMapTabName(mapTabName)sets the name of the tab to bind the Mudlet Map.
emco:enableBlinkFromAll()Enables tab blinking even if you're on the 'all' tab
emco:disableBlinkFromAll()Disables tab blinking when you're on the 'all' tab
emco:enableGag()Enables gagging of the line passed in to :append(tabName)
emco:disableGag()Disables gagging of the line passed in to :append(tabName)
emco:enableBlink()Enables tab blinking when new information comes in to an inactive tab
emco:disableBlink()Disables tab blinking when new information comes in to an inactive tab
emco:enablePreserveBackground()Enables preserving the chat's background over the background of an incoming :append()
emco:disablePreserveBackground()Enables preserving the chat's background over the background of an incoming :append()
emco:setBlinkTime(blinkTime)Sets how long in seconds to wait between blinks
emco:setFontSize(fontSize)Sets the font size of the attached consoles
emco:setInactiveTabCSS(stylesheet)Sets the inactiveTabCSS
emco:setActiveTabCSS(stylesheet)Sets the activeTabCSS
emco:setActiveTabFGColor(color)Sets the FG color for the active tab
emco:setInactiveTabFGColor(color)Sets the FG color for the inactive tab
emco:setActiveTabBGColor(color)Sets the BG color for the active tab.
emco:setInactiveTabBGColor(color)Sets the BG color for the inactive tab.
emco:setConsoleColor(color)Sets the BG color for the consoles attached to this object
emco:setTabBoxCSS(css)Sets the CSS to use for the tab box which contains the tabs for the object
emco:setTabBoxColor(color)Sets the color to use for the tab box background
emco:setConsoleContainerColor(color)Sets the color for the container which holds the consoles attached to this object.
emco:setConsoleContainerCSS(css)Sets the CSS to use for the container which holds the consoles attached to this object
emco:setGap(gap)Sets the amount of space to use between the tabs and the consoles
emco:setTabHeight(tabHeight)Sets the height of the tabs in pixels
emco:enableAutoWrap()Enables autowrap for the object, and by extension all attached consoles.
emco:disableAutoWrap()Disables autowrap for the object, and by extension all attached consoles.
emco:setWrap(wrapAt)Sets the number of characters to wordwrap the attached consoles at.
emco:append(tabName, excludeAll)Appends the current line from the MUD to a tab.
emco:addNotifyTab(tabName)Adds a tab to the list of tabs to send OS toast/popup notifications for
emco:removeNotifyTab(tabName)Removes a tab from the list of tabs to send OS toast/popup notifications for
emco:addGag(pattern)Adds a pattern to the gag list for the EMCO
emco:removeGag(pattern)Removes a pattern from the gag list for the EMCO
emco:matchesGag(str)Checks if a string matches any of the EMCO's gag patterns
emco:enableNotifyWithFocus()Enables sending OS notifications even if Mudlet has focus
emco:disableNotifyWithFocus()Disables sending OS notifications if Mudlet has focus
emco:cecho(tabName, message, excludeAll)cecho to a tab, maintaining functionality
emco:decho(tabName, message, excludeAll)decho to a tab, maintaining functionality
emco:hecho(tabName, message, excludeAll)hecho to a tab, maintaining functionality
emco:echo(tabName, message, excludeAll)echo to a tab, maintaining functionality
emco:cechoLink(tabName, text, command, hint, excludeAll)cechoLink to a tab
emco:dechoLink(tabName, text, command, hint, excludeAll)dechoLink to a tab
emco:hechoLink(tabName, text, command, hint, excludeAll)hechoLink to a tab
emco:echoLink(tabName, text, command, hint, useCurrentFormat, excludeAll)echoLink to a tab
emco:cechoPopup(tabName, text, commands, hints, excludeAll)cechoPopup to a tab
emco:dechoPopup(tabName, text, commands, hints, excludeAll)dechoPopup to a tab
emco:hechoPopup(tabName, text, commands, hints, excludeAll)hechoPopup to a tab
emco:echoPopup(tabName, text, commands, hints, useCurrentFormat, excludeAll)echoPopup to a tab
emco:addAllTabExclusion(tabName)adds a tab to the exclusion list for echoing to the allTab
emco:removeAllTabExclusion(tabName)removess a tab from the exclusion list for echoing to the allTab
emco:enableBlankLine()Enable placing a blank line between all messages.
emco:disableBlankLine()Enable placing a blank line between all messages.
emco:enableScrollbars()Enable scrollbars for the miniconsoles
emco:disableScrollbars()Disable scrollbars for the miniconsoles
emco:save()Save an EMCO's configuration for reloading later.
emco:load()Load and apply a saved config for this EMCO
emco:enableTabLogging(tabName)Enables logging for tabName
emco:disableTabLogging(tabName)Disables logging for tabName
emco:enableAllLogging()Enables logging on all EMCO managed consoles
emco:disableAllLogging()Disables logging on all EMCO managed consoles
+ +
+
+ + +

Methods

+ +
+
+ + emco:new(cons, container) + line 367 +
+
+ Creates a new Embeddable Multi Console Object. +
see https://github.com/demonnic/EMCO/wiki for information on valid constraints and defaults + + +

Parameters:

+
    +
  • cons + table + 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{}
    cmdLineStyleSheetWhat stylesheet to use for the command lines."QPlainTextEdit {\n border: 1px solid grey;\n }\n"
    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.{}
    notifyTabsTables containing the names of all tabs you want to send notifications. IE {"Says", "Tells", "Org"}{}
    notifyWithFocusIf true, EMCO will send notifications even if Mudlet has focus. If false, it will only send them when Mudlet does NOT have focus.false
    +
  • +
  • container + GeyserObject + The container to use as the parent for the EMCO +
  • +
+ +

Returns:

+
    + + the newly created EMCO +
+ + + + +
+
+ + emco:miniConvertYATCO() + line 554 +
+
+ 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 + + + + + + + +
+
+ + emco:convertYATCO() + line 565 +
+
+ 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. + + + + + + + +
+
+ + emco:display(tabName, tabName, item[, any]) + line 611 +
+
+ Display the contents of one or more variables to an EMCO tab. like display() but targets the miniconsole + + +

Parameters:

+
    +
  • tabName + string the tab to displayu to +
  • +
  • tabName + string the tab to displayu to +
  • +
  • item + any The thing to display() +
  • +
  • any + item2 another thing to display() + (optional) +
  • +
+ + + + + +
+
+ + emco:removeTab(tabName) + line 621 +
+
+ Remove a tab from the EMCO + + +

Parameters:

+
    +
  • tabName + string the name of the tab you want to remove from the EMCO +
  • +
+ + + + + +
+
+ + emco:addTab(tabName[, position]) + line 648 +
+
+ Adds a tab to the EMCO object + + +

Parameters:

+
    +
  • tabName + string + the name of the tab to add +
  • +
  • position + number + position in the tab switcher to put this tab + (optional) +
  • +
+ + + + + +
+
+ + emco:switchTab(tabName) + line 669 +
+
+ Switches the active, visible tab of the EMCO to tabName + + +

Parameters:

+
    +
  • tabName + string the name of the tab to show +
  • +
+ + + + + +
+
+ + emco:cycleTab(reverse) + line 697 +
+
+ Cycles between the tabs in order + + +

Parameters:

+
    +
  • reverse + boolean + Defaults to false. When true, moves backward through the tab list rather than forward. +
  • +
+ + + + + +
+
+ + emco:setBufferSize(bufferSize, deleteLines) + line 778 +
+
+ Sets the buffer size and number of lines to delete for all managed miniconsoles. + + +

Parameters:

+
    +
  • bufferSize + number + number of lines of scrollback to maintain in the miniconsoles. Uses current value if nil is passed +
  • +
  • deleteLines + number + number of line to delete if the buffer filles up. Uses current value if nil is passed +
  • +
+ + + + + +
+
+ + emco:setBackgroundImage(tabName, imagePath, mode) + line 795 +
+
+ Sets the background image for a tab's console. use EMCO:resetBackgroundImage(tabName) to remove an image. + + +

Parameters:

+
    +
  • tabName + string + the tab to change the background image for. +
  • +
  • imagePath + string + the path to the image file to use. +
  • +
  • mode + string + the mode to use. Will default to "center" if not provided. +
  • +
+ + + + + +
+
+ + emco:resetBackgroundImage(tabName) + line 817 +
+
+ Resets the background image on a tab's console, returning it to the background color + + +

Parameters:

+
    +
  • tabName + string + the tab to change the background image for. +
  • +
+ + + + + +
+
+ + emco:replay(tabName, numLines) + line 848 +
+
+ Replays the last numLines lines from the log for tabName + + +

Parameters:

+
    +
  • tabName + the name of the tab to replay +
  • +
  • numLines + the number of lines to replay +
  • +
+ + + + + +
+
+ + emco:replayAll(numLines) + line 861 +
+
+ Replays the last numLines in all miniconsoles + + +

Parameters:

+
    +
  • numLines + +
  • +
+ + + + + +
+
+ + emco:processTemplate(str, tabName) + line 874 +
+
+ Formats the string through EMCO's template. |E is replaced with the EMCO's name. |N is replaced with the tab's name. + + +

Parameters:

+
    +
  • str + the string to replace tokens in +
  • +
  • tabName + optional, if included will be used for |N in the templated string. +
  • +
+ + + + + +
+
+ + emco:setPath(path) + line 884 +
+
+ Sets the path for the EMCO for logging + + +

Parameters:

+
    +
  • path + the template for the path. @see EMCO:new() +
  • +
+ + + + + +
+
+ + emco:setFileName(fileName) + line 900 +
+
+ Sets the fileName for the EMCO for logging + + +

Parameters:

+
    +
  • fileName + the template for the path. @see EMCO:new() +
  • +
+ + + + + +
+
+ + emco:setCmdLineStyleSheet(styleSheet) + line 916 +
+
+ Sets the stylesheet for command lines in this EMCO + + +

Parameters:

+
    +
  • styleSheet + string + the stylesheet to use for the command line. See https://wiki.mudlet.org/w/Manual:Lua_Functions#setCmdLineStyleSheet for examples +
  • +
+ + + + + +
+
+ + emco:enableCmdLine(tabName, template) + line 929 +
+
+ Enables the commandLine on the specified tab. + + +

Parameters:

+
    +
  • tabName + string + the name of the tab to turn the commandLine on for +
  • +
  • template + the template for the commandline to use, or the function to run when enter is hit. +
  • +
+ + + + +

Usage:

+
    +
    myEMCO:enableCmdLine(tabName, template)
    +
+ +
+
+ + emco:enableAllCmdLines() + line 942 +
+
+ Enables all command lines, using whatever template they may currently have set + + + + + + + +
+
+ + emco:disableAllCmdLines() + line 949 +
+
+ Disables all commands line, but does not change their template + + + + + + + +
+
+ + emco:disableCmdLine(tabName) + line 957 +
+
+ Disables the command line for a particular tab + + +

Parameters:

+
    +
  • tabName + string + the name of the tab to disable the command line of. +
  • +
+ + + + + +
+
+ + emco:setCmdAction(tabName, template) + line 970 +
+
+ 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 + + +

Parameters:

+
    +
  • tabName + string + the name of the tab to set the command action on +
  • +
  • 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
  • +
  • myEMCO:setCmdAction("CT", function(txt) send("ct " .. txt) end) -- functionally the same as the above
  • +
+ +
+
+ + emco:resetCmdAction(tabName) + line 997 +
+
+ Resets the command action for tabName's miniconsole, which makes it work like the normal commandline + + +

Parameters:

+
    +
  • tabName + string + the name of the tab to reset the cmdAction for +
  • +
+ + + + + +
+
+ + emco:getCmdLine(tabName) + line 1004 +
+
+ Gets the contents of tabName's cmdLine + + +

Parameters:

+
    +
  • tabName + the name of the tab to get the commandline of +
  • +
+ + + + + +
+
+ + emco:printCmd(tabName, txt) + line 1011 +
+
+ Prints to tabName's command line + + +

Parameters:

+
    +
  • tabName + the tab whose command line you want to print to +
  • +
  • txt + the text to print to the command line +
  • +
+ + + + + +
+
+ + emco:clearCmd(tabName) + line 1017 +
+
+ Clears tabName's command line + + +

Parameters:

+
    +
  • tabName + string + the tab whose command line you want to clear +
  • +
+ + + + + +
+
+ + emco:appendCmd(tabName, txt) + line 1024 +
+
+ Appends text to tabName's command line + + +

Parameters:

+
    +
  • tabName + string + the tab whose command line you want to append to +
  • +
  • txt + string + the text to append to the command line +
  • +
+ + + + + +
+
+ + emco:reset() + line 1029 +
+
+ resets the object, redrawing everything + + + + + + + +
+
+ + emco:fuzzyBoolean(bool) + line 1071 +
+
+ 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 + + +

Parameters:

+
    +
  • bool + item to test for truthiness +
  • +
+ + + + + +
+
+ + emco:clear(tabName) + line 1092 +
+
+ clears a specific tab + + +

Parameters:

+
    +
  • tabName + string + the name of the tab to clear +
  • +
+ + + + + +
+
+ + emco:clearAll() + line 1104 +
+
+ clears all the tabs + + + + + + + +
+
+ + emco:setTabFont(font) + line 1114 +
+
+ sets the font for all tabs + + +

Parameters:

+
    +
  • font + string + the font to use. +
  • +
+ + + + + +
+
+ + emco:setSingleTabFont(tabName, font) + line 1124 +
+
+ sets the font for a single tab. If you use setTabFont this will be overridden + + +

Parameters:

+
    +
  • tabName + string + the tab to change the font of +
  • +
  • font + string + the font to use for that tab +
  • +
+ + + + + +
+
+ + emco:setFont(font) + line 1134 +
+
+ sets the font for all the miniconsoles + + +

Parameters:

+
    +
  • font + string + the name of the font to use +
  • +
+ + + + + +
+
+ + emco:setSingleWindowFont(tabName, font) + line 1153 +
+
+ sets the font for a specific miniconsole. If setFont is called this will be overridden + + +

Parameters:

+
    +
  • tabName + string + the name of window to set the font for +
  • +
  • font + string + the name of the font to use +
  • +
+ + + + + +
+
+ + emco:setTabFontSize(fontSize) + line 1170 +
+
+ sets the font size for all tabs + + +

Parameters:

+
    +
  • fontSize + number + the font size to use for the tabs +
  • +
+ + + + + +
+
+ + emco:setTabAlignment(alignment) + line 1179 +
+
+ Sets the alignment for all the tabs + + +

Parameters:

+
    +
  • alignment + Valid alignments are 'c', 'center', 'l', 'left', 'r', 'right', or '' to not include the alignment as part of the echo +
  • +
+ + + + + +
+
+ + emco:enableTabUnderline() + line 1187 +
+
+ enables underline on all tabs + + + + + + + +
+
+ + emco:disableTabUnderline() + line 1195 +
+
+ disables underline on all tabs + + + + + + + +
+
+ + emco:enableTabItalics() + line 1203 +
+
+ enables italics on all tabs + + + + + + + +
+
+ + emco:disableTabItalics() + line 1211 +
+
+ enables italics on all tabs + + + + + + + +
+
+ + emco:enableTabBold() + line 1219 +
+
+ enables bold on all tabs + + + + + + + +
+
+ + emco:disableTabBold() + line 1227 +
+
+ disables bold on all tabs + + + + + + + +
+
+ + emco:enableCustomTimestampColor() + line 1235 +
+
+ enables custom colors for the timestamp, if displayed + + + + + + + +
+
+ + emco:disableCustomTimestampColor() + line 1240 +
+
+ disables custom colors for the timestamp, if displayed + + + + + + + +
+
+ + emco:enableTimestamp() + line 1245 +
+
+ enables the display of timestamps + + + + + + + +
+
+ + emco:disableTimestamp() + line 1250 +
+
+ disables the display of timestamps + + + + + + + +
+
+ + emco:setTimestampFormat(format) + line 1256 +
+
+ Sets the formatting for the timestamp, if enabled + + +

Parameters:

+
    +
  • format + string + Format string which describes the display of the timestamp. See: https://wiki.mudlet.org/w/Manual:Lua_Functions#getTime +
  • +
+ + + + + +
+
+ + emco:setTimestampBGColor(color) + line 1269 +
+
+ Sets the background color for the timestamp, if customTimestampColor is enabled. + + +

Parameters:

+
    +
  • color + Color string suitable for decho or hecho, or color name eg "purple", or table of colors {r,g,b} +
  • +
+ + + + + +
+
+ + emco:setTimestampFGColor(color) + line 1274 +
+
+ Sets the foreground color for the timestamp, if customTimestampColor is enabled. + + +

Parameters:

+
    +
  • color + Color string suitable for decho or hecho, or color name eg "purple", or table of colors {r,g,b} +
  • +
+ + + + + +
+
+ + emco:setAllTabName(allTabName) + line 1281 +
+
+ Sets the 'all' tab name. +
This is the name of the tab itself + + +

Parameters:

+
    +
  • allTabName + string + name of the tab to use as the all tab. Must be a tab which exists in the object. +
  • +
+ + + + + +
+
+ + emco:enableAllTab() + line 1294 +
+
+ Enables use of the 'all' tab + + + + + + + +
+
+ + emco:disableAllTab() + line 1299 +
+
+ Disables use of the 'all' tab + + + + + + + +
+
+ + emco:enableMapTab() + line 1305 +
+
+ 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 + + + + + + + +
+
+ + emco:disableMapTab() + line 1318 +
+
+ 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. + + + + + + + +
+
+ + emco:setMapTabName(mapTabName) + line 1327 +
+
+ 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. + + +

Parameters:

+
    +
  • mapTabName + string + name of the tab to connect the Mudlet Map to. +
  • +
+ + + + + +
+
+ + emco:enableBlinkFromAll() + line 1340 +
+
+ Enables tab blinking even if you're on the 'all' tab + + + + + + + +
+
+ + emco:disableBlinkFromAll() + line 1345 +
+
+ Disables tab blinking when you're on the 'all' tab + + + + + + + +
+
+ + emco:enableGag() + line 1350 +
+
+ Enables gagging of the line passed in to :append(tabName) + + + + + + + +
+
+ + emco:disableGag() + line 1355 +
+
+ Disables gagging of the line passed in to :append(tabName) + + + + + + + +
+
+ + emco:enableBlink() + line 1360 +
+
+ Enables tab blinking when new information comes in to an inactive tab + + + + + + + +
+
+ + emco:disableBlink() + line 1370 +
+
+ Disables tab blinking when new information comes in to an inactive tab + + + + + + + +
+
+ + emco:enablePreserveBackground() + line 1379 +
+
+ Enables preserving the chat's background over the background of an incoming :append() + + + + + + + +
+
+ + emco:disablePreserveBackground() + line 1384 +
+
+ Enables preserving the chat's background over the background of an incoming :append() + + + + + + + +
+
+ + emco:setBlinkTime(blinkTime) + line 1390 +
+
+ Sets how long in seconds to wait between blinks + + +

Parameters:

+
    +
  • blinkTime + number + time in seconds to wait between blinks +
  • +
+ + + + + +
+
+ + emco:setFontSize(fontSize) + line 1417 +
+
+ Sets the font size of the attached consoles + + +

Parameters:

+
    +
  • fontSize + number + font size for attached consoles +
  • +
+ + + + + +
+
+ + emco:setInactiveTabCSS(stylesheet) + line 1475 +
+
+ Sets the inactiveTabCSS + + +

Parameters:

+
    +
  • stylesheet + string + the stylesheet to use for inactive tabs. +
  • +
+ + + + + +
+
+ + emco:setActiveTabCSS(stylesheet) + line 1482 +
+
+ Sets the activeTabCSS + + +

Parameters:

+
    +
  • stylesheet + string + the stylesheet to use for active tab. +
  • +
+ + + + + +
+
+ + emco:setActiveTabFGColor(color) + line 1489 +
+
+ Sets the FG color for the active tab + + +

Parameters:

+
    +
  • color + Color string suitable for decho or hecho, or color name eg "purple", or table of colors {r,g,b} +
  • +
+ + + + + +
+
+ + emco:setInactiveTabFGColor(color) + line 1496 +
+
+ Sets the FG color for the inactive tab + + +

Parameters:

+
    +
  • color + Color string suitable for decho or hecho, or color name eg "purple", or table of colors {r,g,b} +
  • +
+ + + + + +
+
+ + emco:setActiveTabBGColor(color) + line 1504 +
+
+ Sets the BG color for the active tab. +
NOTE: If you set CSS for the active tab, it will override this setting. + + +

Parameters:

+
    +
  • color + Color string suitable for decho or hecho, or color name eg "purple", or table of colors {r,g,b} +
  • +
+ + + + + +
+
+ + emco:setInactiveTabBGColor(color) + line 1512 +
+
+ Sets the BG color for the inactive tab. +
NOTE: If you set CSS for the inactive tab, it will override this setting. + + +

Parameters:

+
    +
  • color + Color string suitable for decho or hecho, or color name eg "purple", or table of colors {r,g,b} +
  • +
+ + + + + +
+
+ + emco:setConsoleColor(color) + line 1519 +
+
+ Sets the BG color for the consoles attached to this object + + +

Parameters:

+
    +
  • color + Color string suitable for decho or hecho, or color name eg "purple", or table of colors {r,g,b} +
  • +
+ + + + + +
+
+ + emco:setTabBoxCSS(css) + line 1536 +
+
+ Sets the CSS to use for the tab box which contains the tabs for the object + + +

Parameters:

+
    +
  • css + string + The css styling to use for the tab box +
  • +
+ + + + + +
+
+ + emco:setTabBoxColor(color) + line 1549 +
+
+ Sets the color to use for the tab box background + + +

Parameters:

+
    +
  • color + Color string suitable for decho or hecho, or color name eg "purple", or table of colors {r,g,b} +
  • +
+ + + + + +
+
+ + emco:setConsoleContainerColor(color) + line 1561 +
+
+ Sets the color for the container which holds the consoles attached to this object. + + +

Parameters:

+
    +
  • color + Color string suitable for decho or hecho, or color name eg "purple", or table of colors {r,g,b} +
  • +
+ + + + + +
+
+ + emco:setConsoleContainerCSS(css) + line 1568 +
+
+ Sets the CSS to use for the container which holds the consoles attached to this object + + +

Parameters:

+
    +
  • css + string + CSS to use for the container +
  • +
+ + + + + +
+
+ + emco:setGap(gap) + line 1580 +
+
+ Sets the amount of space to use between the tabs and the consoles + + +

Parameters:

+
    +
  • gap + number + Number of pixels to keep between the tabs and consoles +
  • +
+ + + + + +
+
+ + emco:setTabHeight(tabHeight) + line 1594 +
+
+ Sets the height of the tabs in pixels + + +

Parameters:

+
    +
  • tabHeight + number + the height of the tabs for the object, in pixels +
  • +
+ + + + + +
+
+ + emco:enableAutoWrap() + line 1609 +
+
+ 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() + + + + + + + +
+
+ + emco:disableAutoWrap() + line 1623 +
+
+ 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() + + + + + + + +
+
+ + emco:setWrap(wrapAt) + line 1637 +
+
+ 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 + + +

Parameters:

+
    +
  • wrapAt + +
  • +
+ + + + + +
+
+ + emco:append(tabName, excludeAll) + line 1660 +
+
+ 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 + + +

Parameters:

+
    +
  • tabName + string + The name of the tab to append the line to +
  • +
  • excludeAll + boolean + if true, will exclude this from being mirrored to the allTab +
  • +
+ + + + + +
+
+ + emco:addNotifyTab(tabName) + line 1692 +
+
+ Adds a tab to the list of tabs to send OS toast/popup notifications for + + +

Parameters:

+
    +
  • tabName + string + the name of a tab to enable notifications for +
  • +
+ +

Returns:

+
    + + true if it was added, false if it was already included, nil if the tab does not exist. +
+ + + + +
+
+ + emco:removeNotifyTab(tabName) + line 1706 +
+
+ Removes a tab from the list of tabs to send OS toast/popup notifications for + + +

Parameters:

+
    +
  • tabName + string + the name of a tab to disable notifications for +
  • +
+ +

Returns:

+
    + + true if it was removed, false if it wasn't enabled to begin with, nil if the tab does not exist. +
+ + + + +
+
+ + emco:addGag(pattern) + line 1720 +
+
+ Adds a pattern to the gag list for the EMCO + + +

Parameters:

+
    +
  • pattern + string + a Lua pattern to gag. http://lua-users.org/wiki/PatternsTutorial +
  • +
+ +

Returns:

+
    + + true if it was added, false if it was already included. +
+ + + + +
+
+ + emco:removeGag(pattern) + line 1731 +
+
+ Removes a pattern from the gag list for the EMCO + + +

Parameters:

+
    +
  • pattern + string + a Lua pattern to no longer gag. http://lua-users.org/wiki/PatternsTutorial +
  • +
+ +

Returns:

+
    + + true if it was removed, false if it was not there to remove. +
+ + + + +
+
+ + emco:matchesGag(str) + line 1742 +
+
+ Checks if a string matches any of the EMCO's gag patterns + + +

Parameters:

+
    +
  • str + string + The text you're testing against the gag patterns +
  • +
+ +

Returns:

+
    + + false if it does not match any gag patterns. true and the matching pattern if it does match. +
+ + + + +
+
+ + emco:enableNotifyWithFocus() + line 1752 +
+
+ Enables sending OS notifications even if Mudlet has focus + + + + + + + +
+
+ + emco:disableNotifyWithFocus() + line 1757 +
+
+ Disables sending OS notifications if Mudlet has focus + + + + + + + +
+
+ + emco:cecho(tabName, message, excludeAll) + line 1875 +
+
+ cecho to a tab, maintaining functionality + + +

Parameters:

+
    +
  • tabName + string + the name of the tab to cecho to +
  • +
  • message + string + the message to cecho to that tab's console +
  • +
  • excludeAll + boolean + if true, will exclude this from being mirrored to the allTab +
  • +
+ + + + + +
+
+ + emco:decho(tabName, message, excludeAll) + line 1885 +
+
+ decho to a tab, maintaining functionality + + +

Parameters:

+
    +
  • tabName + string + the name of the tab to decho to +
  • +
  • message + string + the message to decho to that tab's console +
  • +
  • excludeAll + boolean + if true, will exclude this from being mirrored to the allTab +
  • +
+ + + + + +
+
+ + emco:hecho(tabName, message, excludeAll) + line 1895 +
+
+ hecho to a tab, maintaining functionality + + +

Parameters:

+
    +
  • tabName + string + the name of the tab to hecho to +
  • +
  • message + string + the message to hecho to that tab's console +
  • +
  • excludeAll + boolean + if true, will exclude this from being mirrored to the allTab +
  • +
+ + + + + +
+
+ + emco:echo(tabName, message, excludeAll) + line 1905 +
+
+ echo to a tab, maintaining functionality + + +

Parameters:

+
    +
  • tabName + string + the name of the tab to echo to +
  • +
  • message + string + the message to echo to that tab's console +
  • +
  • excludeAll + boolean + if true, will exclude this from being mirrored to the allTab +
  • +
+ + + + + +
+
+ + emco:cechoLink(tabName, text, command, hint, excludeAll) + line 1982 +
+
+ cechoLink to a tab + + +

Parameters:

+
    +
  • tabName + string + the name of the tab to cechoLink to +
  • +
  • text + string + the text underlying the link +
  • +
  • command + string + the lua code to run in string format +
  • +
  • hint + string + the tooltip hint to use for the link +
  • +
  • excludeAll + boolean + if true, will exclude this from being mirrored to the allTab +
  • +
+ + + + + +
+
+ + emco:dechoLink(tabName, text, command, hint, excludeAll) + line 1994 +
+
+ dechoLink to a tab + + +

Parameters:

+
    +
  • tabName + string + the name of the tab to dechoLink to +
  • +
  • text + string + the text underlying the link +
  • +
  • command + string + the lua code to run in string format +
  • +
  • hint + string + the tooltip hint to use for the link +
  • +
  • excludeAll + boolean + if true, will exclude this from being mirrored to the allTab +
  • +
+ + + + + +
+
+ + emco:hechoLink(tabName, text, command, hint, excludeAll) + line 2006 +
+
+ hechoLink to a tab + + +

Parameters:

+
    +
  • tabName + string + the name of the tab to hechoLink to +
  • +
  • text + string + the text underlying the link +
  • +
  • command + string + the lua code to run in string format +
  • +
  • hint + string + the tooltip hint to use for the link +
  • +
  • excludeAll + boolean + if true, will exclude this from being mirrored to the allTab +
  • +
+ + + + + +
+
+ + emco:echoLink(tabName, text, command, hint, useCurrentFormat, excludeAll) + line 2019 +
+
+ echoLink to a tab + + +

Parameters:

+
    +
  • tabName + string + the name of the tab to echoLink to +
  • +
  • text + string + the text underlying the link +
  • +
  • command + string + the lua code to run in string format +
  • +
  • hint + string + the tooltip hint to use for the link +
  • +
  • useCurrentFormat + boolean + use the format for the window or blue on black (hyperlink colors) +
  • +
  • excludeAll + boolean + if true, will exclude this from being mirrored to the allTab +
  • +
+ + + + + +
+
+ + emco:cechoPopup(tabName, text, commands, hints, excludeAll) + line 2031 +
+
+ cechoPopup to a tab + + +

Parameters:

+
    +
  • tabName + string + the name of the tab to cechoPopup to +
  • +
  • text + string + the text underlying the link +
  • +
  • commands + table + the lua code to run in string format +
  • +
  • hints + table + the tooltip hint to use for the link +
  • +
  • excludeAll + boolean + if true, will exclude this from being mirrored to the allTab +
  • +
+ + + + + +
+
+ + emco:dechoPopup(tabName, text, commands, hints, excludeAll) + line 2043 +
+
+ dechoPopup to a tab + + +

Parameters:

+
    +
  • tabName + string + the name of the tab to dechoPopup to +
  • +
  • text + string + the text underlying the link +
  • +
  • commands + table + the lua code to run in string format +
  • +
  • hints + table + the tooltip hint to use for the link +
  • +
  • excludeAll + boolean + if true, will exclude this from being mirrored to the allTab +
  • +
+ + + + + +
+
+ + emco:hechoPopup(tabName, text, commands, hints, excludeAll) + line 2055 +
+
+ hechoPopup to a tab + + +

Parameters:

+
    +
  • tabName + string + the name of the tab to hechoPopup to +
  • +
  • text + string + the text underlying the link +
  • +
  • commands + table + the lua code to run in string format +
  • +
  • hints + table + the tooltip hint to use for the link +
  • +
  • excludeAll + boolean + if true, will exclude this from being mirrored to the allTab +
  • +
+ + + + + +
+
+ + emco:echoPopup(tabName, text, commands, hints, useCurrentFormat, excludeAll) + line 2068 +
+
+ echoPopup to a tab + + +

Parameters:

+
    +
  • tabName + string + the name of the tab to echoPopup to +
  • +
  • text + string + the text underlying the link +
  • +
  • commands + table + the lua code to run in string format +
  • +
  • hints + table + the tooltip hint to use for the link +
  • +
  • useCurrentFormat + boolean + use the format for the window or blue on black (hyperlink colors) +
  • +
  • excludeAll + boolean + if true, will exclude this from being mirrored to the allTab +
  • +
+ + + + + +
+
+ + emco:addAllTabExclusion(tabName) + line 2076 +
+
+ adds a tab to the exclusion list for echoing to the allTab + + +

Parameters:

+
    +
  • tabName + string + the name of the tab to add to the exclusion list +
  • +
+ + + + + +
+
+ + emco:removeAllTabExclusion(tabName) + line 2086 +
+
+ removess a tab from the exclusion list for echoing to the allTab + + +

Parameters:

+
    +
  • tabName + string + the name of the tab to remove from the exclusion list +
  • +
+ + + + + +
+
+ + emco:enableBlankLine() + line 2124 +
+
+ Enable placing a blank line between all messages. + + + + + + + +
+
+ + emco:disableBlankLine() + line 2129 +
+
+ Enable placing a blank line between all messages. + + + + + + + +
+
+ + emco:enableScrollbars() + line 2134 +
+
+ Enable scrollbars for the miniconsoles + + + + + + + +
+
+ + emco:disableScrollbars() + line 2140 +
+
+ Disable scrollbars for the miniconsoles + + + + + + + +
+
+ + emco:save() + line 2160 +
+
+ Save an EMCO's configuration for reloading later. Filename is based on the EMCO's name property. + + + + + + + +
+
+ + emco:load() + line 2229 +
+
+ Load and apply a saved config for this EMCO + + + + + + + +
+
+ + emco:enableTabLogging(tabName) + line 2310 +
+
+ Enables logging for tabName + + +

Parameters:

+
    +
  • tabName + string + the name of the tab you want to enable logging for +
  • +
+ + + + + +
+
+ + emco:disableTabLogging(tabName) + line 2323 +
+
+ Disables logging for tabName + + +

Parameters:

+
    +
  • tabName + string + the name of the tab you want to disable logging for +
  • +
+ + + + + +
+
+ + emco:enableAllLogging() + line 2335 +
+
+ Enables logging on all EMCO managed consoles + + + + + + + +
+
+ + emco:disableAllLogging() + line 2343 +
+
+ Disables logging on all EMCO managed consoles + + + + + + + +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/classes/LoggingConsole.html b/src/resources/MDK/doc/classes/LoggingConsole.html new file mode 100755 index 0000000..2b4a3e3 --- /dev/null +++ b/src/resources/MDK/doc/classes/LoggingConsole.html @@ -0,0 +1,813 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class LoggingConsole

+

MiniConsole with logging capabilities

+

+

Info:

+
    +
  • Copyright: 2020 Damian Monogue
  • +
  • License: MIT, see LICENSE.lua
  • +
  • Author: Damian Monogue
  • +
+ + +

Methods

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
loggingconsole:new(cons, container)Creates and returns a new LoggingConsole.
loggingconsole:getExtension()Returns the file extension of the logfile this console will log to
loggingconsole:getPath()Returns the path to the logfile for this console
loggingconsole:setPath(path)Sets the path to use for the log file.
loggingconsole:getFileName()Returns the filename for the logfile for this console
loggingconsole:setFileName(fileName)Sets the fileName to use for the log file.
loggingconsole:getFullFilename()Returns the pull path and filename for the logfile for this console
loggingconsole:enableLogging()Turns logging for this console on
loggingconsole:disableLogging()Turns logging for this console off
loggingconsole:cechoLink(text, command, hint, log)cechoLink for LoggingConsole
loggingconsole:dechoLink(text, command, hint, log)dechoLink for LoggingConsole
loggingconsole:hechoLink(text, command, hint, log)hechoLink for LoggingConsole
loggingconsole:echoLink(text, command, hint, useCurrentFormat, log)echoLink for LoggingConsole
loggingconsole:cechoPopup(text, commands, hints, log)cechoPopup for LoggingConsole
loggingconsole:dechoPopup(text, commands, hints, log)dechoPopup for LoggingConsole
loggingconsole:hechoPopup(text, commands, hints, log)hechoPopup for LoggingConsole
loggingconsole:echoPopup(text, commands, hints, useCurrentFormat, log)echoPopup for LoggingConsole
loggingconsole:appendBuffer(log)Append copy()ed text to the console
loggingconsole:append(log)Append copy()ed text to the console
loggingconsole:echo(str, log)echo's a string to the console.
loggingconsole:hecho(str, log)hecho's a string to the console.
loggingconsole:decho(str, log)decho's a string to the console.
loggingconsole:cecho(str, log)cecho's a string to the console.
loggingconsole:replay(numberOfLines)Replays the last X lines from the console's log file, if it exists
+ +
+
+ + +

Methods

+ +
+
+ + loggingconsole:new(cons, container) + line 56 +
+
+ Creates and returns a new LoggingConsole. + + +

Parameters:

+
    +
  • 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"
    +
  • +
  • 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
    +
+ +
+
+ + loggingconsole:getExtension() + line 67 +
+
+ Returns the file extension of the logfile this console will log to + + + + + + + +
+
+ + loggingconsole:getPath() + line 95 +
+
+ Returns the path to the logfile for this console + + + + + + + +
+
+ + loggingconsole:setPath(path) + line 105 +
+
+ Sets the path to use for the log file. + + +

Parameters:

+
    +
  • 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) +
  • +
+ + + + + +
+
+ + loggingconsole:getFileName() + line 110 +
+
+ Returns the filename for the logfile for this console + + + + + + + +
+
+ + loggingconsole:setFileName(fileName) + line 118 +
+
+ Sets the fileName to use for the log file. + + +

Parameters:

+
    +
  • 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) +
  • +
+ + + + + +
+
+ + loggingconsole:getFullFilename() + line 123 +
+
+ Returns the pull path and filename for the logfile for this console + + + + + + + +
+
+ + loggingconsole:enableLogging() + line 132 +
+
+ Turns logging for this console on + + + + + + + +
+
+ + loggingconsole:disableLogging() + line 137 +
+
+ Turns logging for this console off + + + + + + + +
+
+ + loggingconsole:cechoLink(text, command, hint, log) + line 309 +
+
+ cechoLink for LoggingConsole + + +

Parameters:

+
    +
  • text + the text to use for the link +
  • +
  • command + the command to send when the link is clicked, as text. IE [[send("sleep")]] +
  • +
  • hint + A tooltip which is displayed when the mouse is over the link +
  • +
  • log + Should we log this line? Defaults to self.log if not passed. +
  • +
+ + + + + +
+
+ + loggingconsole:dechoLink(text, command, hint, log) + line 318 +
+
+ dechoLink for LoggingConsole + + +

Parameters:

+
    +
  • text + the text to use for the link +
  • +
  • command + the command to send when the link is clicked, as text. IE [[send("sleep")]] +
  • +
  • hint + A tooltip which is displayed when the mouse is over the link +
  • +
  • log + Should we log this line? Defaults to self.log if not passed. +
  • +
+ + + + + +
+
+ + loggingconsole:hechoLink(text, command, hint, log) + line 327 +
+
+ hechoLink for LoggingConsole + + +

Parameters:

+
    +
  • text + the text to use for the link +
  • +
  • command + the command to send when the link is clicked, as text. IE [[send("sleep")]] +
  • +
  • hint + A tooltip which is displayed when the mouse is over the link +
  • +
  • log + Should we log this line? Defaults to self.log if not passed. +
  • +
+ + + + + +
+
+ + loggingconsole:echoLink(text, command, hint, useCurrentFormat, log) + line 340 +
+
+ echoLink for LoggingConsole + + +

Parameters:

+
    +
  • text + the text to use for the link +
  • +
  • command + the command to send when the link is clicked, as text. IE [[send("sleep")]] +
  • +
  • hint + A tooltip which is displayed when the mouse is over the link +
  • +
  • useCurrentFormat + If set to true, will look like the text around it. If false it will be blue and underline. +
  • +
  • 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)
  • +
  • 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
  • +
  • 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.
  • +
+ +
+
+ + loggingconsole:cechoPopup(text, commands, hints, log) + line 349 +
+
+ cechoPopup for LoggingConsole + + +

Parameters:

+
    +
  • text + the text to use for the link +
  • +
  • commands + the commands to send when the popup is activated, as table. IE {[[send("sleep")]], [[send("stand")]]} +
  • +
  • hints + A tooltip which is displayed when the mouse is over the link. IE {{"sleep", "stand"}} +
  • +
  • log + Should we log this line? Defaults to self.log if not passed. +
  • +
+ + + + + +
+
+ + loggingconsole:dechoPopup(text, commands, hints, log) + line 358 +
+
+ dechoPopup for LoggingConsole + + +

Parameters:

+
    +
  • text + the text to use for the link +
  • +
  • commands + the commands to send when the popup is activated, as table. IE {[[send("sleep")]], [[send("stand")]]} +
  • +
  • hints + A tooltip which is displayed when the mouse is over the link. IE {{"sleep", "stand"}} +
  • +
  • log + Should we log this line? Defaults to self.log if not passed. +
  • +
+ + + + + +
+
+ + loggingconsole:hechoPopup(text, commands, hints, log) + line 367 +
+
+ hechoPopup for LoggingConsole + + +

Parameters:

+
    +
  • text + the text to use for the link +
  • +
  • commands + the commands to send when the popup is activated, as table. IE {[[send("sleep")]], [[send("stand")]]} +
  • +
  • hints + A tooltip which is displayed when the mouse is over the link. IE {{"sleep", "stand"}} +
  • +
  • log + Should we log this line? Defaults to self.log if not passed. +
  • +
+ + + + + +
+
+ + loggingconsole:echoPopup(text, commands, hints, useCurrentFormat, log) + line 380 +
+
+ echoPopup for LoggingConsole + + +

Parameters:

+
    +
  • text + the text to use for the link +
  • +
  • commands + the commands to send when the popup is activated, as table. IE {[[send("sleep")]], [[send("stand")]]} +
  • +
  • hints + A tooltip which is displayed when the mouse is over the link. IE {{"sleep", "stand"}} +
  • +
  • useCurrentFormat + If set to true, will look like the text around it. If false it will be blue and underline. +
  • +
  • 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)
  • +
  • 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
  • +
  • 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.
  • +
+ +
+
+ + loggingconsole:appendBuffer(log) + line 386 +
+
+ Append copy()ed text to the console + + +

Parameters:

+
    +
  • log + should we log this? +
  • +
+ + + + + +
+
+ + loggingconsole:append(log) + line 392 +
+
+ Append copy()ed text to the console + + +

Parameters:

+
    +
  • log + should we log this? +
  • +
+ + + + + +
+
+ + loggingconsole:echo(str, log) + line 399 +
+
+ echo's a string to the console. + + +

Parameters:

+
    +
  • str + the string to echo +
  • +
  • log + should this be logged? Used to override the .log constraint +
  • +
+ + + + + +
+
+ + loggingconsole:hecho(str, log) + line 406 +
+
+ hecho's a string to the console. + + +

Parameters:

+
    +
  • str + the string to hecho +
  • +
  • log + should this be logged? Used to override the .log constraint +
  • +
+ + + + + +
+
+ + loggingconsole:decho(str, log) + line 413 +
+
+ decho's a string to the console. + + +

Parameters:

+
    +
  • str + the string to decho +
  • +
  • log + should this be logged? Used to override the .log constraint +
  • +
+ + + + + +
+
+ + loggingconsole:cecho(str, log) + line 420 +
+
+ cecho's a string to the console. + + +

Parameters:

+
    +
  • str + the string to cecho +
  • +
  • log + should this be logged? Used to override the .log constraint +
  • +
+ + + + + +
+
+ + loggingconsole:replay(numberOfLines) + line 426 +
+
+ Replays the last X lines from the console's log file, if it exists + + +

Parameters:

+
    +
  • numberOfLines + The number of lines to replay from the end of the file +
  • +
+ + + + + +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/classes/Loginator.html b/src/resources/MDK/doc/classes/Loginator.html new file mode 100755 index 0000000..7174dae --- /dev/null +++ b/src/resources/MDK/doc/classes/Loginator.html @@ -0,0 +1,546 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class Loginator

+

Loginator creates an object which allows you to log things to file at + various severity levels, with the ability to only log items above a specific + severity to file.

+

+

Info:

+
    +
  • Copyright: 2021 Damian Monogue
  • +
  • License: MIT, see LICENSE.lua
  • +
  • Author: Damian Monogue
  • +
+ + +

Methods

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
loginator:new(options)Creates a new Loginator object
loginator:setColorForLevel(color, level)Set the color to associate with a logging level post-creation
loginator:getFullFilename()Returns the full path and filename to the logfile
loginator:error(msg)Write an error level message to the logfile.
loginator:warn(msg)Write a warn level message to the logfile.
loginator:info(msg)Write an info level message to the logfile.
loginator:debug(msg)Write a debug level message to the logfile.
loginator:log(msg, level)Write a message to the log file and optionally specify the level
loginator:open()Uses openUrl() to request your OS open the logfile in the appropriate application.
loginator:openDir()Uses openUrl() to request your OS open the directory the logfile resides in.
loginator:getPath(filename)Returns the path to the log file (directory in which the file resides) as a string
+ +
+
+ + +

Methods

+ +
+
+ + loginator:new(options) + line 214 +
+
+ Creates a new Loginator object + + +

Parameters:

+
    +
  • options + table + table of options for the logger + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    option namedescriptiondefault
    formatWhat format to log in? "h" for html, "a" for ansi, anything else for plaintext."h"
    nameWhat is the name of the logger? Will replace |n in templateslogname
    levelWhat level should the logger operate at? This will control what level the log function defaults to, as well as what logs will actually be written
    + Only items of an equal or higher severity to this will be written to the log file.
    "info"
    bgColorWhat background color to use for html logs"black"
    fgColorWhat color to use for the main text in html logs"white"
    fontSizeWhat font size to use in html logs12
    levelColorsTable with the log level as the key, and the color which corresponds to it as the value{ error = "red", warn = "DarkOrange", info = "ForestGreen", debug = "ansi_yellow" }
    fileNameTemplateA template which will be transformed into the full filename, with path. See template options below for replacements"|p/log/Loginator/|y-|M-|d-|n.|e"
    entryTemplateThe template which controls the look of each log entry. See template options below for replacements"|y-|M-|d |h:|m:|s.|x [|c|l|r] |t"

    + Table of template options + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    template codewhat it is replaced withexample
    |ythe year in 4 digits2021
    |pgetMudletHomeDir()/home/demonnic/.config/mudlet/profiles/testprofile
    |MMonth as 2 digits05
    |dday, as 2 digits23
    |hhour in 24hr time format, 2 digits03
    |mminute as 2 digits42
    |sseconds as 2 digits34
    |xmilliseconds as 3 digits194
    |eFilename extension expected. "html" for html format, "log" for everything elsehtml
    |lThe logging level of the entry, in ALLCAPSWARN
    |cThe color which corresponds with the logging level. Set via the levelColors table in the options. Example not included.
    |rReset back to standard color. Used to close |c. Example not included
    |nThe name of the logger, set via the options when you have Loginator create it.CoolPackageLog
    +
  • +
+ +

Returns:

+
    + + newly created logger object +
+ + + + +
+
+ + loginator:setColorForLevel(color, level) + line 288 +
+
+ Set the color to associate with a logging level post-creation + + +

Parameters:

+
    +
  • color + The color to set for the level, as a string. Can be any valid color string for cecho, decho, or hecho. +
  • +
  • level + The level to set the color for. Must be one of 'error', 'warn', 'info', or 'debug' +
  • +
+ +

Returns:

+
    + + true if the color is updated, or nil+error if it could not be updated for some reason. +
+ + + + +
+
+ + loginator:getFullFilename() + line 332 +
+
+ Returns the full path and filename to the logfile + + + + + + + +
+
+ + loginator:error(msg) + line 339 +
+
+ Write an error level message to the logfile. Error level messages are always written. + + +

Parameters:

+
    +
  • msg + the message to log +
  • +
+ +

Returns:

+
    + + true if msg written, nil+error if error +
+ + + + +
+
+ + loginator:warn(msg) + line 349 +
+
+ Write a warn level message to the logfile. + Msg is only written if the logger level is <= warn + From most to least severe the levels are: + error > warn > info > debug + + +

Parameters:

+
    +
  • msg + the message to log +
  • +
+ +

Returns:

+
    + + true if msg written, false if skipped due to level, nil+error if error +
+ + + + +
+
+ + loginator:info(msg) + line 359 +
+
+ Write an info level message to the logfile. + Msg is only written if the logger level is <= info + From most to least severe the levels are: + error > warn > info > debug + + +

Parameters:

+
    +
  • msg + the message to log +
  • +
+ +

Returns:

+
    + + true if msg written, false if skipped due to level, nil+error if error +
+ + + + +
+
+ + loginator:debug(msg) + line 369 +
+
+ Write a debug level message to the logfile. + Msg is only written if the logger level is debug + From most to least severe the levels are: + error > warn > info > debug + + +

Parameters:

+
    +
  • msg + the message to log +
  • +
+ +

Returns:

+
    + + true if msg written, false if skipped due to level, nil+error if error +
+ + + + +
+
+ + loginator:log(msg, level) + line 377 +
+
+ Write a message to the log file and optionally specify the level + + +

Parameters:

+
    +
  • msg + the message to log +
  • +
  • level + the level to log the message at. Defaults to the level of the logger itself if not provided. +
  • +
+ +

Returns:

+
    + + true if msg written, false if skipped due to level, nil+error if error +
+ + + + +
+
+ + loginator:open() + line 409 +
+
+ Uses openUrl() to request your OS open the logfile in the appropriate application. Usually your web browser for html and text editor for all others. + + + + + + + +
+
+ + loginator:openDir() + line 414 +
+
+ Uses openUrl() to request your OS open the directory the logfile resides in. This allows for easier browsing if you have more than one file. + + + + + + + +
+
+ + loginator:getPath(filename) + line 420 +
+
+ Returns the path to the log file (directory in which the file resides) as a string + + +

Parameters:

+
    +
  • filename + optional filename to return the path of. If not supplied, with use the logger's current filename +
  • +
+ + + + + +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/classes/MasterMindSolver.html b/src/resources/MDK/doc/classes/MasterMindSolver.html new file mode 100755 index 0000000..7e5c34b --- /dev/null +++ b/src/resources/MDK/doc/classes/MasterMindSolver.html @@ -0,0 +1,277 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class MasterMindSolver

+

Interactive object which helps you solve a Master Mind puzzle.

+

+

Info:

+
    +
  • Copyright: 2021 Damian Monogue,2008,2009 Konstantinos Asimakis for code used to turn an index number into a guess (indexToGuess method)
  • +
  • Author: Damian Monogue
  • +
+ + +

Methods

+ + + + + + + + + + + + + + + + + +
mastermindsolver:new(options)Creates a new Master Mind solver
mastermindsolver:reducePossible(guess, coloredPins, whitePins)Function used to reduce the remaining possible answers, given a guess and the answer to that guess.
mastermindsolver:checkLastSuggestion(coloredPins, whitePins)Function which assumes you used the last suggested guess from the solver, and reduces the number of possible correct solutions based on the answer given
mastermindsolver:getValidGuess(useActions)Used to get one of the remaining valid possible guesses
+ +
+
+ + +

Methods

+ +
+
+ + mastermindsolver:new(options) + line 81 +
+
+ Creates a new Master Mind solver + + +

Parameters:

+
    +
  • options + table + table of configuration options for the solver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    option namedescriptiondefault
    placesHow many spots in the code we're breaking?4
    itemsThe table of colors/gemstones/whatever which can be part of the code{"red", "orange", "yellow", "green", "blue", "purple"}
    templateThe string template to use for the guess. Within the template, |t is replaced by the item. Used as the command if autoSend is true"|t"
    autoSendShould we send the guess directly to the server?false
    allowDuplicatesCan the same item be used more than once in a code?true
    singleCommandIf true, combines the guess into a single command, with each one separated by the separatorfalse
    separatorIf sending the guess as a single command, what should we put between the guesses to separate them?" "
    +
  • +
+ + + + + +
+
+ + mastermindsolver:reducePossible(guess, coloredPins, whitePins) + line 178 +
+
+ Function used to reduce the remaining possible answers, given a guess and the answer to that guess. This is not undoable. + + +

Parameters:

+
    +
  • guess + table + guess which the answer belongs to. Uses numbers, rather than item names. IE { 1, 1, 2, 2} rather than { "blue", "blue", "green", "green" } +
  • +
  • coloredPins + number + how many parts of the guess are both the right color and the right place +
  • +
  • whitePins + number + how many parts of the guess are the right color, but in the wrong place +
  • +
+ +

Returns:

+
    + + true if you solved the puzzle (coloredPins == number of positions in the code), or false otherwise +
+ + + + +
+
+ + mastermindsolver:checkLastSuggestion(coloredPins, whitePins) + line 202 +
+
+ Function which assumes you used the last suggested guess from the solver, and reduces the number of possible correct solutions based on the answer given + + +

Parameters:

+
    +
  • coloredPins + number + how many parts of the guess are both the right color and the right place +
  • +
  • whitePins + number + how many parts of the guess are the right color, but in the wrong place +
  • +
+ +

Returns:

+
    + + true if you solved the puzzle (coloredPins == number of positions in the code), or false otherwise +
+ + +

See also:

+ + + +
+
+ + mastermindsolver:getValidGuess(useActions) + line 208 +
+
+ Used to get one of the remaining valid possible guesses + + +

Parameters:

+
    +
  • useActions + boolean + if true, will return the guess as the commands which would be sent, rather than the numbered items +
  • +
+ + + + + +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/classes/SUG.html b/src/resources/MDK/doc/classes/SUG.html new file mode 100755 index 0000000..fad4257 --- /dev/null +++ b/src/resources/MDK/doc/classes/SUG.html @@ -0,0 +1,407 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class SUG

+

Self Updating Gauge, extends Geyser.Gauge

+

+

Info:

+
    +
  • Copyright: 2020 Damian Monogue
  • +
  • License: MIT, see LICENSE.lua
  • +
  • Author: Damian Monogue
  • +
+ + +

Methods

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
sug:new(cons, container)Creates a new Self Updating Gauge.
sug:setUpdateTime(time)Set how often to update the gauge on a timer
sug:setUpdateEvent(event)Set the event to listen for to update the gauge
sug:setCurrentVariable(variableName)Set the name of the variable the Self Updating Gauge watches for the 'current' value of the gauge
sug:setMaxVariable(variableName)Set the name of the variable the Self Updating Gauge watches for the 'max' value of the gauge
sug:setTextTemplate(template)Set the template for the Self Updating Gauge to set the text with.
sug:setUpdateHook(func)Set the updateHook function which is run just prior to the gauge updating
sug:stop()Stops the Self Updating Gauge from updating
sug:start()Starts the Self Updating Gauge updating.
sug:update()Reads the values from currentVariable and maxVariable, and updates the gauge's value and text.
+ +
+
+ + +

Methods

+ +
+
+ + sug:new(cons, container) + line 91 +
+
+ Creates a new Self Updating Gauge. + + +

Parameters:

+
    +
  • cons + table + 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""
    updateHookA function which is run each time the gauge updates. Should take 3 arguments, the gauge itself, current value, and max value. You can return new current and max values to be used, for example `return 34, 120` would cause the gauge to use 34 for current and 120 for max regardless of what the variables it reads say.
    +
  • +
  • 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,
    +})
    +
+ +
+
+ + sug:setUpdateTime(time) + line 115 +
+
+ Set how often to update the gauge on a timer + + +

Parameters:

+
    +
  • time + number + time in milliseconds. 0 to disable the timer +
  • +
+ + + + + +
+
+ + sug:setUpdateEvent(event) + line 126 +
+
+ Set the event to listen for to update the gauge + + +

Parameters:

+
    +
  • event + string + the name of the event to listen for, use "" to disable events without stopping any existing timers +
  • +
+ + + + + +
+
+ + sug:setCurrentVariable(variableName) + line 137 +
+
+ Set the name of the variable the Self Updating Gauge watches for the 'current' value of the gauge + + +

Parameters:

+
    +
  • variableName + string + The name of the variable to get the current value for the gauge. For instance "currentHP", "gmcp.Char.Vitals.hp" etc +
  • +
+ + + + + +
+
+ + sug:setMaxVariable(variableName) + line 152 +
+
+ Set the name of the variable the Self Updating Gauge watches for the 'max' value of the gauge + + +

Parameters:

+
    +
  • variableName + string + 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 +
  • +
+ + + + + +
+
+ + sug:setTextTemplate(template) + line 172 +
+
+ 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 + + +

Parameters:

+
    +
  • template + string + 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. +
  • +
+ + + + + +
+
+ + sug:setUpdateHook(func) + line 182 +
+
+ Set the updateHook function which is run just prior to the gauge updating + + +

Parameters:

+
    +
  • func + function + The function which will be called when the gauge updates. It should take 3 arguments, the gauge itself, the current value, and the max value. If you wish to override the current or max values used for the gauge, you can return new current and max values, like `return newCurrent newMax` +
  • +
+ + + + + +
+
+ + sug:stop() + line 191 +
+
+ Stops the Self Updating Gauge from updating + + + + + + + +
+
+ + sug:start() + line 204 +
+
+ Starts the Self Updating Gauge updating. If it is already updating, it will restart it. + + + + + + + +
+
+ + sug:update() + line 218 +
+
+ Reads the values from currentVariable and maxVariable, and updates the gauge's value and text. + + + + + + + +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/classes/SortBox.html b/src/resources/MDK/doc/classes/SortBox.html new file mode 100755 index 0000000..5ce7b0c --- /dev/null +++ b/src/resources/MDK/doc/classes/SortBox.html @@ -0,0 +1,593 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class SortBox

+

An H/VBox alternative which can be set to either vertical or horizontal, and will autosort the windows

+

+

Info:

+
    +
  • Copyright: 2020 Damian Monogue
  • +
  • License: MIT, see LICENSE.lua
  • +
  • Author: Damian Monogue
  • +
+ + +

Tables

+ + + + + +
sortbox.SortFunctionsSorting functions for spairs, should you wish
+

Methods

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
sortbox:new(options[, container])Creates a new SortBox
sortbox:organize()Calling this will cause the SortBox to reposition/resize everything
sortbox:enableElastic()Enables elasticity for the SortBox.
sortbox:disableElastic()Disables elasticity for the SortBox
sortbox:setElastic(enabled)Set elasticity specifically
sortbox:setMaxWidth(maxWidth)Set the max width of the SortBox if it's elastic
sortbox:setMaxHeight(maxHeight)Set the max height of the SortBox if it's elastic
sortbox:enableTimer()Starts the SortBox sorting and organizing itself on a timer
sortbox:disableTimer()Stops the SortBox from sorting and organizing itself on a timer
sortbox:setSortInterval(sortInterval)Sets the sortInterval, or amount of time in milliseconds between auto sorting on a timer if timerSort is true
sortbox:enableSort()Enables sorting when items are added/removed, or if timerSort is true, every sortInterval milliseconds
sortbox:disableSort()Disables sorting when items are added or removed
sortbox:setBoxType(boxType)Set whether the SortBox acts as a VBox or HBox.
sortbox:setSortFunction(functionName)Sets the type of sorting in use by this SortBox.
+ +
+
+ + +

Tables

+ +
+
+ + sortbox.SortFunctions + line 29 +
+
+ Sorting functions for spairs, should you wish + + +

Fields:

+
    +
  • gaugeValue + sorts Geyser gauges by value, ascending +
  • +
  • reverseGaugeValue + sorts Geyser gauges by value, descending +
  • +
  • timeLeft + sorts TimerGauges by how much time is left, ascending +
  • +
  • reverseTimeLeft + sorts TimerGauges by how much time is left, descending. +
  • +
  • name + sorts Geyser objects by name, ascending +
  • +
  • reverseName + sorts Geyser objects by name, descending +
  • +
  • message + sorts Geyser labels and gauges by their echoed text, ascending +
  • +
  • reverseMessage + sorts Geyser labels and gauges by their echoed text, descending +
  • +
+ + + + + +
+
+

Methods

+ +
+
+ + sortbox:new(options[, container]) + line 144 +
+
+ Creates a new SortBox + + +

Parameters:

+
    +
  • options + table + the options to use for the SortBox. See table below for added options +
  • +
  • container + the container to add the SortBox into +

    Table of new options + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    option namedescriptiondefault
    autoSortshould the SortBox perform function based sorting? If false, will behave like a normal H/VBoxtrue
    timerSortshould the SortBox automatically perform sorting on a timer?true
    sortIntervalhow frequently should we sort on a timer if timerSort is true, in milliseconds500
    boxTypeShould we stack like an HBox or VBox? use 'h' for hbox and 'v' for vboxv
    sortFunctionhow should we sort the items in the SortBox? see setSortFunction for valid optionsgaugeValue
    elasticShould this container stretch to fit its contents? boxType v stretches in height, h stretches in width.false
    maxHeightIf elastic, what's the biggest a 'v' style box should grow in height? Use 0 for unlimited0
    maxWidthIf elastic, what's the biggest a 'h' style box should grow in width? Use 0 for unlimited0
    + (optional) +
  • +
+ + + + +

Usage:

+
    +
    local SortBox = require("MDK.sortbox")
    +mySortBox = SortBox:new({
    +  name = "mySortBox",
    +  x = 400,
    +  y = 100,
    +  height = 150,
    +  width = 300,
    +  sortFunction = "timeLeft"
    +})
    +
+ +
+
+ + sortbox:organize() + line 196 +
+
+ Calling this will cause the SortBox to reposition/resize everything + + + + + + + +
+
+ + sortbox:enableElastic() + line 372 +
+
+ Enables elasticity for the SortBox. + + + + + + + +
+
+ + sortbox:disableElastic() + line 377 +
+
+ Disables elasticity for the SortBox + + + + + + + +
+
+ + sortbox:setElastic(enabled) + line 383 +
+
+ Set elasticity specifically + + +

Parameters:

+
    +
  • enabled + boolean + if true, enable elasticity. If false, disable it. +
  • +
+ + + + + +
+
+ + sortbox:setMaxWidth(maxWidth) + line 389 +
+
+ Set the max width of the SortBox if it's elastic + + +

Parameters:

+
    +
  • maxWidth + number + The maximum width in pixels to resize the SortBox to. Use 0 for unlimited. +
  • +
+ + + + + +
+
+ + sortbox:setMaxHeight(maxHeight) + line 398 +
+
+ Set the max height of the SortBox if it's elastic + + +

Parameters:

+
    +
  • maxHeight + number + The maximum height in pixels to resize the SortBox to. Use 0 for unlimited. +
  • +
+ + + + + +
+
+ + sortbox:enableTimer() + line 406 +
+
+ Starts the SortBox sorting and organizing itself on a timer + + + + + + + +
+
+ + sortbox:disableTimer() + line 417 +
+
+ Stops the SortBox from sorting and organizing itself on a timer + + + + + + + +
+
+ + sortbox:setSortInterval(sortInterval) + line 425 +
+
+ Sets the sortInterval, or amount of time in milliseconds between auto sorting on a timer if timerSort is true + + +

Parameters:

+
    +
  • sortInterval + number + time in milliseconds between auto sorting if timerSort is true +
  • +
+ + + + + +
+
+ + sortbox:enableSort() + line 436 +
+
+ Enables sorting when items are added/removed, or if timerSort is true, every sortInterval milliseconds + + + + + + + +
+
+ + sortbox:disableSort() + line 442 +
+
+ Disables sorting when items are added or removed + + + + + + + +
+
+ + sortbox:setBoxType(boxType) + line 451 +
+
+ Set whether the SortBox acts as a VBox or HBox. + + +

Parameters:

+
    +
  • boxType + string + If you pass 'h' or 'horizontal' it will act like an HBox. Anything else it will act like a VBox. +
  • +
+ + + + +

Usage:

+
    +
    mySortBox:setBoxType("v") -- behave like a VBox
    +mySortBox:setBoxType("h") -- behave like an HBox
    +mySortBox:setBoxType("beeblebrox") -- why?! Why would you do this? It'll behave like a VBox
    +
+ +
+
+ + sortbox:setSortFunction(functionName) + line 508 +
+
+ Sets the type of sorting in use by this SortBox. +
If an item in the box does not have the appropriate property or function, then 999999999 is used for sorting except as otherwise noted. +
If an invalid option is given, then existing H/VBox behaviour is maintained, just like if autoSort is false. + + +

Parameters:

+
    +
  • functionName + string + what type of sorting should we use? See table below for valid options and their descriptions. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    sort typedescription
    gaugeValuesort gauges based on how full the gauge is, from less full to more
    reverseGaugeValuesort gauges based on how full the gauge is, from more full to less
    timeLeftsort TimerGauges based on the total time left in the gauge, from less time to more
    reverseTimeLeftsort TimerGauges based on the total time left in the gauge, from more time to less
    namesort any item (and mixed types) by name, alphabetically.
    reverseNamesort any item (and mixed types) by name, reverse alphabetically.
    messagesorts Labels based on their echoed message, alphabetically. If not a label, the empty string will be used
    reverseMessagesorts Labels based on their echoed message, reverse alphabetically. If not a label, the empty string will be used
    +
  • +
+ + + + +

Usage:

+
    +
    mySortBox:setSortFunction("gaugeValue")
    +
+ +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/classes/TextGauge.html b/src/resources/MDK/doc/classes/TextGauge.html new file mode 100755 index 0000000..da9ce37 --- /dev/null +++ b/src/resources/MDK/doc/classes/TextGauge.html @@ -0,0 +1,622 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class TextGauge

+

Creates a text based gauge, for use in miniconsoles and the like.

+

+

Info:

+
    +
  • Copyright: 2020 Damian Monogue,2021 Damian Monogue
  • +
  • License: MIT, see LICENSE.lua
  • +
  • Author: Damian Monogue
  • +
+ + +

Methods

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
textgauge:new([options])Creates a new TextGauge.
textgauge:setWidth(width)Sets the width in characters of the gauge
textgauge:setFillCharacter(character)Sets the character to use for the 'full' part of the gauge
textgauge:setOverflowCharacter(character)Sets the character to use for the 'overflow' (>100%) part of the gauge
textgauge:setEmptyCharacter(character)Sets the character to use for the 'full' part of the gauge
textgauge:setFillColor(color)Sets the fill color for the gauge.
textgauge:setOverflowColor(color)Sets the overflow color for the gauge.
textgauge:setEmptyColor(color)Sets the empty color for the gauge.
textgauge:setPercentColor(color)Sets the fill color for the gauge.
textgauge:setPercentSymbolColor(color)Sets the fill color for the gauge.
textgauge:enableReverse()Enables reversing the fill direction (right to left instead of the usual left to right)
textgauge:disableReverse()Disables reversing the fill direction (go back to the usual left to right)
textgauge:enableShowPercent()Enables showing the percent value of the gauge
textgauge:disableShowPercent()Disables showing the percent value of the gauge
textgauge:enableShowPercentSymbol()Enables showing the percent symbol (appears after the value)
textgauge:disableShowPercentSymbol()Enables showing the percent symbol (appears after the value)
textgauge:setValue([current[, max]])Used to set the gauge's value and return the string representation of the gauge
textgauge:print(...)Synonym for setValue
+ +
+
+ + +

Methods

+ +
+
+ + textgauge:new([options]) + line 92 +
+
+ Creates a new TextGauge. + + +

Parameters:

+
    +
  • options + table + The table of options you would like the TextGauge to start with. +

    Table of new options + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    option namedescriptiondefault
    widthHow many characters wide to make the gauge24
    fillCharacterWhat character to use for the 'full' part of the gauge:
    overflowCharacterWhat character to use for >100% part of the gaugeif not set, it uses whatever you set fillCharacter to
    emptyCharacterWhat character to use for the 'empty' part of the gauge-
    showPercentSymbolShould we show the % sign itself?true
    showPercentShould we show what % of the gauge is filled?true
    valueHow much of the gauge should be filled50
    formatWhat type of color formatting to use? 'c' for cecho, 'd' for decho, 'h' for hechoc
    fillColorWhat color to make the full part of the bar?"DarkOrange" or equivalent for your format type
    emptyColorwhat color to use for the empty part of the bar?"white" or format appropriate equivalent
    percentColorWhat color to print the percentage numvers in, if shown?"white" or fortmat appropriate equivalent
    percentSymbolColorWhat color to make the % if shown?If not set, uses what percentColor is set to.
    overflowColorWhat color to make the >100% portion of the bar?If not set, will use the same color as fillColor
    + (optional) +
  • +
+ + + + +

Usage:

+
    +
    local TextGauge = require("MDK.textgauge")
    +myTextGauge = TextGauge:new()
    +gaugeText = myTextGauge:setValue(382, 830)
    +
+ +
+
+ + textgauge:setWidth(width) + line 105 +
+
+ Sets the width in characters of the gauge + + +

Parameters:

+
    +
  • width + number + number of characters wide to make the gauge +
  • +
+ + + + + +
+
+ + textgauge:setFillCharacter(character) + line 118 +
+
+ Sets the character to use for the 'full' part of the gauge + + +

Parameters:

+
    +
  • character + string + the character to use. +
  • +
+ + + + + +
+
+ + textgauge:setOverflowCharacter(character) + line 126 +
+
+ Sets the character to use for the 'overflow' (>100%) part of the gauge + + +

Parameters:

+
    +
  • character + string + the character to use. +
  • +
+ + + + + +
+
+ + textgauge:setEmptyCharacter(character) + line 134 +
+
+ Sets the character to use for the 'full' part of the gauge + + +

Parameters:

+
    +
  • character + string + the character to use. +
  • +
+ + + + + +
+
+ + textgauge:setFillColor(color) + line 142 +
+
+ Sets the fill color for the gauge. + + +

Parameters:

+
    +
  • color + string + the color to use for the full portion of the gauge. Will be run through Geyser.Golor +
  • +
+ + + + + +
+
+ + textgauge:setOverflowColor(color) + line 149 +
+
+ Sets the overflow color for the gauge. + + +

Parameters:

+
    +
  • color + string + the color to use for the full portion of the gauge. Will be run through Geyser.Golor +
  • +
+ + + + + +
+
+ + textgauge:setEmptyColor(color) + line 156 +
+
+ Sets the empty color for the gauge. + + +

Parameters:

+
    +
  • color + string + the color to use for the empty portion of the gauge. Will be run through Geyser.Golor +
  • +
+ + + + + +
+
+ + textgauge:setPercentColor(color) + line 163 +
+
+ Sets the fill color for the gauge. + + +

Parameters:

+
    +
  • color + string + the color to use for the numeric value. Will be run through Geyser.Golor +
  • +
+ + + + + +
+
+ + textgauge:setPercentSymbolColor(color) + line 169 +
+
+ Sets the fill color for the gauge. + + +

Parameters:

+
    +
  • color + string + the color to use for the numeric value. Will be run through Geyser.Golor +
  • +
+ + + + + +
+
+ + textgauge:enableReverse() + line 175 +
+
+ Enables reversing the fill direction (right to left instead of the usual left to right) + + + + + + + +
+
+ + textgauge:disableReverse() + line 180 +
+
+ Disables reversing the fill direction (go back to the usual left to right) + + + + + + + +
+
+ + textgauge:enableShowPercent() + line 185 +
+
+ Enables showing the percent value of the gauge + + + + + + + +
+
+ + textgauge:disableShowPercent() + line 190 +
+
+ Disables showing the percent value of the gauge + + + + + + + +
+
+ + textgauge:enableShowPercentSymbol() + line 195 +
+
+ Enables showing the percent symbol (appears after the value) + + + + + + + +
+
+ + textgauge:disableShowPercentSymbol() + line 200 +
+
+ Enables showing the percent symbol (appears after the value) + + + + + + + +
+
+ + textgauge:setValue([current[, max]]) + line 270 +
+
+ Used to set the gauge's value and return the string representation of the gauge + + +

Parameters:

+
    +
  • current + number + current value. If no value is passed it will use the stored value. Defaults to 50 to prevent errors. + (optional) +
  • +
  • max + number + maximum value. If not passed, the internally stored one will be used. Defaults to 100 so that it can be used with single values as a percent + (optional) +
  • +
+ + + + +

Usage:

+
    +
  • myGauge:setValue(55) -- sets the gauge to 55% full
  • +
  • myGauge:setValue(2345, 2780) -- will figure out what the percentage fill is based on the given current/max values
  • +
+ +
+
+ + textgauge:print(...) + line 331 +
+
+ Synonym for setValue + + +

Parameters:

+
    +
  • ... + +
  • +
+ + + + + +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/classes/TimerGauge.html b/src/resources/MDK/doc/classes/TimerGauge.html new file mode 100755 index 0000000..6410508 --- /dev/null +++ b/src/resources/MDK/doc/classes/TimerGauge.html @@ -0,0 +1,631 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class TimerGauge

+

Animated countdown timer, extends Geyser.Gauge

+

+

Info:

+
    +
  • Copyright: 2020 Damian Monogue
  • +
  • License: MIT, see LICENSE.lua
  • +
  • Author: Damian Monogue
  • +
+ + +

Methods

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
timergauge:show2()Shows the TimerGauge.
timergauge:hide2()Hides the TimerGauge.
timergauge:start([show])Starts the timergauge.
timergauge:stop([hide])Stops the timergauge.
timergauge:pause([hide])Alias for stop.
timergauge:reset()Resets the time on the timergauge to its original value.
timergauge:restart([show])Resets and starts the timergauge.
timergauge:getTime(format)Get the amount of time remaining on the timer, in seconds
timergauge:finish([skipHook])Sets the timer's remaining time to 0, stops it, and executes the hook if one exists.
timergauge:setTime(time)Sets the amount of time the timer will run for.
timergauge:setUpdateTime(updateTime)Changes the time between gauge updates.
timergauge:new(cons, parent)Creates a new TimerGauge instance.
+ +
+
+ + +

Methods

+ +
+
+ + timergauge:show2() + line 40 +
+
+ Shows the TimerGauge. If the manageContainer property is true, then will add it back to its container + + + + + + + +
+
+ + timergauge:hide2() + line 49 +
+
+ Hides the TimerGauge. If manageContainer property is true, then it will remove it from its container and if the container is an HBox or VBox it will initiate size/position management + + + + + + + +
+
+ + timergauge:start([show]) + line 70 +
+
+ Starts the timergauge. Works whether the timer is stopped or not. Does not start a timer which is already at 0 + + +

Parameters:

+
    +
  • show + boolean + override the autoShow property. True will always show, false will never show. + (optional) +
  • +
+ + + + +

Usage:

+
    +
    myTimerGauge:start() --starts the timer, will show or not based on autoShow property
    +myTimerGauge:start(false) --starts the timer, will not change hidden status, regardless of autoShow property
    +myTimerGauge:start(true) --starts the timer, will show it regardless of autoShow property
    +
+ +
+
+ + timergauge:stop([hide]) + line 94 +
+
+ Stops the timergauge. Works whether the timer is started or not. + + +

Parameters:

+
    +
  • hide + boolean + override the autoHide property. True will always hide, false will never hide. + (optional) +
  • +
+ + + + +

Usage:

+
    +
    myTimerGauge:stop() --stops the timer, will hide or not based on autoHide property
    +myTimerGauge:stop(false) --stops the timer, will not change hidden status, regardless of autoHide property
    +myTimerGauge:stop(true) --stops the timer, will hide it regardless of autoHide property
    +
+ +
+
+ + timergauge:pause([hide]) + line 111 +
+
+ Alias for stop. + + +

Parameters:

+
    +
  • hide + boolean + override the autoHide property. True will always hide, false will never hide. + (optional) +
  • +
+ + + + + +
+
+ + timergauge:reset() + line 116 +
+
+ Resets the time on the timergauge to its original value. Does not alter the running state of the timer + + + + + + + +
+
+ + timergauge:restart([show]) + line 127 +
+
+ Resets and starts the timergauge. + + +

Parameters:

+
    +
  • show + boolean + override the autoShow property. true will always show, false will never show + (optional) +
  • +
+ + + + +

Usage:

+
    +
    myTimerGauge:restart() --restarts the timer, will show or not based on autoShow property
    +myTimerGauge:restart(false) --restarts the timer, will not change hidden status, regardless of autoShow property
    +myTimerGauge:restart(true) --restarts the timer, will show it regardless of autoShow property
    +
+ +
+
+ + timergauge:getTime(format) + line 197 +
+
+ Get the amount of time remaining on the timer, in seconds + + +

Parameters:

+
    +
  • format + string + Format string for how to return the time. If not provided defaults to self.timeFormat(which defaults to "S.t").
    + If "" is passed will return "" as the time. See below table for formatting codes
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    format codewhat it is replaced with
    STime left in seconds, unbroken down. Does not include milliseconds.
    + IE a timer with 2 minutes left it would replace S with 120 +
    ddDays, with 1 leading 0 (0, 01, 02-...)
    dDays, with no leading 0 (1,2,3-...)
    hhhours, with leading 0 (00-24)
    hhours, without leading 0 (0-24)
    MMminutes, with a leading 0 (00-59)
    Mminutes, no leading 0 (0-59)
    ssseconds, with leading 0 (00-59)
    sseconds, no leading 0 (0-59)
    ttenths of a second
    + timer with 12.345 seconds left, t would
    + br replaced by 3. +
    mmmilliseconds with leadings 0s (000-999)
    mmilliseconds with no leading 0s (0-999)

    +
  • +
+ + + + +

Usage:

+
    +
    myTimerGauge:getTime() --returns the time using myTimerGauge.format
    +myTimerGauge:getTime("hh:MM:ss") --returns the time as hours, minutes, and seconds, with leading 0s (01:23:04)
    +myTimerGauge:getTime("S.mm") --returns the time as the total number of seconds, including milliseconds (114.004)
    +
+ +
+
+ + timergauge:finish([skipHook]) + line 263 +
+
+ Sets the timer's remaining time to 0, stops it, and executes the hook if one exists. + + +

Parameters:

+
    +
  • skipHook + boolean + use true to have it set the timer to 0 and stop, but not execute the hook. + (optional) +
  • +
+ + + + +

Usage:

+
    +
    myTimerGauge:finish() --executes the hook if it has one
    + myTimerGauge:finish(false) --will not execute the hook
    +
+ +
+
+ + timergauge:setTime(time) + line 294 +
+
+ Sets the amount of time the timer will run for. Make sure to call :reset() or :restart() + if you want to cause the timer to run for that amount of time. If you set it to a time lower + than the time left on the timer currently, it will reset the current time, otherwise it is left alone + + +

Parameters:

+
    +
  • time + number + how long in seconds the timer should run for +
  • +
+ + + + +

Usage:

+
    +
    myTimerGauge:setTime(50) -- sets myTimerGauge's max time to 50.
    +
+ +
+
+ + timergauge:setUpdateTime(updateTime) + line 318 +
+
+ Changes the time between gauge updates. + + +

Parameters:

+
    +
  • updateTime + number + amount of time in milliseconds between gauge updates. Must be a positive number. +
  • +
+ + + + + +
+
+ + timergauge:new(cons, parent) + line 474 +
+
+ Creates a new TimerGauge instance. + + +

Parameters:

+
    +
  • cons + table + a table of options (or constraints) for how the TimerGauge will behave. Valid options include: +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    namedescriptiondefault
    timehow long the timer should run for
    activewhether the timer should run or nottrue
    showTimeshould we show the time remaining on the gauge?true
    prefixtext you want shown before the time.""
    suffixtext you want shown after the time.""
    timerCaptionAlias for suffix. Deprecated and may be remove in the future +
    updateTimenumber of milliseconds between gauge updates.10
    autoHideshould the timer :hide() itself when it runs out/you stop it?true
    autoShowshould the timer :show() itself when you start it?true
    manageContainershould the timer remove itself from its container when you call
    :hide() and add itself back when you call :show()?
    false
    timeFormathow should the time be displayed/returned if you call :getTime()?
    See table below for more information
    "S.t"
    +
    Table of time format options + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    format codewhat it is replaced with
    STime left in seconds, unbroken down. Does not include milliseconds.
    + IE a timer with 2 minutes left it would replace S with 120 +
    ddDays, with 1 leading 0 (0, 01, 02-...)
    dDays, with no leading 0 (1,2,3-...)
    hhhours, with leading 0 (00-24)
    hhours, without leading 0 (0-24)
    MMminutes, with a leading 0 (00-59)
    Mminutes, no leading 0 (0-59)
    ssseconds, with leading 0 (00-59)
    sseconds, no leading 0 (0-59)
    ttenths of a second
    + timer with 12.345 seconds left, t would
    + br replaced by 3. +
    mmmilliseconds with leadings 0s (000-999)
    mmilliseconds with no leading 0s (0-999)

    +
  • +
  • parent + The Geyser parent for this TimerGauge +
  • +
+ + + + +

Usage:

+
    +
    local TimerGauge = require("MDK.timergauge")
    +myTimerGauge = TimerGauge:new({
    +  name = "testGauge",
    +  x = 100,
    +  y = 100,
    +  height = 40,
    +  width = 200,
    +  time = 10
    +})
    +
+ +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/classes/aliasmgr.html b/src/resources/MDK/doc/classes/aliasmgr.html new file mode 100755 index 0000000..adbad80 --- /dev/null +++ b/src/resources/MDK/doc/classes/aliasmgr.html @@ -0,0 +1,415 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class aliasmgr

+

Alias Manager

+

+

Info:

+
    +
  • Copyright: 2022 Damian Monogue
  • +
  • License: MIT, see LICENSE.lua
  • +
  • Author: Damian Monogue
  • +
+ + +

Methods

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
aliasmgr:new()Creates a new alias manager
aliasmgr:register(name, regex, func)Registers an alias with the alias manager
aliasmgr:add(name, regex, func)Registers an alias with the alias manager.
aliasmgr:disable(name)Disables an alias, but does not delete it so it can be enabled later without being redefined
aliasmgr:disableAll()Disables all aliases registered with the manager
aliasmgr:enable(name)Enables an alias by name
aliasmgr:enableAll()Enables all aliases registered with the manager
aliasmgr:kill(name)Kill an alias, deleting it from the manager
aliasmgr:killAll()Kills all aliases registered with the manager, clearing it out
aliasmgr:delete(name)Kills an alias, deleting it from the manager
aliasmgr:deleteAll()Kills all aliases, deleting them from the manager
aliasmgr:getAliases()Returns the list of aliases and the information being tracked for them
+ +
+
+ + +

Methods

+ +
+
+ + aliasmgr:new() + line 10 +
+
+ Creates a new alias manager + + + + + + + +
+
+ + aliasmgr:register(name, regex, func) + line 27 +
+
+ Registers an alias with the alias manager + + +

Parameters:

+
    +
  • name + the name for the alias +
  • +
  • regex + the regular expression the alias matches against +
  • +
  • func + The code to run when the alias matches. Can wrap code in [[ ]] or pass an actual function +
  • +
+ + + + + +
+
+ + aliasmgr:add(name, regex, func) + line 63 +
+
+ Registers an alias with the alias manager. Alias for register + + +

Parameters:

+
    +
  • name + the name for the alias +
  • +
  • regex + the regular expression the alias matches against +
  • +
  • func + The code to run when the alias matches. Can wrap code in [[ ]] or pass an actual function +
  • +
+ + + +

See also:

+ + + +
+
+ + aliasmgr:disable(name) + line 70 +
+
+ Disables an alias, but does not delete it so it can be enabled later without being redefined + + +

Parameters:

+
    +
  • name + the name of the alias to disable +
  • +
+ +

Returns:

+
    + + true if the alias exists and gets disabled, false if it does not exist or is already disabled +
+ + + + +
+
+ + aliasmgr:disableAll() + line 86 +
+
+ Disables all aliases registered with the manager + + + + + + + +
+
+ + aliasmgr:enable(name) + line 96 +
+
+ Enables an alias by name + + +

Parameters:

+
    +
  • name + the name of the alias to enable +
  • +
+ +

Returns:

+
    + + true if the alias exists and was enabled, false if it does not exist. +
+ + + + +
+
+ + aliasmgr:enableAll() + line 110 +
+
+ Enables all aliases registered with the manager + + + + + + + +
+
+ + aliasmgr:kill(name) + line 121 +
+
+ Kill an alias, deleting it from the manager + + +

Parameters:

+
    +
  • name + the name of the alias to kill +
  • +
+ +

Returns:

+
    + + true if the alias exists and gets deleted, false if the alias does not exist +
+ + + + +
+
+ + aliasmgr:killAll() + line 137 +
+
+ Kills all aliases registered with the manager, clearing it out + + + + + + + +
+
+ + aliasmgr:delete(name) + line 148 +
+
+ Kills an alias, deleting it from the manager + + +

Parameters:

+
    +
  • name + the name of the alias to delete +
  • +
+ +

Returns:

+
    + + true if the alias exists and gets deleted, false if the alias does not exist +
+ + +

See also:

+ + + +
+
+ + aliasmgr:deleteAll() + line 154 +
+
+ Kills all aliases, deleting them from the manager + + + + + +

See also:

+ + + +
+
+ + aliasmgr:getAliases() + line 160 +
+
+ Returns the list of aliases and the information being tracked for them + + + +

Returns:

+
    + + the table of alias information, with names as keys and a table of information as the values. +
+ + + + +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/classes/revisionator.html b/src/resources/MDK/doc/classes/revisionator.html new file mode 100755 index 0000000..b132fae --- /dev/null +++ b/src/resources/MDK/doc/classes/revisionator.html @@ -0,0 +1,252 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class revisionator

+

The revisionator provides a standardized way of migrating configurations between revisions + for instance, it will track what the currently applied revision number is, and when you tell + tell it to migrate, it will apply every individual migration between the currently applied + revision and the latest/current revision.

+

This should allow for more seamlessly moving from + an older version of a package to a new one.

+

Info:

+
    +
  • Copyright: 2023
  • +
  • License: MIT, see https://raw.githubusercontent.com/demonnic/MDK/main/src/scripts/LICENSE.lua
  • +
  • Author: Damian Monogue
  • +
+ + +

Methods

+ + + + + + + + + + + + + + + + + +
revisionator:new(options)Creates a new revisionator
revisionator:getAppliedPatch()Get the currently applied revision from file
revisionator:migrate()go through all the patches in order and apply any which are still necessary
revisionator:addPatch(func[, position])add a patch to the table of patches
+ +
+
+ + +

Methods

+ +
+
+ + revisionator:new(options) + line 47 +
+
+ Creates a new revisionator + + +

Parameters:

+
    +
  • options + table + the options to create the revisionator with. + + + + + + + + + + + + + + + + + + + + +
    option namedescriptiondefault
    nameThe name of the revisionator. This is absolutely required, as the name is used for tracking the currently applied patch levelraises an error if not provided
    patchesA table of patch functions. It is traversed using ipairs, so must be in the form of {function1, function2, function3} etc. If you do not provide it, you can add the patches by calling :addPatch for each patch in order.{}
    +
  • +
+ + + + + +
+
+ + revisionator:getAppliedPatch() + line 65 +
+
+ Get the currently applied revision from file + + + +

Returns:

+
    + + number + the revision number currently applied, or 0 if it can't read a current version +
+

Or

+
    +
  1. + nil + nil
  2. +
  3. + string + error message
  4. +
+ + + + +
+
+ + revisionator:migrate() + line 86 +
+
+ go through all the patches in order and apply any which are still necessary + + + +

Returns:

+
    + + boolean + true if it successfully applied patches, false if it was already at the latest patch level +
+

Or

+
    +
  1. + nil +
  2. +
  3. + string + error message
  4. +
+ + + + +
+
+ + revisionator:addPatch(func[, position]) + line 111 +
+
+ add a patch to the table of patches + + +

Parameters:

+
    +
  • func + function + the function to run as the patch +
  • +
  • position + number + which patch to insert it as? If not supplied, inserts it as the last patch. Which is usually what you want. + (optional) +
  • +
+ + + + + +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/classes/spinbox.html b/src/resources/MDK/doc/classes/spinbox.html new file mode 100755 index 0000000..53623b5 --- /dev/null +++ b/src/resources/MDK/doc/classes/spinbox.html @@ -0,0 +1,337 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class spinbox

+

A Geyser object to create a spinbox for adjusting a number

+

+

Info:

+
    +
  • Copyright: 2023
  • +
  • License: MIT, see https://raw.githubusercontent.com/demonnic/MDK/main/src/scripts/LICENSE.lua
  • +
  • Author: Damian Monogue
  • +
+ + +

Methods

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
spinbox:new(cons, container)Creates a new spinbox.
spinbox:setValue(value)Used to directly set the value of the spinbox.
spinbox:generateStyles()(Re)generates the stylesheets for the spinbox + Should not need to call but if you change something and it doesn't take effect + you can try calling this followed by applyStyles
spinbox:applyStyles()Applies updated stylesheets to the components of the spinbox + Should not need to call this directly
spinbox:setActiveButtonColor(color)sets the color for active buttons on the spinbox
spinbox:setInactiveButtonColor(color)sets the color for inactive buttons on the spinbox
spinbox:setCallBack(func)Set a callback function for the spinbox to call any time the value of the spinbox is changed + the function will be called as func(self.value, self.name)
+ +
+
+ + +

Methods

+ +
+
+ + spinbox:new(cons, container) + line 89 +
+
+ Creates a new spinbox. + + +

Parameters:

+
    +
  • cons + table + a table containing the options for this spinbox. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    option namedescriptiondefault
    minThe minimum value for this spinbox0
    maxThe maximum value for this spinbox10
    activeButtonColorThe color the up/down buttons should be when they are active/able to be usedgray
    inactiveButtonColorThe color the up/down buttons should be when they are inactive/unable to be useddimgray
    integerBoolean value. When true, values must always be integers (no decimal place)true
    deltaThe amount to change the spinbox's value when the up or down button is pressed.1
    upArrowLocationThe location of the up arrow image. Either a web URL where it can be downloaded, or the location on disk to read it fromhttps://demonnic.github.io/image-assets/uparrow.png
    downArrowLocationThe location of the down arrow image. Either a web URL where it can be downloaded, or the location on disk to read it fromhttps://demonnic.github.io/image-assets/downarrow.png
    callBackThe function to run when the spinbox's value is updated. Is called with parameters (self.name, value, oldValue)nil
    +
  • +
  • container + The Geyser container for this spinbox +
  • +
+ + + + + +
+
+ + spinbox:setValue(value) + line 270 +
+
+ Used to directly set the value of the spinbox. + + +

Parameters:

+
    +
  • value + The new value to set + Rounds the value to an integer if the spinbox is integer only. + Checks if the new value is within the min/max range and clamps it if not. + Updates the display label with the new value. + Applies any styles that depend on the value. +
  • +
+ + + + + +
+
+ + spinbox:generateStyles() + line 387 +
+
+ (Re)generates the stylesheets for the spinbox + Should not need to call but if you change something and it doesn't take effect + you can try calling this followed by applyStyles + + + + + + + +
+
+ + spinbox:applyStyles() + line 412 +
+
+ Applies updated stylesheets to the components of the spinbox + Should not need to call this directly + + + + + + + +
+
+ + spinbox:setActiveButtonColor(color) + line 430 +
+
+ sets the color for active buttons on the spinbox + + +

Parameters:

+
    +
  • color + any valid color formatting string, such a "red" or "#880000" or "<128,0,0>" or a table of colors, like {128, 0,0}. See Geyser.Color.parse at https://www.mudlet.org/geyser/files/geyser/GeyserColor.html#Geyser.Color.parse +
  • +
+ + + + + +
+
+ + spinbox:setInactiveButtonColor(color) + line 445 +
+
+ sets the color for inactive buttons on the spinbox + + +

Parameters:

+
    +
  • color + any valid color formatting string, such a "" or "red" or "<128,0,0>" or a table of colors, like {128, 0,0}. See Geyser.Color.parse at https://www.mudlet.org/geyser/files/geyser/GeyserColor.html#Geyser.Color.parse +
  • +
+ + + + + +
+
+ + spinbox:setCallBack(func) + line 472 +
+
+ Set a callback function for the spinbox to call any time the value of the spinbox is changed + the function will be called as func(self.value, self.name) + + +

Parameters:

+
    +
  • func + +
  • +
+ + + + + +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/index.html b/src/resources/MDK/doc/index.html new file mode 100755 index 0000000..a398531 --- /dev/null +++ b/src/resources/MDK/doc/index.html @@ -0,0 +1,237 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ + + +

Modules

+ + + + + + + + + + + + + + + + + + + + + +
demontoolsCollection of miscellaneous functions and tools which don't necessarily warrant their own module/class
echofileset of functions for echoing files to things.
figletFiglet + A module to read figlet fonts and produce figlet ascii art from text
ftextftext + functions to format and print text, and the objects which use them
GradientMakerModule which provides for creating color gradients for your text.
+

Classes

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
aliasmgrAlias Manager
ChyronCreates a label with a scrolling text element.
EMCOEmbeddable Multi Console Object.
LoggingConsoleMiniConsole with logging capabilities
LoginatorLoginator creates an object which allows you to log things to file at + various severity levels, with the ability to only log items above a specific + severity to file.
MasterMindSolverInteractive object which helps you solve a Master Mind puzzle.
revisionatorThe revisionator provides a standardized way of migrating configurations between revisions + for instance, it will track what the currently applied revision number is, and when you tell + tell it to migrate, it will apply every individual migration between the currently applied + revision and the latest/current revision.
SortBoxAn H/VBox alternative which can be set to either vertical or horizontal, and will autosort the windows
spinboxA Geyser object to create a spinbox for adjusting a number
SUGSelf Updating Gauge, extends Geyser.Gauge
TextGaugeCreates a text based gauge, for use in miniconsoles and the like.
TimerGaugeAnimated countdown timer, extends Geyser.Gauge
+

Source

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LICENSE.lua
aliasmgr.lua
chyron.lua
demontools.lua
echofile.lua
emco.lua
figlet.lua
ftext.lua
gradientmaker.lua
loggingconsole.lua
loginator.lua
mastermindsolver.lua
revisionator.lua
schema.lua
sortbox.lua
spinbox.lua
sug.lua
ftext_spec.lua
textgauge.lua
timergauge.lua
+ +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/ldoc.css b/src/resources/MDK/doc/ldoc.css new file mode 100755 index 0000000..74e9a46 --- /dev/null +++ b/src/resources/MDK/doc/ldoc.css @@ -0,0 +1,315 @@ +/* BEGIN RESET + +Copyright (c) 2010, Yahoo! Inc. All rights reserved. +Code licensed under the BSD License: +http://developer.yahoo.com/yui/license.html +version: 2.8.2r1 +*/ +html { + color: #ccc; + background: #222; +} +body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td { + margin: 0; + padding: 0; +} +table { + border-collapse: collapse; + border-spacing: 0; +} +fieldset,img { + border: 0; +} +address,caption,cite,code,dfn,em,strong,th,var,optgroup { + font-style: inherit; + font-weight: inherit; +} +del,ins { + text-decoration: none; +} +li { + margin-left: 20px; +} +caption,th { + text-align: left; +} +h1,h2,h3,h4,h5,h6 { + font-size: 100%; + font-weight: bold; +} +q:before,q:after { + content: ''; +} +abbr,acronym { + border: 0; + font-variant: normal; +} +sup { + vertical-align: baseline; +} +sub { + vertical-align: baseline; +} +legend { + color: #000; +} +input,button,textarea,select,optgroup,option { + font-family: inherit; + font-size: inherit; + font-style: inherit; + font-weight: inherit; +} +input,button,textarea,select {*font-size:100%; +} +/* END RESET */ + +body { + margin-left: 1em; + margin-right: 1em; + font-family: arial, helvetica, geneva, sans-serif; + background-color: #222222; margin: 0px; +} + +code, tt { font-family: monospace; font-size: 1.1em; } +span.parameter { font-family:monospace; } +span.parameter:after { content:":"; } +span.types:before { content:"("; } +span.types:after { content:")"; } +.type { font-weight: bold; font-style:italic } + +body, p, td, th { font-size: .95em; line-height: 1.2em;} + +p, ul { margin: 10px 0 0 0px;} + +strong { font-weight: bold;} + +em { font-style: italic;} + +h1 { + font-size: 1.5em; + margin: 20px 0 20px 0; +} +h2, h3, h4 { margin: 15px 0 10px 0; } +h2 { font-size: 1.25em; } +h3 { font-size: 1.15em; } +h4 { font-size: 1.06em; } + +a:link { font-weight: bold; color: #2266ee; text-decoration: none; } +a:visited { font-weight: bold; color: #0099bb; text-decoration: none; } +a:link:hover { text-decoration: underline; } + +hr { + color:#cccccc; + background: #00007f; + height: 1px; +} + +blockquote { margin-left: 3em; } + +ul { list-style-type: disc; } + +p.name { + font-family: "Andale Mono", monospace; + padding-top: 1em; +} + +pre { + background-color: #000000; + border: 1px solid #C0C0C0; /* silver */ + padding: 10px; + margin: 10px 0 10px 0; + overflow: auto; + font-family: "Andale Mono", monospace; +} + +pre.example { + font-size: .85em; +} + +table.index { border: 1px #00007f; } +table.index td { text-align: left; vertical-align: top; } + +#container { + margin-left: 1em; + margin-right: 1em; + background-color: #222222; +} + +#product { + text-align: center; + border-bottom: 1px solid #cccccc; + background-color: #222222; +} + +#product big { + font-size: 2em; +} + +#main { + background-color: #222222; + border-left: 2px solid #cccccc; +} + +#navigation { + float: left; + width: 14em; + vertical-align: top; + background-color: #222222; + overflow: visible; +} + +#navigation h1 { + color: #cccccc; + font-size: 1.5em; + margin: 20px 0 20px 0; +} + +#navigation h2 { + background-color:#333333; + font-size:1.1em; + color:#cccccc; + text-align: left; + padding:0.2em; + border-top:1px solid #dddddd; + border-bottom:1px solid #dddddd; +} + +#navigation ul +{ + font-size:1em; + list-style-type: none; + margin: 1px 1px 10px 1px; +} + +#navigation li { + text-indent: -1em; + display: block; + margin: 3px 0px 0px 22px; +} + +#navigation li li a { + margin: 0px 3px 0px -1em; +} + +#content { + margin-left: 14em; + padding: 1em; + border-left: 2px solid #cccccc; + border-right: 2px solid #cccccc; + background-color: #222222; +} + +#about { + clear: both; + padding: 5px; + border-top: 2px solid #cccccc; + background-color: #222222; +} + +@media print { + body { + font: 12pt "Times New Roman", "TimeNR", Times, serif; + } + a { font-weight: bold; color: #004080; text-decoration: underline; } + + #main { + background-color: #ffffff; + border-left: 0px; + } + + #container { + margin-left: 2%; + margin-right: 2%; + background-color: #ffffff; + } + + #content { + padding: 1em; + background-color: #ffffff; + } + + #navigation { + display: none; + } + pre.example { + font-family: "Andale Mono", monospace; + font-size: 10pt; + page-break-inside: avoid; + } +} + +table.module_list { + border-width: 1px; + border-style: solid; + border-color: #cccccc; + border-collapse: collapse; +} +table.module_list td { + border-width: 1px; + padding: 3px; + border-style: solid; + border-color: #cccccc; +} +table.module_list td.name { background-color: #222222; min-width: 200px; } +table.module_list td.summary { width: 100%; } + + +table.function_list { + border-width: 1px; + border-style: solid; + border-color: #cccccc; + border-collapse: collapse; +} +table.function_list td { + border-width: 1px; + padding: 3px; + border-style: solid; + border-color: #cccccc; +} +table.function_list td.name { background-color: #222222; min-width: 200px; } +table.function_list td.summary { width: 100%; } + +ul.nowrap { + overflow:auto; + white-space:nowrap; +} + +dl.table dt, dl.function dt {border-top: 1px solid #ccc; padding-top: 1em;} +dl.table dd, dl.function dd {padding-bottom: 1em; margin: 10px 0 0 20px;} +dl.table h3, dl.function h3 {font-size: .95em;} + +/* stop sublists from having initial vertical space */ +ul ul { margin-top: 0px; } +ol ul { margin-top: 0px; } +ol ol { margin-top: 0px; } +ul ol { margin-top: 0px; } + +/* make the target distinct; helps when we're navigating to a function */ +a:target + * { + background-color: #060; +} + + +/* styles for prettification of source */ +pre .comment { color: #558817; } +pre .constant { color: #a8660d; } +pre .escape { color: #844631; } +pre .keyword { color: #aa5050; font-weight: bold; } +pre .library { color: #0e7c6b; } +pre .marker { color: #512b1e; background: #fedc56; font-weight: bold; } +pre .string { color: #8080ff; } +pre .number { color: #f8660d; } +pre .operator { color: #2239a8; font-weight: bold; } +pre .preprocessor, pre .prepro { color: #a33243; } +pre .global { color: #800080; } +pre .user-keyword { color: #800080; } +pre .prompt { color: #558817; } +pre .url { color: #272fc2; text-decoration: underline; } + +.tg {border-collapse:collapse;border-color:#ccc;border-spacing:0;} +.tg td{background-color:#8b8b8b;border-color:#ccc;border-style:solid;border-width:1px;color:#000; + font-family:Arial, sans-serif;font-size:14px;overflow:hidden;padding:10px 5px;word-break:normal;} +.tg th{background-color:#000000;border-color:#ccc;border-style:solid;border-width:1px;color:#FFF; + font-family:Arial, sans-serif;font-size:14px;font-weight:normal;overflow:hidden;padding:10px 5px;word-break:normal;} +.tg .tg-2{background-color:#797979;border-color:inherit;text-align:left;vertical-align:top;color:#DDD} +.tg .tg-1{background-color:#8b8b8b;border-color:#ccc;border-style:solid;border-width:1px;color:#000;text-align:left;vertical-align:top} diff --git a/src/resources/MDK/doc/modules/GradientMaker.html b/src/resources/MDK/doc/modules/GradientMaker.html new file mode 100755 index 0000000..626c9a8 --- /dev/null +++ b/src/resources/MDK/doc/modules/GradientMaker.html @@ -0,0 +1,422 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Module GradientMaker

+

Module which provides for creating color gradients for your text.

+

+ Original functions found on the Lusternia Forums +
I added functions to work with hecho. +
I also made performance enhancements by storing already calculated gradients after first use for the session and only including the colorcode in the returned string if the color changed.

+

Info:

+
    +
  • Copyright: 2018 Sylphas,2020 Damian Monogue
  • +
  • Author: Sylphas on the Lusternia forums,Damian Monogue
  • +
+ + +

Functions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
color_name(r, g, b)Returns the closest color name to a given r,g,b color
dgradient(text, first_color, second_color, next_color)Returns the text, with the defined color gradients applied and formatted for us with decho.
cgradient(text, first_color, second_color, next_color)Returns the text, with the defined color gradients applied and formatted for us with cecho.
hgradient(text, first_color, second_color, next_color)Returns the text, with the defined color gradients applied and formatted for us with hecho.
cgradient_table(text, first_color, second_color, next_color)Returns a table, each element of which is a table, the first element of which is the color name to use and the character which should be that color
dgradient_table(text, first_color, second_color, next_color)Returns a table, each element of which is a table, the first element of which is the color({r,g,b} format) to use and the character which should be that color
hgradient_table(text, first_color, second_color, next_color)Returns a table, each element of which is a table, the first element of which is the color(in hex) to use and the second element of which is the character which should be that color
install_global()Creates global copies of the c/d/hgradient(_table) functions and color_name for use without accessing the module table
+ +
+
+ + +

Functions

+ +
+
+ + color_name(r, g, b) + line 228 +
+
+ Returns the closest color name to a given r,g,b color + + +

Parameters:

+
    +
  • r + The red component. Can also pass the full color as a table, IE { 255, 0, 0 } +
  • +
  • g + The green component. If you pass the color as a table as noted above, this param should be empty +
  • +
  • b + the blue components. If you pass the color as a table as noted above, this param should be empty +
  • +
+ + + + +

Usage:

+
    +
    closest_color = GradientMaker.color_name(128,200,30) -- returns "ansi_149"
    +closest_color = GradientMaker.color_name({128, 200, 30}) -- this is functionally equivalent to the first one
    +
+ +
+
+ + dgradient(text, first_color, second_color, next_color) + line 244 +
+
+ Returns the text, with the defined color gradients applied and formatted for us with decho. Usage example below produces the following text +
dgradient example + + +

Parameters:

+
    +
  • text + string + The text you want to apply the color gradients to +
  • +
  • first_color + The color you want it to start at. Table of colors in { r, g, b } format +
  • +
  • second_color + The color you want the gradient to transition to first. Table of colors in { r, g, b } format +
  • +
  • next_color + Keep repeating if you want it to transition from the second color to a third, then a third to a fourth, etc +
  • +
+ + + +

See also:

+ + +

Usage:

+
    +
    decho(GradientMaker.dgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {255,0,0}, {255,128,0}, {255,255,0}, {0,255,0}, {0,255,255}, {0,128,255}, {128,0,255}))
    +decho(GradientMaker.dgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {255,0,0}, {0,0,255}))
    +decho(GradientMaker.dgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {50,50,50}, {0,255,0}, {50,50,50}))
    +
+ +
+
+ + cgradient(text, first_color, second_color, next_color) + line 260 +
+
+ Returns the text, with the defined color gradients applied and formatted for us with cecho. Usage example below produces the following text +
cgradient example + + +

Parameters:

+
    +
  • text + string + The text you want to apply the color gradients to +
  • +
  • first_color + The color you want it to start at. Table of colors in { r, g, b } format +
  • +
  • second_color + The color you want the gradient to transition to first. Table of colors in { r, g, b } format +
  • +
  • next_color + Keep repeating if you want it to transition from the second color to a third, then a third to a fourth, etc +
  • +
+ + + +

See also:

+ + +

Usage:

+
    +
    cecho(GradientMaker.cgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {255,0,0}, {255,128,0}, {255,255,0}, {0,255,0}, {0,255,255}, {0,128,255}, {128,0,255}))
    +cecho(GradientMaker.cgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {255,0,0}, {0,0,255}))
    +cecho(GradientMaker.cgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {50,50,50}, {0,255,0}, {50,50,50}))
    +
+ +
+
+ + hgradient(text, first_color, second_color, next_color) + line 276 +
+
+ Returns the text, with the defined color gradients applied and formatted for us with hecho. Usage example below produces the following text +
hgradient example + + +

Parameters:

+
    +
  • text + string + The text you want to apply the color gradients to +
  • +
  • first_color + The color you want it to start at. Table of colors in { r, g, b } format +
  • +
  • second_color + The color you want the gradient to transition to first. Table of colors in { r, g, b } format +
  • +
  • next_color + Keep repeating if you want it to transition from the second color to a third, then a third to a fourth, etc +
  • +
+ + + +

See also:

+ + +

Usage:

+
    +
    hecho(GradientMaker.hgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {255,0,0}, {255,128,0}, {255,255,0}, {0,255,0}, {0,255,255}, {0,128,255}, {128,0,255}))
    +hecho(GradientMaker.hgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {255,0,0}, {0,0,255}))
    +hecho(GradientMaker.hgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {50,50,50}, {0,255,0}, {50,50,50}))
    +
+ +
+
+ + cgradient_table(text, first_color, second_color, next_color) + line 286 +
+
+ Returns a table, each element of which is a table, the first element of which is the color name to use and the character which should be that color + + +

Parameters:

+
    +
  • text + string + The text you want to apply the color gradients to +
  • +
  • first_color + The color you want it to start at. Table of colors in { r, g, b } format +
  • +
  • second_color + The color you want the gradient to transition to first. Table of colors in { r, g, b } format +
  • +
  • next_color + Keep repeating if you want it to transition from the second color to a third, then a third to a fourth, etc +
  • +
+ + + +

See also:

+ + + +
+
+ + dgradient_table(text, first_color, second_color, next_color) + line 296 +
+
+ Returns a table, each element of which is a table, the first element of which is the color({r,g,b} format) to use and the character which should be that color + + +

Parameters:

+
    +
  • text + string + The text you want to apply the color gradients to +
  • +
  • first_color + The color you want it to start at. Table of colors in { r, g, b } format +
  • +
  • second_color + The color you want the gradient to transition to first. Table of colors in { r, g, b } format +
  • +
  • next_color + Keep repeating if you want it to transition from the second color to a third, then a third to a fourth, etc +
  • +
+ + + +

See also:

+ + + +
+
+ + hgradient_table(text, first_color, second_color, next_color) + line 306 +
+
+ Returns a table, each element of which is a table, the first element of which is the color(in hex) to use and the second element of which is the character which should be that color + + +

Parameters:

+
    +
  • text + string + The text you want to apply the color gradients to +
  • +
  • first_color + The color you want it to start at. Table of colors in { r, g, b } format +
  • +
  • second_color + The color you want the gradient to transition to first. Table of colors in { r, g, b } format +
  • +
  • next_color + Keep repeating if you want it to transition from the second color to a third, then a third to a fourth, etc +
  • +
+ + + +

See also:

+ + + +
+
+ + install_global() + line 314 +
+
+ Creates global copies of the c/d/hgradient(_table) functions and color_name for use without accessing the module table + + + + + + +

Usage:

+
    +
    GradientMaker.install_global()
    +cecho(cgradient(...)) -- use cgradient directly now
    +
+ +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/modules/demontools.html b/src/resources/MDK/doc/modules/demontools.html new file mode 100755 index 0000000..dcb64f2 --- /dev/null +++ b/src/resources/MDK/doc/modules/demontools.html @@ -0,0 +1,1330 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Module demontools

+

Collection of miscellaneous functions and tools which don't necessarily warrant their own module/class

+

+

Info:

+
    +
  • Copyright: 2020 Damian Monogue
  • +
  • License: MIT, see LICENSE.lua
  • +
  • Author: Damian Monogue
  • +
+ + +

Functions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DemonTools.chunkify(tbl, num_chunks)Takes a list table and returns it as a table of 'chunks'.
DemonTools.ansi2cecho(text)Takes an ansi colored text string and returns a cecho colored one
DemonTools.ansi2decho(text)Takes an ansi colored text string and returns a decho colored one.
DemonTools.ansi2hecho(text)Takes an ansi colored text string and returns a hecho colored one
DemonTools.cecho2decho(text)Takes an cecho colored text string and returns a decho colored one
DemonTools.cecho2ansi(text)Takes an cecho colored text string and returns an ansi colored one
DemonTools.cecho2hecho(text)Takes an cecho colored text string and returns a hecho colored one
DemonTools.decho2cecho(text)Takes an decho colored text string and returns a cecho colored one
DemonTools.decho2ansi(text)Takes an decho colored text string and returns an ansi colored one
DemonTools.decho2hecho(text)Takes an decho colored text string and returns an hecho colored one
DemonTools.decho2html(text)Takes a decho colored text string and returns html.
DemonTools.cecho2html(text)Takes a cecho colored text string and returns html.
DemonTools.hecho2html(text)Takes a hecho colored text string and returns html.
DemonTools.ansi2html(text)Takes an ansi colored text string and returns html.
DemonTools.html2cecho(text)Takes an html colored string of the sort turned out by the DemonTools *2html functions and returns a cecho string
DemonTools.html2decho(text)Takes an html colored string of the sort turned out by the DemonTools *2html functions and returns a decho string
DemonTools.html2ansi(text)Takes an html colored string of the sort turned out by the DemonTools *2html functions and returns an ansi string
DemonTools.html2hecho(text)Takes an html colored string of the sort turned out by the DemonTools *2html functions and returns an hecho string
DemonTools.cecho2string(text)Takes a cecho string and returns it without the formatting
DemonTools.decho2string(text)Takes a decho string and returns it without the formatting
DemonTools.hecho2string(text)Takes a hecho string and returns it without the formatting
DemonTools.html2string(text)Takes an html colored string of the sort turned out by the DemonTools *2html functions and returns a clean string
DemonTools.hecho2ansi(text)Takes an hecho colored text string and returns a ansi colored one
DemonTools.hecho2cecho(text)Takes an hecho colored text string and returns a cecho colored one
DemonTools.hecho2decho(text)Takes an hecho colored text string and returns a decho colored one
DemonTools.append2decho()Takes the currently copy()ed item and returns it as a decho string
DemonTools.consoleToString(options)Dump the contents of a miniconsole, user window, or the main window in one of several formats, as determined by a table of options
DemonTools.displayColors(options)Alternative to Mudlet's showColors(), this one has additional options.
DemonTools.roundInt(number)Rounds a number to the nearest whole integer
DemonTools.scientific_round(number, sig_digits)Rounds a number to a specified number of significant digits
DemonTools.string2color(str)Returns a color table {r,g,b} derived from str.
DemonTools.colorMunge(strForColor, strToColor, format)Returns a colored string where strForColor is run through DemonTools.string2color and applied to strToColor based on format.
DemonTools.colorMungeEcho(strForColor, strToEcho, format, win)Like colorMunge but also echos the result to win.
DemonTools.milliToHuman(milliseconds, tbl)Converts milliseconds to hours:minutes:seconds:milliseconds
DemonTools.getValueAt(variableString)Takes the name of a variable as a string and returns the value.
DemonTools.exists(path)Returns if a file or directory exists on the filesystem
DemonTools.isDir(path)Returns if a path is a directory or not
DemonTools.isWindows()Returns true if running on windows, false otherwise
DemonTools.mkdir_p(path)Creates a directory, creating each directory as necessary along the way.
+ +
+
+ + +

Functions

+ +
+
+ + DemonTools.chunkify(tbl, num_chunks) + line 905 +
+
+ 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 + + +

Parameters:

+
    +
  • tbl + table + The table you want to turn into chunks. Must be traversable using ipairs() +
  • +
  • num_chunks + number + 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"
    +  }
    +}
    +
+ +
+
+ + DemonTools.ansi2cecho(text) + line 913 +
+
+ Takes an ansi colored text string and returns a cecho colored one + + +

Parameters:

+
    +
  • text + string + the text to convert +
  • +
+ + + + +

Usage:

+
    +
    dt.ansi2cecho("Test")
    + --returns "<ansiRed>Test"
    +
+ +
+
+ + DemonTools.ansi2decho(text) + line 923 +
+
+ Takes an ansi colored text string and returns a decho colored one. Handles 256 color SGR codes better than Mudlet's ansi2decho + + +

Parameters:

+
    +
  • text + string + the text to convert +
  • +
+ + + + +

Usage:

+
    +
  • dt.ansi2decho("Test") --returns "<128,0,0>Test"
  • +
  • dt.ansi2decho("[38:2::127:0:0mTest") --returns "<127,0,0>Test"
  • +
  • ansi2decho("[38:2::127:0:0mTest") -- doesn't parse this format of colors and so returns "[38:2::127:0:0mTest"
  • +
+ +
+
+ + DemonTools.ansi2hecho(text) + line 931 +
+
+ Takes an ansi colored text string and returns a hecho colored one + + +

Parameters:

+
    +
  • text + string + the text to convert +
  • +
+ + + + +

Usage:

+
    +
    dt.ansi2hecho("Test")
    + --returns "#800000Test"
    +
+ +
+
+ + DemonTools.cecho2decho(text) + line 938 +
+
+ Takes an cecho colored text string and returns a decho colored one + + +

Parameters:

+
    +
  • text + string + the text to convert +
  • +
+ + + + +

Usage:

+
    +
    dt.cecho2decho("<green>Test") --returns "<0,255,0>Test"
    +
+ +
+
+ + DemonTools.cecho2ansi(text) + line 945 +
+
+ Takes an cecho colored text string and returns an ansi colored one + + +

Parameters:

+
    +
  • text + string + the text to convert +
  • +
+ + + + +

Usage:

+
    +
    dt.cecho2ansi("<green>Test") --returns "[38:2::0:255:0mTest"
    +
+ +
+
+ + DemonTools.cecho2hecho(text) + line 952 +
+
+ Takes an cecho colored text string and returns a hecho colored one + + +

Parameters:

+
    +
  • text + string + the text to convert +
  • +
+ + + + +

Usage:

+
    +
    dt.cecho2hecho("<green>Test") --returns "#00ff00Test"
    +
+ +
+
+ + DemonTools.decho2cecho(text) + line 959 +
+
+ Takes an decho colored text string and returns a cecho colored one + + +

Parameters:

+
    +
  • text + string + the text to convert +
  • +
+ + + + +

Usage:

+
    +
    dt.decho2cecho("<127,0,0:0,0,127>Test") --returns "<ansiRed:ansi_blue>Test"
    +
+ +
+
+ + DemonTools.decho2ansi(text) + line 966 +
+
+ Takes an decho colored text string and returns an ansi colored one + + +

Parameters:

+
    +
  • text + string + 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"
    +
+ +
+
+ + DemonTools.decho2hecho(text) + line 973 +
+
+ Takes an decho colored text string and returns an hecho colored one + + +

Parameters:

+
    +
  • text + string + the text to convert +
  • +
+ + + + +

Usage:

+
    +
    dt.decho2hecho("<127,0,0:0,0,127>Test") --returns "#7f0000,00007fTest"
    +
+ +
+
+ + DemonTools.decho2html(text) + line 979 +
+
+ Takes a decho colored text string and returns html. + + +

Parameters:

+
    +
  • text + string + the text to convert +
  • +
+ + + + + +
+
+ + DemonTools.cecho2html(text) + line 985 +
+
+ Takes a cecho colored text string and returns html. + + +

Parameters:

+
    +
  • text + string + the text to convert +
  • +
+ + + + + +
+
+ + DemonTools.hecho2html(text) + line 991 +
+
+ Takes a hecho colored text string and returns html. + + +

Parameters:

+
    +
  • text + string + the text to convert +
  • +
+ + + + + +
+
+ + DemonTools.ansi2html(text) + line 997 +
+
+ Takes an ansi colored text string and returns html. + + +

Parameters:

+
    +
  • text + string + the text to convert +
  • +
+ + + + + +
+
+ + DemonTools.html2cecho(text) + line 1003 +
+
+ Takes an html colored string of the sort turned out by the DemonTools *2html functions and returns a cecho string + + +

Parameters:

+
    +
  • text + string + the text to convert +
  • +
+ + + + + +
+
+ + DemonTools.html2decho(text) + line 1009 +
+
+ Takes an html colored string of the sort turned out by the DemonTools *2html functions and returns a decho string + + +

Parameters:

+
    +
  • text + string + the text to convert +
  • +
+ + + + + +
+
+ + DemonTools.html2ansi(text) + line 1015 +
+
+ Takes an html colored string of the sort turned out by the DemonTools *2html functions and returns an ansi string + + +

Parameters:

+
    +
  • text + string + the text to convert +
  • +
+ + + + + +
+
+ + DemonTools.html2hecho(text) + line 1021 +
+
+ Takes an html colored string of the sort turned out by the DemonTools *2html functions and returns an hecho string + + +

Parameters:

+
    +
  • text + string + the text to convert +
  • +
+ + + + + +
+
+ + DemonTools.cecho2string(text) + line 1027 +
+
+ Takes a cecho string and returns it without the formatting + + +

Parameters:

+
    +
  • text + the text to transform +
  • +
+ + + + + +
+
+ + DemonTools.decho2string(text) + line 1033 +
+
+ Takes a decho string and returns it without the formatting + + +

Parameters:

+
    +
  • text + the text to transform +
  • +
+ + + + + +
+
+ + DemonTools.hecho2string(text) + line 1039 +
+
+ Takes a hecho string and returns it without the formatting + + +

Parameters:

+
    +
  • text + the text to transform +
  • +
+ + + + + +
+
+ + DemonTools.html2string(text) + line 1044 +
+
+ Takes an html colored string of the sort turned out by the DemonTools *2html functions and returns a clean string + + +

Parameters:

+
    +
  • text + +
  • +
+ + + + + +
+
+ + DemonTools.hecho2ansi(text) + line 1051 +
+
+ Takes an hecho colored text string and returns a ansi colored one + + +

Parameters:

+
    +
  • text + string + the text to convert +
  • +
+ + + + +

Usage:

+
    +
    dt.hecho2ansi("#7f0000,00007fTest") --returns "[38:2::127:0:0m[48:2::0:0:127mTest"
    +
+ +
+
+ + DemonTools.hecho2cecho(text) + line 1058 +
+
+ Takes an hecho colored text string and returns a cecho colored one + + +

Parameters:

+
    +
  • text + string + the text to convert +
  • +
+ + + + +

Usage:

+
    +
    dt.hecho2cecho("#7f0000,00007fTest") --returns "<ansiRed:ansi_blue>Test"
    +
+ +
+
+ + DemonTools.hecho2decho(text) + line 1065 +
+
+ Takes an hecho colored text string and returns a decho colored one + + +

Parameters:

+
    +
  • text + string + the text to convert +
  • +
+ + + + +

Usage:

+
    +
    dt.hecho2decho("#7f0000,00007fTest") --returns "<127,0,0:0,0,127>Test"
    +
+ +
+
+ + DemonTools.append2decho() + line 1070 +
+
+ Takes the currently copy()ed item and returns it as a decho string + + + + + + + +
+
+ + DemonTools.consoleToString(options) + line 1112 +
+
+ Dump the contents of a miniconsole, user window, or the main window in one of several formats, as determined by a table of options + + +

Parameters:

+
    +
  • options + table + 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
    +
  • +
+ + + + + +
+
+ + DemonTools.displayColors(options) + line 1174 +
+
+ Alternative to Mudlet's showColors(), this one has additional options. + + +

Parameters:

+
    +
  • options + table + 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
    +
  • +
+ + + + + +
+
+ + DemonTools.roundInt(number) + line 1182 +
+
+ Rounds a number to the nearest whole integer + + +

Parameters:

+
    +
  • number + the number to round off +
  • +
+ + + + +

Usage:

+
    +
  • dt.roundInt(8.3) -- returns 8
  • +
  • dt.roundInt(10.7) -- returns 11
  • +
+ +
+
+ + DemonTools.scientific_round(number, sig_digits) + line 1194 +
+
+ Rounds a number to a specified number of significant digits + + +

Parameters:

+
    +
  • number + number + the number to round +
  • +
  • sig_digits + number + the number of significant digits to keep +
  • +
+ + + + +

Usage:

+
    +
  • dt.scientific_round(1348290, 3) -- will return 1350000
  • +
  • dt.scientific_found(123.3452, 5) -- will return 123.34
  • +
+ +
+
+ + DemonTools.string2color(str) + line 1201 +
+
+ Returns a color table {r,g,b} derived from str. Will return the same color every time for the same string. + + +

Parameters:

+
    +
  • str + string + the string to turn into a color. +
  • +
+ + + + +

Usage:

+
    +
    dt.string2color("Demonnic") --returns { 131, 122, 209 }
    +
+ +
+
+ + DemonTools.colorMunge(strForColor, strToColor, format) + line 1210 +
+
+ Returns a colored string where strForColor is run through DemonTools.string2color and applied to strToColor based on format. + + +

Parameters:

+
    +
  • strForColor + string + the string to turn into a color using DemonTools.string2color +
  • +
  • strToColor + string + the string you want to color based on strForColor +
  • +
  • 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"
    +
+ +
+
+ + DemonTools.colorMungeEcho(strForColor, strToEcho, format, win) + line 1219 +
+
+ Like colorMunge but also echos the result to win. + + +

Parameters:

+
    +
  • strForColor + string + the string to turn into a color using DemonTools.string2color +
  • +
  • strToEcho + string + the string you want to color and echo based on strForColor +
  • +
  • format + What format to use for the color portion. "d" for decho, "c" for cecho, or "h" for hecho. Defaults to "d" +
  • +
  • win + the window to echo to. You must provide the format if you want to change the window. Defaults to "main" +
  • +
+ + + + + +
+
+ + DemonTools.milliToHuman(milliseconds, tbl) + line 1235 +
+
+ Converts milliseconds to hours:minutes:seconds:milliseconds + + +

Parameters:

+
    +
  • milliseconds + number + the number of milliseconds to convert +
  • +
  • tbl + boolean + if true, returns the time as a key/value table instead +
  • +
+ + + + +

Usage:

+
    +
  • dt.milliToHuman(37194572) --returns "10:19:54:572"
  • +
  • display(dt.milliToHuman(37194572, true))
    +{
    +  minutes = 19,
    +  original = 37194572,
    +  hours = 10,
    +  milliseconds = 572,
    +  seconds = 54
    +}
  • +
+ +
+
+ + DemonTools.getValueAt(variableString) + line 1257 +
+
+ 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 + + +

Parameters:

+
    +
  • variableString + string + the string name of the variable you want the value of +
  • +
+ + + + +

Usage:

+
    +
    currentHP = 50
    + dt.getValueAt("currentHP") -- returns 50
    +
+ +
+
+ + DemonTools.exists(path) + line 1263 +
+
+ Returns if a file or directory exists on the filesystem + + +

Parameters:

+
    +
  • path + string + the path to the file or directory to check +
  • +
+ + + + + +
+
+ + DemonTools.isDir(path) + line 1269 +
+
+ Returns if a path is a directory or not + + +

Parameters:

+
    +
  • path + string + the path to check +
  • +
+ + + + + +
+
+ + DemonTools.isWindows() + line 1274 +
+
+ Returns true if running on windows, false otherwise + + + + + + + +
+
+ + DemonTools.mkdir_p(path) + line 1280 +
+
+ Creates a directory, creating each directory as necessary along the way. + + +

Parameters:

+
    +
  • path + string + the path to create +
  • +
+ + + + + +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/modules/echofile.html b/src/resources/MDK/doc/modules/echofile.html new file mode 100755 index 0000000..36c0add --- /dev/null +++ b/src/resources/MDK/doc/modules/echofile.html @@ -0,0 +1,552 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Module echofile

+

set of functions for echoing files to things.

+

Uses a slightly hacked up version of f-strings for interpolation/templating

+

Info:

+
    +
  • Copyright: 2021 Damian Monogue,2016 Hisham Muhammad (https://github.com/hishamhm/f-strings/blob/master/LICENSE)
  • +
  • License: MIT, see LICENSE.lua
  • +
  • Author: Damian Monogue
  • +
+ + +

Functions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
f(str)Takes a string and performs interpolation + Uses {} as the delimiter.
aechoFile(window, filename)reads the contents of a file, converts it to decho and then dechos it
aechoFilef(window, filename)reads the contents of a file and then cechos it
cechoFile(window, filename)reads the contents of a file and then cechos it
cechoFilef(window, filename)reads the contents of a file, interpolates it as per echofile.f and then cechos it
dechoFile(window, filename)reads the contents of a file and then dechos it
dechoFilef(window, filename)reads the contents of a file, interpolates it as per echofile.f and then dechos it
hechoFile(window, filename)reads the contents of a file and then hechos it
hechoFilef(window, filename)reads the contents of a file, interpolates it as per echofile.f and then hechos it
echoFile(window, filename)reads the contents of a file, interpolates it as per echofile.f and then echos it
echoFilef(window, filename)reads the contents of a file, interpolates it as per echofile.f and then echos it
patchGeyser()Adds c/d/h/echoFile functions to Geyser miniconsole and userwindow objects
installGlobal()Installs c/d/h/echoFile and f to the global namespace, and adds functions to Geyser
+ +
+
+ + +

Functions

+ +
+
+ + f(str) + line 109 +
+
+ Takes a string and performs interpolation + Uses {} as the delimiter. Expressions will be evaluated + + +

Parameters:

+
    +
  • str + string: The string to interpolate +
  • +
+ + + + +

Usage:

+
    +
    echofile = require("MDK.echofile")
    +echofile.f("{1+1}") -- returns "2"
    +local x = 4
    +echofile.f"4+3 = {x+3}" -- returns "4+3 = 7"
    +
+ +
+
+ + aechoFile(window, filename) + line 124 +
+
+ reads the contents of a file, converts it to decho and then dechos it + + +

Parameters:

+
    +
  • window + string: Optional window to cecho to +
  • +
  • filename + string: Full path to file +
  • +
+ + + +

See also:

+ + +

Usage:

+
    +
    local ec = require("MDK.echofile")
    +local cechoFile,f = ec.cechoFile, ec.f
    +cechoFile("C:/path/to/file") -- windows1
    +cechoFile("C:\\path\\to\\file") -- windows2
    +cechoFile("/path/to/file") -- Linux/MacOS
    +cechoFile("aMiniConsole", f"{getMudletHomeDir()}/myPkgName/helpfile") -- cecho a file from your pkg to a miniconsole
    +
+ +
+
+ + aechoFilef(window, filename) + line 140 +
+
+ reads the contents of a file and then cechos it + + +

Parameters:

+
    +
  • window + string: Optional window to cecho to +
  • +
  • filename + string: Full path to file +
  • +
+ + + +

See also:

+ + +

Usage:

+
    +
    local ec = require("MDK.echofile")
    +local cechoFile,f = ec.cechoFile, ec.f
    +cechoFile("C:/path/to/file") -- windows1
    +cechoFile("C:\\path\\to\\file") -- windows2
    +cechoFile("/path/to/file") -- Linux/MacOS
    +cechoFile("aMiniConsole", f"{getMudletHomeDir()}/myPkgName/helpfile") -- cecho a file from your pkg to a miniconsole
    +
+ +
+
+ + cechoFile(window, filename) + line 155 +
+
+ reads the contents of a file and then cechos it + + +

Parameters:

+
    +
  • window + string: Optional window to cecho to +
  • +
  • filename + string: Full path to file +
  • +
+ + + +

See also:

+ + +

Usage:

+
    +
    local ec = require("MDK.echofile")
    +local cechoFile,f = ec.cechoFile, ec.f
    +cechoFile("C:/path/to/file") -- windows1
    +cechoFile("C:\\path\\to\\file") -- windows2
    +cechoFile("/path/to/file") -- Linux/MacOS
    +cechoFile("aMiniConsole", f"{getMudletHomeDir()}/myPkgName/helpfile") -- cecho a file from your pkg to a miniconsole
    +
+ +
+
+ + cechoFilef(window, filename) + line 170 +
+
+ reads the contents of a file, interpolates it as per echofile.f and then cechos it + + +

Parameters:

+
    +
  • window + string: Optional window to cecho to +
  • +
  • filename + string: Full path to file +
  • +
+ + + +

See also:

+ + +

Usage:

+
    +
    local ec = require("MDK.echofile")
    +local cechoFile,f = ec.cechoFile, ec.f
    +cechoFile("C:/path/to/file") -- windows1
    +cechoFile("C:\\path\\to\\file") -- windows2
    +cechoFile("/path/to/file") -- Linux/MacOS
    +cechoFile("aMiniConsole", f"{getMudletHomeDir()}/myPkgName/helpfile") -- cecho a file from your pkg to a miniconsole
    +
+ +
+
+ + dechoFile(window, filename) + line 180 +
+
+ reads the contents of a file and then dechos it + + +

Parameters:

+
    +
  • window + string: Optional window to decho to +
  • +
  • filename + string: Full path to file +
  • +
+ + + +

See also:

+ + + +
+
+ + dechoFilef(window, filename) + line 190 +
+
+ reads the contents of a file, interpolates it as per echofile.f and then dechos it + + +

Parameters:

+
    +
  • window + string: Optional window to decho to +
  • +
  • filename + string: Full path to file +
  • +
+ + + +

See also:

+ + + +
+
+ + hechoFile(window, filename) + line 200 +
+
+ reads the contents of a file and then hechos it + + +

Parameters:

+
    +
  • window + string: Optional window to hecho to +
  • +
  • filename + string: Full path to file +
  • +
+ + + +

See also:

+ + + +
+
+ + hechoFilef(window, filename) + line 210 +
+
+ reads the contents of a file, interpolates it as per echofile.f and then hechos it + + +

Parameters:

+
    +
  • window + string: Optional window to hecho to +
  • +
  • filename + string: Full path to file +
  • +
+ + + +

See also:

+ + + +
+
+ + echoFile(window, filename) + line 220 +
+
+ reads the contents of a file, interpolates it as per echofile.f and then echos it + + +

Parameters:

+
    +
  • window + string: Optional window to echo to +
  • +
  • filename + string: Full path to file +
  • +
+ + + +

See also:

+ + + +
+
+ + echoFilef(window, filename) + line 230 +
+
+ reads the contents of a file, interpolates it as per echofile.f and then echos it + + +

Parameters:

+
    +
  • window + string: Optional window to echo to +
  • +
  • filename + string: Full path to file +
  • +
+ + + +

See also:

+ + + +
+
+ + patchGeyser() + line 239 +
+
+ Adds c/d/h/echoFile functions to Geyser miniconsole and userwindow objects + + + + + + +

Usage:

+
    +
    require("MDK.echofile").patchGeyser()
    +myMC = Geyser.MiniConsole:new({name = "myMC"})
    +myMC:cechoFile(f"{getMudletHomeDir()}/helpfile")
    +
+ +
+
+ + installGlobal() + line 281 +
+
+ Installs c/d/h/echoFile and f to the global namespace, and adds functions to Geyser + + + + + + +

Usage:

+
    +
    require("MDK.echofile").installGlobal()
    +f"{1+2}" -- returns "2"
    +dechoFile(f"{getMudletHomeDir()}/fileWithDechoLines.txt")
    +-- reads contents of fileWithDechoLines.txt from profile directory
    +-- and dechos them to the main console
    +
+ +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/modules/figlet.html b/src/resources/MDK/doc/modules/figlet.html new file mode 100755 index 0000000..a798f9e --- /dev/null +++ b/src/resources/MDK/doc/modules/figlet.html @@ -0,0 +1,260 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Module figlet

+

Figlet + A module to read figlet fonts and produce figlet ascii art from text

+

+

Info:

+
    +
  • Copyright: 2010,2011 Nick Gammon,2022 Damian Monogue
  • +
+ + +

Functions

+ + + + + + + + + + + + + + + + + + + + + +
Figlet.readfont(filename)Reads a figlet font file (.flf) into memory and readies it for use by the next figlet + These files are cached in memory so that future calls to load a font just read from there.
Figlet.ascii_art(s, kern, smush)Returns a table of lines representing a string as figlet
Figlet.getString(str, kern, smush)Returns the figlet as a string, rather than a table
Figlet.getKern(str)Returns a figlet as a string, with kern set to true.
Figlet.getSmush(str)Returns a figlet as a string, with smush set to true.
+ +
+
+ + +

Functions

+ +
+
+ + Figlet.readfont(filename) + line 79 +
+
+ Reads a figlet font file (.flf) into memory and readies it for use by the next figlet + These files are cached in memory so that future calls to load a font just read from there. + + +

Parameters:

+
    +
  • filename + the full path to the file to read the font from +
  • +
+ + + + + +
+
+ + Figlet.ascii_art(s, kern, smush) + line 214 +
+
+ Returns a table of lines representing a string as figlet + + +

Parameters:

+
    +
  • s + string + the text to make into a figlet +
  • +
  • kern + boolean + should we reduce spacing +
  • +
  • smush + boolean + causes the letters to share edges, condensing it even further +
  • +
+ + + + + +
+
+ + Figlet.getString(str, kern, smush) + line 248 +
+
+ Returns the figlet as a string, rather than a table + + +

Parameters:

+
    +
  • str + string + the string the make into a figlet +
  • +
  • kern + boolean + should we reduce the space between letters? +
  • +
  • smush + boolean + should the letters share edges, further condensing the output? +
  • +
+ + + +

See also:

+ + + +
+
+ + Figlet.getKern(str) + line 256 +
+
+ Returns a figlet as a string, with kern set to true. + + +

Parameters:

+
    +
  • str + string + The string to turn into a figlet +
  • +
+ + + +

See also:

+ + + +
+
+ + Figlet.getSmush(str) + line 263 +
+
+ Returns a figlet as a string, with smush set to true. + + +

Parameters:

+
    +
  • str + string + The string to turn into a figlet +
  • +
+ + + +

See also:

+ + + +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/modules/ftext.html b/src/resources/MDK/doc/modules/ftext.html new file mode 100755 index 0000000..182f3ee --- /dev/null +++ b/src/resources/MDK/doc/modules/ftext.html @@ -0,0 +1,1780 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Module ftext

+

ftext + functions to format and print text, and the objects which use them

+

+

Info:

+
    +
  • Copyright: 2020 Damian Monogue,2021 Damian Monogue,2022 Damian Monogue
  • +
  • License: MIT, see LICENSE.lua
  • +
  • Author: Damian Monogue
  • +
+ + +

Functions

+ + + + + + + + + + + + + +
wordWrap(str, limit)Performs wordwrapping on a string, given a length limit.
xwrap(text, limit, type)Performs wordwrapping on a string, while ignoring color tags of a given type.
fText(str, opts)The main course, this function returns a formatted string, based on a table of options
+

Class ftext.TextFormatter

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TextFormatter:setType(typeToSet)Set's the formatting type whether it's for cecho, decho, or hecho
TextFormatter:setWrap(shouldWrap)Sets whether or not we should do word wrapping.
TextFormatter:setWidth(width)Sets the width we should format for
TextFormatter:setCap(cap)Sets the cap for the formatter
TextFormatter:setCapColor(capColor)Sets the color for the format cap
TextFormatter:setSpacerColor(spacerColor)Sets the color for spacing character
TextFormatter:setTextColor(textColor)Sets the color for formatted text
TextFormatter:setSpacer(spacer)Sets the spacing character to use.
TextFormatter:setAlignment(alignment)Set the alignment to format for
TextFormatter:setInside(spacerInside)Set whether the the spacer should go inside the the cap or outside of it
TextFormatter:setMirror(shouldMirror)Set whether we should mirror/reverse the caps.
TextFormatter:setNoGap(noGap)Set whether we should remove the gap spaces between the text and spacer characters.
TextFormatter:enableTruncate()Enables truncation (cutting to length).
TextFormatter:disableTruncate()Disables truncation (cutting to length).
TextFormatter:format(str)Format a string based on the stored options
TextFormatter:new(options)Creates and returns a new TextFormatter.
+

Class ftext.TableMaker

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TableMaker:getColumn(position)Get the TextFormatter which defines the format of a specific column
TableMaker:addColumn(options, position)Adds a column definition for the table.
TableMaker:deleteColumn(position)Deletes a column at the given position
TableMaker:replaceColumn(options, position)Replaces a column at a specific position with the newly provided formatting
TableMaker:getRow(position)Gets the row of output at a specific position
TableMaker:addRow(columnEntries, position)Adds a row of output to the table
TableMaker:deleteRow(position)Deletes the row at the given position
TableMaker:replaceRow(columnEntries, position)Replaces a row of output in the table
TableMaker:getCell(row, column)Get the contents and formatter for a specific cell
TableMaker:setCell(row, column, entry)Sets a specific cell's display information
TableMaker:setTitle(title)set the title of the table
TableMaker:setRowSeparator(char)set the rowSeparator for the table
TableMaker:setEdgeCharacter(char)set the edgeCharacter for the table
TableMaker:setFootCharacter(char)set the foot character for the table
TableMaker:setHeadCharacter(char)set the head character for the table
TableMaker:setSeparator(char)set the column separator character for the table
TableMaker:setTitleColor(color)set the title color for the table
TableMaker:setSeparatorColor(color)set the title color for the table
TableMaker:setFrameColor(color)set the title color for the table
TableMaker:enableForceHeaderSeparator()Force a separator between the header and first row, even if the row separator is disabled for the overall table
TableMaker:disableForceHeaderSeparator()Do not force a separator between the header and first row, even if the row separator is disabled for the overall table
TableMaker:enableHeaderTitle()Enable using the title separator for the column headers as well
TableMaker:disableHeaderTitle()Disable using the title separator for the column headers as well
TableMaker:enablePrintTitle()enable printing the title of the table
TableMaker:disablePrintTitle()disable printing the title of the table
TableMaker:enablePrintHeaders()enable printing of the column headers
TableMaker:disablePrintHeaders()disable printing of the column headers
TableMaker:enableRowSeparator()enable printing the separator line between rows
TableMaker:disableRowSeparator()enable printing the separator line between rows
TableMaker:enablePopups()enables making cells which incorporate insertLink/insertPopup
TableMaker:enableAutoEcho()enables autoEcho so that when assemble is called it echos automatically
TableMaker:disableAutoEcho()disables autoecho.
TableMaker:enableAutoClear()Enables automatically clearing the miniconsole we echo to
TableMaker:disableAutoClear()Disables automatically clearing the miniconsole we echo to
TableMaker:setAutoEchoConsole(console)Set the miniconsole to echo to
TableMaker:assemble()Assemble the table.
TableMaker:new(options)Creates and returns a new TableMaker.
+ +
+
+ + +

Functions

+ +
+
+ + wordWrap(str, limit) + line 17 +
+
+ Performs wordwrapping on a string, given a length limit. Does not understand colour tags and will count them as characters in the string + + +

Parameters:

+
    +
  • str + string + the string to wordwrap +
  • +
  • limit + number + the line length to wrap at +
  • +
+ + + + + +
+
+ + xwrap(text, limit, type) + line 36 +
+
+ Performs wordwrapping on a string, while ignoring color tags of a given type. + + +

Parameters:

+
    +
  • text + string + the string you are wordwrapping +
  • +
  • limit + number + the line length to wrap at +
  • +
  • type + string + What type of color codes to ignore. 'c' for cecho, 'd' for decho, 'h' for hecho, and anything else or nil to pass the string on to wordWrap +
  • +
+ + + + + +
+
+ + fText(str, opts) + line 203 +
+
+ The main course, this function returns a formatted string, based on a table of options + + +

Parameters:

+
    +
  • str + string + the string to format +
  • +
  • opts + table + the table of options which control the formatting +

    Table of options + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    option namedescriptiondefault
    wrapShould it wordwrap to multiple lines?true
    formatTypeDetermines how it formats for color. 'c' for cecho, 'd' for decho, 'h' for hecho, and anything else for no colors""
    widthHow wide should we format the text?80
    capwhat characters to use for the endcap.""
    capColorwhat color to make the endcap?the correct 'white' for your formatType
    spacerWhat character to use for empty space. Must be a single character" "
    spacerColorwhat color should the spacer be?the correct 'white' for your formatType
    textColorwhat color should the text itself be?the correct 'white' for your formatType
    alignmentHow should the text be aligned within the width. "center", "left", or "right""center"
    nogapShould we put a literal space between the spacer character and the text?false
    insidePut the spacers inside the caps?false
    mirrorShould the endcap be reversed on the right? IE [[ becomes ]]true
    truncateCut the string to width. Is superceded by wrap being true.false
    +
  • +
+ + + + + +
+
+

Class ftext.TextFormatter

+ +
+ Stand alone text formatter object. Remembers the options you set and can be adjusted as needed +
+
+
+ + TextFormatter:setType(typeToSet) + line 512 +
+
+ Set's the formatting type whether it's for cecho, decho, or hecho + + +

Parameters:

+
    +
  • typeToSet + string + What type of formatter is this? Valid options are { 'd', 'dec', 'decimal', 'h', 'hex', 'hexidecimal', 'c', 'color', 'colour', 'col', 'name'} +
  • +
+ + + + + +
+
+ + TextFormatter:setWrap(shouldWrap) + line 546 +
+
+ Sets whether or not we should do word wrapping. + + +

Parameters:

+
    +
  • shouldWrap + boolean + should we do wordwrapping? +
  • +
+ + + + + +
+
+ + TextFormatter:setWidth(width) + line 558 +
+
+ Sets the width we should format for + + +

Parameters:

+
    +
  • width + number + the width we should format for +
  • +
+ + + + + +
+
+ + TextFormatter:setCap(cap) + line 571 +
+
+ Sets the cap for the formatter + + +

Parameters:

+
    +
  • cap + string + the string to use for capping the formatted string. +
  • +
+ + + + + +
+
+ + TextFormatter:setCapColor(capColor) + line 582 +
+
+ Sets the color for the format cap + + +

Parameters:

+
    +
  • capColor + string + Color which can be formatted via Geyser.Color.parse() +
  • +
+ + + + + +
+
+ + TextFormatter:setSpacerColor(spacerColor) + line 593 +
+
+ Sets the color for spacing character + + +

Parameters:

+
    +
  • spacerColor + string + Color which can be formatted via Geyser.Color.parse() +
  • +
+ + + + + +
+
+ + TextFormatter:setTextColor(textColor) + line 604 +
+
+ Sets the color for formatted text + + +

Parameters:

+
    +
  • textColor + string + Color which can be formatted via Geyser.Color.parse() +
  • +
+ + + + + +
+
+ + TextFormatter:setSpacer(spacer) + line 615 +
+
+ Sets the spacing character to use. Should be a single character + + +

Parameters:

+
    +
  • spacer + string + the character to use for spacing +
  • +
+ + + + + +
+
+ + TextFormatter:setAlignment(alignment) + line 626 +
+
+ Set the alignment to format for + + +

Parameters:

+
    +
  • alignment + string + How to align the formatted string. Valid options are 'left', 'right', or 'center' +
  • +
+ + + + + +
+
+ + TextFormatter:setInside(spacerInside) + line 637 +
+
+ Set whether the the spacer should go inside the the cap or outside of it + + +

Parameters:

+
    +
  • spacerInside + boolean + +
  • +
+ + + + + +
+
+ + TextFormatter:setMirror(shouldMirror) + line 648 +
+
+ Set whether we should mirror/reverse the caps. IE << becomes >> if set to true + + +

Parameters:

+
    +
  • shouldMirror + boolean + +
  • +
+ + + + + +
+
+ + TextFormatter:setNoGap(noGap) + line 659 +
+
+ Set whether we should remove the gap spaces between the text and spacer characters. "===some text===" if set to true, "== some text ==" if set to false + + +

Parameters:

+
    +
  • noGap + boolean + +
  • +
+ + + + + +
+
+ + TextFormatter:enableTruncate() + line 669 +
+
+ Enables truncation (cutting to length). You still need to ensure wrap is disabled, as it supercedes. + + + + + + + +
+
+ + TextFormatter:disableTruncate() + line 674 +
+
+ Disables truncation (cutting to length). You still need to ensure wrap is enabled if you want it to wrap. + + + + + + + +
+
+ + TextFormatter:format(str) + line 680 +
+
+ Format a string based on the stored options + + +

Parameters:

+
    +
  • str + string + The string to format +
  • +
+ + + + + +
+
+ + TextFormatter:new(options) + line 773 +
+
+ Creates and returns a new TextFormatter. + + +

Parameters:

+
    +
  • options + table + the options for the text formatter to use when running format() +

    Table of options + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    option namedescriptiondefault
    wrapShould it wordwrap to multiple lines?true
    formatTypeDetermines how it formats for color. 'c' for cecho, 'd' for decho, 'h' for hecho, and anything else for no colors"c"
    widthHow wide should we format the text?80
    capwhat characters to use for the endcap.""
    capColorwhat color to make the endcap?the correct 'white' for your formatType
    spacerWhat character to use for empty space. Must be a single character" "
    spacerColorwhat color should the spacer be?the correct 'white' for your formatType
    textColorwhat color should the text itself be?the correct 'white' for your formatType
    alignmentHow should the text be aligned within the width. "center", "left", or "right""center"
    nogapShould we put a literal space between the spacer character and the text?false
    insidePut the spacers inside the caps?false
    mirrorShould the endcap be reversed on the right? IE [[ becomes ]]true
    truncateCut the string to width. Is superceded by wrap being true.false
    +
  • +
+ + + + +

Usage:

+
    +
    local TextFormatter = require("MDK.ftext").TextFormatter
    +myFormatter = TextFormatter:new( {
    +  width = 40,
    +  cap = "[CAP]",
    +  capColor = "<orange>",
    +  textColor = "<light_blue>"
    +})
    +myMessage = "This is a test of the emergency broadcasting system. This is only a test"
    +cecho(myFormatter:format(myMessage))
    +
+ +
+
+

Class ftext.TableMaker

+ +
+ Easy formatting for text tables +
+
+
+ + TableMaker:getColumn(position) + line 839 +
+
+ Get the TextFormatter which defines the format of a specific column + + +

Parameters:

+
    +
  • position + number + The position of the column you're getting, counting from the left. If not provided will return the last column. +
  • +
+ + + + + +
+
+ + TableMaker:addColumn(options, position) + line 848 +
+
+ Adds a column definition for the table. + + +

Parameters:

+
    +
  • options + table + Table of options suitable for a TextFormatter object. See https://github.com/demonnic/fText/wiki/fText +
  • +
  • position + number + The position of the column you're adding, counting from the left. If not provided will add it as the last column +
  • +
+ + + + + +
+
+ + TableMaker:deleteColumn(position) + line 865 +
+
+ Deletes a column at the given position + + +

Parameters:

+
    +
  • position + number + the column you wish to delete +
  • +
+ + + + + +
+
+ + TableMaker:replaceColumn(options, position) + line 882 +
+
+ Replaces a column at a specific position with the newly provided formatting + + +

Parameters:

+
    +
  • options + table + table of options suitable for a TextFormatter object. See https://github.com/demonnic/fText/wiki/fText +
  • +
  • position + number + which column you are replacing, counting from the left. +
  • +
+ + + + + +
+
+ + TableMaker:getRow(position) + line 904 +
+
+ Gets the row of output at a specific position + + +

Parameters:

+
    +
  • position + number + The position of the row you want to get, coutning from the top down. If not provided defaults to the last row in the table +
  • +
+ +

Returns:

+
    + + table of entries in the specified row +
+ + + + +
+
+ + TableMaker:addRow(columnEntries, position) + line 913 +
+
+ Adds a row of output to the table + + +

Parameters:

+
    +
  • columnEntries + table + This indexed table contains an entry for each column in the table. Entries in the table must be strings, a table of options for insertPopup or insertLink, or a function which returns one of these things +
  • +
  • position + number + position for the row you want to add, counting from the top down. If not provided defaults to the last line in the table. +
  • +
+ + + + + +
+
+ + TableMaker:deleteRow(position) + line 936 +
+
+ Deletes the row at the given position + + +

Parameters:

+
    +
  • position + number + the row to delete +
  • +
+ + + + + +
+
+ + TableMaker:replaceRow(columnEntries, position) + line 951 +
+
+ Replaces a row of output in the table + + +

Parameters:

+
    +
  • columnEntries + table + This indexed table contains an entry for each column in the table. Entries in the table must be strings, a table of options for insertPopup or insertLink, or a function which returns one of these things +
  • +
  • position + number + position for the row you want to add, counting from the top down. +
  • +
+ + + + + +
+
+ + TableMaker:getCell(row, column) + line 1006 +
+
+ Get the contents and formatter for a specific cell + + +

Parameters:

+
    +
  • row + number + the row number of the cell, counted top down. +
  • +
  • column + number + the column number of the cell, counted from the left. +
  • +
+ +

Returns:

+
    + + the base text and TextFormatter for the cell at the specific row and column number +
+ + + + +
+
+ + TableMaker:setCell(row, column, entry) + line 1038 +
+
+ Sets a specific cell's display information + + +

Parameters:

+
    +
  • row + number + the row number of the cell, counted from the top down +
  • +
  • column + number + the column number of the cell, counted from the left +
  • +
  • entry + What to set the entry to. Must be a string, or a table of options for insertLink/insertPopup if allowPopups is set. Or a function which returns one of these things +
  • +
+ + + + + +
+
+ + TableMaker:setTitle(title) + line 1282 +
+
+ set the title of the table + + +

Parameters:

+
    +
  • title + string + The title of the table. +
  • +
+ + + + + +
+
+ + TableMaker:setRowSeparator(char) + line 1289 +
+
+ set the rowSeparator for the table + + +

Parameters:

+
    +
  • char + string + The row separator to use +
  • +
+ + + + + +
+
+ + TableMaker:setEdgeCharacter(char) + line 1296 +
+
+ set the edgeCharacter for the table + + +

Parameters:

+
    +
  • char + string + The edge character to use +
  • +
+ + + + + +
+
+ + TableMaker:setFootCharacter(char) + line 1303 +
+
+ set the foot character for the table + + +

Parameters:

+
    +
  • char + string + The foot character to use +
  • +
+ + + + + +
+
+ + TableMaker:setHeadCharacter(char) + line 1310 +
+
+ set the head character for the table + + +

Parameters:

+
    +
  • char + string + The head character to use +
  • +
+ + + + + +
+
+ + TableMaker:setSeparator(char) + line 1317 +
+
+ set the column separator character for the table + + +

Parameters:

+
    +
  • char + string + The separator character to use +
  • +
+ + + + + +
+
+ + TableMaker:setTitleColor(color) + line 1324 +
+
+ set the title color for the table + + +

Parameters:

+
    +
  • color + string + The title color to use. Should match the color type of the tablemaker (cecho by default) +
  • +
+ + + + + +
+
+ + TableMaker:setSeparatorColor(color) + line 1331 +
+
+ set the title color for the table + + +

Parameters:

+
    +
  • color + string + The separator color to use. Should match the color type of the tablemaker (cecho by default) +
  • +
+ + + + + +
+
+ + TableMaker:setFrameColor(color) + line 1338 +
+
+ set the title color for the table + + +

Parameters:

+
    +
  • color + string + The frame color to use. Should match the color type of the tablemaker (cecho by default) +
  • +
+ + + + + +
+
+ + TableMaker:enableForceHeaderSeparator() + line 1344 +
+
+ Force a separator between the header and first row, even if the row separator is disabled for the overall table + + + + + + + +
+
+ + TableMaker:disableForceHeaderSeparator() + line 1350 +
+
+ Do not force a separator between the header and first row, even if the row separator is disabled for the overall table + + + + + + + +
+
+ + TableMaker:enableHeaderTitle() + line 1356 +
+
+ Enable using the title separator for the column headers as well + + + + + + + +
+
+ + TableMaker:disableHeaderTitle() + line 1362 +
+
+ Disable using the title separator for the column headers as well + + + + + + + +
+
+ + TableMaker:enablePrintTitle() + line 1368 +
+
+ enable printing the title of the table + + + + + + + +
+
+ + TableMaker:disablePrintTitle() + line 1374 +
+
+ disable printing the title of the table + + + + + + + +
+
+ + TableMaker:enablePrintHeaders() + line 1380 +
+
+ enable printing of the column headers + + + + + + + +
+
+ + TableMaker:disablePrintHeaders() + line 1386 +
+
+ disable printing of the column headers + + + + + + + +
+
+ + TableMaker:enableRowSeparator() + line 1392 +
+
+ enable printing the separator line between rows + + + + + + + +
+
+ + TableMaker:disableRowSeparator() + line 1398 +
+
+ enable printing the separator line between rows + + + + + + + +
+
+ + TableMaker:enablePopups() + line 1404 +
+
+ enables making cells which incorporate insertLink/insertPopup + + + + + + + +
+
+ + TableMaker:enableAutoEcho() + line 1411 +
+
+ enables autoEcho so that when assemble is called it echos automatically + + + + + + + +
+
+ + TableMaker:disableAutoEcho() + line 1417 +
+
+ disables autoecho. Cannot be used if allowPopups is set + + + + + + + +
+
+ + TableMaker:enableAutoClear() + line 1426 +
+
+ Enables automatically clearing the miniconsole we echo to + + + + + + + +
+
+ + TableMaker:disableAutoClear() + line 1432 +
+
+ Disables automatically clearing the miniconsole we echo to + + + + + + + +
+
+ + TableMaker:setAutoEchoConsole(console) + line 1438 +
+
+ Set the miniconsole to echo to + + +

Parameters:

+
    +
  • console + The miniconsole to autoecho to. Set to "main" or do not pass the parameter to autoecho to the main console. Can be a string name of the console, or a Geyser MiniConsole +
  • +
+ + + + + +
+
+ + TableMaker:assemble() + line 1455 +
+
+ Assemble the table. If autoEcho is enabled/set to true, will automatically echo. Otherwise, returns the formatted string to echo the table + + + + + + + +
+
+ + TableMaker:new(options) + line 1619 +
+
+ Creates and returns a new TableMaker. + see https://github.com/demonnic/MDK/wiki/fText%3A-TableMaker%3A-Examples for usage + + +

Parameters:

+
    +
  • options + table + table of options for the TableMaker object +

    Table of new options + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    option namedescriptiondefault
    formatTypeDetermines how it formats for color. 'c' for cecho, 'd' for decho, 'h' for hecho, and anything else for no colorsc
    printHeadersprint top row as headertrue
    headCharacterThe character used to construct the very top of the table. A solid line of these characters is used"*"
    footCharacterThe character used to construct the very bottom of the table. A solid line of these characters is used"*"
    edgeCharacterthe character used to form the left and right edges of the table. There is one on either side of every line"*"
    frameColorThe color to use for the frame. The frame is the border around the outside edge of the table (headCharacter, footCharacter, and edgeCharacters will all be this color).the correct 'white' for your formatType
    rowSeparatorthe character used to form the lines between rows of text"-"
    separatorCharacter used between columns."|"
    separatorColorthe color used for the separators, the things which divide the rows and columns internally. (separator and rowSeparator will be this color)frameColor
    autoEchoecho the table automatically in addition to returning the string representation?false
    autoEchoConsoleMiniConsole to autoEcho to"main"
    autoClearIf autoEchoing, and not echoing to the main console, should we clear the console before outputting?false
    allowPopupssetting this to true allows you to make cells in the table clickable, as well as give them right-click menus.
    + Please see Clickable Tables HERE
    false
    separateRowsWhen false, will not print the separator line between rowstrue
    titleThe overall title of the table. Displayed at the top""
    titleColorWhat color to use for the title textformatColor
    printTitleShould we print the title of the table at the very tip-top?false
    headerTitleUse the same separator for the column headers as the title/top, rather than the row separatorformatColor
    forceHeaderSeparatorForce a separator between the column headers and the first row, even if rowSeparator is false.false
    +
  • +
+ + + + + +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/source/LICENSE.lua.html b/src/resources/MDK/doc/source/LICENSE.lua.html new file mode 100755 index 0000000..2f74e29 --- /dev/null +++ b/src/resources/MDK/doc/source/LICENSE.lua.html @@ -0,0 +1,147 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

LICENSE.lua

+
+--[===[
+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.
+]]
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/source/aliasmgr.lua.html b/src/resources/MDK/doc/source/aliasmgr.lua.html new file mode 100755 index 0000000..393acca --- /dev/null +++ b/src/resources/MDK/doc/source/aliasmgr.lua.html @@ -0,0 +1,263 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

aliasmgr.lua

+
+--- Alias Manager
+-- @classmod aliasmgr
+-- @author Damian Monogue <demonnic@gmail.com>
+-- @copyright 2022 Damian Monogue
+-- @license MIT, see LICENSE.lua
+local aliasmgr = {}
+aliasmgr.__index = aliasmgr
+
+--- Creates a new alias manager
+function aliasmgr:new()
+  local mgr = {
+    aliases = {}
+  }
+  setmetatable(mgr, self)
+  return mgr
+end
+
+local function argError(funcName, argument, expected, actual)
+  local msg = string.format("%s: %s as %s expected, got %s", funcName, argument, expected, actual)
+  printError(msg, true, true)
+end
+
+--- Registers an alias with the alias manager
+-- @param name the name for the alias
+-- @param regex the regular expression the alias matches against
+-- @param func The code to run when the alias matches. Can wrap code in [[ ]] or pass an actual function
+function aliasmgr:register(name, regex, func)
+  local funcName = "aliasmgr:register(name, regex, func)"
+  if func == nil then
+    printError(f"{funcName} takes 3 arguments and you have provided less than that", true, true)
+  end
+  local nameType = type(name)
+  if nameType ~= "string" then
+    argError(funcName, "name", "string", nameType)
+  end
+  local regexType = type(regex)
+  if regexType ~= "string" then
+    argError(funcName, "regex", "string", regexType)
+  end
+  local funcType = type(func)
+  if funcType ~= "string" and funcType ~= "function" then
+    argError(funcName, "func", "string or function", funcType)
+  end
+  local object = {
+    regex = regex,
+    func = func
+  }
+  self:kill(name)
+  local ok, err = pcall(tempAlias, regex, func)
+  if not ok then
+    return nil, err
+  end
+  object.handlerID = err
+  self.aliases[name] = object
+  return true
+end
+
+--- Registers an alias with the alias manager. Alias for register
+-- @param name the name for the alias
+-- @param regex the regular expression the alias matches against
+-- @param func The code to run when the alias matches. Can wrap code in [[ ]] or pass an actual function
+-- @see register
+function aliasmgr:add(name, regex, func)
+  self:register(name, regex, func)
+end
+
+--- Disables an alias, but does not delete it so it can be enabled later without being redefined
+-- @param name the name of the alias to disable
+-- @return true if the alias exists and gets disabled, false if it does not exist or is already disabled
+function aliasmgr:disable(name)
+  local funcName = "aliasmgr:disable(name)"
+  local nameType = type(name)
+  if nameType ~= "string" then
+    argError(funcName, "name", "string", nameType)
+  end
+  local object = self.aliases[name]
+  if not object or object.handlerID == -1 then
+    return false
+  end
+  killAlias(object.handlerID)
+  object.handlerID = -1
+  return true
+end
+
+--- Disables all aliases registered with the manager
+function aliasmgr:disableAll()
+  local aliases = self.aliases
+  for name, object in pairs(aliases) do
+    self:disable(name)
+  end
+end
+
+--- Enables an alias by name
+-- @param name the name of the alias to enable
+-- @return true if the alias exists and was enabled, false if it does not exist.
+function aliasmgr:enable(name)
+  local funcName = "aliasmgr:enable(name)"
+  local nameType = type(name)
+  if nameType ~= "string" then
+    argError(funcName, "name", "string", nameType)
+  end
+  local object = self.aliases[name]
+  if not object then
+    return false
+  end
+  self:register(name, object.regex, object.func)
+end
+
+--- Enables all aliases registered with the manager
+function aliasmgr:enableAll()
+  local aliases = self.aliases
+  for name,_ in pairs(aliases) do
+    self:enable(name)
+  end
+  return true
+end
+
+--- Kill an alias, deleting it from the manager
+-- @param name the name of the alias to kill
+-- @return true if the alias exists and gets deleted, false if the alias does not exist
+function aliasmgr:kill(name)
+  local funcName = "aliasmgr:kill(name)"
+  local nameType = type(name)
+  if nameType ~= "string" then
+    argError(funcName, "name", "string", nameType)
+  end
+  local object = self.aliases[name]
+  if not object then
+    return false
+  end
+  self:disable(name)
+  self.aliases[name] = nil
+  return true
+end
+
+--- Kills all aliases registered with the manager, clearing it out
+function aliasmgr:killAll()
+  local aliases = self.aliases
+  for name, _ in pairs(aliases) do
+    self:kill(name)
+  end
+end
+
+--- Kills an alias, deleting it from the manager
+-- @param name the name of the alias to delete
+-- @return true if the alias exists and gets deleted, false if the alias does not exist
+-- @see kill
+function aliasmgr:delete(name)
+  return self:kill(name)
+end
+
+--- Kills all aliases, deleting them from the manager
+-- @see killAll
+function aliasmgr:deleteAll()
+  return self:killAll()
+end
+
+--- Returns the list of aliases and the information being tracked for them
+-- @return the table of alias information, with names as keys and a table of information as the values.
+function aliasmgr:getAliases()
+  return self.aliases
+end
+
+return aliasmgr
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/source/chyron.lua.html b/src/resources/MDK/doc/source/chyron.lua.html new file mode 100755 index 0000000..03b74c9 --- /dev/null +++ b/src/resources/MDK/doc/source/chyron.lua.html @@ -0,0 +1,334 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

chyron.lua

+
+--- Creates a label with a scrolling text element. It is highly recommended you use a monospace font for this label.
+-- @classmod Chyron
+-- @author Delra
+-- @copyright 2019
+-- @author Damian Monogue
+-- @copyright 2020
+local Chyron = {
+  name = "ChyronClass",
+  text = "",
+  displayWidth = 28,
+  updateTime = 200,
+  font = "Bitstream Vera Sans Mono",
+  fontSize = "9",
+  autoWidth = true,
+  delimiter = "|",
+  pos = 1,
+  enabled = true,
+  alignment = "center",
+}
+
+--- Creates a new Chyron label
+-- @tparam table cons table of constraints which configures the EMCO.
+-- <table class="tg">
+-- <thead>
+--   <tr>
+--     <th>option name</th>
+--     <th>description</th>
+--     <th>default</th>
+--   </tr>
+-- </thead>
+-- <tbody>
+--   <tr>
+--     <td class="tg-1">text</td>
+--     <td class="tg-1">The text to scroll on the label</td>
+--     <td class="tg-1">""</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">updateTime</td>
+--     <td class="tg-2">Milliseconds between movements (one letter shift)</td>
+--     <td class="tg-2">200</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">displayWidth</td>
+--     <td class="tg-1">How many chars wide to display the text</td>
+--     <td class="tg-1">28</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">delimiter</td>
+--     <td class="tg-2">This character will be inserted with a space either side to mark the stop/start of the message</td>
+--     <td class="tg-2">"|"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">enabled</td>
+--     <td class="tg-1">Should the chyron scroll?</td>
+--     <td class="tg-1">true</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">font</td>
+--     <td class="tg-2">What font to use for the Chyron? Available in Geyser.Label but we define a default.</td>
+--     <td class="tg-2">"Bitstream Vera Sans Mono"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">fontSize</td>
+--     <td class="tg-1">What font size to use for the Chyron? Available in Geyser.Label but we define a default.</td>
+--     <td class="tg-1">9</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">autoWidth</td>
+--     <td class="tg-2">Should the Chyron resize to just fit the text?</td>
+--     <td class="tg-2">true</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">alignment</td>
+--     <td class="tg-1">What alignment(left/right/center) to use for the Chyron text? Available in Geyser.Label but we define a default.</td>
+--     <td class="tg-1">"center"</td>
+--   </tr>
+-- </tbody>
+-- </table>
+-- @tparam GeyserObject container The container to use as the parent for the Chyron
+function Chyron:new(cons, container)
+  cons = cons or {}
+  cons.type = cons.type or "Chyron"
+  local me = self.parent:new(cons, container)
+  setmetatable(me, self)
+  self.__index = self
+  me.pos = 0
+  me:setDisplayWidth(me.displayWidth)
+  me:setMessage(me.text)
+  if me.enabled then
+    me:start()
+  else
+    me:stop()
+  end
+  return me
+end
+
+--- Sets the numver of characters of the text to display at once
+-- @tparam number displayWidth number of characters to show at once
+function Chyron:setDisplayWidth(displayWidth)
+  displayWidth = displayWidth or self.displayWidth
+  self.displayWidth = displayWidth
+  if self.autoWidth then
+    local width = calcFontSize(self.fontSize, self.font)
+    self:resize(width * (displayWidth + 2), self.height)
+  end
+  if not self.enabled then
+    self.pos = self.pos - 1
+    self:doScroll()
+  end
+end
+
+--- Override setFontSize to call setDisplayWidth in order to resize if necessary
+-- @local
+function Chyron:setFontSize(fontSize)
+  Geyser.Label.setFontSize(self, fontSize)
+  self:setDisplayWidth()
+end
+
+--- Override setFont to call setDisplayWidth in order to resize if necessary
+-- @local
+function Chyron:setFont(font)
+  Geyser.Label.setFont(self, font)
+  self:setDisplayWidth()
+end
+
+--- Returns the proper section of text
+-- @local
+-- @param start number the character to start at
+-- @param length number the length of the text you want to extract
+function Chyron:scrollText(start, length)
+  local t = self.textTable
+  local s = ''
+  local e = start + length
+  for i = start - 1, e - 2 do
+    local n = (i % #t) + 1
+    s = s .. t[n]
+  end
+  return s
+end
+
+--- scroll the text
+-- @local
+function Chyron:doScroll()
+  self.pos = self.pos + 1
+  local displayString = self:scrollText(self.pos, self.displayWidth)
+  self:echo('&lt;' .. displayString .. '&gt;')
+  self.message = self.text
+end
+
+--- Sets the Chyron from the first position, without changing enabled status
+function Chyron:reset()
+  self.pos = 0
+  if not self.enabled then
+    self:doScroll()
+  end
+end
+
+--- Stops the Chyron with its current display
+function Chyron:pause()
+  self.enabled = false
+  if self.timer then
+    killTimer(self.timer)
+  end
+end
+
+--- Start the Chyron back up from wherever it currently is
+function Chyron:start()
+  self.enabled = true
+  if self.timer then
+    killTimer(self.timer)
+  end
+  self.timer = tempTimer(self.updateTime / 1000, function()
+    self:doScroll()
+  end, true)
+end
+
+--- Change the update time for the Chyron
+-- @param updateTime number new updateTime in milliseconds
+function Chyron:setUpdateTime(updateTime)
+  self.updateTime = updateTime or self.updateTime
+  if self.timer then
+    killTimer(self.timer)
+  end
+  if self.enabled then
+    self:start()
+  end
+end
+
+--- Enable autoWidth adjustment
+function Chyron:enableAutoWidth()
+  self.autoWidth = true
+  self:setDisplayWidth()
+end
+
+--- Disable autoWidth adjustment
+function Chyron:disableAutoWidth()
+  self.autoWidth = false
+end
+
+--- Stop the Chyron, and reset it to the original position
+function Chyron:stop()
+  if self.timer then
+    killTimer(self.timer)
+  end
+  self.enabled = false
+  self.pos = 0
+  self:doScroll()
+end
+
+--- Change the text being scrolled on the Chyron
+-- @param message string message the text you want to have scroll on the Chyron
+function Chyron:setMessage(message)
+  self.text = message
+  self.pos = 0
+  message = string.format("%s %s ", message, self.delimiter)
+  local t = {}
+  for i = 1, #message do
+    t[i] = message:sub(i, i)
+  end
+  self.textTable = t
+  if not self.enabled then
+    self:doScroll()
+  end
+end
+
+--- Change the delimiter used to show the beginning and end of the message
+-- @param delimiter string the new delimiter to use. I recommend using one character.
+function Chyron:setDelimiter(delimiter)
+  self.delimiter = delimiter
+end
+
+Chyron.parent = Geyser.Label
+setmetatable(Chyron, Geyser.Label)
+
+return Chyron
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/source/demontools.lua.html b/src/resources/MDK/doc/source/demontools.lua.html new file mode 100755 index 0000000..47d8365 --- /dev/null +++ b/src/resources/MDK/doc/source/demontools.lua.html @@ -0,0 +1,1585 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

demontools.lua

+
+--- Collection of miscellaneous functions and tools which don't necessarily warrant their own module/class
+-- @module demontools
+-- @author Damian Monogue <demonnic@gmail.com>
+-- @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([[\$]], "")
+  path = path:gsub([[/$]], "")
+  return io.exists(path)
+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 = [=[  <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+"http://www.w3.org/TR/html4/loose.dtd">
+<html>
+  <head>
+    <meta http-equiv="Content-Type" content="text/html;charset=utf-8" >
+    <link href='http://fonts.googleapis.com/css?family=Droid+Sans+Mono' rel='stylesheet' type='text/css'>
+    <style type="text/css">
+      body {
+        background-color: black;
+        font-family: 'Droid Sans Mono';
+        white-space: pre;
+        font-size: 12px;
+      }
+    </style>
+  </head>
+<body><span>
+]=]
+
+local htmlHeaderPattern = [=[  <!DOCTYPE HTML PUBLIC "%-//W3C//DTD HTML 4.01 Transitional//EN"
+"http://www.w3.org/TR/html4/loose.dtd">
+<html>
+  <head>
+    <meta http%-equiv="Content%-Type" content="text/html;charset=utf%-8" >
+    <link href='http://fonts.googleapis.com/css%?family=Droid%+Sans%+Mono' rel='stylesheet' type='text/css'>
+    <style type="text/css">
+      body {
+        background%-color: black;
+        font%-family: 'Droid Sans Mono';
+        white%-space: pre;
+        font%-size: 12px;
+      }
+    </style>
+  </head>
+<body><span>
+]=]
+
+-- Internal function, used to turn a string variable name into a value
+local function getValueAt(accessString)
+  local ok, err = pcall(loadstring("return " .. tostring(accessString)))
+  if ok then return err end
+  return nil, err
+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<reset> ', color.name, 'black', color.name)
+  else
+    colorString = string.format('<%s:%s> %-23s<reset> ', 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 = "<r>", h = "#r", c = "<reset>", 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 .. "<r>"
+      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("<span style='color: rgb%((%d+,%d+,%d+)%);background: rgb%((%d+,%d+,%d+)%);'>", "<%1:%2>")
+  text = text:gsub("<br>", "\n")
+  text = text:gsub("</span>", "")
+  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] = "</span></body></html>"
+  end
+  moveCursor(current_x, current_y)
+  return table.concat(lines, "\n")
+end
+
+local function decho2html(text)
+  cheatConsole:clear()
+  text = text:gsub("\n", "<br>")
+  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 "<ansiRed>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("<green>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("<green>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("<green>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 "<ansiRed:ansi_blue>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 "<ansiRed:ansi_blue>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.
+-- <table class="tg">
+-- <thead>
+--   <tr>
+--     <th>option name</th>
+--     <th>description</th>
+--     <th>default</th>
+--   </tr>
+-- </thead>
+-- <tbody>
+--   <tr>
+--     <td class="tg-1">format</td>
+--     <td class="tg-1">What format to return the text as? 'h' for html, 'c' for cecho, 'a' for ansi, 'd' for decho, and 'x' for hecho</td>
+--     <td class="tg-1">"d"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">win</td>
+--     <td class="tg-2">what console/window to dump the buffer of?</td>
+--     <td class="tg-2">"main"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">start_line</td>
+--     <td class="tg-1">What line to start dumping the buffer from?</td>
+--     <td class="tg-1">0</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">end_line</td>
+--     <td class="tg-2">What line to stop dumping the buffer on?</td>
+--     <td class="tg-2">Last line of the console</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">includeHtmlWrapper</td>
+--     <td class="tg-1">If the format is html, should it include the front and back portions required to make it a functioning html page?</td>
+--     <td class="tg-1">true</td>
+--   </tr>
+-- </tbody>
+-- </table>
+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
+-- <table class="tg">
+-- <thead>
+--   <tr>
+--     <th>option name</th>
+--     <th>description</th>
+--     <th>default</th>
+--   </tr>
+-- </thead>
+-- <tbody>
+--   <tr>
+--     <td class="tg-1">cols</td>
+--     <td class="tg-1">Number of columsn wide to display the colors in</td>
+--     <td class="tg-1">4</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">search</td>
+--     <td class="tg-2">If not the empty string, will check colors against string.find using this property.<br>IE if set to "blue" only colors which include the word 'blue' would be listed</td>
+--     <td class="tg-2">""</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">sort</td>
+--     <td class="tg-1">If true, sorts alphabetically. Otherwise sorts based on the color value</td>
+--     <td class="tg-1">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">echoOnly</td>
+--     <td class="tg-2">If true, colors will not be clickable links</td>
+--     <td class="tg-2">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">window</td>
+--     <td class="tg-1">What window/console to echo the colors out to.</td>
+--     <td class="tg-1">"main"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">removeDupes</td>
+--     <td class="tg-2">If true, will remove snake_case entries and 'gray' in favor of 'grey'</td>
+--     <td class="tg-2">true</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">columnSort</td>
+--     <td class="tg-1">If true, will print top-to-bottom, then left-to-right. false is like showColors</td>
+--     <td class="tg-1">true</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">justText</td>
+--     <td class="tg-2">If true, will echo the text in the color and leave the background black.<br>If false, the background will be the colour(like showColors).</td>
+--     <td class="tg-2">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">color_table</td>
+--     <td class="tg-1">Table of colors to display. If you provide your own table, it must be in the same format as Mudlet's own color_table</td>
+--     <td class="tg-1">color_table</td>
+--   </tr>
+-- </tbody>
+-- </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
+
+local echoOutputs = {
+  Color = {
+    ["\27reset"] = "<reset>",
+    ["\27bold"] = "<b>",
+    ["\27boldoff"] = "</b>",
+    ["\27italics"] = "<i>",
+    ["\27italicsoff"] = "</i>",
+    ["\27underline"] = "<u>",
+    ["\27underlineoff"] = "</u>",
+    ["\27strikethrough"] = "<s>",
+    ["\27strikethroughoff"] = "</s>",
+    ["\27overline"] = "<o>",
+    ["\27overlineoff"] = "</o>",
+  },
+  Decimal = {
+    ["\27reset"] = "<r>",
+    ["\27bold"] = "<b>",
+    ["\27boldoff"] = "</b>",
+    ["\27italics"] = "<i>",
+    ["\27italicsoff"] = "</i>",
+    ["\27underline"] = "<u>",
+    ["\27underlineoff"] = "</u>",
+    ["\27strikethrough"] = "<s>",
+    ["\27strikethroughoff"] = "</s>",
+    ["\27overline"] = "<o>",
+    ["\27overlineoff"] = "</o>",
+  },
+  Hex = {
+    ["\27reset"] = "#r",
+    ["\27bold"] = "#b",
+    ["\27boldoff"] = "#/b",
+    ["\27italics"] = "#i",
+    ["\27italicsoff"] = "#/i",
+    ["\27underline"] = "#u",
+    ["\27underlineoff"] = "#/u",
+    ["\27strikethrough"] = "#s",
+    ["\27strikethroughoff"] = "#/s",
+    ["\27overline"] = "#o",
+    ["\27overlineoff"] = "#/o",
+  }
+}
+
+local echoPatterns = _Echos.Patterns
+local echoProcess = _Echos.Process
+
+function DemonTools.toHTML(t, reset)
+  reset = reset or {
+    background = { 0, 0, 0 },
+    bold = false,
+    foreground = { 255, 255, 255 },
+    italic = false,
+    overline = false,
+    reverse = false,
+    strikeout = false,
+    underline = false
+  }
+  local format = table.deepcopy(reset)
+  local result = getHTMLformat(format)
+  for _,v in ipairs(t) do
+    local formatChanged = false
+    if type(v) == "table" then
+      if v.fg then
+        format.foreground = {v.fg[1], v.fg[2], v.fg[3]}
+        formatChanged = true
+      end
+      if v.bg then
+        format.background = {v.bg[1], v.bg[2], v.bg[3]}
+        formatChanged = true
+      end
+    elseif v == "\27bold" then
+      format.bold = true
+      formatChanged = true
+    elseif v == "\27boldoff" then
+      format.bold = false
+      formatChanged = true
+    elseif v == "\27italics" then
+      format.italic = true
+      formatChanged = true
+    elseif v == "\27italicsoff" then
+      format.italic = false
+      formatChanged = true
+    elseif v == "\27underline" then
+      format.underline = true
+      formatChanged = true
+    elseif v == "\27underlineoff" then
+      format.underline = false
+      formatChanged = true
+    elseif v == "\27strikethrough" then
+      format.strikeout = true
+      formatChanged = true
+    elseif v == "\27strikethroughoff" then
+      format.strikeout = false
+      formatChanged = true
+    elseif v == "\27overline" then
+      format.overline = true
+      formatChanged = true
+    elseif v == "\27overlineoff" then
+      format.overline = false
+      formatChanged = true
+    elseif v == "\27reset" then
+      format = table.deepcopy(reset)
+      formatChanged = true
+    end
+    v = formatChanged and getHTMLformat(format) or v
+    result = result .. v
+  end
+  return result
+end
+
+local function toEcho(colorType, colors)
+  colorType = colorType:lower()
+  local result
+  if colorType == "hex" then
+    local fg,bg = "", ""
+    if colors.fg then
+      fg = string.format("%02x%02x%02x", unpack(colors.fg))
+    end
+    if colors.bg then
+      bg = string.format(",%02x%02x%02x", unpack(colors.bg))
+    end
+    result = string.format("#%s%s", fg, bg)
+  elseif colorType == "color" then
+    local fg,bg = "",""
+    if colors.fg then
+      fg = closestColor(colors.fg)
+    end
+    if colors.bg then
+      bg = ":" .. closestColor(colors.bg[1], colors.bg[2], colors.bg[3])
+    end
+    result = string.format("<%s%s>", fg, bg)
+  elseif colorType == "decimal" then
+    local fg,bg = "", ""
+    if colors.fg then
+      fg = string.format("%d,%d,%d", unpack(colors.fg))
+    end
+    if colors.bg then
+      bg = string.format(":%d,%d,%d", unpack(colors.bg))
+    end
+    result = string.format("<%s%s>", fg, bg)
+  end
+  return result
+end
+
+function DemonTools.echoConverter(str, from, to, resetFormat)
+  local strType, fromType, toType, resetType = type(str), type(from), type(to), type(resetFormat)
+  local errTemplate = "bad argument #{argNum} type ({argName} as string expected, got {argType})"
+  local argNum, argName, argType
+  local err = false
+  if strType ~= "string" then
+    argNum = 1
+    argName = "str"
+    argType = strType
+    err = true
+  elseif fromType ~= "string" then
+    argNum = 2
+    argName = "from"
+    argType = fromType
+    err = true
+  elseif toType ~= "string" then
+    argNum = 3
+    argName = "to"
+    argType = toType
+    err = true
+  elseif resetFormat and resetType ~= "table" then
+    argType = resetType
+    errTemplate = "bad argument #4 type (optional resetFormat as table of formatting options expected, got {argType})"
+    err = true
+  end
+  if err then
+    printError(f(errTemplate), true, true)
+  end
+  from = from:title()
+  local t = echoProcess(str, from)
+  if not echoPatterns[from] then
+    local msg = "argument #4 (from) must be a valid echo type. Valid types are: " .. table.concat(table.keys(echoPatterns), ",")
+  end
+  local processed = echoProcess(str, from)
+  if to:lower() == "html" then
+    return DemonTools.toHTML(processed, resetFormat)
+  end
+  local outputs = echoOutputs[to]
+  if not outputs then
+    local msg = "argument #3 (to) must be a valid echo type. Valid types are: " .. table.concat(table.keys(echoOutputs), ",")
+    printError(msg, true, true)
+  end
+  local result = ""
+  for _, token in ipairs(processed) do
+    local formatter = outputs[token]
+    if formatter and token:find("\27") then
+      result = result .. formatter
+    elseif type(token) == "table" then
+      result = result .. toEcho(to, token)
+    else
+      result = result .. token
+    end
+  end
+  return result
+end
+
+return DemonTools
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/source/echofile.lua.html b/src/resources/MDK/doc/source/echofile.lua.html new file mode 100755 index 0000000..c9a17f1 --- /dev/null +++ b/src/resources/MDK/doc/source/echofile.lua.html @@ -0,0 +1,395 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

echofile.lua

+
+--- set of functions for echoing files to things. Uses a slightly hacked up version of f-strings for interpolation/templating
+-- @module echofile
+-- @author Damian Monogue <demonnic@gmail.com>
+-- @copyright 2021 Damian Monogue
+-- @copyright 2016 Hisham Muhammad (https://github.com/hishamhm/f-strings/blob/master/LICENSE)
+-- @license MIT, see LICENSE.lua
+local echofile = {}
+
+-- following functions fiddled with from https://github.com/hishamhm/f-strings/blob/master/F.lua and https://hisham.hm/2016/01/04/string-interpolation-in-lua/
+-- it seems to work :shrug:
+local load = load
+
+if _VERSION == "Lua 5.1" then
+  load = function(code, name, _, env)
+    local fn, err = loadstring(code, name)
+    if fn then
+      setfenv(fn, env)
+      return fn
+    end
+    return nil, err
+  end
+end
+
+local function f(str)
+  local outer_env = _ENV or getfenv(1)
+  return (str:gsub("%b{}", function(block)
+    local code = block:match("{(.*)}")
+    local exp_env = {}
+    setmetatable(exp_env, {
+      __index = function(_, k)
+        local stack_level = 5
+        while debug.getinfo(stack_level, "") ~= nil do
+          local i = 1
+          repeat
+            local name, value = debug.getlocal(stack_level, i)
+            if name == k then
+              return value
+            end
+            i = i + 1
+          until name == nil
+          stack_level = stack_level + 1
+        end
+        return rawget(outer_env, k)
+      end,
+    })
+    local fn, err = load("return " .. code, "expression `" .. code .. "`", "t", exp_env)
+    if fn then
+      return tostring(fn())
+    else
+      error(err, 0)
+    end
+  end))
+end
+
+local function xechoFile(options)
+  local filename = options.filename
+  local window = options.window
+  local func = options.func
+  local functionName = options.functionName
+  local fntype = type(filename)
+  if fntype ~= "string" then
+    return nil, f("{functionName}: filename as string expected, got {fnType}")
+  end
+  if not io.exists(filename) then
+    return nil, f("{functionName}: {filename} not found")
+  end
+  local file, err = io.open(filename, "r")
+  if not file then
+    return nil, err
+  end
+  local lines = file:read("*a")
+  if options.ansi then
+    lines = ansi2decho(lines)
+  end
+  if options.filter then
+    lines = f(lines)
+  end
+  return func(window, lines)
+end
+
+local function getOptions(etype, filter, window, filename)
+  if filename == nil then
+    filename = window
+    window = "main"
+  end
+  local ansi = false
+  if etype == "a" then
+    etype = 'd'
+    ansi = true
+  end
+  local options = {
+    filename = filename,
+    window = window,
+    func = _G[etype .. "echo"],
+    functionName = etype .. "echoFile([window,] filename)",
+    ansi = ansi,
+    filter = filter,
+  }
+  return options
+end
+
+--- Takes a string and performs interpolation
+--- Uses {} as the delimiter. Expressions will be evaluated
+---@param str string: The string to interpolate
+---@usage echofile = require("MDK.echofile")
+--- echofile.f("{1+1}") -- returns "2"
+--- local x = 4
+--- echofile.f"4+3 = {x+3}" -- returns "4+3 = 7"
+function echofile.f(str)
+  return f(str)
+end
+
+--- reads the contents of a file, converts it to decho and then dechos it
+---@param window string: Optional window to cecho to
+---@param filename string: Full path to file
+---@see echofile.f
+---@see echofile.cechoFile
+---@usage local ec = require("MDK.echofile")
+--- local cechoFile,f = ec.cechoFile, ec.f
+--- cechoFile("C:/path/to/file") -- windows1
+--- cechoFile("C:\\path\\to\\file") -- windows2
+--- cechoFile("/path/to/file") -- Linux/MacOS
+--- cechoFile("aMiniConsole", f"{getMudletHomeDir()}/myPkgName/helpfile") -- cecho a file from your pkg to a miniconsole
+function echofile.aechoFile(window, filename)
+  local options = getOptions("a", false, window, filename)
+  return xechoFile(options)
+end
+
+--- reads the contents of a file and then cechos it
+---@param window string: Optional window to cecho to
+---@param filename string: Full path to file
+---@see echofile.f
+---@see echofile.cechoFilef
+---@usage local ec = require("MDK.echofile")
+--- local cechoFile,f = ec.cechoFile, ec.f
+--- cechoFile("C:/path/to/file") -- windows1
+--- cechoFile("C:\\path\\to\\file") -- windows2
+--- cechoFile("/path/to/file") -- Linux/MacOS
+--- cechoFile("aMiniConsole", f"{getMudletHomeDir()}/myPkgName/helpfile") -- cecho a file from your pkg to a miniconsole
+function echofile.aechoFilef(window, filename)
+  local options = getOptions("a", true, window, filename)
+  return xechoFile(options)
+end
+
+--- reads the contents of a file and then cechos it
+---@param window string: Optional window to cecho to
+---@param filename string: Full path to file
+---@see echofile.f
+---@usage local ec = require("MDK.echofile")
+--- local cechoFile,f = ec.cechoFile, ec.f
+--- cechoFile("C:/path/to/file") -- windows1
+--- cechoFile("C:\\path\\to\\file") -- windows2
+--- cechoFile("/path/to/file") -- Linux/MacOS
+--- cechoFile("aMiniConsole", f"{getMudletHomeDir()}/myPkgName/helpfile") -- cecho a file from your pkg to a miniconsole
+function echofile.cechoFile(window, filename)
+  local options = getOptions("c", false, window, filename)
+  return xechoFile(options)
+end
+
+--- reads the contents of a file, interpolates it as per echofile.f and then cechos it
+---@param window string: Optional window to cecho to
+---@param filename string: Full path to file
+---@see echofile.f
+---@usage local ec = require("MDK.echofile")
+--- local cechoFile,f = ec.cechoFile, ec.f
+--- cechoFile("C:/path/to/file") -- windows1
+--- cechoFile("C:\\path\\to\\file") -- windows2
+--- cechoFile("/path/to/file") -- Linux/MacOS
+--- cechoFile("aMiniConsole", f"{getMudletHomeDir()}/myPkgName/helpfile") -- cecho a file from your pkg to a miniconsole
+function echofile.cechoFilef(window, filename)
+  local options = getOptions("c", true, window, filename)
+  return xechoFile(options)
+end
+
+--- reads the contents of a file and then dechos it
+---@param window string: Optional window to decho to
+---@param filename string: Full path to file
+---@see echofile.f
+---@see echofile.cechoFile
+function echofile.dechoFile(window, filename)
+  local options = getOptions("d", false, window, filename)
+  return xechoFile(options)
+end
+
+--- reads the contents of a file, interpolates it as per echofile.f and then dechos it
+---@param window string: Optional window to decho to
+---@param filename string: Full path to file
+---@see echofile.f
+---@see echofile.cechoFile
+function echofile.dechoFilef(window, filename)
+  local options = getOptions("d", true, window, filename)
+  return xechoFile(options)
+end
+
+--- reads the contents of a file and then hechos it
+---@param window string: Optional window to hecho to
+---@param filename string: Full path to file
+---@see echofile.f
+---@see echofile.cechoFile
+function echofile.hechoFile(window, filename)
+  local options = getOptions("h", false, window, filename)
+  return xechoFile(options)
+end
+
+--- reads the contents of a file, interpolates it as per echofile.f and then hechos it
+---@param window string: Optional window to hecho to
+---@param filename string: Full path to file
+---@see echofile.f
+---@see echofile.cechoFile
+function echofile.hechoFilef(window, filename)
+  local options = getOptions("h", true, window, filename)
+  return xechoFile(options)
+end
+
+--- reads the contents of a file, interpolates it as per echofile.f and then echos it
+---@param window string: Optional window to echo to
+---@param filename string: Full path to file
+---@see echofile.f
+---@see echofile.cechoFile
+function echofile.echoFile(window, filename)
+  local options = getOptions("", false, window, filename)
+  return xechoFile(options)
+end
+
+--- reads the contents of a file, interpolates it as per echofile.f and then echos it
+---@param window string: Optional window to echo to
+---@param filename string: Full path to file
+---@see echofile.f
+---@see echofile.cechoFile
+function echofile.echoFilef(window, filename)
+  local options = getOptions("", true, window, filename)
+  return xechoFile(options)
+end
+
+--- Adds c/d/h/echoFile functions to Geyser miniconsole and userwindow objects
+---@usage require("MDK.echofile").patchGeyser()
+--- myMC = Geyser.MiniConsole:new({name = "myMC"})
+--- myMC:cechoFile(f"{getMudletHomeDir()}/helpfile")
+function echofile.patchGeyser()
+  if Geyser.MiniConsole.echoFile then
+    return
+  end
+  function Geyser.MiniConsole:echoFile(filename)
+    return echofile.echoFile(self.name, filename)
+  end
+  function Geyser.MiniConsole:echoFilef(filename)
+    return echofile.echoFilef(self.name, filename)
+  end
+  function Geyser.MiniConsole:aechoFile(filename)
+    return echofile.aechoFile(self.name, filename)
+  end
+  function Geyser.MiniConsole:aechoFilef(filename)
+    return echofile.aechoFilef(self.name, filename)
+  end
+  function Geyser.MiniConsole:cechoFile(filename)
+    return echofile.cechoFile(self.name, filename)
+  end
+  function Geyser.MiniConsole:cechoFilef(filename)
+    return echofile.cechoFilef(self.name, filename)
+  end
+  function Geyser.MiniConsole:dechoFile(filename)
+    return echofile.dechoFile(self.name, filename)
+  end
+  function Geyser.MiniConsole:dechoFilef(filename)
+    return echofile.dechoFilef(self.name, filename)
+  end
+  function Geyser.MiniConsole:hechoFile(filename)
+    return echofile.hechoFile(self.name, filename)
+  end
+  function Geyser.MiniConsole:hechoFilef(filename)
+    return echofile.hechoFilef(self.name, filename)
+  end
+end
+
+--- Installs c/d/h/echoFile and f to the global namespace, and adds functions to Geyser
+---@usage require("MDK.echofile").installGlobal()
+--- f"{1+2}" -- returns "2"
+--- dechoFile(f"{getMudletHomeDir()}/fileWithDechoLines.txt")
+--- -- reads contents of fileWithDechoLines.txt from profile directory
+--- -- and dechos them to the main console
+function echofile.installGlobal()
+  _G.f = f
+  _G.echoFile = echofile.echoFile
+  _G.echoFilef = echofile.echoFilef
+  _G.aechoFile = echofile.aechoFile
+  _G.aechoFilef = echofile.aechoFilef
+  _G.cechoFile = echofile.cechoFile
+  _G.cechoFilef = echofile.cechoFilef
+  _G.dechoFile = echofile.dechoFile
+  _G.dechoFilef = echofile.dechoFilef
+  _G.hechoFile = echofile.hechoFile
+  _G.hechoFilef = echofile.hechoFilef
+  echofile.patchGeyser()
+end
+
+return echofile
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/source/emco.lua.html b/src/resources/MDK/doc/source/emco.lua.html new file mode 100755 index 0000000..d74c92e --- /dev/null +++ b/src/resources/MDK/doc/source/emco.lua.html @@ -0,0 +1,2452 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

emco.lua

+
+--- 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 <demonnic@gmail.com>
+-- @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 = {},
+  logFormat = "h",
+  gags = {},
+  notifyTabs = {},
+  notifyWithFocus = false,
+  cmdLineStyleSheet = [[
+    QPlainTextEdit {
+      border: 1px solid grey;
+    }
+  ]]
+})
+
+-- 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.
+-- <br>see https://github.com/demonnic/EMCO/wiki for information on valid constraints and defaults
+-- @tparam table cons table of constraints which configures the EMCO.
+-- <table class="tg">
+-- <thead>
+--   <tr>
+--     <th>option name</th>
+--     <th>description</th>
+--     <th>default</th>
+--   </tr>
+-- </thead>
+-- <tbody>
+--   <tr>
+--     <td class="tg-1">timestamp</td>
+--     <td class="tg-1">display timestamps on the miniconsoles?</td>
+--     <td class="tg-1">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">blankLine</td>
+--     <td class="tg-2">put a blank line between appends/echos?</td>
+--     <td class="tg-2">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">scrollbars</td>
+--     <td class="tg-1">enable scrollbars for the miniconsoles?</td>
+--     <td class="tg-1">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">customTimestampColor</td>
+--     <td class="tg-2">if showing timestamps, use a custom color?</td>
+--     <td class="tg-2">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">mapTab</td>
+--     <td class="tg-1">should we attach the Mudlet Mapper to this EMCO?</td>
+--     <td class="tg-1">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">mapTabName</td>
+--     <td class="tg-2">Which tab should we attach the map to?
+--                     <br>If mapTab is true and you do not set this, it will throw an error</td>
+--     <td class="tg-2"></td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">blinkFromAll</td>
+--     <td class="tg-1">should tabs still blink, even if you're on the 'all' tab?</td>
+--     <td class="tg-1">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">preserveBackground</td>
+--     <td class="tg-2">preserve the miniconsole background color during append()?</td>
+--     <td class="tg-2">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">gag</td>
+--     <td class="tg-1">when running :append(), should we also gag the line?</td>
+--     <td class="tg-1">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">timestampFormat</td>
+--     <td class="tg-2">Format string for the timestamp. Uses getTime()</td>
+--     <td class="tg-2">"HH:mm:ss"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">timestampBGColor</td>
+--     <td class="tg-1">Custom BG color to use for timestamps. Any valid Geyser.Color works.</td>
+--     <td class="tg-1">"blue"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">timestampFGColor</td>
+--     <td class="tg-2">Custom FG color to use for timestamps. Any valid Geyser.Color works</td>
+--     <td class="tg-2">"red"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">allTab</td>
+--     <td class="tg-1">Should we send everything to an 'all' tab?</td>
+--     <td class="tg-1">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">allTabName</td>
+--     <td class="tg-2">And which tab should we use for the 'all' tab?</td>
+--     <td class="tg-2">"All"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">blink</td>
+--     <td class="tg-1">Should we blink tabs that have been written to since you looked at them?</td>
+--     <td class="tg-1">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">blinkTime</td>
+--     <td class="tg-2">How long to wait between blinks, in seconds?</td>
+--     <td class="tg-2">3</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">fontSize</td>
+--     <td class="tg-1">What font size to use for the miniconsoles?</td>
+--     <td class="tg-1">9</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">font</td>
+--     <td class="tg-2">What font to use for the miniconsoles?</td>
+--     <td class="tg-2"></td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">tabFont</td>
+--     <td class="tg-1">What font to use for the tabs?</td>
+--     <td class="tg-1"></td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">activeTabCss</td>
+--     <td class="tg-2">What css to use for the active tab?</td>
+--     <td class="tg-2">""</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">inactiveTabCSS</td>
+--     <td class="tg-1">What css to use for the inactive tabs?</td>
+--     <td class="tg-1">""</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">activeTabFGColor</td>
+--     <td class="tg-2">What color to use for the text on the active tab. Any Geyser.Color works.</td>
+--     <td class="tg-2">"purple"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">inactiveTabFGColor</td>
+--     <td class="tg-1">What color to use for the text on the inactive tabs. Any Geyser.Color works.</td>
+--     <td class="tg-1">"white"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">activeTabBGColor</td>
+--     <td class="tg-2">What BG color to use for the active tab? Any Geyser.Color works. Overriden by activeTabCSS</td>
+--     <td class="tg-2">"<0,180,0>"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">inactiveTabBGColor</td>
+--     <td class="tg-1">What BG color to use for the inactavie tabs? Any Geyser.Color works. Overridden by inactiveTabCSS</td>
+--     <td class="tg-1">"<60,60,60>"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">consoleColor</td>
+--     <td class="tg-2">Default background color for the miniconsoles. Any Geyser.Color works</td>
+--     <td class="tg-2">"black"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">tabBoxCSS</td>
+--     <td class="tg-1">tss for the entire tabBox (not individual tabs)</td>
+--     <td class="tg-1">""</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">tabBoxColor</td>
+--     <td class="tg-2">What color to use for the tabBox? Any Geyser.Color works. Overridden by tabBoxCSS</td>
+--     <td class="tg-2">"black"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">consoleContainerCSS</td>
+--     <td class="tg-1">CSS to use for the container holding the miniconsoles</td>
+--     <td class="tg-1">""</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">consoleContainerColor</td>
+--     <td class="tg-2">Color to use for the container holding the miniconsole. Any Geyser.Color works. Overridden by consoleContainerCSS</td>
+--     <td class="tg-2">"black"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">gap</td>
+--     <td class="tg-1">How many pixels to place between the tabs and the miniconsoles?</td>
+--     <td class="tg-1">1</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">consoles</td>
+--     <td class="tg-2">List of the tabs for this EMCO in table format</td>
+--     <td class="tg-2">{ "All" }</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">allTabExclusions</td>
+--     <td class="tg-1">List of the tabs which should never echo to the 'all' tab in table format</td>
+--     <td class="tg-1">{}</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">tabHeight</td>
+--     <td class="tg-2">How many pixels high should the tabs be?</td>
+--     <td class="tg-2">25</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">autoWrap</td>
+--     <td class="tg-1">Use autoWrap for the miniconsoles?</td>
+--     <td class="tg-1">true</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">wrapAt</td>
+--     <td class="tg-2">How many characters to wrap it, if autoWrap is turned off?</td>
+--     <td class="tg-2">300</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">leftMargin</td>
+--     <td class="tg-1">Number of pixels to put between the left edge of the EMCO and the miniconsole?</td>
+--     <td class="tg-1">0</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">rightMargin</td>
+--     <td class="tg-2">Number of pixels to put between the right edge of the EMCO and the miniconsole?</td>
+--     <td class="tg-2">0</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">bottomMargin</td>
+--     <td class="tg-1">Number of pixels to put between the bottom edge of the EMCO and the miniconsole?</td>
+--     <td class="tg-1">0</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">topMargin</td>
+--     <td class="tg-2">Number of pixels to put between the top edge of the miniconsole container, and the miniconsole? This is in addition to gap</td>
+--     <td class="tg-2">0</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">timestampExceptions</td>
+--     <td class="tg-1">Table of tabnames which should not get timestamps even if timestamps are turned on</td>
+--     <td class="tg-1">{}</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">tabFontSize</td>
+--     <td class="tg-2">Font size for the tabs</td>
+--     <td class="tg-2">8</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">tabBold</td>
+--     <td class="tg-1">Should the tab text be bold? Boolean value</td>
+--     <td class="tg-1">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">tabItalics</td>
+--     <td class="tg-2">Should the tab text be italicized?</td>
+--     <td class="tg-2">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">tabUnderline</td>
+--     <td class="tg-1">Should the tab text be underlined?</td>
+--     <td class="tg-1">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">tabAlignment</td>
+--     <td class="tg-2">Valid 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)</td>
+--     <td class="tg-2">'c'</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">commandLine</td>
+--     <td class="tg-1">Should we enable commandlines for the miniconsoles?</td>
+--     <td class="tg-1">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">cmdActions</td>
+--     <td class="tg-2">A table with console names as keys, and values which are templates for the command to send. see the setCustomCommandline function for more</td>
+--     <td class="tg-2">{}</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">cmdLineStyleSheet</td>
+--     <td class="tg-1">What stylesheet to use for the command lines.</td>
+--     <td class="tg-1">"QPlainTextEdit {\n      border: 1px solid grey;\n    }\n"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">backgroundImages</td>
+--     <td class="tg-2">A 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,<br>and "mode" which determines how it is displayed. "border" stretches, "center" center, "tile" tiles, and "style". See Mudletwikilink for details.</td>
+--     <td class="tg-2">{}</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">bufferSize</td>
+--     <td class="tg-1">Number of lines of scrollback to keep for the miniconsoles</td>
+--     <td class="tg-1">100000</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">deleteLines</td>
+--     <td class="tg-2">Number of lines to delete if a console's buffer fills up.</td>
+--     <td class="tg-2">1000</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">gags</td>
+--     <td class="tg-1">A 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 <a href="http://lua-users.org/wiki/PatternsTutorial">this tutorial</a> on Lua patterns for more information.</td>
+--     <td class="tg-1">{}</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">notifyTabs</td>
+--     <td class="tg-2">Tables containing the names of all tabs you want to send notifications. IE {"Says", "Tells", "Org"}</td>
+--     <td class="tg-2">{}</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">notifyWithFocus</td>
+--     <td class="tg-1">If true, EMCO will send notifications even if Mudlet has focus. If false, it will only send them when Mudlet does NOT have focus.</td>
+--     <td class="tg-1">false</td>
+--   </tr>
+-- </tbody>
+-- </table>
+-- @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
+  for _,tname in ipairs(cons.notifyTabs or {}) do
+    me:addNotifyTab(tname)
+  end
+  if me:fuzzyBoolean(cons.notifyWithFocus) then
+    self:enableNotifyWithFocus()
+  end
+  me:reset()
+  if me.allTab then
+    me:setAllTabName(me.allTabName or me.consoles[1])
+  end
+  return me
+end
+
+function EMCO:readYATCO()
+  local config
+  if demonnic and demonnic.chat and demonnic.chat.config then
+    config = demonnic.chat.config
+  else
+    cecho("<white>(<blue>EMCO<white>)<reset> 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(
+    "<white>(<blue>EMCO<white>)<reset> 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 = [[
+  <white>(<blue>EMCO<white>)<reset> Found a YATCO config. Make a new script, then copy and paste the following output into it.
+  <white>(<blue>EMCO<white>)<reset> Afterward, uninstall YATCO (you can leave YATCOConfig until you're sure everything is right) and restart Mudlet
+  <white>(<blue>EMCO<white>)<reset> 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
+  if self.currentTab == tabName then
+    if self.allTab and self.allTabName then
+      self:switchTab(self.allTabName)
+    else
+      self:switchTab(self.consoles[1])
+    end
+  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
+  self.currentTab = tabName
+  if oldTab ~= tabName and oldTab ~= "" then
+    self.mc[oldTab]:hide()
+    self:adjustTabBackground(oldTab)
+    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:adjustTabBackground(tabName)
+  self.tabs[tabName]:echo(tabName, self.activeTabFGColor)
+  -- if oldTab and self.mc[oldTab] then
+  --   self.mc[oldTab]:hide()
+  -- end
+  self.mc[tabName]:show()
+  if oldTab ~= tabName then
+    raiseEvent("EMCO tab change", self.name, oldTab, tabName)
+  end
+end
+
+--- Cycles between the tabs in order
+-- @tparam boolean reverse Defaults to false. When true, moves backward through the tab list rather than forward.
+function EMCO:cycleTab(reverse)
+  -- add the property to demonnic.chat
+  local consoles = self.consoles
+  local cycleIndex = table.index_of(consoles, self.currentTab)
+
+  local maxIndex = #consoles
+  cycleIndex = reverse and cycleIndex - 1 or cycleIndex + 1
+  if cycleIndex > maxIndex then cycleIndex = 1 end
+  if cycleIndex < 1 then cycleIndex = maxIndex end
+  self:switchTab(consoles[cycleIndex])
+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:setAlignment(self.tabAlignment)
+  tab:setFontSize(self.tabFontSize)
+  tab:setItalics(self.tabItalics)
+  tab:setBold(self.tabBold)
+  tab:setUnderline(self.tabUnderline)
+  tab:setClickCallback(self.switchTab, self, tabName)
+  self.tabs[tabName] = tab
+  self:adjustTabBackground(tabName)
+  tab:echo(tabName, self.inactiveTabFGColor)
+  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,
+    cmdLineStyleSheet = self.cmdLineStyleSheet,
+    path = self:processTemplate(self.path, tabName),
+    fileName = self:processTemplate(self.fileName, tabName),
+    logFormat = self.logFormat
+  }
+  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, nil)
+  end
+  window:hide()
+  self:processImage(tabName)
+  self:switchTab(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)
+  local safeName = self.name:gsub("[<>:'\"?*]", "_")
+  local safeTabName = tabName and tabName:gsub("[<>:'\"?*]", "_") or ""
+  str = str:gsub("|E", safeName)
+  str = str:gsub("|N", safeTabName)
+  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 stylesheet for command lines in this EMCO
+-- @tparam string styleSheet the stylesheet to use for the command line. See https://wiki.mudlet.org/w/Manual:Lua_Functions#setCmdLineStyleSheet for examples
+function EMCO:setCmdLineStyleSheet(styleSheet)
+  self.cmdLineStyleSheet = styleSheet
+  if not styleSheet then
+    return
+  end
+  for _, window in pairs(self.mc) do
+    window:setCmdLineStyleSheet(styleSheet)
+  end
+end
+--- Enables the commandLine on the specified tab.
+-- @tparam string tabName the name of the tab to turn the commandLine on for
+-- @param template the template for the commandline to use, or the function to run when enter is hit.
+-- @usage myEMCO:enableCmdLine(tabName, template)
+function EMCO:enableCmdLine(tabName, template)
+  if not table.contains(self.consoles, tabName) then
+    return nil, f"{self.name}:enableCmdLine(tabName,template) tabName is not in the console list. Valid options are {table.concat(self.consoles, 'm')}"
+  end
+  local window = self.mc[tabName]
+  window:enableCommandLine()
+  if self.cmdLineStyleSheet then
+    window:setCmdLineStyleSheet(self.cmdLineStyleSheet)
+  end
+  self:setCmdAction(tabName, template)
+end
+
+--- Enables all command lines, using whatever template they may currently have set
+function EMCO:enableAllCmdLines()
+  for _, tabName in ipairs(self.consoles) do
+    self:enableCmdLine(tabName, self.cmdActions[tabName])
+  end
+end
+
+--- Disables all commands line, but does not change their template
+function EMCO:disableAllCmdLines()
+  for _, tabName in ipairs(self.consoles) do
+    self:disableCmdLine(tabName)
+  end
+end
+
+--- Disables the command line for a particular tab
+-- @tparam string tabName the name of the tab to disable the command line of.
+function EMCO:disableCmdLine(tabName)
+  if not table.contains(self.consoles, tabName) then
+    return nil, f"{self.name}:disableCmdLine(tabName,template) tabName is not in the console list. Valid options are {table.concat(self.consoles, 'm')}"
+  end
+  local window = self.mc[tabName]
+  window:disableCommandLine()
+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 = self.allTabName or self.consoles[1]
+  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), '[ThHmMszZaApPdy0-9%-%+:. ]', '')
+end
+
+--- Expands boolean definitions to be more flexible.
+-- <br>True values are "true", "yes", "0", 0, and true
+-- <br>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.
+-- <br>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.consoles, ","))
+  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.
+-- <br>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.
+-- <br>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.
+-- <br>Forces a redraw of the object
+-- <br>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:adjustTabBackground(console)
+  local tab = self.tabs[console]
+  local activeTabCSS = self.activeTabCSS
+  local inactiveTabCSS = self.inactiveTabCSS
+  local activeTabBGColor = self.activeTabBGColor
+  local inactiveTabBGColor = self.inactiveTabBGColor
+  if console == self.currentTab then
+    if activeTabCSS and activeTabCSS ~= "" then
+      tab:setStyleSheet(activeTabCSS)
+    elseif activeTabBGColor then
+      tab:setColor(activeTabBGColor)
+    end
+  else
+    if inactiveTabCSS and inactiveTabCSS ~= "" then
+      tab:setStyleSheet(inactiveTabCSS)
+    elseif inactiveTabBGColor then
+      tab:setColor(inactiveTabBGColor)
+    end
+  end
+end
+
+function EMCO:adjustTabBackgrounds()
+  for _, console in ipairs(self.consoles) do
+    self:adjustTabBackground(console)
+  end
+end
+
+--- Sets the inactiveTabCSS
+-- @tparam string stylesheet the stylesheet to use for inactive tabs.
+function EMCO:setInactiveTabCSS(stylesheet)
+  self.inactiveTabCSS = stylesheet
+  self:adjustTabBackgrounds()
+end
+
+--- Sets the activeTabCSS
+-- @tparam string stylesheet the stylesheet to use for active tab.
+function EMCO:setActiveTabCSS(stylesheet)
+  self.activeTabCSS = stylesheet
+  self:adjustTabBackgrounds()
+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.
+-- <br>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.
+-- <br>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.
+-- <br>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.
+-- <br>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.
+-- <br>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.
+-- <br>depending on this object's configuration, may gag the line
+-- <br>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 tab to the list of tabs to send OS toast/popup notifications for
+--@tparam string tabName the name of a tab to enable notifications for
+--@return true if it was added, false if it was already included, nil if the tab does not exist.
+function EMCO:addNotifyTab(tabName)
+  if not table.contains(self.consoles, tabName) then
+    return nil, "Tab does not exist"
+  end
+  if self.notifyTabs[tabName] then
+    return false
+  end
+  self.notifyTabs[tabName] = true
+  return true
+end
+
+--- Removes a tab from the list of tabs to send OS toast/popup notifications for
+--@tparam string tabName the name of a tab to disable notifications for
+--@return true if it was removed, false if it wasn't enabled to begin with, nil if the tab does not exist.
+function EMCO:removeNotifyTab(tabName)
+  if not table.contains(self.consoles, tabName) then
+    return nil, "Tab does not exist"
+  end
+  if not self.notifyTabs[tabName] then
+    return false
+  end
+  self.notifyTabs[tabName] = nil
+  return true
+end
+
+--- Adds a pattern to the gag list for the EMCO
+--@tparam string pattern 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 string pattern 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 string str 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
+
+--- Enables sending OS notifications even if Mudlet has focus
+function EMCO:enableNotifyWithFocus()
+  self.notifyWithFocus = true
+end
+
+--- Disables sending OS notifications if Mudlet has focus
+function EMCO:disableNotifyWithFocus()
+  self.notifyWithFocus = false
+end
+
+function EMCO:strip(message, xtype)
+  local strippers = {
+    a = function(msg) return msg end,
+    echo = function(msg) return msg end,
+    cecho = cecho2string,
+    decho = decho2string,
+    hecho = hecho2string,
+  }
+  local result = strippers[xtype](message)
+  return result
+end
+
+function EMCO:sendNotification(tabName, msg)
+  if self.notifyWithFocus or not hasFocus() then
+    if self.notifyTabs[tabName] then
+      showNotification(f'{self.name}:{tabName}', msg)
+    end
+  end
+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<r> ", 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()
+    local txt = self:strip(getCurrentLine(), xtype)
+    self:sendNotification(tabName, txt)
+    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)
+    self:sendNotification(tabName, self:strip(message, xtype))
+    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<r> ", 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
+
+--- 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,
+    timestampFGColor = self.timestampFGColor,
+    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,
+    notifyTabs = self.notifyTabs,
+    notifyWithFocus = self.notifyWithFocus,
+    cmdLineStyleSheet = self.cmdLineStyleSheet,
+  }
+  local dirname = getMudletHomeDir() .. "/EMCO/"
+  local filename = dirname .. self.name:gsub("[<>:'\"/\\|?*]", "_") .. ".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))
+    return
+  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.timestampFGColor = configTable.timestampFGColor
+  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.notifyTabs = configTable.notifyTabs
+  self.notifyWithFocus = configTable.notifyWithFocus
+  self.cmdLineStyleSheet = configTable.cmdLineStyleSheet
+  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
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/source/figlet.lua.html b/src/resources/MDK/doc/source/figlet.lua.html new file mode 100755 index 0000000..6cc06fb --- /dev/null +++ b/src/resources/MDK/doc/source/figlet.lua.html @@ -0,0 +1,366 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

figlet.lua

+
+--- Figlet
+-- A module to read figlet fonts and produce figlet ascii art from text
+-- @module figlet
+-- @copyright 2010,2011 Nick Gammon
+-- @copyright 2022 Damian Monogue
+local Figlet = {}
+
+--[[
+  Based on figlet.
+
+  FIGlet Copyright 1991, 1993, 1994 Glenn Chappell and Ian Chai
+  FIGlet Copyright 1996, 1997 John Cowan
+  Portions written by Paul Burton
+  Internet: <ianchai@usa.net>
+  FIGlet, along with the various FIGlet fonts and documentation, is
+    copyrighted under the provisions of the Artistic License (as listed
+    in the file "artistic.license" which is included in this package.
+
+--]]
+
+--[[
+   Latin-1 codes for German letters, respectively:
+     LATIN CAPITAL LETTER A WITH DIAERESIS = A-umlaut
+     LATIN CAPITAL LETTER O WITH DIAERESIS = O-umlaut
+     LATIN CAPITAL LETTER U WITH DIAERESIS = U-umlaut
+     LATIN SMALL LETTER A WITH DIAERESIS = a-umlaut
+     LATIN SMALL LETTER O WITH DIAERESIS = o-umlaut
+     LATIN SMALL LETTER U WITH DIAERESIS = u-umlaut
+     LATIN SMALL LETTER SHARP S = ess-zed
+--]]
+
+local deutsch = {196, 214, 220, 228, 246, 252, 223}
+local fcharlist = {}
+local magic, hardblank, charheight, maxlen, smush, cmtlines, ffright2left, smush2
+
+local function readfontchar(fontfile, theord)
+
+  local t = {}
+  fcharlist[theord] = t
+
+  -- read each character line
+
+  --[[
+
+  eg.
+
+  __  __ @
+ |  \/  |@
+ | \  / |@
+ | |\/| |@
+ | |  | |@
+ |_|  |_|@
+         @
+         @@
+--]]
+
+  for i = 1, charheight do
+    local line = assert(fontfile:read("*l"), "Not enough character lines for character " .. theord)
+    local line = string.gsub(line, "%s+$", "") -- remove trailing spaces
+    assert(line ~= "", "Unexpected empty line")
+
+    -- find the last character (eg. @)
+    local endchar = line:sub(-1) -- last character
+
+    -- trim one or more of the last character from the end
+    while line:sub(-1) == endchar do
+      line = line:sub(1, #line - 1)
+    end -- while line ends with endchar
+
+    table.insert(t, line)
+
+  end -- for each line
+
+end -- readfontchar
+
+--- Reads a figlet font file (.flf) into memory and readies it for use by the next figlet
+-- These files are cached in memory so that future calls to load a font just read from there.
+-- @param filename the full path to the file to read the font from
+function Figlet.readfont(filename)
+  local fontfile = assert(io.open(filename, "r"))
+  local s
+
+  fcharlist = {}
+
+  -- header line
+  s = assert(fontfile:read("*l"), "Empty FIGlet file")
+
+  -- eg.  flf2a$ 8 6          59     15     10        0             24463   153
+  --      magic  charheight  maxlen  smush  cmtlines  ffright2left  smush2  ??
+
+  -- configuration line
+  magic, hardblank, charheight, maxlen, smush, cmtlines, ffright2left, smush2 = string.match(s,
+                                                                                             "^(flf2).(.) (%d+) %d+ (%d+) (%-?%d+) (%d+) ?(%d*) ?(%d*) ?(%-?%d*)")
+
+  assert(magic, "Not a FIGlet 2 font file")
+
+  -- convert to numbers
+  charheight = tonumber(charheight)
+  maxlen = tonumber(maxlen)
+  smush = tonumber(smush)
+  cmtlines = tonumber(cmtlines)
+
+  -- sanity check
+  if charheight < 1 then
+    charheight = 1
+  end -- if
+
+  -- skip comment lines
+  for i = 1, cmtlines do
+    assert(fontfile:read("*l"), "Not enough comment lines")
+  end -- for
+
+  -- get characters space to tilde
+  for theord = string.byte(' '), string.byte('~') do
+    readfontchar(fontfile, theord)
+  end -- for
+
+  -- get 7 German characters
+  for theord = 1, 7 do
+    readfontchar(fontfile, deutsch[theord])
+  end -- for
+
+  -- get extra ones like:
+  -- 0x0395  GREEK CAPITAL LETTER EPSILON
+  -- 246  LATIN SMALL LETTER O WITH DIAERESIS
+
+  repeat
+    local extra = fontfile:read("*l")
+    if not extra then
+      break
+    end -- if eof
+
+    local negative, theord = string.match(extra, "^(%-?)0[xX](%x+)")
+    if theord then
+      theord = tonumber(theord, 16)
+      if negative == "-" then
+        theord = -theord
+      end -- if negative
+    else
+      theord = string.match(extra, "^%d+")
+      assert(theord, "Unexpected line:" .. extra)
+      theord = tonumber(theord)
+    end -- if
+
+    readfontchar(fontfile, theord)
+
+  until false
+
+  fontfile:close()
+
+  -- remove leading/trailing spaces
+
+  for k, v in pairs(fcharlist) do
+
+    -- first see if all lines have a leading space or a trailing space
+    local leading_space = true
+    local trailing_space = true
+    for _, line in ipairs(v) do
+      if line:sub(1, 1) ~= " " then
+        leading_space = false
+      end -- if
+      if line:sub(-1, -1) ~= " " then
+        trailing_space = false
+      end -- if
+    end -- for each line
+
+    -- now remove them if necessary
+    for i, line in ipairs(v) do
+      if leading_space then
+        v[i] = line:sub(2)
+      end -- removing leading space
+      if trailing_space then
+        v[i] = line:sub(1, -2)
+      end -- removing trailing space
+    end -- for each line
+  end -- for each character
+end -- readfont
+
+-- add one character to output lines
+local function addchar(which, output, kern, smush)
+  local c = fcharlist[string.byte(which)]
+  if not c then
+    return
+  end -- if doesn't exist
+
+  for i = 1, charheight do
+
+    if smush and output[i] ~= "" and which ~= " " then
+      local lhc = output[i]:sub(-1)
+      local rhc = c[i]:sub(1, 1)
+      output[i] = output[i]:sub(1, -2) -- remove last character
+      if rhc ~= " " then
+        output[i] = output[i] .. rhc
+      else
+        output[i] = output[i] .. lhc
+      end
+      output[i] = output[i] .. c[i]:sub(2)
+
+    else
+      output[i] = output[i] .. c[i]
+    end -- if
+
+    if not (kern or smush) or which == " " then
+      output[i] = output[i] .. " "
+    end -- if
+  end -- for
+
+end -- addchar
+
+--- Returns a table of lines representing a string as figlet
+-- @tparam string s the text to make into a figlet
+-- @tparam boolean kern should we reduce spacing
+-- @tparam boolean smush causes the letters to share edges, condensing it even further
+function Figlet.ascii_art(s, kern, smush)
+  assert(fcharlist)
+  assert(charheight > 0)
+
+  -- make table of output lines
+  local output = {}
+  for i = 1, charheight do
+    output[i] = ""
+  end -- for
+
+  for i = 1, #s do
+    local c = s:sub(i, i)
+
+    if c >= " " and c < "\127" then
+      addchar(c, output, kern, smush)
+    end -- if in range
+
+  end -- for
+
+  -- fix up blank character so we can do a string.gsub on it
+  local fixedblank = string.gsub(hardblank, "[%%%]%^%-$().[*+?]", "%%%1")
+
+  for i, line in ipairs(output) do
+    output[i] = string.gsub(line, fixedblank, " ")
+  end -- for
+
+  return output
+end -- function ascii_art
+
+--- Returns the figlet as a string, rather than a table
+-- @tparam string str the string the make into a figlet
+-- @tparam boolean kern should we reduce the space between letters?
+-- @tparam boolean smush should the letters share edges, further condensing the output?
+-- @see ascii_art
+function Figlet.getString(str, kern, smush)
+  local tbl = Figlet.ascii_art(str, kern, smush)
+  return table.concat(tbl, "\n")
+end
+
+--- Returns a figlet as a string, with kern set to true.
+-- @tparam string str The string to turn into a figlet
+-- @see getString
+function Figlet.getKern(str)
+  return Figlet.getString(str, true)
+end
+
+--- Returns a figlet as a string, with smush set to true.
+-- @tparam string str The string to turn into a figlet
+-- @see getString
+function Figlet.getSmush(str)
+  return Figlet.getString(str, true, true)
+end
+
+return Figlet
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/source/ftext.lua.html b/src/resources/MDK/doc/source/ftext.lua.html new file mode 100755 index 0000000..89e04d7 --- /dev/null +++ b/src/resources/MDK/doc/source/ftext.lua.html @@ -0,0 +1,1796 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

ftext.lua

+
+--- ftext
+-- functions to format and print text, and the objects which use them
+-- @module ftext
+-- @author Damian Monogue <demonnic@gmail.com>
+-- @copyright 2020 Damian Monogue
+-- @copyright 2021 Damian Monogue
+-- @copyright 2022 Damian Monogue
+-- @license MIT, see LICENSE.lua
+local ftext = {}
+local dec = {"d", "decimal", "dec"}
+local hex = {"h", "hexidecimal", "hex"}
+local col = {"c", "color", "colour", "col", "name"}
+
+--- Performs wordwrapping on a string, given a length limit. Does not understand colour tags and will count them as characters in the string
+-- @tparam string str the string to wordwrap
+-- @tparam number limit the line length to wrap at
+function ftext.wordWrap(str, limit, indent, indent1)
+  -- pulled from http://lua-users.org/wiki/StringRecipes
+  indent = indent or ""
+  indent1 = indent1 or indent
+  limit = limit or 72
+  local here = 1 - #indent1
+  local function check(sp, st, word, fi)
+    if fi - here > limit then
+      here = st - #indent
+      return "\n" .. indent .. word
+    end
+  end
+  return indent1 .. str:gsub("(%s+)()(%S+)()", check)
+end
+
+--- Performs wordwrapping on a string, while ignoring color tags of a given type.
+-- @tparam string text the string you are wordwrapping
+-- @tparam number limit the line length to wrap at
+-- @tparam string type What type of color codes to ignore. 'c' for cecho, 'd' for decho, 'h' for hecho, and anything else or nil to pass the string on to wordWrap
+function ftext.xwrap(text, limit, type)
+  local colorPattern
+  if table.contains(dec, type) then
+    colorPattern = _Echos.Patterns.Decimal[1]
+  elseif table.contains(hex, type) then
+    colorPattern = _Echos.Patterns.Hex[1]
+  elseif table.contains(col, type) then
+    colorPattern = _Echos.Patterns.Color[1]
+  else
+    return ftext.wordWrap(text, limit)
+  end
+  local strippedString = rex.gsub(text, colorPattern, "")
+  local strippedLines = ftext.wordWrap(strippedString, limit):split("\n")
+  local lineIndex = 1
+  local line = ""
+  local strLine = ""
+  local lines = {}
+  local strLines = {}
+  local workingLine = strippedLines[lineIndex]:split("")
+  local workingLineLength = #workingLine
+  local lineColumn = 0
+  for str, color, res in rex.split(text, colorPattern) do
+    if res then
+      if type == "Hex" then
+        color = "#r"
+      elseif type == "Dec" then
+        color = "<r>"
+      elseif type == "Color" then
+        color = "<reset>"
+      end
+    end
+    color = color or ""
+    local strLen = str:len()
+    if lineColumn + strLen <= workingLineLength then
+      strLine = strLine .. str
+      line = line .. str .. color
+      lineColumn = lineColumn + strLen
+    else
+      local neededChars = workingLineLength - lineColumn
+      local take = str:sub(1, neededChars)
+      local leave = str:sub(neededChars + 1, -1)
+      strLine = strLine .. take
+      line = line .. take
+      table.insert(lines, line)
+      table.insert(strLines, strLine)
+      line = ""
+      strLine = ""
+      lineIndex = lineIndex + 1
+      workingLine = strippedLines[lineIndex]:split("")
+      workingLineLength = #workingLine
+      lineColumn = 0
+      if leave:sub(1, 1) == " " then
+        leave = leave:sub(2, -1)
+      end
+      while leave ~= "" do
+        take = leave:sub(1, workingLineLength)
+        leave = leave:sub(workingLineLength + 1, -1)
+        if leave:sub(1, 1) == " " then
+          leave = leave:sub(2, -1)
+        end
+        if take:len() < workingLineLength then
+          lineColumn = take:len()
+          line = line .. take .. color
+          strLine = strLine .. take
+        else
+          lineIndex = lineIndex + 1
+          workingLine = strippedLines[lineIndex]
+          if workingLine then
+            workingLine = strippedLines[lineIndex]:split("")
+            workingLineLength = #workingLine
+          end
+          table.insert(lines, take)
+          table.insert(strLines, take)
+        end
+        if leave == "\n" then
+          table.insert(lines, leave)
+          table.insert(strLines, leave)
+          leave = ""
+        end
+      end
+    end
+  end
+  if line ~= "" then
+    table.insert(lines, line)
+  end
+  return table.concat(lines, "\n")
+end
+
+--- The main course, this function returns a formatted string, based on a table of options
+-- @tparam string str the string to format
+-- @tparam table opts the table of options which control the formatting
+-- <br><br>Table of options
+-- <table class="tg">
+-- <thead>
+--   <tr>
+--     <th>option name</th>
+--     <th>description</th>
+--     <th>default</th>
+--   </tr>
+-- </thead>
+-- <tbody>
+--   <tr>
+--     <td class="tg-1">wrap</td>
+--     <td class="tg-1">Should it wordwrap to multiple lines?</td>
+--     <td class="tg-1">true</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">formatType</td>
+--     <td class="tg-2">Determines how it formats for color. 'c' for cecho, 'd' for decho, 'h' for hecho, and anything else for no colors</td>
+--     <td class="tg-2">""</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">width</td>
+--     <td class="tg-1">How wide should we format the text?</td>
+--     <td class="tg-1">80</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">cap</td>
+--     <td class="tg-2">what characters to use for the endcap.</td>
+--     <td class="tg-2">""</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">capColor</td>
+--     <td class="tg-1">what color to make the endcap?</td>
+--     <td class="tg-1">the correct 'white' for your formatType</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">spacer</td>
+--     <td class="tg-2">What character to use for empty space. Must be a single character</td>
+--     <td class="tg-2">" "</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">spacerColor</td>
+--     <td class="tg-1">what color should the spacer be?</td>
+--     <td class="tg-1">the correct 'white' for your formatType</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">textColor</td>
+--     <td class="tg-2">what color should the text itself be?</td>
+--     <td class="tg-2">the correct 'white' for your formatType</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">alignment</td>
+--     <td class="tg-1">How should the text be aligned within the width. "center", "left", or "right"</td>
+--     <td class="tg-1">"center"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">nogap</td>
+--     <td class="tg-2">Should we put a literal space between the spacer character and the text?</td>
+--     <td class="tg-2">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">inside</td>
+--     <td class="tg-1">Put the spacers inside the caps?</td>
+--     <td class="tg-1">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">mirror</td>
+--     <td class="tg-2">Should the endcap be reversed on the right? IE [[ becomes ]]</td>
+--     <td class="tg-2">true</td>
+--   </tr>
+--     <td class="tg-1">truncate</td>
+--     <td class="tg-1">Cut the string to width. Is superceded by wrap being true.</td>
+--     <td class="tg-1">false</td>
+--   </tr>
+-- </tbody>
+-- </table>
+function ftext.fText(str, opts)
+  local options = ftext.fixFormatOptions(str, opts)
+  if options.wrap and (options.strLen > options.effWidth) then
+    local wrapped = ""
+    if str:find("\n") then
+      for _,line in ipairs(str:split("\n")) do
+        local newline = "\n"
+        if _ == 1 then newline = "" end
+        wrapped = wrapped .. newline .. ftext.xwrap(line, options.effWidth, options.formatType)
+      end
+    else
+      wrapped = ftext.xwrap(str, options.effWidth, options.formatType)
+    end
+    local lines = wrapped:split("\n")
+    local formatted = {}
+    options.fixed = false
+    for _, line in ipairs(lines) do
+      table.insert(formatted, ftext.fLine(line, options))
+    end
+    return table.concat(formatted, "\n")
+  else
+    return ftext.fLine(str, options)
+  end
+end
+
+-- internal function, used to set defaults and type correct the options table
+function ftext.fixFormatOptions(str, opts)
+  if opts.fixed then
+    return table.deepcopy(opts)
+  end
+  -- Set up all the things we might call the different echo types
+  if opts == nil then
+    opts = {}
+  end -- don't overwrite options if they passed them
+  -- but if they passed something other than a table as the options than oopsie!
+  if type(opts) ~= "table" then
+    error("Improper argument: options expected to be passed as table")
+  end
+  -- now we make a copy of the table, so we don't edit the original during all this
+  local options = table.deepcopy(opts)
+  if options.wrap == nil then
+    options.wrap = true
+  end -- wrap by default.
+  if options.truncate == nil then
+    options.truncate = false
+  end -- do not truncate by default
+  options.formatType = options.formatType or "" -- by default, no color formatting.
+  options.width = options.width or 80 -- default 80 width
+  options.cap = options.cap or "" -- no cap by default
+  options.spacer = options.spacer or " " -- default spacer is the space character
+  options.alignment = options.alignment or "center" -- default alignment is centered
+  if options.nogap == nil then
+    options.nogap = false
+  end
+  if options.inside == nil then
+    options.inside = false
+  end -- by default, we don't put the spacer inside
+  if not options.mirror == false then
+    options.mirror = options.mirror or true
+  end -- by default, we do want to use mirroring for the caps
+  -- setup default options for colors based on the color formatting type
+  if table.contains(dec, options.formatType) then
+    options.capColor = options.capColor or "<255,255,255>"
+    options.spacerColor = options.spacerColor or "<255,255,255>"
+    options.textColor = options.textColor or "<255,255,255>"
+    options.colorReset = "<r>"
+    options.colorPattern = _Echos.Patterns.Decimal[1]
+  elseif table.contains(hex, options.formatType) then
+    options.capColor = options.capColor or "#FFFFFF"
+    options.spacerColor = options.spacerColor or "#FFFFFF"
+    options.textColor = options.textColor or "#FFFFFF"
+    options.colorReset = "#r"
+    options.colorPattern = _Echos.Patterns.Hex[1]
+  elseif table.contains(col, options.formatType) then
+    options.capColor = options.capColor or "<white>"
+    options.spacerColor = options.spacerColor or "<white>"
+    options.textColor = options.textColor or "<white>"
+    options.colorReset = "<reset>"
+    options.colorPattern = _Echos.Patterns.Color[1]
+  else
+    options.capColor = ""
+    options.spacerColor = ""
+    options.textColor = ""
+    options.colorReset = ""
+    options.colorPattern = ""
+  end
+  options.originalString = str
+  options.strippedString = rex.gsub(tostring(str), options.colorPattern, "")
+  options.strLen = string.len(options.strippedString)
+  options.leftCap = options.cap
+  options.rightCap = options.cap
+  options.capLen = string.len(options.cap)
+  local gapSpaces = 0
+  if not options.nogap then
+    if options.alignment == "center" then
+      gapSpaces = 2
+    else
+      gapSpaces = 1
+    end
+  end
+  options.nontextlength = options.width - options.strLen - gapSpaces
+  options.leftPadLen = math.floor(options.nontextlength / 2)
+  options.rightPadLen = options.nontextlength - options.leftPadLen
+  options.effWidth = options.width - ((options.capLen * gapSpaces) + gapSpaces)
+  if options.capLen > options.leftPadLen then
+    options.cap = options.cap:sub(1, options.leftPadLen)
+    options.capLen = string.len(options.cap)
+  end
+  options.fixed = true
+  return options
+end
+
+-- internal function, processes a single line of the wrapped string.
+function ftext.fLine(str, opts)
+  local options = ftext.fixFormatOptions(str, opts)
+  local truncate, strLen, width = options.truncate, options.strLen, options.width
+  if truncate and strLen > width then
+    local wrapped = ftext.xwrap(str, options.effWidth, options.formatType)
+    local lines = wrapped:split("\n")
+    str = lines[1]
+  end
+  local leftCap = options.leftCap
+  local rightCap = options.rightCap
+  local leftPadLen = options.leftPadLen
+  local rightPadLen = options.rightPadLen
+  local capLen = options.capLen
+
+  if options.alignment == "center" then -- we're going to center something
+    if options.mirror then -- if we're reversing the left cap and the right cap (IE {{[[ turns into ]]}} )
+      rightCap = string.gsub(rightCap, "<", ">")
+      rightCap = string.gsub(rightCap, "%[", "%]")
+      rightCap = string.gsub(rightCap, "{", "}")
+      rightCap = string.gsub(rightCap, "%(", "%)")
+      rightCap = string.reverse(rightCap)
+    end -- otherwise, they'll be the same, so don't do anything
+    if not options.nogap then
+      str = string.format(" %s ", str)
+    end
+
+  elseif options.alignment == "right" then -- we'll right-align the text
+    leftPadLen = leftPadLen + rightPadLen
+    rightPadLen = 0
+    rightCap = ""
+    if not options.nogap then
+      str = string.format(" %s", str)
+    end
+
+  else -- Ok, so if it's not center or right, we assume it's left. We don't do justified. Sorry.
+    rightPadLen = rightPadLen + leftPadLen
+    leftPadLen = 0
+    leftCap = ""
+    if not options.nogap then
+      str = string.format("%s ", str)
+    end
+  end -- that's it, took care of both left, right, and center formattings, now to output the durn thing.
+  local fullLeftCap = string.format("%s%s%s", options.capColor, leftCap, options.colorReset)
+  local fullLeftSpacer = string.format("%s%s%s", options.spacerColor, string.rep(options.spacer, (leftPadLen - capLen)), options.colorReset)
+  local fullText = string.format("%s%s%s", options.textColor, str, options.colorReset)
+  local fullRightSpacer = string.format("%s%s%s", options.spacerColor, string.rep(options.spacer, (rightPadLen - capLen)), options.colorReset)
+  local fullRightCap = string.format("%s%s%s", options.capColor, rightCap, options.colorReset)
+
+  if options.inside then
+    -- "endcap===== some text =====endcap"
+    -- "endcap===== some text =====pacdne"
+    -- "endcap================= some text"
+    -- "some text =================endcap"
+    local finalString = string.format("%s%s%s%s%s", fullLeftCap, fullLeftSpacer, fullText, fullRightSpacer, fullRightCap)
+    return finalString
+  else
+    -- "=====endcap some text endcap====="
+    -- "=====endcap some text pacdne====="
+    -- "=================endcap some text"
+    -- "some text endcap================="
+
+    local finalString = string.format("%s%s%s%s%s", fullLeftSpacer, fullLeftCap, fullText, fullRightCap, fullRightSpacer)
+    return finalString
+  end
+end
+
+-- Functions below here are honestly for backwards compatibility and subject to removal soon.
+-- They just force some options table overrides for the most part.
+
+-- no colors, no wrap
+function ftext.align(str, opts)
+  local options = {}
+  if opts == nil then
+    opts = {}
+  end
+  if type(opts) == "table" then
+    options = table.deepcopy(opts)
+    options.formatType = ""
+    options.wrap = false
+  else
+    error("Improper argument: options expected to be passed as table")
+  end
+  options = ftext.fixFormatOptions(str, options)
+  return ftext.fLine(str, options)
+end
+
+-- decho formatting, no wrap
+function ftext.dalign(str, opts)
+  local options = {}
+  if opts == nil then
+    opts = {}
+  end
+  if type(opts) == "table" then
+    options = table.deepcopy(opts)
+    options.formatType = "d"
+    options.wrap = false
+  else
+    error("Improper argument: options expected to be passed as table")
+  end
+  options = ftext.fixFormatOptions(str, options)
+  return ftext.fLine(str, options)
+end
+
+-- cecho formatting, no wrap
+function ftext.calign(str, opts)
+  local options = {}
+  if opts == nil then
+    opts = {}
+  end
+  if type(opts) == "table" then
+    options = table.deepcopy(opts)
+    options.formatType = "c"
+    options.wrap = false
+  else
+    error("Improper argument: options expected to be passed as table")
+  end
+  options = ftext.fixFormatOptions(str, options)
+  return ftext.fLine(str, options)
+end
+
+-- hecho formatting, no wrap
+function ftext.halign(str, opts)
+  local options = {}
+  if opts == nil then
+    opts = {}
+  end
+  if type(opts) == "table" then
+    options = table.deepcopy(opts)
+    options.formatType = "h"
+    options.wrap = false
+  else
+    error("Improper argument: options expected to be passed as table")
+  end
+  options = ftext.fixFormatOptions(str, options)
+  return ftext.fLine(str, options)
+end
+
+-- literally just fText but forces cecho formatting
+function ftext.cfText(str, opts)
+  local options = {}
+  if opts == nil then
+    opts = {}
+  end
+  if type(opts) == "table" then
+    options = table.deepcopy(opts)
+    options.formatType = "c"
+  else
+    error("Improper argument: options expected to be passed as table")
+  end
+  options = ftext.fixFormatOptions(str, options)
+  return ftext.fText(str, options)
+end
+
+-- fText but forces decho formatting
+function ftext.dfText(str, opts)
+  local options = {}
+  if opts == nil then
+    opts = {}
+  end
+  if type(opts) == "table" then
+    options = table.deepcopy(opts)
+    options.formatType = "d"
+  else
+    error("Improper argument: options expected to be passed as table")
+  end
+  options = ftext.fixFormatOptions(str, options)
+  return ftext.fText(str, options)
+end
+
+-- fText but forces hecho formatting
+function ftext.hfText(str, opts)
+  local options = {}
+  if opts == nil then
+    opts = {}
+  end
+  if type(opts) == "table" then
+    options = table.deepcopy(opts)
+    options.formatType = "h"
+  else
+    error("Improper argument: options expected to be passed as table")
+  end
+  options = ftext.fixFormatOptions(str, options)
+  return ftext.fText(str, options)
+end
+
+--- Stand alone text formatter object. Remembers the options you set and can be adjusted as needed
+-- @type ftext.TextFormatter
+-- @author Damian Monogue <demonnic@gmail.com>
+-- @copyright 2020 Damian Monogue
+-- @license MIT, see LICENSE.lua
+
+local TextFormatter = {}
+TextFormatter.validFormatTypes = {'d', 'dec', 'decimal', 'h', 'hex', 'hexidecimal', 'c', 'color', 'colour', 'col', 'name', 'none', 'e', 'plain', ''}
+
+--- Set's the formatting type whether it's for cecho, decho, or hecho
+-- @tparam string typeToSet What type of formatter is this? Valid options are { 'd', 'dec', 'decimal', 'h', 'hex', 'hexidecimal', 'c', 'color', 'colour', 'col', 'name'}
+function TextFormatter:setType(typeToSet)
+  local isNotValid = not table.contains(self.validFormatTypes, typeToSet)
+  if isNotValid then
+    error("TextFormatter:setType: Invalid argument, valid types are:" .. table.concat(self.validFormatTypes, ", "))
+  end
+  self.options.formatType = typeToSet
+end
+
+function TextFormatter:toBoolean(thing)
+  if type(thing) ~= "boolean" then
+    if thing == "true" then
+      thing = true
+    elseif thing == "false" then
+      thing = false
+    else
+      return nil
+    end
+  end
+  return thing
+end
+
+function TextFormatter:checkString(str)
+  if type(str) ~= "string" then
+    if tostring(str) then
+      str = tostring(str)
+    else
+      return nil
+    end
+  end
+  return str
+end
+
+--- Sets whether or not we should do word wrapping.
+-- @tparam boolean shouldWrap should we do wordwrapping?
+function TextFormatter:setWrap(shouldWrap)
+  local argumentType = type(shouldWrap)
+  shouldWrap = self:toBoolean(shouldWrap)
+  if shouldWrap == nil then
+    error("TextFormatter:setWrap(shouldWrap) Argument error, boolean expected, got " .. argumentType ..
+            ", if you want to set the number of characters wide to format for, use setWidth()")
+  end
+  self.options.wrap = shouldWrap
+end
+
+--- Sets the width we should format for
+-- @tparam number width the width we should format for
+function TextFormatter:setWidth(width)
+  if type(width) ~= "number" then
+    if tonumber(width) then
+      width = tonumber(width)
+    else
+      error("TextFormatter:setWidth(width): Argument error, number expected, got " .. type(width))
+    end
+  end
+  self.options.width = width
+end
+
+--- Sets the cap for the formatter
+-- @tparam string cap the string to use for capping the formatted string.
+function TextFormatter:setCap(cap)
+  local argumentType = type(cap)
+  local cap = self:checkString(cap)
+  if cap == nil then
+    error("TextFormatter:setCap(cap): Argument error, string expect, got " .. argumentType)
+  end
+  self.options.cap = cap
+end
+
+--- Sets the color for the format cap
+-- @tparam string capColor Color which can be formatted via Geyser.Color.parse()
+function TextFormatter:setCapColor(capColor)
+  local argumentType = type(capColor)
+  local capColor = self:checkString(capColor)
+  if capColor == nil then
+    error("TextFormatter:setCapColor(capColor): Argument error, string expected, got " .. argumentType)
+  end
+  self.options.capColor = capColor
+end
+
+--- Sets the color for spacing character
+-- @tparam string spacerColor Color which can be formatted via Geyser.Color.parse()
+function TextFormatter:setSpacerColor(spacerColor)
+  local argumentType = type(spacerColor)
+  local spacerColor = self:checkString(spacerColor)
+  if spacerColor == nil then
+    error("TextFormatter:setSpacerColor(spacerColor): Argument error, string expected, got " .. argumentType)
+  end
+  self.options.spacerColor = spacerColor
+end
+
+--- Sets the color for formatted text
+-- @tparam string textColor Color which can be formatted via Geyser.Color.parse()
+function TextFormatter:setTextColor(textColor)
+  local argumentType = type(textColor)
+  local textColor = self:checkString(textColor)
+  if textColor == nil then
+    error("TextFormatter:setTextColor(textColor): Argument error, string expected, got " .. argumentType)
+  end
+  self.options.textColor = textColor
+end
+
+--- Sets the spacing character to use. Should be a single character
+-- @tparam string spacer the character to use for spacing
+function TextFormatter:setSpacer(spacer)
+  local argumentType = type(spacer)
+  local spacer = self:checkString(spacer)
+  if spacer == nil then
+    error("TextFormatter:setSpacer(spacer): Argument error, string expect, got " .. argumentType)
+  end
+  self.options.spacer = spacer
+end
+
+--- Set the alignment to format for
+-- @tparam string alignment How to align the formatted string. Valid options are 'left', 'right', or 'center'
+function TextFormatter:setAlignment(alignment)
+  local validAlignments = {"left", "right", "center"}
+  if not table.contains(validAlignments, alignment) then
+    error("TextFormatter:setAlignment(alignment): Argument error: Only valid arguments for setAlignment are 'left', 'right', or 'center'. You sent" ..
+            alignment)
+  end
+  self.options.alignment = alignment
+end
+
+--- Set whether the the spacer should go inside the the cap or outside of it
+-- @tparam boolean spacerInside
+function TextFormatter:setInside(spacerInside)
+  local argumentType = type(spacerInside)
+  spacerInside = self:toBoolean(spacerInside)
+  if spacerInside == nil then
+    error("TextFormatter:setInside(spacerInside) Argument error, boolean expected, got " .. argumentType)
+  end
+  self.options.inside = spacerInside
+end
+
+--- Set whether we should mirror/reverse the caps. IE << becomes >> if set to true
+-- @tparam boolean shouldMirror
+function TextFormatter:setMirror(shouldMirror)
+  local argumentType = type(shouldMirror)
+  shouldMirror = self:toBoolean(shouldMirror)
+  if shouldMirror == nil then
+    error("TextFormatter:setMirror(shouldMirror): Argument error, boolean expected, got " .. argumentType)
+  end
+  self.options.mirror = shouldMirror
+end
+
+--- Set whether we should remove the gap spaces between the text and spacer characters. "===some text===" if set to true, "== some text ==" if set to false
+-- @tparam boolean noGap
+function TextFormatter:setNoGap(noGap)
+  local argumentType = type(noGap)
+  noGap = self:toBoolean(noGap)
+  if noGap == nil then
+    error("TextFormatter:setNoGap(noGap): Argument error, boolean expected, got " .. argumentType)
+  end
+  self.options.noGap = noGap
+end
+
+--- Enables truncation (cutting to length). You still need to ensure wrap is disabled, as it supercedes.
+function TextFormatter:enableTruncate()
+  self.options.truncate = true
+end
+
+--- Disables truncation (cutting to length). You still need to ensure wrap is enabled if you want it to wrap.
+function TextFormatter:disableTruncate()
+  self.options.truncate = false
+end
+
+--- Format a string based on the stored options
+-- @tparam string str The string to format
+function TextFormatter:format(str)
+  return ftext.fText(str, self.options)
+end
+
+--- Creates and returns a new TextFormatter.
+-- @tparam table options the options for the text formatter to use when running format()
+-- <br><br>Table of options
+-- <table class="tg">
+-- <thead>
+--   <tr>
+--     <th>option name</th>
+--     <th>description</th>
+--     <th>default</th>
+--   </tr>
+-- </thead>
+-- <tbody>
+--   <tr>
+--     <td class="tg-1">wrap</td>
+--     <td class="tg-1">Should it wordwrap to multiple lines?</td>
+--     <td class="tg-1">true</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">formatType</td>
+--     <td class="tg-2">Determines how it formats for color. 'c' for cecho, 'd' for decho, 'h' for hecho, and anything else for no colors</td>
+--     <td class="tg-2">"c"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">width</td>
+--     <td class="tg-1">How wide should we format the text?</td>
+--     <td class="tg-1">80</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">cap</td>
+--     <td class="tg-2">what characters to use for the endcap.</td>
+--     <td class="tg-2">""</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">capColor</td>
+--     <td class="tg-1">what color to make the endcap?</td>
+--     <td class="tg-1">the correct 'white' for your formatType</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">spacer</td>
+--     <td class="tg-2">What character to use for empty space. Must be a single character</td>
+--     <td class="tg-2">" "</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">spacerColor</td>
+--     <td class="tg-1">what color should the spacer be?</td>
+--     <td class="tg-1">the correct 'white' for your formatType</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">textColor</td>
+--     <td class="tg-2">what color should the text itself be?</td>
+--     <td class="tg-2">the correct 'white' for your formatType</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">alignment</td>
+--     <td class="tg-1">How should the text be aligned within the width. "center", "left", or "right"</td>
+--     <td class="tg-1">"center"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">nogap</td>
+--     <td class="tg-2">Should we put a literal space between the spacer character and the text?</td>
+--     <td class="tg-2">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">inside</td>
+--     <td class="tg-1">Put the spacers inside the caps?</td>
+--     <td class="tg-1">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">mirror</td>
+--     <td class="tg-2">Should the endcap be reversed on the right? IE [[ becomes ]]</td>
+--     <td class="tg-2">true</td>
+--   </tr>
+--     <td class="tg-1">truncate</td>
+--     <td class="tg-1">Cut the string to width. Is superceded by wrap being true.</td>
+--     <td class="tg-1">false</td>
+--   </tr>
+-- </tbody>
+-- </table>
+-- @usage
+-- local TextFormatter = require("MDK.ftext").TextFormatter
+-- myFormatter = TextFormatter:new( {
+--   width = 40,
+--   cap = "[CAP]",
+--   capColor = "<orange>",
+--   textColor = "<light_blue>"
+-- })
+-- myMessage = "This is a test of the emergency broadcasting system. This is only a test"
+-- cecho(myFormatter:format(myMessage))
+
+function TextFormatter:new(options)
+  if options == nil then
+    options = {}
+  end
+  if options and type(options) ~= "table" then
+    error("TextFormatter:new(options): Argument error, table expected, got " .. type(options))
+  end
+  local me = {}
+  me.options = {formatType = "c", wrap = true, width = 80, cap = "", spacer = " ", alignment = "center", inside = true, mirror = false}
+  for option, value in pairs(options) do
+    me.options[option] = value
+  end
+  setmetatable(me, self)
+  self.__index = self
+  return me
+end
+ftext.TextFormatter = TextFormatter
+
+--- Easy formatting for text tables
+-- @type ftext.TableMaker
+-- @author Damian Monogue <demonnic@gmail.com>
+-- @copyright 2020 Damian Monogue
+-- @license MIT, see LICENSE.lua
+
+local TableMaker = {
+  headCharacter = "*",
+  footCharacter = "*",
+  edgeCharacter = "*",
+  rowSeparator = "-",
+  separator = "|",
+  separateRows = true,
+  colorReset = "<reset>",
+  formatType = "c",
+  printHeaders = true,
+  autoEcho = false,
+  title = "",
+  printTitle = false,
+  headerTitle = false,
+  forceHeaderSeparator = false,
+  autoEchoConsole = "main",
+}
+
+function TableMaker:checkPosition(position, func)
+  if position == nil then
+    position = 0
+  end
+  if type(position) ~= "number" then
+    if tonumber(position) then
+      position = tonumber(position)
+    else
+      error(func .. ": Argument error: position expected as number, got " .. type(position))
+    end
+  end
+  return position
+end
+
+function TableMaker:insert(tbl, pos, item)
+  if pos ~= 0 then
+    table.insert(tbl, pos, item)
+  else
+    table.insert(tbl, item)
+  end
+end
+
+--- Get the TextFormatter which defines the format of a specific column
+-- @tparam number position The position of the column you're getting, counting from the left. If not provided will return the last column.
+function TableMaker:getColumn(position)
+  position = position or #self.columns
+  position = self:checkPosition(position, "TableMaker:getColumn(position)")
+  return self.columns[position]
+end
+
+--- Adds a column definition for the table.
+-- @tparam table options Table of options suitable for a TextFormatter object. See https://github.com/demonnic/fText/wiki/fText
+-- @tparam number position The position of the column you're adding, counting from the left. If not provided will add it as the last column
+function TableMaker:addColumn(options, position)
+  if options == nil then
+    options = {}
+  end
+  if not type(options) == "table" then
+    error("TableMaker:addColumn(options, position): Argument error: options expected as table, got " .. type(options))
+  end
+  local options = table.deepcopy(options)
+  position = self:checkPosition(position, "TableMaker:addColumn(options, position)")
+  options.width = options.width or 20
+  options.name = options.name or ""
+  local formatter = TextFormatter:new(options)
+  self:insert(self.columns, position, formatter)
+end
+
+--- Deletes a column at the given position
+-- @tparam number position the column you wish to delete
+function TableMaker:deleteColumn(position)
+  if position == nil then
+    error("TableMaker:deleteColumn(position): Argument Error: position as number expected, got nil")
+  end
+  position = self:checkPosition(position)
+  local maxColumn = #self.columns
+  if position > maxColumn then
+    error(
+      "TableMaker:deleteColumn(position): Argument Error: position provided was larger than number of columns in the table. Number of columns: " ..
+        #self.columns)
+  end
+  table.remove(self.columns, position)
+end
+
+--- Replaces a column at a specific position with the newly provided formatting
+-- @tparam table options table of options suitable for a TextFormatter object. See https://github.com/demonnic/fText/wiki/fText
+-- @tparam number position which column you are replacing, counting from the left.
+function TableMaker:replaceColumn(options, position)
+  if position == nil then
+    error("TableMaker:replaceColumn(options, position): Argument error: position as number expected, got nil")
+  end
+  position = self:checkPosition(position)
+  if type(options) ~= "table" then
+    error("TableMaker:replaceColumn(options, position): Argument error: options as table expected, got " .. type(options))
+  end
+  if #self.columns < position then
+    error(
+      "TableMaker:replaceColumn(options, position): you cannot specify a position higher than the number of columns currently in the TableMaker. You sent:" ..
+        position .. " and there are: " .. #self.columns .. "columns in the TableMaker")
+  end
+  options.width = options.width or 20
+  options.name = options.name or ""
+  local formatter = TextFormatter:new(options)
+  self.columns[position] = formatter
+end
+
+--- Gets the row of output at a specific position
+-- @tparam number position The position of the row you want to get, coutning from the top down. If not provided defaults to the last row in the table
+-- @return table of entries in the specified row
+function TableMaker:getRow(position)
+  position = position or #self.rows
+  position = self:checkPosition(position, "TableMaker:getRow(position)")
+  return self.rows[position]
+end
+
+--- Adds a row of output to the table
+-- @tparam table columnEntries This indexed table contains an entry for each column in the table. Entries in the table must be strings, a table of options for insertPopup or insertLink, or a function which returns one of these things
+-- @tparam number position position for the row you want to add, counting from the top down. If not provided defaults to the last line in the table.
+function TableMaker:addRow(columnEntries, position)
+  local columnEntriesType = type(columnEntries)
+  if columnEntriesType ~= "table" then
+    error("TableMaker:addRow(columnEntries, position): Argument error, columnEntries expected as table, got " .. columnEntriesType)
+  end
+  for _, entry in ipairs(columnEntries) do
+    local entryCheck = self:checkEntry(entry)
+    if entryCheck == 0 then
+      if type(entry) == "function" then
+        error(
+          "TableMaker:addRow(columnEntries, position): Argument Error, you provided a function for a columnEntry but it does not return a string. We need a string. It was entry number " ..
+            _ .. "in columnEntries")
+      else
+        error("TableMaker:addRow(columnEntries, position): Argument error, columnEntries items expected as string, got:" .. type(entry))
+      end
+    end
+  end
+  position = self:checkPosition(position, "TableMaker:addRow(columnEntries, position)")
+  self:insert(self.rows, position, columnEntries)
+end
+
+--- Deletes the row at the given position
+-- @tparam number position the row to delete
+function TableMaker:deleteRow(position)
+  if position == nil then
+    error("TableMaker:deleteRow(position): Argument Error: position as number expected, got nil")
+  end
+  position = self:checkPosition(position, "TableMaker:deleteRow(position)")
+  local maxRow = #self.rows
+  if position > maxRow then
+    error("TableMaker:deleteRow(position): Argument Error: position given was > the number of rows we have # of rows is:" .. maxRow)
+  end
+  table.remove(self.rows, position)
+end
+
+--- Replaces a row of output in the table
+-- @tparam table columnEntries This indexed table contains an entry for each column in the table. Entries in the table must be strings, a table of options for insertPopup or insertLink, or a function which returns one of these things
+-- @tparam number position position for the row you want to add, counting from the top down.
+function TableMaker:replaceRow(columnEntries, position)
+  if position == nil then
+    error("TableMaker:replaceRow(columnEntries, position): ArgumentError: position expected as number, received nil")
+  end
+  position = self:checkPosition(position, "TableMaker:replaceRow(columnEntries, position)")
+  if #self.rows < position then
+    error(
+      "TableMaker:replaceRow(columnEntries, position): position cannot be greater than the number of rows already in the tablemaker. You provided: " ..
+        position .. " and there are " .. #self.rows .. "rows in the TableMaker")
+  end
+  for _, entry in ipairs(columnEntries) do
+    local entryCheck = self:checkEntry(entry)
+    if entryCheck == 0 then
+      if type(entry) == "function" then
+        error(
+          "TableMaker:replaceRow(columnEntries, position): Argument Error: you provided a function for a columnEntry but it does not return a string. We need a string. It was entry number " ..
+            _ .. "in columnEntries")
+      else
+        error("TableMaker:replaceRow(columnEntries, position): Argument error: columnEntries items expected as string, got:" .. type(entry))
+      end
+    end
+  end
+  self.rows[position] = columnEntries
+end
+
+function TableMaker:checkEntry(entry)
+  local allowedTypes = {"string"}
+  if self.allowPopups then
+    table.insert(allowedTypes, "table")
+  end
+  local entryType = type(entry)
+  if entryType == "function" then
+    entryType = type(entry())
+  end
+  if table.contains(allowedTypes, entryType) then
+    return entry
+  else
+    return 0
+  end
+end
+
+function TableMaker:checkNumber(num)
+  if num == nil then
+    num = 0
+  end
+  if not tonumber(num) then
+    num = 0
+  end
+  return tonumber(num)
+end
+
+--- Get the contents and formatter for a specific cell
+-- @tparam number row the row number of the cell, counted top down.
+-- @tparam number column the column number of the cell, counted from the left.
+-- @return the base text and TextFormatter for the cell at the specific row and column number
+function TableMaker:getCell(row, column)
+  local rowType = type(row)
+  local columnType = type(column)
+  local maxRow = #self.rows
+  local maxColumn = #self.columns
+  local ae = "TableMaker:getCell(row, column): Argument error:"
+  row = self:checkNumber(row)
+  column = self:checkNumber(column)
+  if row == 0 then
+    if rowType ~= "number" then
+      printError(f"{ae} row as number expected, got {rowType}", true, true)
+    else
+      printError(f"{ae} rows start at 1, and you asked for row 0", true, true)
+    end
+  elseif column == 0 then
+    if columnType ~= "number" then
+      printError(f"{ae} column as number expected, got {columnType}", true, true)
+    else
+      printError(f"{ae} columns start at 1, and you asked for column 0", true, true)
+    end
+  elseif row > maxRow then
+    printError(f"{ae} row exceeds number of rows in table ({maxRow})")
+  elseif column > maxColumn then
+    printError(f"{ae} column exceeds number of columns in table ({maxColumn})", true, true)
+  end
+  return self.rows[row][column], self.columns[column]
+end
+
+--- Sets a specific cell's display information
+-- @tparam number row the row number of the cell, counted from the top down
+-- @tparam number column the column number of the cell, counted from the left
+-- @param entry What to set the entry to. Must be a string, or a table of options for insertLink/insertPopup if allowPopups is set. Or a function which returns one of these things
+function TableMaker:setCell(row, column, entry)
+  local maxRow = #self.rows
+  local maxColumn = #self.columns
+  local ae = "TableMaker:setCell(row, column, entry): Argument Error:"
+  row = self:checkNumber(row)
+  if row == 0 then
+    error(ae .. " row must be a number, you provided " .. type(row))
+  end
+  column = self:checkNumber(column)
+  if column == 0 then
+    error(ae .. " column must be a number, you provided " .. type(column))
+  end
+  if row > maxRow then
+    error(ae .. " row is higher than the number of rows in the table. Highest row:" .. maxRow)
+  end
+  if column > maxColumn then
+    error(ae .. " column is higher than the number of columns in the table. Highest column:" .. maxColumn)
+  end
+  local entryType = type(entry)
+  entry = self:checkEntry(entry)
+  if entry == 0 then
+    if entryType == "function" then
+      error(ae .. " entry was provided as a function, but does not return a string. We need a string in the end")
+    else
+      error("TableMaker:setCell(row, column, entry): Argument Error: entry must be a string, or a function which returns a string. You provided a " .. entryType)
+    end
+  end
+  self.rows[row][column] = entry
+end
+
+function TableMaker:totalWidth()
+  local width = 0
+  local numberOfColumns = #self.columns
+  local separatorWidth = string.len(self.separator)
+  local edgeWidth = string.len(self.edgeCharacter) * 2
+  for _, column in ipairs(self.columns) do
+    width = width + column.options.width
+  end
+  separatorWidth = separatorWidth * (numberOfColumns - 1)
+  width = width + edgeWidth + separatorWidth
+  return width
+end
+
+function TableMaker:getType()
+  local dec = {"d", "decimal", "dec"}
+  local hex = {"h", "hexidecimal", "hex"}
+  local col = {"c", "color", "colour", "col", "name"}
+  if table.contains(dec, self.formatType) then
+    return 'd'
+  elseif table.contains(hex, self.formatType) then
+    return 'h'
+  elseif table.contains(col, self.formatType) then
+    return 'c'
+  else
+    return ''
+  end
+end
+
+function TableMaker:echo(message, echoType, ...)
+  local fType = self:getType()
+  local consoleType = type(self.autoEchoConsole)
+  local console = ""
+  if echoType == nil then
+    echoType = ""
+  end
+  if consoleType == "string" then
+    console = self.autoEchoConsole
+  elseif consoleType == "nil" then
+    console = "main"
+  else
+    console = self.autoEchoConsole.name
+  end
+  local functionName = string.format("%secho%s", fType, echoType)
+  local func = _G[functionName]
+  if echoType == "" then
+    func(console, message)
+  else
+    func(console, message, ...)
+  end
+end
+
+function TableMaker:scanRow(rowToScan)
+  local row = table.deepcopy(rowToScan)
+  local rowEntries = #row
+  local numberOfColumns = #self.columns
+  local columns = {}
+  local linesInRow = 0
+  local rowText = ""
+  local ec = self.frameColor .. self.edgeCharacter .. self.colorReset
+  local sep = self.separatorColor .. self.separator .. self.colorReset
+
+  if rowEntries < numberOfColumns then
+    local entriesNeeded = numberOfColumns - rowEntries
+    for i = 1, entriesNeeded do
+      table.insert(row, "")
+    end
+  end
+  for index, formatter in ipairs(self.columns) do
+    local str = row[index]
+    local column = ""
+    if type(str) == "function" then
+      str = str()
+    end
+    column = formatter:format(str)
+    table.insert(columns, column:split("\n"))
+  end
+  for _, rowLines in ipairs(columns) do
+    if linesInRow < #rowLines then
+      linesInRow = #rowLines
+    end
+  end
+  for index, rowLines in ipairs(columns) do
+    if #rowLines < linesInRow then
+      local neededLines = linesInRow - #rowLines
+      for i = 1, neededLines do
+        table.insert(rowLines, self.columns[index]:format(""))
+      end
+    end
+  end
+  for i = 1, linesInRow do
+    local thisLine = ec
+    for index, column in ipairs(columns) do
+      if index == 1 then
+        thisLine = string.format("%s%s", thisLine, column[i])
+      else
+        thisLine = string.format("%s%s%s", thisLine, sep, column[i])
+      end
+    end
+    thisLine = string.format("%s%s", thisLine, ec)
+    if rowText == "" then
+      rowText = thisLine
+    else
+      rowText = string.format("%s\n%s", rowText, thisLine)
+    end
+  end
+  return rowText
+end
+
+function TableMaker:echoRow(rowToScan)
+  local row = table.deepcopy(rowToScan)
+  local rowEntries = #row
+  local numberOfColumns = #self.columns
+  local columns = {}
+  local linesInRow = 0
+  local ec = self.frameColor .. self.edgeCharacter .. self.colorReset
+  local sep = self.separatorColor .. self.separator .. self.colorReset
+  if rowEntries < numberOfColumns then
+    local entriesNeeded = numberOfColumns - rowEntries
+    for i = 1, entriesNeeded do
+      table.insert(row, "")
+    end
+  end
+  for index, formatter in ipairs(self.columns) do
+    local str = row[index]
+    local column = ""
+    if type(str) == "function" then
+      str = str()
+    end
+    if type(str) == "table" then
+      str = str[1]
+    end
+    column = formatter:format(str)
+    table.insert(columns, column:split("\n"))
+  end
+  for _, rowLines in ipairs(columns) do
+    if linesInRow < #rowLines then
+      linesInRow = #rowLines
+    end
+  end
+  for index, rowLines in ipairs(columns) do
+    if #rowLines < linesInRow then
+      local neededLines = linesInRow - #rowLines
+      for i = 1, neededLines do
+        table.insert(rowLines, self.columns[index]:format(""))
+      end
+    end
+  end
+  for i = 1, linesInRow do
+    self:echo(ec)
+    for index, column in ipairs(columns) do
+      local message = column[i]
+      if index ~= 1 then
+        self:echo(sep)
+      end
+      if type(row[index]) == "string" then
+        self:echo(message)
+      elseif type(row[index]) == "table" then
+        local rowEntry = row[index]
+        local echoType = ""
+        if type(rowEntry[2]) == "string" then
+          echoType = "Link"
+        elseif type(rowEntry[2]) == "table" then
+          echoType = "Popup"
+        end
+        self:echo(message, echoType, rowEntry[2], rowEntry[3], rowEntry[4] or true)
+      end
+    end
+    self:echo(ec)
+    self:echo("\n")
+  end
+end
+
+function TableMaker:makeHeader()
+  local totalWidth = self:totalWidth()
+  local ec = self.frameColor .. self.edgeCharacter .. self.colorReset
+  local sep = self.separatorColor .. self.separator .. self.colorReset
+  local header = self.frameColor .. string.rep(self.headCharacter, totalWidth) .. self.colorReset
+  local columnHeaders = ""
+  if self.printHeaders then
+    local columnEntries = {}
+    for _, v in ipairs(self.columns) do
+      table.insert(columnEntries, v:format(v.options.name))
+    end
+    local divWithNewlines = self.headerTitle and header or self:createRowDivider()
+    divWithNewlines = "\n" .. divWithNewlines
+    columnHeaders = string.format("\n%s%s%s%s", ec, table.concat(columnEntries, sep), ec, (self.separateRows or self.forceHeaderSeparator) and divWithNewlines or '')
+  end
+  local title = self:makeTitle(totalWidth, header)
+  header = string.format("%s%s%s", header, title, columnHeaders)
+  return header
+end
+
+function TableMaker:makeTitle(totalWidth, header)
+  if not self.printTitle then
+    return ""
+  end
+  local title = ftext.fText(self.title, {width = totalWidth, alignment = "center", cap = self.headCharacter, capColor = self.frameColor, inside = true, textColor = self.titleColor, formatType = self.formatType})
+  title = string.format("\n%s\n%s", title, header)
+  return title
+end
+
+function TableMaker:createRowDivider()
+  local columnPieces = {}
+  for _, v in ipairs(self.columns) do
+    local piece = string.format("%s%s%s", self.separatorColor, string.rep(self.rowSeparator, v.options.width), self.colorReset)
+    table.insert(columnPieces, piece)
+  end
+  local ec = self.frameColor .. self.edgeCharacter .. self.colorReset
+  local sep = self.separatorColor .. self.separator .. self.colorReset
+  return string.format("%s%s%s", ec, table.concat(columnPieces, sep), ec)
+end
+
+--- set the title of the table
+-- @tparam string title The title of the table.
+function TableMaker:setTitle(title)
+  self.title = title
+  if self.autoEcho then self:assemble() end
+end
+
+--- set the rowSeparator for the table
+-- @tparam string char The row separator to use
+function TableMaker:setRowSeparator(char)
+  self.rowSeparator = char
+  if self.autoEcho then self:assemble() end
+end
+
+--- set the edgeCharacter for the table
+-- @tparam string char The edge character to use
+function TableMaker:setEdgeCharacter(char)
+  self.edgeCharacter = char
+  if self.autoEcho then self:assemble() end
+end
+
+--- set the foot character for the table
+-- @tparam string char The foot character to use
+function TableMaker:setFootCharacter(char)
+  self.footCharacter = char
+  if self.autoEcho then self:assemble() end
+end
+
+--- set the head character for the table
+-- @tparam string char The head character to use
+function TableMaker:setHeadCharacter(char)
+  self.headCharacter = char
+  if self.autoEcho then self:assemble() end
+end
+
+--- set the column separator character for the table
+-- @tparam string char The separator character to use
+function TableMaker:setSeparator(char)
+  self.separator = char
+  if self.autoEcho then self:assemble() end
+end
+
+--- set the title color for the table
+-- @tparam string color The title color to use. Should match the color type of the tablemaker (cecho by default)
+function TableMaker:setTitleColor(color)
+  self.titleColor = color
+  if self.autoEcho then self:assemble() end
+end
+
+--- set the title color for the table
+-- @tparam string color The separator color to use. Should match the color type of the tablemaker (cecho by default)
+function TableMaker:setSeparatorColor(color)
+  self.separatorColor = color
+  if self.autoEcho then self:assemble() end
+end
+
+--- set the title color for the table
+-- @tparam string color The frame color to use. Should match the color type of the tablemaker (cecho by default)
+function TableMaker:setFrameColor(color)
+  self.frameColor = color
+  if self.autoEcho then self:assemble() end
+end
+
+--- Force a separator between the header and first row, even if the row separator is disabled for the overall table
+function TableMaker:enableForceHeaderSeparator()
+  self.forceHeaderSeparator = true
+  if self.autoEcho then self:assemble() end
+end
+
+--- Do not force a separator between the header and first row, even if the row separator is disabled for the overall table
+function TableMaker:disableForceHeaderSeparator()
+  self.forceHeaderSeparator = false
+  if self.autoEcho then self:assemble() end
+end
+
+--- Enable using the title separator for the column headers as well
+function TableMaker:enableHeaderTitle()
+  self.headerTitle = true
+  if self.autoEcho then self:assemble() end
+end
+
+--- Disable using the title separator for the column headers as well
+function TableMaker:disableHeaderTitle()
+  self.headerTitle = false
+  if self.autoEcho then self:assemble() end
+end
+
+--- enable printing the title of the table
+function TableMaker:enablePrintTitle()
+  self.printTitle = true
+  if self.autoEcho then self:assemble() end
+end
+
+--- disable printing the title of the table
+function TableMaker:disablePrintTitle()
+  self.printTitle = false
+  if self.autoEcho then self:assemble() end
+end
+
+--- enable printing of the column headers
+function TableMaker:enablePrintHeaders()
+  self.printHeaders = true
+  if self.autoEcho then self:assemble() end
+end
+
+--- disable printing of the column headers
+function TableMaker:disablePrintHeaders()
+  self.printHeaders = false
+  if self.autoEcho then self:assemble() end
+end
+
+--- enable printing the separator line between rows
+function TableMaker:enableRowSeparator()
+  self.separateRows = true
+  if self.autoEcho then self:assemble() end
+end
+
+--- enable printing the separator line between rows
+function TableMaker:disableRowSeparator()
+  self.separateRows = false
+  if self.autoEcho then self:assemble() end
+end
+
+--- enables making cells which incorporate insertLink/insertPopup
+function TableMaker:enablePopups()
+  self.autoEcho = true
+  self.allowPopups = true
+  if self.autoEcho then self:assemble() end
+end
+
+--- enables autoEcho so that when assemble is called it echos automatically
+function TableMaker:enableAutoEcho()
+  self.autoEcho = true
+  self:assemble()
+end
+
+--- disables autoecho. Cannot be used if allowPopups is set
+function TableMaker:disableAutoEcho()
+  if self.allowPopups then
+    error("TableMaker:disableAutoEcho(): you cannot disable autoEcho once you have enabled popups.")
+  else
+    self.autoEcho = false
+  end
+end
+
+--- Enables automatically clearing the miniconsole we echo to
+function TableMaker:enableAutoClear()
+  self.autoClear = true
+  if self.autoEcho then self:assemble() end
+end
+
+--- Disables automatically clearing the miniconsole we echo to
+function TableMaker:disableAutoClear()
+  self.autoClear = false
+end
+
+--- Set the miniconsole to echo to
+-- @param console The miniconsole to autoecho to. Set to "main" or do not pass the parameter to autoecho to the main console. Can be a string name of the console, or a Geyser MiniConsole
+function TableMaker:setAutoEchoConsole(console)
+  local funcName = "TableMaker:setAutoEchoConsole(console)"
+  if console == nil then
+    console = "main"
+  end
+  local consoleType = type(console)
+  if consoleType ~= "string" and consoleType ~= "table" then
+    error(funcName .. " ArgumentError: console as string or a Geyser MiniConsole or UserWindow expected, got " .. consoleType)
+  elseif consoleType == "table" and not (console.type == "miniConsole" or console.type == "userwindow") then
+    error(funcName .. " ArgumentError: console received was a table and may be a Geyser object, but console.type is not miniConsole, it is " ..
+            console.type)
+  end
+  self.autoEchoConsole = console
+  if self.autoEcho then self:assemble() end
+end
+
+--- Assemble the table. If autoEcho is enabled/set to true, will automatically echo. Otherwise, returns the formatted string to echo the table
+function TableMaker:assemble()
+  if self.allowPopups and self.autoEcho then
+    self:popupAssemble()
+  else
+    return self:textAssemble()
+  end
+end
+
+function TableMaker:popupAssemble()
+  if self.autoClear then
+    local console = self.autoEchoConsole
+    if console and console ~= "main" then
+      if type(console) == "table" then
+        console = console.name
+      end
+      clearWindow(console)
+    end
+  end
+  local divWithNewLines = string.format("%s\n", self:createRowDivider())
+  local header = self:makeHeader() .. "\n"
+  local footer = string.format("%s%s%s\n", self.frameColor, string.rep(self.footCharacter, self:totalWidth()), self.colorReset)
+  self:echo(header)
+  for _, row in ipairs(self.rows) do
+    if _ ~= 1 and self.separateRows then
+      self:echo(divWithNewLines)
+    end
+    self:echoRow(row)
+  end
+  self:echo(footer)
+end
+
+function TableMaker:textAssemble()
+  local sheet = ""
+  local rows = {}
+  for _, row in ipairs(self.rows) do
+    table.insert(rows, self:scanRow(row))
+  end
+  local divWithNewlines = string.format("\n%s\n", self:createRowDivider())
+  local footer = string.format("%s%s%s", self.frameColor, string.rep(self.footCharacter, self:totalWidth()), self.colorReset)
+  sheet = string.format("%s\n%s\n%s\n", self:makeHeader(), table.concat(rows, self.separateRows and divWithNewlines or "\n"), footer)
+  if self.autoEcho then
+    local console = self.autoEchoConsole or "main"
+    if type(console) == "table" then
+      console = console.name
+    end
+    if self.autoClear and console ~= "main" then
+      clearWindow(console)
+    end
+    self:echo(sheet)
+  end
+  return sheet
+end
+
+--- Creates and returns a new TableMaker.
+-- see https://github.com/demonnic/MDK/wiki/fText%3A-TableMaker%3A-Examples for usage
+-- @tparam table options table of options for the TableMaker object
+-- <br><br>Table of new options
+-- <table class="tg">
+-- <thead>
+--   <tr>
+--     <th>option name</th>
+--     <th>description</th>
+--     <th>default</th>
+--   </tr>
+-- </thead>
+-- <tbody>
+--   <tr>
+--     <td class="tg-1">formatType</td>
+--     <td class="tg-1">Determines how it formats for color. 'c' for cecho, 'd' for decho, 'h' for hecho, and anything else for no colors</td>
+--     <td class="tg-1">c</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">printHeaders</td>
+--     <td class="tg-2">print top row as header</td>
+--     <td class="tg-2">true</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">headCharacter</td>
+--     <td class="tg-1">The character used to construct the very top of the table. A solid line of these characters is used</td>
+--     <td class="tg-1">"*"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">footCharacter</td>
+--     <td class="tg-2">The character used to construct the very bottom of the table. A solid line of these characters is used</td>
+--     <td class="tg-2">"*"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">edgeCharacter</td>
+--     <td class="tg-1">the character used to form the left and right edges of the table. There is one on either side of every line</td>
+--     <td class="tg-1">"*"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">frameColor</td>
+--     <td class="tg-2">The color to use for the frame. The frame is the border around the outside edge of the table (headCharacter, footCharacter, and edgeCharacters will all be this color).</td>
+--     <td class="tg-2">the correct 'white' for your formatType</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">rowSeparator</td>
+--     <td class="tg-1">the character used to form the lines between rows of text</td>
+--     <td class="tg-1">"-"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">separator</td>
+--     <td class="tg-2">Character used between columns.</td>
+--     <td class="tg-2">"|"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">separatorColor</td>
+--     <td class="tg-1">the color used for the separators, the things which divide the rows and columns internally. (separator and rowSeparator will be this color)</td>
+--     <td class="tg-1">frameColor</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">autoEcho</td>
+--     <td class="tg-2">echo the table automatically in addition to returning the string representation?</td>
+--     <td class="tg-2">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">autoEchoConsole</td>
+--     <td class="tg-1">MiniConsole to autoEcho to</td>
+--     <td class="tg-1">"main"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">autoClear</td>
+--     <td class="tg-2">If autoEchoing, and not echoing to the main console, should we clear the console before outputting?</td>
+--     <td class="tg-2">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">allowPopups</td>
+--     <td class="tg-1">setting this to true allows you to make cells in the table clickable, as well as give them right-click menus.<br>
+--                        Please see Clickable Tables <a href="https://github.com/demonnic/fText/wiki/ClickableTables">HERE</a></td>
+--     <td class="tg-1">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">separateRows</td>
+--     <td class="tg-2">When false, will not print the separator line between rows</td>
+--     <td class="tg-2">true</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">title</td>
+--     <td class="tg-1">The overall title of the table. Displayed at the top</td>
+--     <td class="tg-1">""</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">titleColor</td>
+--     <td class="tg-2">What color to use for the title text</td>
+--     <td class="tg-2">formatColor</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">printTitle</td>
+--     <td class="tg-1">Should we print the title of the table at the very tip-top?</td>
+--     <td class="tg-1">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">headerTitle</td>
+--     <td class="tg-2">Use the same separator for the column headers as the title/top, rather than the row separator</td>
+--     <td class="tg-2">formatColor</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">forceHeaderSeparator</td>
+--     <td class="tg-1">Force a separator between the column headers and the first row, even if rowSeparator is false.</td>
+--     <td class="tg-1">false</td>
+--   </tr>
+-- </tbody>
+-- </table>
+function TableMaker:new(options)
+  local funcName = "TableMaker:new(options)"
+  local me = {}
+  setmetatable(me, self)
+  self.__index = self
+  if options == nil then
+    options = {}
+  end
+  if type(options) ~= "table" then
+    error("TableMaker:new(options): ArgumentError: options expected as table, got " .. type(options))
+  end
+  local options = table.deepcopy(options)
+  if options.allowPopups == true then
+    options.autoEcho = true
+  else
+    options.allowPopups = false
+  end
+  local columns = false
+  if options.columns then
+    if type(options.columns) ~= "table" then
+      error("TableMaker:new(options): option error: You provided an options.columns entry of type " .. type(options.columns) ..
+              " and columns must a table with entries suitable for TableFormatter:addColumn().")
+    end
+    columns = table.deepcopy(options.columns)
+    options.columns = nil
+  end
+  local rows = false
+  if options.rows then
+    if type(options.rows) ~= "table" then
+      error("TableMaker:new(options): option error: You provided an options.rows entry of type " .. type(options.rows) ..
+              " and rows must be a table with entrys suitable for TableFormatter:addRow()")
+    end
+    rows = table.deepcopy(options.rows)
+    options.rows = nil
+  end
+  for option, value in pairs(options) do
+    me[option] = value
+  end
+  local dec = {"d", "decimal", "dec"}
+  local hex = {"h", "hexidecimal", "hex"}
+  local col = {"c", "color", "colour", "col", "name"}
+  if table.contains(dec, me.formatType) then
+    me.frameColor = me.frameColor or "<255,255,255>"
+    me.separatorColor = me.separatorColor or me.frameColor
+    me.titleColor = me.titleColor or me.frameColor
+    me.colorReset = "<r>"
+  elseif table.contains(hex, me.formatType) then
+    me.frameColor = me.frameColor or "#ffffff"
+    me.separatorColor = me.separatorColor or me.frameColor
+    me.titleColor = me.titleColor or me.frameColor
+    me.colorReset = "#r"
+  elseif table.contains(col, me.formatType) then
+    me.frameColor = me.frameColor or "<white>"
+    me.separatorColor = me.separatorColor or me.frameColor
+    me.titleColor = me.titleColor or me.frameColor
+    me.colorReset = "<reset>"
+  else
+    me.frameColor = ""
+    me.separatorColor = ""
+    me.titleColor = ""
+    me.colorReset = ""
+  end
+  me.columns = {}
+  me.rows = {}
+  if columns then
+    for _, column in ipairs(columns) do
+      me:addColumn(column)
+    end
+  end
+  if rows then
+    for _, row in ipairs(rows) do
+      me:addRow(row)
+    end
+  end
+  return me
+end
+ftext.TableMaker = TableMaker
+
+return ftext
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/source/ftext_spec.lua.html b/src/resources/MDK/doc/source/ftext_spec.lua.html new file mode 100755 index 0000000..4cd397e --- /dev/null +++ b/src/resources/MDK/doc/source/ftext_spec.lua.html @@ -0,0 +1,546 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

ftext_spec.lua

+
+local ftext = require("MDK.ftext")
+
+describe("ftext:", function()
+  describe("ftext.fText:", function()
+    local fText = ftext.fText
+
+    it("Should properly center text", function()
+      local expected = "     some text      "
+      local actual = fText("some text", {width = 20})
+      assert.equals(expected, actual)
+      assert.equals(20, actual:len())
+    end)
+
+    it("Should properly pad left aligned text", function()
+      local expected = "some text           "
+      local actual = fText("some text", {width = 20, alignment = "left"})
+      assert.equals(expected, actual)
+      assert.equals(20, actual:len())
+    end)
+
+    it("Should properly pad right aligned text", function()
+      local expected = "           some text"
+      local actual = fText("some text", {width = 20, alignment = "right"})
+      assert.equals(expected, actual)
+      assert.equals(20, actual:len())
+    end)
+
+    it("Should wrap lines to the correct length", function()
+      local str = "This is a test of the emergency broadcast system. This is only a test"
+      local options = {width = 10, alignment = "centered"}
+      local actual = fText(str, options)
+      for _, line in ipairs(actual:split("\n")) do
+        assert.equals(line:len(), 10)
+      end
+      options.width = 15
+      actual = fText(str, options)
+      for _, line in ipairs(actual:split("\n")) do
+        assert.equals(line:len(), 15)
+      end
+    end)
+
+    describe("non-space spacer character:", function()
+      local str = "some text"
+      local options = {width = "20", alignment = "left", spacer = "="}
+      it("Should work with left align", function()
+        local expected = "some text =========="
+        local actual = fText(str, options)
+        assert.equals(expected, actual)
+        assert.equals(20, actual:len())
+      end)
+
+      it("Should work with right align", function()
+        local expected = "========== some text"
+        options.alignment = "right"
+        local actual = fText(str, options)
+        assert.equals(expected, actual)
+        assert.equals(20, actual:len())
+      end)
+
+      it("Should work with center align", function()
+        local expected = ("==== some text =====")
+        options.alignment = "center"
+        local actual = fText(str, options)
+        assert.equals(expected, actual)
+        assert.equals(20, actual:len())
+      end)
+    end)
+
+    describe("nogap option:", function()
+      local str = "some text"
+      local options = {width = "20", alignment = "left", spacer = "=", nogap = true}
+
+      it("Should work with left align", function()
+        local expected = "some text==========="
+        local actual = fText(str, options)
+        assert.equals(expected, actual)
+        assert.equals(20, actual:len())
+      end)
+
+      it("Should work with right align", function()
+        local expected = "===========some text"
+        options.alignment = "right"
+        local actual = fText(str, options)
+        assert.equals(expected, actual)
+        assert.equals(20, actual:len())
+      end)
+
+      it("Should work with center align", function()
+        local expected = "=====some text======"
+        options.alignment = "center"
+        local actual = fText(str, options)
+        assert.equals(expected, actual)
+        assert.equals(20, actual:len())
+      end)
+    end)
+
+    describe("cap functionality", function()
+      local str = "some text"
+      local options = {width = 20, spacer = "=", cap = "|"}
+
+      it("Should place the spacer outside the cap by default", function()
+        local expected = "===| some text |===="
+        local actual = fText(str, options)
+        assert.equals(expected, actual)
+        assert.equals(20, actual:len())
+      end)
+
+      it("Should place it inside the cap if inside option is true", function()
+        local expected = "|=== some text ====|"
+        options.inside = true
+        local actual = fText(str, options)
+        options.inside = nil
+        assert.equals(expected, actual)
+        assert.equals(20, actual:len())
+      end)
+
+      it("Should mirror certain characters with their opposites", function()
+        local expected = "===[ some text ]===="
+        options.mirror = true
+        options.cap = "["
+        local actual = fText(str, options)
+        assert.equals(expected, actual)
+        options.inside = true
+        expected = "[=== some text ====]"
+        actual = fText(str, options)
+        assert.equals(expected, actual)
+        options.inside = nil
+        options.cap = "<"
+        expected = "===< some text >===="
+        actual = fText(str, options)
+        assert.equals(expected, actual)
+        options.cap = "{"
+        expected = "==={ some text }===="
+        actual = fText(str, options)
+        assert.equals(expected, actual)
+        options.cap = "("
+        expected = "===( some text )===="
+        actual = fText(str, options)
+        assert.equals(expected, actual)
+        options.cap = "|"
+        expected = "===| some text |===="
+        actual = fText(str, options)
+        assert.equals(expected, actual)
+      end)
+    end)
+  end)
+
+  describe("ftext.cfText", function()
+    local cfText = ftext.cfText
+    local str = "some text"
+    local options = {
+      width = 20,
+      spacer = "=",
+      cap = "[",
+      inside = true,
+      mirror = true,
+      capColor = "<purple>",
+      spacerColor = "<green>",
+      textColor = "<red>",
+    }
+    it("Should handle cecho colored text", function()
+      local expectedStripped = "[=== some text ====]"
+      local expected = "<purple>[<reset><green>===<reset><red> some text <reset><green>====<reset><purple>]<reset>"
+      local actual = cfText(str, options)
+      local actualStripped = cecho2string(actual)
+      assert.equals(expected, actual)
+      assert.equals(expectedStripped, actualStripped)
+      assert.equals(20, actualStripped:len())
+      expectedStripped = "===[ some text ]===="
+      expected = "<green>===<reset><purple>[<reset><red> some text <reset><purple>]<reset><green>====<reset>"
+      options.inside = false
+      actual = cfText(str, options)
+      actualStripped = cecho2string(actual)
+      assert.equals(expected, actual)
+      assert.equals(expectedStripped, actualStripped)
+      assert.equals(20, actualStripped:len())
+    end)
+
+    it("Should wrap cecho lines to the correct length", function()
+      local str = "This is a test of the emergency broadcast system. This is only a test"
+      local options = {width = 10, alignment = "centered"}
+      local actual = cfText(str, options)
+      for _, line in ipairs(actual:split("\n")) do
+        assert.equals(cecho2string(line):len(), 10)
+      end
+      options.width = 15
+      actual = cfText(str, options)
+      for _, line in ipairs(actual:split("\n")) do
+        assert.equals(cecho2string(line):len(), 15)
+      end
+    end)
+
+  end)
+
+  describe("ftext.dfText", function()
+    local dfText = ftext.dfText
+    local str = "some text"
+    local options = {
+      width = 20,
+      spacer = "=",
+      cap = "[",
+      inside = true,
+      mirror = true,
+      capColor = "<160,32,240>",
+      spacerColor = "<0,255,0>",
+      textColor = "<255,0,0>",
+    }
+    it("Should handle decho colored text", function()
+      local expectedStripped = "[=== some text ====]"
+      local expected = "<160,32,240>[<r><0,255,0>===<r><255,0,0> some text <r><0,255,0>====<r><160,32,240>]<r>"
+      local actual = dfText(str, options)
+      local actualStripped = decho2string(actual)
+      assert.equals(expected, actual)
+      assert.equals(expectedStripped, actualStripped)
+      assert.equals(20, actualStripped:len())
+      expectedStripped = "===[ some text ]===="
+      expected = "<0,255,0>===<r><160,32,240>[<r><255,0,0> some text <r><160,32,240>]<r><0,255,0>====<r>"
+      options.inside = false
+      actual = dfText(str, options)
+      actualStripped = decho2string(actual)
+      assert.equals(expected, actual)
+      assert.equals(expectedStripped, actualStripped)
+      assert.equals(20, actualStripped:len())
+    end)
+
+    it("Should wrap decho lines to the correct length", function()
+      local str = "This is a test of the emergency broadcast system. This is only a test"
+      local options = {width = 10, alignment = "centered"}
+      local actual = dfText(str, options)
+      for _, line in ipairs(actual:split("\n")) do
+        assert.equals(decho2string(line):len(), 10)
+      end
+      options.width = 15
+      actual = dfText(str, options)
+      for _, line in ipairs(actual:split("\n")) do
+        assert.equals(decho2string(line):len(), 15)
+      end
+    end)
+  end)
+
+  describe("ftext.hfText", function()
+    local hfText = ftext.hfText
+    local str = "some text"
+    local options = {
+      width = 20,
+      spacer = "=",
+      cap = "[",
+      inside = true,
+      mirror = true,
+      capColor = "#a020f0",
+      spacerColor = "#00ff00",
+      textColor = "#ff0000",
+    }
+    it("Should handle hecho colored text", function()
+      local expectedStripped = "[=== some text ====]"
+      local expected = "#a020f0[#r#00ff00===#r#ff0000 some text #r#00ff00====#r#a020f0]#r"
+      local actual = hfText(str, options)
+      local actualStripped = hecho2string(actual)
+      assert.equals(expected, actual)
+      assert.equals(expectedStripped, actualStripped)
+      assert.equals(20, actualStripped:len())
+      expectedStripped = "===[ some text ]===="
+      expected = "#00ff00===#r#a020f0[#r#ff0000 some text #r#a020f0]#r#00ff00====#r"
+      options.inside = false
+      actual = hfText(str, options)
+      actualStripped = hecho2string(actual)
+      assert.equals(expected, actual)
+      assert.equals(expectedStripped, actualStripped)
+      assert.equals(20, actualStripped:len())
+    end)
+
+    it("Should wrap hecho lines to the correct length", function()
+      local str = "This is a test of the emergency broadcast system. This is only a test"
+      local options = {width = 10, alignment = "centered"}
+      local actual = hfText(str, options)
+      for _, line in ipairs(actual:split("\n")) do
+        assert.equals(hecho2string(line):len(), 10)
+      end
+      options.width = 15
+      actual = hfText(str, options)
+      for _, line in ipairs(actual:split("\n")) do
+        assert.equals(hecho2string(line):len(), 15)
+      end
+    end)
+  end)
+
+  describe("ftext.TextFormatter", function()
+    local tf = ftext.TextFormatter
+    local str = "some text"
+    local formatter
+
+    before_each(function()
+      formatter = tf:new({width = 20})
+    end)
+
+    it("Should let you change width using :setWidth", function()
+      formatter:setWidth(80)
+      local expected =
+        "<white><reset><white>                                  <reset><white> some text <reset><white>                                   <reset><white><reset>"
+      local actual = formatter:format(str)
+      assert.equals(expected, actual)
+      assert.equals(80, cecho2string(actual):len())
+    end)
+
+    it("Should format for cecho by default", function()
+      local expected = "<white><reset><white>    <reset><white> some text <reset><white>     <reset><white><reset>"
+      local expectedStripped = "     some text      "
+      local actual = formatter:format(str)
+      local actualStripped = cecho2string(actual)
+      assert.equals(expected, actual)
+      assert.equals(expectedStripped, actualStripped)
+      assert.equals(20, actualStripped:len())
+    end)
+
+    it("Should produce the same line as cfText given the same options", function()
+      local expected = ftext.cfText(str, formatter.options)
+      local actual = formatter:format(str)
+      assert.equals(expected, actual)
+    end)
+
+    it("Should let you change type using :setType", function()
+      formatter:setType("h")
+      local expected = ftext.hfText(str, formatter.options)
+      local actual = formatter:format(str)
+      assert.equals(expected, actual)
+      formatter:setType("d")
+      expected = ftext.dfText(str, formatter.options)
+      actual = formatter:format(str)
+      assert.equals(expected, actual)
+      formatter:setType("")
+      expected = ftext.fText(str, formatter.options)
+      actual = formatter:format(str)
+      assert.equals(expected, actual)
+    end)
+
+    it("Should default to word wrapping, and let you change it with :setWrap", function()
+      formatter:setWidth(10)
+      local expected =
+        "<white><reset><white>  <reset><white> some <reset><white>  <reset><white><reset>\n<white><reset><white>  <reset><white> text <reset><white>  <reset><white><reset>"
+      local actual = formatter:format(str)
+      assert.equals(expected, actual)
+      expected = "<white><reset><white><reset><white> some text <reset><white><reset><white><reset>"
+      formatter:setWrap(false)
+      actual = formatter:format(str)
+      assert.equals(expected, actual)
+    end)
+
+    it("Should allow you to change the cap using :setCap", function()
+      formatter:setCap('|')
+      local expected = "<white>|<reset><white>   <reset><white> some text <reset><white>    <reset><white>|<reset>"
+      local actual = formatter:format(str)
+      assert.equals(expected, actual)
+    end)
+
+    it("Should allow you to change the capColor using :setCapColor", function()
+      formatter:setCapColor('<red>')
+      local expected = "<red><reset><white>    <reset><white> some text <reset><white>     <reset><red><reset>"
+      local actual = formatter:format(str)
+      assert.equals(expected, actual)
+    end)
+
+    it("Should allow you to change the spacer color using :setSpacerColor", function()
+      formatter:setSpacerColor("<red>")
+      local expected = "<white><reset><red>    <reset><white> some text <reset><red>     <reset><white><reset>"
+      local actual = formatter:format(str)
+      assert.equals(expected, actual)
+    end)
+
+    it("Should allow you to change the text color using :setTextColor", function()
+      formatter:setTextColor("<red>")
+      local expected = "<white><reset><white>    <reset><red> some text <reset><white>     <reset><white><reset>"
+      local actual = formatter:format(str)
+      assert.equals(expected, actual)
+    end)
+
+    it("Should allow you to change the spacer using :setSpacer", function()
+      formatter:setSpacer("=")
+      -- local expected = "<white><reset><white>    <reset><white> some text <reset><white>     <reset><white><reset>"
+      local expected = "<white><reset><white>====<reset><white> some text <reset><white>=====<reset><white><reset>"
+      local actual = formatter:format(str)
+      assert.equals(expected, actual)
+    end)
+
+    it("Should allow you to set the alignment using :setAlignment", function()
+      formatter:setAlignment("left")
+      local expected = "<white><reset><white><reset><white>some text <reset><white>          <reset><white><reset>"
+      local actual = formatter:format(str)
+      assert.equals(expected, actual)
+      formatter:setAlignment("right")
+      expected = "<white><reset><white>          <reset><white> some text<reset><white><reset><white><reset>"
+      actual = formatter:format(str)
+      assert.equals(expected, actual)
+    end)
+
+    it("Should allow you to change the 'inside' option using :setInside", function()
+      formatter:setInside(false)
+      local expected = "<white>    <reset><white><reset><white> some text <reset><white><reset><white>     <reset>"
+      local actual = formatter:format(str)
+      assert.equals(expected, actual)
+    end)
+
+    it("Should allow you to change the mirror option using :setMirror", function()
+      formatter:setCap('<')
+      formatter:setMirror(true)
+      local expected = "<white><<reset><white>   <reset><white> some text <reset><white>    <reset><white>><reset>"
+      local actual = formatter:format(str)
+      assert.equal(expected, actual)
+    end)
+  end)
+
+  describe("ftext.TableMaker", function()
+    local TableMaker = ftext.TableMaker
+    local tm
+    before_each(function()
+      tm = TableMaker:new()
+      tm:addColumn({name = "col1", width = 15, textColor = "<red>"})
+      tm:addColumn({name = "col2", width = 15, textColor = "<blue>"})
+      tm:addColumn({name = "col3", width = 15, textColor = "<green>"})
+      tm:addRow({"some text", "more text", "other text"})
+      tm:addRow({"little text", "bigger text", "text"})
+    end)
+
+    it("Should assemble a formatted table given default options", function()
+      local expected = [[<white>*************************************************<reset>
+<white>*<reset><white><reset><white>    <reset><red> col1 <reset><white>     <reset><white><reset><white>|<reset><white><reset><white>    <reset><blue> col2 <reset><white>     <reset><white><reset><white>|<reset><white><reset><white>    <reset><green> col3 <reset><white>     <reset><white><reset><white>*<reset>
+<white>*<reset><white>---------------<reset><white>|<reset><white>---------------<reset><white>|<reset><white>---------------<reset><white>*<reset>
+<white>*<reset><white><reset><white>  <reset><red> some text <reset><white>  <reset><white><reset><white>|<reset><white><reset><white>  <reset><blue> more text <reset><white>  <reset><white><reset><white>|<reset><white><reset><white> <reset><green> other text <reset><white>  <reset><white><reset><white>*<reset>
+<white>*<reset><white>---------------<reset><white>|<reset><white>---------------<reset><white>|<reset><white>---------------<reset><white>*<reset>
+<white>*<reset><white><reset><white> <reset><red> little text <reset><white> <reset><white><reset><white>|<reset><white><reset><white> <reset><blue> bigger text <reset><white> <reset><white><reset><white>|<reset><white><reset><white>    <reset><green> text <reset><white>     <reset><white><reset><white>*<reset>
+<white>*************************************************<reset>
+]]
+      local actual = tm:assemble()
+      assert.equals(expected, actual)
+    end)
+
+    it("TableMaker:getCell should return the text and formatter for a specific cell", function()
+      local expectedText = "more text"
+      local expectedFormatter = tm.columns[2]
+      local actualText, actualFormatter = tm:getCell(1, 2)
+      assert.equals(expectedText, actualText)
+      assert.equals(expectedFormatter, actualFormatter)
+      local expectedFormatted = "<white><reset><white>  <reset><blue> more text <reset><white>  <reset><white><reset>"
+      local actualFormatted = actualFormatter:format(actualText)
+      assert.equals(expectedFormatted, actualFormatted)
+    end)
+  end)
+end)
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/source/gradientmaker.lua.html b/src/resources/MDK/doc/source/gradientmaker.lua.html new file mode 100755 index 0000000..0205df3 --- /dev/null +++ b/src/resources/MDK/doc/source/gradientmaker.lua.html @@ -0,0 +1,441 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

gradientmaker.lua

+
+--- Module which provides for creating color gradients for your text.
+-- Original functions found on <a href="https://forums.lusternia.com/discussion/3261/anyone-want-text-gradients">the Lusternia Forums</a>
+-- <br> I added functions to work with hecho.
+-- <br> I also made performance enhancements by storing already calculated gradients after first use for the session and only including the colorcode in the returned string if the color changed.
+-- @module GradientMaker
+-- @author Sylphas on the Lusternia forums
+-- @author Damian Monogue <demonnic@gmail.com>
+-- @copyright 2018 Sylphas
+-- @copyright 2020 Damian Monogue
+local GradientMaker = {}
+local gradient_table = {}
+
+local function _clamp(num1, num2, num3)
+  local smaller = math.min(num2, num3)
+  local larger = math.max(num2, num3)
+  local minimum = math.max(0, smaller)
+  local maximum = math.min(255, larger)
+  return math.min(maximum, math.max(minimum, num1))
+end
+
+local function _gradient(length, rgb1, rgb2)
+  assert(length > 0)
+  if length == 1 then
+    return {rgb1}
+  elseif length == 2 then
+    return {rgb1, rgb2}
+  else
+    local step = {}
+    for color = 1, 3 do
+      step[color] = (rgb2[color] - rgb1[color]) / (length - 2)
+    end
+    local gradient = {rgb1}
+    for iter = 1, length - 2 do
+      gradient[iter + 1] = {}
+      for color = 1, 3 do
+        gradient[iter + 1][color] = math.ceil(rgb1[color] + (iter * step[color]))
+      end
+    end
+    gradient[length] = rgb2
+    for index, color in ipairs(gradient) do
+      for iter = 1, 3 do
+        gradient[index][iter] = _clamp(color[iter], rgb1[iter], rgb2[iter])
+      end
+    end
+    return gradient
+  end
+end
+
+local function gradient_to_string(gradient)
+  local gradstring = ""
+  for _, grad in ipairs(gradient) do
+    local nodestring = ""
+    for _, col in ipairs(grad) do
+      nodestring = string.format("%s%03d", nodestring, col)
+    end
+    if _ == 1 then
+      gradstring = nodestring
+    else
+      gradstring = gradstring .. "|" .. nodestring
+    end
+  end
+  return gradstring
+end
+
+local function _gradients(length, ...)
+  local arg = {...}
+  local argkey = gradient_to_string(arg)
+  local gradients_for_length = gradient_table[length]
+  if not gradients_for_length then
+    gradient_table[length] = {}
+    gradients_for_length = gradient_table[length]
+  end
+  local grads = gradients_for_length[argkey]
+  if grads then
+    return grads
+  end
+  if #arg == 0 then
+    gradients_for_length[argkey] = {}
+    return {}
+  elseif #arg == 1 then
+    gradients_for_length[argkey] = arg[1]
+    return arg[1]
+  elseif #arg == 2 then
+    gradients_for_length[argkey] = _gradient(length, arg[1], arg[2])
+    return gradients_for_length[argkey]
+  else
+    local quotient = math.floor(length / (#arg - 1))
+    local remainder = length % (#arg - 1)
+    local gradients = {}
+    for section = 1, #arg - 1 do
+      local slength = quotient
+      if section <= remainder then
+        slength = slength + 1
+      end
+      local gradient = _gradient(slength, arg[section], arg[section + 1])
+      for _, rgb in ipairs(gradient) do
+        table.insert(gradients, rgb)
+      end
+    end
+    gradients_for_length[argkey] = gradients
+    return gradients
+  end
+end
+
+local function _color_name(rgb)
+  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
+  return cname
+end
+
+local function errorIfEmpty(text, funcName)
+  assert(#text > 0, string.format("%s: you passed in an empty string, and I cannot make a gradient out of an empty string", funcName))
+end
+
+local function dgradient_table(text, ...)
+  errorIfEmpty(text, "dgradient_table")
+  local gradients = _gradients(#text, ...)
+  local dgrads = {}
+  for character = 1, #text do
+    table.insert(dgrads, {gradients[character], text:sub(character, character)})
+  end
+  return dgrads
+end
+
+local function dgradient(text, ...)
+  errorIfEmpty(text, "dgradient")
+  local gradients = _gradients(#text, ...)
+  local dgrad = ""
+  local current_color = ""
+  for character = 1, #text do
+    local new_color = "<" .. table.concat(gradients[character], ",") .. ">"
+    local char = text:sub(character, character)
+    if new_color == current_color then
+      dgrad = dgrad .. char
+    else
+      dgrad = dgrad .. new_color .. char
+      current_color = new_color
+    end
+  end
+  return dgrad
+end
+
+local function cgradient_table(text, ...)
+  errorIfEmpty(text, "cgradient_table")
+  local gradients = _gradients(#text, ...)
+  local cgrads = {}
+  for character = 1, #text do
+    table.insert(cgrads, {_color_name(gradients[character]), text:sub(character, character)})
+  end
+  return cgrads
+end
+
+local function cgradient(text, ...)
+  errorIfEmpty(text, "cgradient")
+  local gradients = _gradients(#text, ...)
+  local cgrad = ""
+  local current_color = ""
+  for character = 1, #text do
+    local new_color = "<" .. _color_name(gradients[character]) .. ">"
+    local char = text:sub(character, character)
+    if new_color == current_color then
+      cgrad = cgrad .. char
+    else
+      cgrad = cgrad .. new_color .. char
+      current_color = new_color
+    end
+  end
+  return cgrad
+end
+
+local hex = Geyser.Color.hex
+
+local function hgradient_table(text, ...)
+  errorIfEmpty(text, "hgradient_table")
+  local grads = _gradients(#text, ...)
+  local hgrads = {}
+  for character = 1, #text do
+    table.insert(hgrads, {hex(unpack(grads[character])):sub(2, -1), text:sub(character, character)})
+  end
+  return hgrads
+end
+
+local function hgradient(text, ...)
+  errorIfEmpty(text, "hgradient")
+  local grads = _gradients(#text, ...)
+  local hgrads = ""
+  local current_color = ""
+  for character = 1, #text do
+    local new_color = hex(unpack(grads[character]))
+    local char = text:sub(character, character)
+    if new_color == current_color then
+      hgrads = hgrads .. char
+    else
+      hgrads = hgrads .. new_color .. char
+      current_color = new_color
+    end
+  end
+  return hgrads
+end
+
+local function color_name(...)
+  local arg = {...}
+  if #arg == 1 then
+    return _color_name(arg[1])
+  elseif #arg == 3 then
+    return _color_name(arg)
+  else
+    local errmsg =
+      "color_name: You must pass either a table of r,g,b values: color_name({r,g,b})\nor the three r,g,b values separately: color_name(r,g,b)"
+    error(errmsg)
+  end
+end
+
+--- Returns the closest color name to a given r,g,b color
+-- @param r The red component. Can also pass the full color as a table, IE { 255, 0, 0 }
+-- @param g The green component. If you pass the color as a table as noted above, this param should be empty
+-- @param b the blue components. If you pass the color as a table as noted above, this param should be empty
+-- @usage
+-- closest_color = GradientMaker.color_name(128,200,30) -- returns "ansi_149"
+-- closest_color = GradientMaker.color_name({128, 200, 30}) -- this is functionally equivalent to the first one
+function GradientMaker.color_name(...)
+  return color_name(...)
+end
+
+--- Returns the text, with the defined color gradients applied and formatted for us with decho. Usage example below produces the following text
+-- <br><img src="https://demonnic.github.io/mdk/images/dechogradient.png" alt="dgradient example">
+-- @tparam string text The text you want to apply the color gradients to
+-- @param first_color The color you want it to start at. Table of colors in { r, g, b } format
+-- @param second_color The color you want the gradient to transition to first. Table of colors in { r, g, b } format
+-- @param next_color Keep repeating if you want it to transition from the second color to a third, then a third to a fourth, etc
+-- @see cgradient
+-- @see hgradient
+-- @usage
+-- decho(GradientMaker.dgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {255,0,0}, {255,128,0}, {255,255,0}, {0,255,0}, {0,255,255}, {0,128,255}, {128,0,255}))
+-- decho(GradientMaker.dgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {255,0,0}, {0,0,255}))
+-- decho(GradientMaker.dgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {50,50,50}, {0,255,0}, {50,50,50}))
+function GradientMaker.dgradient(text, ...)
+  return dgradient(text, ...)
+end
+
+--- Returns the text, with the defined color gradients applied and formatted for us with cecho. Usage example below produces the following text
+-- <br><img src="https://demonnic.github.io/mdk/images/cechogradient.png" alt="cgradient example">
+-- @tparam string text The text you want to apply the color gradients to
+-- @param first_color The color you want it to start at. Table of colors in { r, g, b } format
+-- @param second_color The color you want the gradient to transition to first. Table of colors in { r, g, b } format
+-- @param next_color Keep repeating if you want it to transition from the second color to a third, then a third to a fourth, etc
+-- @see dgradient
+-- @see hgradient
+-- @usage
+-- cecho(GradientMaker.cgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {255,0,0}, {255,128,0}, {255,255,0}, {0,255,0}, {0,255,255}, {0,128,255}, {128,0,255}))
+-- cecho(GradientMaker.cgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {255,0,0}, {0,0,255}))
+-- cecho(GradientMaker.cgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {50,50,50}, {0,255,0}, {50,50,50}))
+function GradientMaker.cgradient(text, ...)
+  return cgradient(text, ...)
+end
+
+--- Returns the text, with the defined color gradients applied and formatted for us with hecho. Usage example below produces the following text
+-- <br><img src="https://demonnic.github.io/mdk/images/hechogradient.png" alt="hgradient example">
+-- @tparam string text The text you want to apply the color gradients to
+-- @param first_color The color you want it to start at. Table of colors in { r, g, b } format
+-- @param second_color The color you want the gradient to transition to first. Table of colors in { r, g, b } format
+-- @param next_color Keep repeating if you want it to transition from the second color to a third, then a third to a fourth, etc
+-- @see cgradient
+-- @see dgradient
+-- @usage
+-- hecho(GradientMaker.hgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {255,0,0}, {255,128,0}, {255,255,0}, {0,255,0}, {0,255,255}, {0,128,255}, {128,0,255}))
+-- hecho(GradientMaker.hgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {255,0,0}, {0,0,255}))
+-- hecho(GradientMaker.hgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {50,50,50}, {0,255,0}, {50,50,50}))
+function GradientMaker.hgradient(text, ...)
+  return hgradient(text, ...)
+end
+
+--- Returns a table, each element of which is a table, the first element of which is the color name to use and the character which should be that color
+-- @tparam string text The text you want to apply the color gradients to
+-- @param first_color The color you want it to start at. Table of colors in { r, g, b } format
+-- @param second_color The color you want the gradient to transition to first. Table of colors in { r, g, b } format
+-- @param next_color Keep repeating if you want it to transition from the second color to a third, then a third to a fourth, etc
+-- @see cgradient
+function GradientMaker.cgradient_table(text, ...)
+  return cgradient_table(text, ...)
+end
+
+--- Returns a table, each element of which is a table, the first element of which is the color({r,g,b} format) to use and the character which should be that color
+-- @tparam string text The text you want to apply the color gradients to
+-- @param first_color The color you want it to start at. Table of colors in { r, g, b } format
+-- @param second_color The color you want the gradient to transition to first. Table of colors in { r, g, b } format
+-- @param next_color Keep repeating if you want it to transition from the second color to a third, then a third to a fourth, etc
+-- @see dgradient
+function GradientMaker.dgradient_table(text, ...)
+  return dgradient_table(text, ...)
+end
+
+--- Returns a table, each element of which is a table, the first element of which is the color(in hex) to use and the second element of which is the character which should be that color
+-- @tparam string text The text you want to apply the color gradients to
+-- @param first_color The color you want it to start at. Table of colors in { r, g, b } format
+-- @param second_color The color you want the gradient to transition to first. Table of colors in { r, g, b } format
+-- @param next_color Keep repeating if you want it to transition from the second color to a third, then a third to a fourth, etc
+-- @see hgradient
+function GradientMaker.hgradient_table(text, ...)
+  return hgradient_table(text, ...)
+end
+
+--- Creates global copies of the c/d/hgradient(_table) functions and color_name for use without accessing the module table
+-- @usage
+-- GradientMaker.install_global()
+-- cecho(cgradient(...)) -- use cgradient directly now
+function GradientMaker.install_global()
+  _G["hgradient"] = function(...)
+    return hgradient(...)
+  end
+  _G["dgradient"] = function(...)
+    return dgradient(...)
+  end
+  _G["cgradient"] = function(...)
+    return cgradient(...)
+  end
+  _G["hgradient_table"] = function(...)
+    return hgradient_table(...)
+  end
+  _G["dgradient_table"] = function(...)
+    return dgradient_table(...)
+  end
+  _G["cgradient_table"] = function(...)
+    return cgradient_table(...)
+  end
+  _G["color_name"] = function(...)
+    return color_name(...)
+  end
+end
+
+-- function GradientMaker.getGrads()
+--   return gradient_table
+-- end
+
+return GradientMaker
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/source/loggingconsole.lua.html b/src/resources/MDK/doc/source/loggingconsole.lua.html new file mode 100755 index 0000000..f8e4016 --- /dev/null +++ b/src/resources/MDK/doc/source/loggingconsole.lua.html @@ -0,0 +1,560 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

loggingconsole.lua

+
+--- MiniConsole with logging capabilities
+-- @classmod LoggingConsole
+-- @author Damian Monogue <demonnic@gmail.com>
+-- @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
+-- <table class="tg">
+-- <thead>
+--   <tr>
+--     <th>option name</th>
+--     <th>description</th>
+--     <th>default</th>
+--   </tr>
+-- </thead>
+-- <tbody>
+--   <tr>
+--     <td class="tg-1">log</td>
+--     <td class="tg-1">Should the miniconsole be logging?</td>
+--     <td class="tg-1">true</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">logFormat</td>
+--     <td class="tg-2">"h" for html, "t" for plaintext, "l" for log (with ansi)</td>
+--     <td class="tg-2">h</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">path</td>
+--     <td class="tg-1">The path the file lives in. It is templated.<br>|h is replaced by the profile homedir.<br>|y by 4 digit year.<br>|m by 2 digit month<br>|d by 2 digit day<br>|n by the name constraint<br>|e by the file extension (html for h logType, log for others)</td>
+--     <td class="tg-1">"|h/log/consoleLogs/|y/|m/|d/"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">fileName</td>
+--     <td class="tg-2">The name of the log file. It is templated, same as path above</td>
+--     <td class="tg-2">"|n.|e"</td>
+--   </tr>
+-- </tbody>
+-- </table>
+-- @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.<br>|h is replaced by the profile homedir.<br>|y by 4 digit year.<br>|m by 2 digit month<br>|d by 2 digit day<br>|n by the name constraint<br>|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)
+  fileName = fileName:gsub("[<>:'\"/\\?*]", "_")
+  return fileName
+end
+
+--- Sets the fileName to use for the log file.
+-- @param fileName the fileName to use for the logfile. It is templated.<br>|h is replaced by the profile homedir.<br>|y by 4 digit year.<br>|m by 2 digit month<br>|d by 2 digit day<br>|n by the name constraint<br>|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
+  fullPath = fullPath:gsub("|", "_")
+  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"
+  elseif logType ~= "t" then
+    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
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/source/loginator.lua.html b/src/resources/MDK/doc/source/loginator.lua.html new file mode 100755 index 0000000..026a4d6 --- /dev/null +++ b/src/resources/MDK/doc/source/loginator.lua.html @@ -0,0 +1,555 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

loginator.lua

+
+--- Loginator creates an object which allows you to log things to file at
+-- various severity levels, with the ability to only log items above a specific
+-- severity to file.
+-- @classmod Loginator
+-- @author Damian Monogue <demonnic@gmail.com>
+-- @copyright 2021 Damian Monogue
+-- @license MIT, see LICENSE.lua
+local Loginator = {
+  format = "h",
+  name = "logname",
+  fileNameTemplate = "|p/log/Loginator/|y-|M-|d-|n.|e",
+  entryTemplate = "|y-|M-|d |h:|m:|s.|x [|c|l|r] |t",
+  level = "warn",
+  bgColor = "black",
+  fontSize = 12,
+  fgColor = "white",
+}
+
+local levelColors = {error = "red", warn = "DarkOrange", info = "ForestGreen", debug = "ansi_yellow"}
+local loggerLevels = {error = 1, warn = 2, info = 3, debug = 4}
+
+local function exists(path)
+  local ok, err, code = os.rename(path, path)
+  if not ok and code == 13 then
+    return true
+  end
+  return ok, err
+end
+
+local function isWindows()
+  return package.config:sub(1, 1) == [[\]]
+end
+
+local function mkdir_p(path)
+  path = path:gsub("\\", "/")
+  local pathTbl = path:split("/")
+  local cwd = "/"
+  if isWindows() then
+    cwd = ""
+  end
+  for index, dirName in ipairs(pathTbl) do
+    if index == 1 then
+      cwd = cwd .. dirName
+    else
+      cwd = cwd .. "/" .. dirName
+      cwd = cwd:gsub("//", "/")
+    end
+    if not table.contains({"/", "C:"}, cwd) and not exists(cwd) then
+      local ok, err = lfs.mkdir(cwd)
+      if not ok then
+        return ok, err
+      end
+    end
+  end
+  return true
+end
+
+local htmlHeaderTemplate = [=[  <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+"http://www.w3.org/TR/html4/loose.dtd">
+<html>
+  <head>
+    <meta http-equiv="Content-Type" content="text/html;charset=utf-8" >
+    <link href='http://fonts.googleapis.com/css?family=Droid+Sans+Mono' rel='stylesheet' type='text/css'>
+    <style type="text/css">
+      body {
+        background-color: |b;
+        color: |c;
+        font-family: 'Droid Sans Mono';
+        white-space: pre;
+        font-size: |fpx;
+      }
+    </style>
+  </head>
+<body><span>
+]=]
+
+--- Creates a new Loginator object
+--@tparam table options table of options for the logger
+-- <table class="tg">
+-- <thead>
+--   <tr>
+--     <th>option name</th>
+--     <th>description</th>
+--     <th>default</th>
+--   </tr>
+-- </thead>
+-- <tbody>
+--   <tr>
+--     <td class="tg-1">format</td>
+--     <td class="tg-1">What format to log in? "h" for html, "a" for ansi, anything else for plaintext.</td>
+--     <td class="tg-1">"h"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">name</td>
+--     <td class="tg-2">What is the name of the logger? Will replace |n in templates</td>
+--     <td class="tg-2">logname</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">level</td>
+--     <td class="tg-1">What level should the logger operate at? This will control what level the log function defaults to, as well as what logs will actually be written<br>
+--                        Only items of an equal or higher severity to this will be written to the log file.</td>
+--     <td class="tg-1">"info"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">bgColor</td>
+--     <td class="tg-2">What background color to use for html logs</td>
+--     <td class="tg-2">"black"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">fgColor</td>
+--     <td class="tg-1">What color to use for the main text in html logs</td>
+--     <td class="tg-1">"white"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">fontSize</td>
+--     <td class="tg-2">What font size to use in html logs</td>
+--     <td class="tg-2">12</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">levelColors</td>
+--     <td class="tg-1">Table with the log level as the key, and the color which corresponds to it as the value</td>
+--     <td class="tg-1">{ error = "red", warn = "DarkOrange", info = "ForestGreen", debug = "ansi_yellow" }</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">fileNameTemplate</td>
+--     <td class="tg-2">A template which will be transformed into the full filename, with path. See template options below for replacements</td>
+--     <td class="tg-2">"|p/log/Loginator/|y-|M-|d-|n.|e"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">entryTemplate</td>
+--     <td class="tg-1">The template which controls the look of each log entry. See template options below for replacements</td>
+--     <td class="tg-1">"|y-|M-|d |h:|m:|s.|x [|c|l|r] |t"</td>
+--   </tr>
+-- </tbody>
+-- </table><br>
+-- Table of template options
+-- <table class="tg">
+-- <thead>
+--   <tr>
+--     <th>template code</th>
+--     <th>what it is replaced with</th>
+--     <th>example</th>
+--   </tr>
+-- </thead>
+-- <tbody>
+--   <tr>
+--     <td class="tg-1">|y</td>
+--     <td class="tg-1">the year in 4 digits</td>
+--     <td class="tg-1">2021</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">|p</td>
+--     <td class="tg-2">getMudletHomeDir()</td>
+--     <td class="tg-2">/home/demonnic/.config/mudlet/profiles/testprofile</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">|M</td>
+--     <td class="tg-1">Month as 2 digits</td>
+--     <td class="tg-1">05</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">|d</td>
+--     <td class="tg-2">day, as 2 digits</td>
+--     <td class="tg-2">23</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">|h</td>
+--     <td class="tg-1">hour in 24hr time format, 2 digits</td>
+--     <td class="tg-1">03</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">|m</td>
+--     <td class="tg-2">minute as 2 digits</td>
+--     <td class="tg-2">42</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">|s</td>
+--     <td class="tg-1">seconds as 2 digits</td>
+--     <td class="tg-1">34</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">|x</td>
+--     <td class="tg-2">milliseconds as 3 digits</td>
+--     <td class="tg-2">194</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">|e</td>
+--     <td class="tg-1">Filename extension expected. "html" for html format, "log" for everything else</td>
+--     <td class="tg-1">html</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">|l</td>
+--     <td class="tg-2">The logging level of the entry, in ALLCAPS</td>
+--     <td class="tg-2">WARN</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">|c</td>
+--     <td class="tg-1">The color which corresponds with the logging level. Set via the levelColors table in the options. Example not included.</td>
+--     <td class="tg-1"></td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">|r</td>
+--     <td class="tg-2">Reset back to standard color. Used to close |c. Example not included</td>
+--     <td class="tg-2"></td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">|n</td>
+--     <td class="tg-1">The name of the logger, set via the options when you have Loginator create it.</td>
+--     <td class="tg-1">CoolPackageLog</td>
+--   </tr>
+--</tbody>
+--</table>
+--@return newly created logger object
+function Loginator:new(options)
+  options = options or {}
+  local optionsType = type(options)
+  if optionsType ~= "table" then
+    return nil, f "Loginator:new(options) options as table expected, got {optionsType}"
+  end
+  local me = table.deepcopy(options)
+  me.levelColors = me.levelColors or {}
+  local lcType = type(me.levelColors)
+  if lcType ~= "table" then
+    return nil, f "Loginator:new(options) provided options.levelColors must be a table, but you provided a {lcType}"
+  end
+  for lvl,clr in pairs(levelColors) do
+    me.levelColors[lvl] = me.levelColors[lvl] or clr
+  end
+  setmetatable(me, self)
+  self.__index = self
+  return me
+end
+
+---@local
+function Loginator:processTemplate(str, level)
+  local lvl = level or self.level
+  local timeTable = getTime()
+  for what, with in pairs({
+    ["|y"] = function()
+      return timeTable.year
+    end,
+    ["|p"] = getMudletHomeDir,
+    ["|M"] = function()
+      return string.format("%02d", timeTable.month)
+    end,
+    ["|d"] = function()
+      return string.format("%02d", timeTable.day)
+    end,
+    ["|h"] = function()
+      return string.format("%02d", timeTable.hour)
+    end,
+    ["|m"] = function()
+      return string.format("%02d", timeTable.min)
+    end,
+    ["|s"] = function()
+      return string.format("%02d", timeTable.sec)
+    end,
+    ["|x"] = function()
+      return string.format("%03d", timeTable.msec)
+    end,
+    ["|e"] = function()
+      return (self.format:starts("h") and "html" or "log")
+    end,
+    ["|l"] = function()
+      return lvl:upper()
+    end,
+    ["|c"] = function()
+      return self:getColor(lvl)
+    end,
+    ["|r"] = function()
+      return self:getReset()
+    end,
+    ["|n"] = function()
+      return self.name
+    end,
+  }) do
+    if str:find(what) then
+      str = str:gsub(what, with())
+    end
+  end
+  return str
+end
+
+--- Set the color to associate with a logging level post-creation
+--@param color The color to set for the level, as a string. Can be any valid color string for cecho, decho, or hecho.
+--@param level The level to set the color for. Must be one of 'error', 'warn', 'info', or 'debug'
+--@return true if the color is updated, or nil+error if it could not be updated for some reason.
+function Loginator:setColorForLevel(color, level)
+  if not color then
+    return nil, "You must provide a color to set"
+  end
+  if not level then
+    return nil, "You must provide a level to set the color for"
+  end
+  if not loggerLevels[level] then
+    return nil, "Invalid level. Valid levels are 'error', 'warn', 'info', or 'debug'"
+  end
+  if not Geyser.Color.parse(color) then
+    return nil, "You must provide a color which can be parsed by Geyser.Color.parse. Examples are 'blue' (cecho), '<128,0,0>' (decho), '#aa3388' (hecho), or {128,0,0} (table of r,g,b values)"
+  end
+  self.levelColors[level] = color
+  return true
+end
+
+---@local
+function Loginator:getColor(level)
+  if self.format == "t" then
+    return ""
+  end
+  local r, g, b = Geyser.Color.parse((self.levelColors[level] or {128, 128, 128}))
+  if self.format == "h" then
+    return string.format("<span style='color: rgb(%d,%d,%d);'>", r, g, b)
+  elseif self.format == "a" then
+    return string.format("\27[38:2::%d:%d:%dm", r, g, b)
+  end
+  return ""
+end
+
+---@local
+function Loginator:getReset()
+  if self.format == "t" then
+    return ""
+  elseif self.format == "h" then
+    return "</span>"
+  elseif self.format == "a" then
+    return "\27[39;49m"
+  end
+  return ""
+end
+
+--- Returns the full path and filename to the logfile
+function Loginator:getFullFilename()
+  return self:processTemplate(self.fileNameTemplate)
+end
+
+--- Write an error level message to the logfile. Error level messages are always written.
+--@param msg the message to log
+--@return true if msg written, nil+error if error
+function Loginator:error(msg)
+  return self:log(msg, "error")
+end
+
+--- Write a warn level message to the logfile.
+-- Msg is only written if the logger level is <= warn
+-- From most to least severe the levels are:
+-- error > warn > info > debug
+--@param msg the message to log
+--@return true if msg written, false if skipped due to level, nil+error if error
+function Loginator:warn(msg)
+  return self:log(msg, "warn")
+end
+
+--- Write an info level message to the logfile.
+-- Msg is only written if the logger level is <= info
+-- From most to least severe the levels are:
+-- error > warn > info > debug
+--@param msg the message to log
+--@return true if msg written, false if skipped due to level, nil+error if error
+function Loginator:info(msg)
+  return self:log(msg, "info")
+end
+
+--- Write a debug level message to the logfile.
+-- Msg is only written if the logger level is debug
+-- From most to least severe the levels are:
+-- error > warn > info > debug
+--@param msg the message to log
+--@return true if msg written, false if skipped due to level, nil+error if error
+function Loginator:debug(msg)
+  return self:log(msg, "debug")
+end
+
+--- Write a message to the log file and optionally specify the level
+--@param msg the message to log
+--@param level the level to log the message at. Defaults to the level of the logger itself if not provided.
+--@return true if msg written, false if skipped due to level, nil+error if error
+function Loginator:log(msg, level)
+  level = level or self.level
+  local levelNumber = loggerLevels[level]
+  if not levelNumber then
+    return nil, f"Unknown logging level: {level}. Valid levels are 'error', 'warn', 'info', and 'debug'"
+  end
+  local displayLevelNumber = loggerLevels[self.level]
+  if levelNumber > displayLevelNumber then
+    return false
+  end
+  local filename = self:getFullFilename()
+  local filteredMsg = self:processTemplate(self.entryTemplate, level):gsub("|t", msg)
+  local ok, err = self:createPathIfNotExists(filename)
+  if err then
+    debugc(err)
+    return ok, err
+  end
+  if self.format == "h" and not io.exists(filename) then
+    filteredMsg = self:getHtmlHeader() .. filteredMsg
+  end
+  local file, err = io.open(filename, "a")
+  if not file then
+    err = string.format("Logger %s failed to open %s because: %s\n", self.name, filename, err)
+    debugc(err)
+    return nil, err
+  end
+  file:write(filteredMsg .. "\n")
+  file:close()
+  return true
+end
+
+--- Uses openUrl() to request your OS open the logfile in the appropriate application. Usually your web browser for html and text editor for all others.
+function Loginator:open()
+  openUrl(self:getFullFilename())
+end
+
+--- Uses openUrl() to request your OS open the directory the logfile resides in. This allows for easier browsing if you have more than one file.
+function Loginator:openDir()
+  openUrl(self:getPath())
+end
+
+--- Returns the path to the log file (directory in which the file resides) as a string
+--@param filename optional filename to return the path of. If not supplied, with use the logger's current filename
+function Loginator:getPath(filename)
+  filename = filename or self:getFullFilename()
+  filename = filename:gsub([[\]], "/")
+  local filenameTable = filename:split("/")
+  filenameTable[#filenameTable] = nil
+  local path = table.concat(filenameTable, "/")
+  return path
+end
+
+---@local
+function Loginator:createPathIfNotExists(filename)
+  if exists(filename) then
+    return false
+  end
+  filename = filename:gsub([[\]], "/")
+  local path = self:getPath(filename)
+  if exists(path) then
+    return false
+  end
+  local ok, err = mkdir_p(path)
+  if not ok then
+    err = string.format("Could not create directory for log files: %s\n Reason was: %s", path, err)
+    return nil, err
+  end
+  return true
+end
+
+---@local
+function Loginator:getHtmlHeader()
+  local header = htmlHeaderTemplate
+  header = header:gsub("|b", self.bgColor)
+  header = header:gsub("|c", self.fgColor)
+  header = header:gsub("|f", self.fontSize)
+  return header
+end
+
+return Loginator
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/source/mastermindsolver.lua.html b/src/resources/MDK/doc/source/mastermindsolver.lua.html new file mode 100755 index 0000000..a9f95b2 --- /dev/null +++ b/src/resources/MDK/doc/source/mastermindsolver.lua.html @@ -0,0 +1,353 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

mastermindsolver.lua

+
+--- Interactive object which helps you solve a Master Mind puzzle.
+-- @classmod MasterMindSolver
+-- @author Damian Monogue <demonnic@gmail.com>
+-- @copyright 2021 Damian Monogue
+-- @copyright 2008,2009 Konstantinos Asimakis for code used to turn an index number into a guess (indexToGuess method)
+local MasterMindSolver = {
+  places = 4,
+  items = {"red", "orange", "yellow", "green", "blue", "purple"},
+  template = "|t",
+  autoSend = false,
+  singleCommand = false,
+  separator = " ",
+  allowDuplicates = true,
+}
+local mod, floor, random, randomseed = math.mod, math.floor, math.random, math.randomseed
+local initialGuess = {{1}, {1, 2}, {1, 1, 2}, {1, 1, 2, 2}, {1, 1, 1, 2, 2}, {1, 1, 1, 2, 2, 2}, {1, 1, 1, 1, 2, 2, 2}, {1, 1, 1, 1, 2, 2, 2, 2}}
+
+--- Removes duplicate elements from a list
+-- @param tbl the table you want to remove dupes from
+-- @local
+local function tableUnique(tbl)
+  local used = {}
+  local result = {}
+  for _, item in ipairs(tbl) do
+    if not used[item] then
+      result[#result + 1] = item
+      used[item] = true
+    end
+  end
+  return result
+end
+
+--- Creates a new Master Mind solver
+-- @tparam table options table of configuration options for the solver
+-- <table class="tg">
+-- <thead>
+--   <tr>
+--     <th>option name</th>
+--     <th>description</th>
+--     <th>default</th>
+--   </tr>
+-- </thead>
+-- <tbody>
+--   <tr>
+--     <td class="tg-1">places</td>
+--     <td class="tg-1">How many spots in the code we're breaking?</td>
+--     <td class="tg-1">4</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">items</td>
+--     <td class="tg-2">The table of colors/gemstones/whatever which can be part of the code</td>
+--     <td class="tg-2">{"red", "orange", "yellow", "green", "blue", "purple"}</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">template</td>
+--     <td class="tg-1">The string template to use for the guess. Within the template, |t is replaced by the item. Used as the command if autoSend is true</td>
+--     <td class="tg-1">"|t"</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">autoSend</td>
+--     <td class="tg-2">Should we send the guess directly to the server?</td>
+--     <td class="tg-2">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">allowDuplicates</td>
+--     <td class="tg-1">Can the same item be used more than once in a code?</td>
+--     <td class="tg-1">true</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">singleCommand</td>
+--     <td class="tg-2">If true, combines the guess into a single command, with each one separated by the separator</td>
+--     <td class="tg-2">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">separator</td>
+--     <td class="tg-1">If sending the guess as a single command, what should we put between the guesses to separate them?</td>
+--     <td class="tg-1">" "</td>
+--   </tr>
+-- </tbody>
+-- </table>
+function MasterMindSolver:new(options)
+  if options == nil then
+    options = {}
+  end
+  local optionsType = type(options)
+  if optionsType ~= "table" then
+    error(f "MasterMindSolver:new(options): options as table expected, got {tostring(options)} of type: {optionsType}")
+  end
+  local me = options
+  setmetatable(me, self)
+  self.__index = self
+  me:populateInitialSet()
+  if not me.allowDuplicates then
+    me.initialGuessMade = true -- skip the preset initial guess, they assume duplicates
+  end
+  return me
+end
+
+--- Takes a guess number (4, or 1829, or any number from 1 - <total possible combinations>) and returns the
+-- actual guess.
+-- @tparam number index which guess to generate
+-- @local
+function MasterMindSolver:indexToGuess(index)
+  local guess = {}
+  local options = #self.items
+  for place = 1, self.places do
+    guess[place] = mod(floor((index - 1) / options ^ (place - 1)), options) + 1
+  end
+  return guess
+end
+
+--- Compares a guess with the solution and returns the answer
+-- @tparam table guess The guess you are checking, as numbers. { 1 , 1, 2, 2 } as an example
+-- @tparam table solution the solution you are checking against, as numbers. { 3, 4, 1, 6 } as an example.
+-- @local
+function MasterMindSolver:compare(guess, solution)
+  local coloredPins = 0
+  local whitePins = 0
+  local usedGuessPlace = {}
+  local usedSolutionPlace = {}
+  local places = self.places
+  for place = 1, places do
+    if guess[place] == solution[place] then
+      coloredPins = coloredPins + 1
+      usedGuessPlace[place] = true
+      usedSolutionPlace[place] = true
+    end
+  end
+  for guessPlace = 1, places do
+    if not usedGuessPlace[guessPlace] then
+      for solutionPlace = 1, places do
+        if not usedSolutionPlace[solutionPlace] then
+          if guess[guessPlace] == solution[solutionPlace] then
+            whitePins = whitePins + 1
+            usedSolutionPlace[solutionPlace] = true
+            break
+          end
+        end
+      end
+    end
+  end
+  return coloredPins, whitePins
+end
+
+--- Generates an initial table of all guesses from 1 to <total possible> that are valid.
+-- If allowDuplicates is false, will filter out any of the possible combinations which contain duplicates
+-- @local
+function MasterMindSolver:populateInitialSet()
+  local possible = {}
+  local allowDuplicates = self.allowDuplicates
+  local places = self.places
+  local numberOfItems = #self.items
+  local totalCombos = numberOfItems ^ places
+  local numberRemaining = 0
+  for entry = 1, totalCombos do
+    local useItem = true
+    if not allowDuplicates then
+      local guess = self:indexToGuess(entry)
+      local guessUnique = tableUnique(guess)
+      if #guessUnique ~= self.places then
+        useItem = false
+      end
+    end
+    if useItem then
+      possible[entry] = true
+      numberRemaining = numberRemaining + 1
+    end
+  end
+  self.possible = possible
+  self.numberRemaining = numberRemaining
+end
+
+--- Function used to reduce the remaining possible answers, given a guess and the answer to that guess. This is not undoable.
+-- @tparam table guess guess which the answer belongs to. Uses numbers, rather than item names. IE { 1, 1, 2, 2} rather than { "blue", "blue", "green", "green" }
+-- @tparam number coloredPins how many parts of the guess are both the right color and the right place
+-- @tparam number whitePins how many parts of the guess are the right color, but in the wrong place
+-- @return true if you solved the puzzle (coloredPins == number of positions in the code), or false otherwise
+function MasterMindSolver:reducePossible(guess, coloredPins, whitePins)
+  if coloredPins == #guess then
+    return true
+  end
+  local possible = self.possible
+  local numberRemaining = 0
+  for entry, _ in pairs(possible) do
+    local testColor, testWhite = self:compare(guess, self:indexToGuess(entry))
+    if testColor ~= coloredPins or testWhite ~= whitePins then
+      possible[entry] = nil
+    else
+      numberRemaining = numberRemaining + 1
+    end
+  end
+  self.possible = possible
+  self.numberRemaining = numberRemaining
+  return false
+end
+
+--- Function which assumes you used the last suggested guess from the solver, and reduces the number of possible correct solutions based on the answer given
+-- @see MasterMindSolver:reducePossible
+-- @tparam number coloredPins how many parts of the guess are both the right color and the right place
+-- @tparam number whitePins how many parts of the guess are the right color, but in the wrong place
+-- @return true if you solved the puzzle (coloredPins == number of positions in the code), or false otherwise
+function MasterMindSolver:checkLastSuggestion(coloredPins, whitePins)
+  return self:reducePossible(self.guess, coloredPins, whitePins)
+end
+
+--- Used to get one of the remaining valid possible guesses
+-- @tparam boolean useActions if true, will return the guess as the commands which would be sent, rather than the numbered items
+function MasterMindSolver:getValidGuess(useActions)
+  local guess
+  if not self.initialGuessMade then
+    self.initialGuessMade = true
+    guess = initialGuess[self.places]
+  end
+  if not guess then
+    local possible = self.possible
+    local keys = table.keys(possible)
+    randomseed(os.time())
+    guess = self:indexToGuess(keys[random(#keys)])
+  end
+  self.guess = guess
+  if self.autoSend then
+    self:sendGuess(guess)
+  end
+  if useActions then
+    return self:guessToActions(guess)
+  end
+  return guess
+end
+
+--- Takes a guess and converts the numbers to commands/actions. IE guessToActions({1, 1, 2, 2}) might return { "blue", "blue", "green", "green" }
+-- @tparam table guess the guess to convert as numbers. IE { 1, 1, 2, 2}
+-- @return table of commands/actions correlating to the numbers in the guess.
+-- @local
+function MasterMindSolver:guessToActions(guess)
+  local actions = {}
+  for index, itemNumber in ipairs(guess) do
+    local item = self.items[itemNumber]
+    actions[index] = self.template:gsub("|t", item)
+  end
+  return actions
+end
+
+--- Handles sending the commands to the game for a guess
+-- @local
+function MasterMindSolver:sendGuess(guess)
+  local actions = self:guessToActions(guess)
+  if self.singleCommand then
+    send(table.concat(actions, self.separator))
+  else
+    sendAll(unpack(actions))
+  end
+end
+
+return MasterMindSolver
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/source/revisionator.lua.html b/src/resources/MDK/doc/source/revisionator.lua.html new file mode 100755 index 0000000..5118d59 --- /dev/null +++ b/src/resources/MDK/doc/source/revisionator.lua.html @@ -0,0 +1,240 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

revisionator.lua

+
+--- The revisionator provides a standardized way of migrating configurations between revisions
+-- for instance, it will track what the currently applied revision number is, and when you tell
+-- tell it to migrate, it will apply every individual migration between the currently applied
+-- revision and the latest/current revision. This should allow for more seamlessly moving from
+-- an older version of a package to a new one.
+-- @classmod revisionator
+-- @author Damian Monogue <demonnic@gmail.com>
+-- @copyright 2023
+-- @license MIT, see https://raw.githubusercontent.com/demonnic/MDK/main/src/scripts/LICENSE.lua
+local revisionator = {
+  name = "Revisionator",
+  patches = {},
+}
+revisionator.__index = revisionator
+local dataDir = getMudletHomeDir() .. "/revisionator"
+revisionator.dataDir = dataDir
+if not io.exists(dataDir) then
+  local ok,err = lfs.mkdir(dataDir)
+  if not ok then
+    printDebug(f"Error creating the directory for storing applied revisions: {err}", true)
+  end
+end
+
+--- Creates a new revisionator
+-- @tparam table options the options to create the revisionator with.
+-- <table class="tg">
+-- <thead>
+--   <tr>
+--     <th>option name</th>
+--     <th>description</th>
+--     <th>default</th>
+--   </tr>
+-- </thead>
+-- <tbody>
+--   <tr>
+--     <td class="tg-1">name</td>
+--     <td class="tg-1">The name of the revisionator. This is absolutely required, as the name is used for tracking the currently applied patch level</td>
+--     <td class="tg-1">raises an error if not provided</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">patches</td>
+--     <td class="tg-2">A table of patch functions. It is traversed using ipairs, so must be in the form of {function1, function2, function3} etc. If you do not provide it, you can add the patches by calling :addPatch for each patch in order.</td>
+--     <td class="tg-2">{}</td>
+--   </tr>
+--</tbody>
+--</table>
+function revisionator:new(options)
+  options = options or {}
+  local optionsType = type(options)
+  if optionsType ~= "table" then
+    printError(f"revisionator:new bad argument #1 type, options as table expected, got {optionsType}", true, true)
+  end
+  if not options.name then
+    printError("revisionator:new(options) options must include a 'name' key as this is used as part of tracking the applied patch level.", true, true)
+  end
+  local me = table.deepcopy(options)
+  setmetatable(me, self)
+  return me
+end
+
+--- Get the currently applied revision from file
+--- @treturn[1] number the revision number currently applied, or 0 if it can't read a current version
+--- @treturn[2] nil nil
+--- @treturn[2] string error message
+function revisionator:getAppliedPatch()
+  local fileName = f"{self.dataDir}/{self.name}.txt"
+  debugc(fileName)
+  local revision = 0
+  if io.exists(fileName) then
+    local file = io.open(fileName, "r")
+    local fileContents = file:read("*a")
+    file:close()
+    local revNumber = tonumber(fileContents)
+    if revNumber then
+      revision = revNumber
+    else
+      return nil, f"Error while attempting to read current patch version from file: {fileName}\nThe contents of the file are {fileContents} and it was unable to be converted to a revision number"
+    end
+  end
+  return revision
+end
+
+--- go through all the patches in order and apply any which are still necessary
+--- @treturn boolean true if it successfully applied patches, false if it was already at the latest patch level
+--- @error error message
+function revisionator:migrate()
+  local applied,err = self:getAppliedPatch()
+  if not applied then
+    printError(err, true, true)
+  end
+  local patches = self.patches
+  if applied >= #patches then
+    return false
+  end
+  for revision, patch in ipairs(patches) do
+    if applied < revision then
+      local ok, err = pcall(patch)
+      if not ok then
+        self:setAppliedPatch(revision - 1)
+        return nil, f"Error while running patch #{revision}: {err}"
+      end
+    end
+  end
+  self:setAppliedPatch(#patches)
+  return true
+end
+
+--- add a patch to the table of patches
+--- @tparam function  func the function to run as the patch
+--- @number[opt] position which patch to insert it as? If not supplied, inserts it as the last patch. Which is usually what you want.
+function revisionator:addPatch(func, position)
+  if position then
+    table.insert(self.patches, position, func)
+  else
+    table.insert(self.patches, func)
+  end
+end
+
+--- Remove a patch from the table of patches
+--- this is primarily used for testing
+--- @local
+--- @number[opt] patchNumber the patch number to remove. Will remove the last item if not provided.
+function revisionator:removePatch(patchNumber)
+  table.remove(self.patches, patchNumber)
+end
+
+--- set the currently applied patch number
+-- only directly called for testing
+--- @local
+--- @number patchNumber the patch number to set as the currently applied patch
+function revisionator:setAppliedPatch(patchNumber)
+  local fileName = f"{self.dataDir}/{self.name}.txt"
+  local revFile, err = io.open(fileName, "w+")
+  if not revFile then
+    printError(err, true, true)
+  end
+  revFile:write(patchNumber)
+  revFile:close()
+end
+
+return revisionator
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/source/schema.lua.html b/src/resources/MDK/doc/source/schema.lua.html new file mode 100755 index 0000000..e06f454 --- /dev/null +++ b/src/resources/MDK/doc/source/schema.lua.html @@ -0,0 +1,743 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

schema.lua

+
+--[[
+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.
+]]
+
+local schema = {}
+
+-- Checks an object against a schema.
+function schema.CheckSchema(obj, schem, path)
+    if path == nil then
+        path = schema.Path.new()
+        path:setBase(obj)
+    end
+    if type(schem) == "function" then
+        return schem(obj, path)
+    else -- attempt to simply compare the values
+        if schem == obj then
+            return nil
+        end
+        return schema.Error("Invalid value: "..path.." should be "..tostring(schem), path)
+    end
+end
+
+function schema.FormatOutput(output)
+    local format = schema.List()
+    for k,v in ipairs(output) do
+        format:append(v:format())
+    end
+    return table.concat(format, "\n")
+end
+
+--
+-- Infrastructure
+--
+
+-- Path class. Represents paths to values in a table (the path's *base*).
+local Path = {}
+function Path.new(...)
+    local arg = {...}
+    local self = setmetatable({}, Path)
+    self.p = {}
+    for k,v in ipairs(arg) do
+        self.p[k] = v
+    end
+    return self
+end
+
+-- Sets the base of the path, i.e. the table to which the path is relative.
+-- Note that this is the actual *table*, not the table's name.
+function Path:setBase(base)
+    self.base = base
+end
+
+-- Gets the base of the path.
+function Path:getBase()
+    return self.base
+end
+
+-- Returns the target of the path or 'nil' if the path is invalid.
+function Path:target()
+    if self.base == nil then
+        error("Path:target() called on a path without a base!")
+    end
+    local current = self.base
+    for k,v in ipairs(self.p) do
+        current = current[v]
+        if current == nil then
+            return nil
+        end
+    end
+    return current
+end
+
+-- Pushes an entry to the end of the path.
+function Path:push(obj)
+    self.p[#self.p + 1] = obj
+    return self
+end
+
+-- Pops an entry from the end of the path.
+function Path:pop()
+    local tmp = self.p[#self.p]
+    self.p[#self.p] = nil
+    return tmp
+end
+
+-- Returns the topmost entry of the end of the path.
+function Path:top()
+    return self.p[#self.p]
+end
+
+-- Returns the length of the path.
+function Path:length()
+    return #self.p
+end
+
+-- Returns the element at the specified index.
+function Path:get(index)
+    return self.p[index]
+end
+
+-- Copies the path.
+function Path:copy()
+    local cp = Path.new()
+    cp.base = self.base
+    for k,v in ipairs(self) do
+        cp.p[k] = v
+    end
+    return cp
+end
+
+Path.__index = Path
+Path.__tostring = function(tbl)
+    if #tbl.p == 0 then
+        return '<val>'
+    end
+    return table.concat(tbl.p,".")
+end
+Path.__concat = function(lhs, rhs)
+    if type(lhs) == "table" then
+        return tostring(lhs)..rhs
+    elseif type(rhs) == "table" then
+        return lhs..tostring(rhs)
+    end
+end
+Path.__len = function(self)
+    return #self.p
+end
+
+setmetatable(Path, {
+    __call = function (cls, ...)
+        return Path.new(...)
+    end
+})
+schema.Path = Path
+
+-- List class
+local List = {}
+function List.new(...)
+    local self = setmetatable({}, List)
+    local arg = {...}
+    for k,v in ipairs(arg) do
+        self[k] = v
+    end
+    return self
+end
+
+function List:add(obj)
+    self[#self+1] = obj
+    return self
+end
+
+function List:append(list)
+    for k,v in ipairs(list) do
+        self[#self+k] = v
+    end
+    return self
+end
+
+List.__index = List
+List.__tostring = function(self)
+    local tmp = {}
+    for k,v in ipairs(self) do
+        tmp[k] = tostring(v)
+    end
+    return table.concat(tmp, "\n")
+end
+setmetatable(List, {
+    __call = function(cls, ...)
+        return List.new(...)
+    end
+})
+schema.List = List
+
+-- Error class. Describes mismatches that occured during the schema-checking.
+local Error = {}
+function Error.new(msg, path, suberrors)
+    local self = setmetatable({}, Error)
+    self.message   = msg
+    self.path      = path:copy()
+    self.suberrors = suberrors
+    return self
+end
+
+-- Returns a list of strings which represent the error (with indenttation for
+-- suberrors).
+function Error:format()
+    local output = List.new(self.message)
+    if self.suberrors ~= nil then
+        for k,sub in pairs(self.suberrors) do
+            local subout = sub:format()
+            for k1,msg in pairs(subout) do
+                output = output:add("  "..msg)
+            end
+        end
+    end
+    return output
+end
+
+Error.__tostring = function(self)
+    return table.concat(self:format(), "\n")
+end
+Error.__index = Error
+setmetatable(Error, {
+    __call = function(cls, ...)
+        return List(Error.new(...))
+    end
+})
+schema.Error = Error
+
+--
+-- Schema Building Blocks
+-- A schema is a function taking the object to be checked and the path to the
+-- current value in the environment.
+-- It returns either 'true' if the schema accepted the object or an Error
+-- object which describes why it was rejected.
+-- The schemata below are just some basic building blocks. Expand them to your
+-- liking.
+--
+
+-- Always accepts.
+function schema.Any(obj, path)
+    return nil
+end
+
+-- Always fails.
+function schema.Nothing(obj, path)
+    return schema.Error("Failure: '"..path.."' will always fail.", path)
+end
+
+-- Checks a value against a specific type.
+local function TypeSchema(obj, path, typeId)
+    if type(obj) ~= typeId then
+        return schema.Error("Type mismatch: '"..path.."' should be "..typeId..", is "..type(obj), path)
+    else
+        return nil
+    end
+end
+
+function schema.Boolean (obj, path) return TypeSchema(obj, path, "boolean")  end
+function schema.Function(obj, path) return TypeSchema(obj, path, "function") end
+function schema.Nil     (obj, path) return TypeSchema(obj, path, "nil")      end
+function schema.Number  (obj, path) return TypeSchema(obj, path, "number")   end
+function schema.String  (obj, path) return TypeSchema(obj, path, "string")   end
+function schema.Table   (obj, path) return TypeSchema(obj, path, "table")    end
+function schema.UserData(obj, path) return TypeSchema(obj, path, "userdata") end
+
+-- Checks that some value is a string matching a given pattern.
+function schema.Pattern(pattern)
+    local userPattern = pattern
+    if not pattern:match("^^") then
+        pattern = "^" .. pattern
+    end
+    if not pattern:match("$$") then
+        pattern = pattern .. "$"
+    end
+    local function CheckPattern(obj, path)
+        local err = schema.String(obj, path)
+        if err then
+            return err
+        end
+        if string.match(obj, pattern) then
+            return nil
+        else
+            return schema.Error("Invalid value: '"..path.."' must match pattern '"..userPattern.."'", path)
+        end
+    end
+    return CheckPattern
+end
+
+-- Checks that some number is an integer.
+function schema.Integer(obj, path)
+    local err = schema.Number(obj, path)
+    if err then
+        return err
+    end
+    if math.floor(obj) == obj then
+        return nil
+    end
+    return schema.Error("Invalid value: '"..path.."' must be an integral number", path)
+end
+
+-- Checks that some number is >= 0.
+function schema.NonNegativeNumber(obj, path)
+    local err = schema.Number(obj, path)
+    if err then
+        return err
+    end
+    if obj >= 0 then
+        return nil
+    end
+    return schema.Error("Invalid value: '"..path.."' must be >= 0", path)
+end
+
+-- Checks that some number is > 0.
+function schema.PositiveNumber(obj, path)
+    local err = schema.Number(obj, path)
+    if err then
+        return err
+    end
+    if obj > 0 then
+        return nil
+    end
+    return schema.Error("Invalid value: '"..path.."' must be > 0", path)
+end
+
+-- Checks that some value is a number from the interval [lower, upper].
+function schema.NumberFrom(lower, upper)
+    local function CheckNumberFrom(obj, path)
+        local err = schema.Number(obj, path)
+        if err then
+            return err
+        end
+        if lower <= obj and upper >= obj then
+            return nil
+        else
+            return schema.Error("Invalid value: '"..path.."' must be between "..lower.." and "..upper, path)
+        end
+    end
+    return CheckNumberFrom
+end
+
+-- Takes schemata and accepts their disjunction.
+function schema.OneOf(...)
+    local arg = {...}
+    local function CheckOneOf(obj, path)
+        for k,v in ipairs(arg) do
+            local err = schema.CheckSchema(obj, v, path)
+            if not err then return nil end
+        end
+        return schema.Error("No suitable alternative: No schema matches '"..path.."'", path)
+    end
+    return CheckOneOf
+end
+
+-- Takes a schema and returns an optional schema.
+function schema.Optional(s)
+    return schema.OneOf(s, schema.Nil)
+end
+
+-- Takes schemata and accepts their conjuction.
+function schema.AllOf(...)
+    local arg = {...}
+    local function CheckAllOf(obj, path)
+        local errmsg = nil
+        for k,v in ipairs(arg) do
+            local err = schema.CheckSchema(obj, v, path)
+            if err then
+                if errmsg == nil then
+                    errmsg = err
+                else
+                    errmsg = errmsg:append(err)
+                end
+            end
+        end
+        return errmsg
+    end
+    return CheckAllOf
+end
+
+-- Builds a record type schema, i.e. a table with a fixed set of keys (strings)
+-- with corresponding values. Use as in
+-- Record({
+--  name  = schema,
+--  name2 = schema2
+--  })
+function schema.Record(recordschema, additionalValues)
+    if additionalValues == nil then
+        additionalValues = false
+    end
+    local function CheckRecord(obj, path)
+        if type(obj) ~= "table" then
+            return schema.Error("Type mismatch: '"..path.."' should be a record (table), is "..type(obj), path)
+        end
+
+        local errmsg = nil
+        local function AddError(msg)
+            if errmsg == nil then
+                errmsg = msg
+            else
+                errmsg = errmsg:append(msg)
+            end
+        end
+
+        for k,v in pairs(recordschema) do
+            path:push(k)
+            local err = schema.CheckSchema(obj[k], v, path)
+            if err then
+                AddError(err)
+            end
+            path:pop()
+        end
+
+        for k, v in pairs(obj) do
+            path:push(k)
+            if type(k) ~= "string" then
+                AddError(schema.Error("Invalid key: '"..path.."' must be of type 'string'", path))
+            end
+            if recordschema[k] == nil and not additionalValues then
+                AddError(schema.Error("Superfluous value: '"..path.."' does not appear in the record schema", path))
+            end
+            path:pop()
+        end
+        return errmsg
+    end
+    return CheckRecord
+end
+
+function schema.MixedTable(t_schema, additional_values)
+    local function CheckMixedTable(obj, path)
+        local obj_t = type(obj)
+        if obj_t ~= "table" then
+            local msg = ("Type mismatch: '%s' should be a table, is %s"):format(path, obj_t)
+            return schema.Error(msg, path)
+        end
+
+        local errmsg = nil
+        local function AddError(msg)
+            if errmsg == nil then
+                errmsg = msg
+            else
+                errmsg = errmsg:append(msg)
+            end
+        end
+
+        local checked_keys = {}
+        for k, v in pairs(t_schema) do
+            path:push(k)
+            local err = schema.CheckSchema(obj[k], v, path)
+            if err then
+                AddError(err)
+            end
+            checked_keys[k] = true
+            path:pop()
+        end
+
+        for k, v in pairs(obj) do
+            if not checked_keys[k] then
+                path:push(k)
+                local k_type = type(k)
+                if k_type ~= "string" and k_type ~= "number" then
+                    local msg = ("Invalid key: '%s' must be of type 'string' or 'number'"):format(k_type)
+                    AddError(schema.Error(msg, path))
+                end
+
+                local t_schema_v = t_schema[k]
+                if t_schema_v then
+                    local err = schema.CheckSchema(v, t_schema_v, path)
+                    if err then
+                        AddError(err)
+                    end
+                else
+                    if not additional_values then
+                        local msg = ("Superfluous value: '%s' does not appear in the table schema")
+                                            :format(path)
+                        AddError(schema.Error(msg, path))
+                    end
+                end
+                path:pop()
+            end
+        end
+        return errmsg
+    end
+    return CheckMixedTable
+end
+
+-- Builds a map type schema, i.e. a table with an arbitraty number of
+-- entries where both all keys (and all vaules) fit a common schema.
+function schema.Map(keyschema, valschema)
+    local function CheckMap(obj, path)
+        if type(obj) ~= "table" then
+            return schema.Error("Type mismatch: '"..path.."' should be a map (table), is "..type(obj), path)
+        end
+
+        local errmsg = nil
+        local function AddError(msg)
+            if errmsg == nil then
+                errmsg = msg
+            else
+                errmsg = errmsg:append(msg)
+            end
+        end
+
+        -- aggregate error message
+        for k, v in pairs(obj) do
+            path:push(k)
+            local keyErr = schema.CheckSchema(k, keyschema, path)
+            if keyErr then
+                AddError(schema.Error("Invalid map key", path, keyErr))
+            end
+
+            local valErr = schema.CheckSchema(v, valschema, path)
+            if valErr then
+                AddError(valErr)
+            end
+            path:pop()
+        end
+        return errmsg
+    end
+    return CheckMap
+end
+
+-- Builds a collection type schema, i.e. a table with an arbitrary number of
+-- entries where we only care about the type of the values.
+function schema.Collection(valschema)
+    return schema.Map(schema.Any, valschema)
+end
+
+-- Builds a tuple type schema, i.e. a table with a fixed number of entries,
+-- each indexed by a number and with a fixed type.
+function schema.Tuple(...)
+    local arg = {...}
+
+    local function CheckTuple(obj, path)
+        if type(obj) ~= "table" then
+            return schema.Error("Type mismatch: '"..path.."' should be a map (tuple), is "..type(obj), path)
+        end
+
+        if #obj ~= #arg then
+            return schema.Error("Invalid length: '"..path.." should have exactly "..#arg.." elements", path)
+        end
+
+        local errmsg = nil
+        local function AddError(msg)
+            if errmsg == nil then
+                errmsg = msg
+            else
+                errmsg = errmsg:append(msg)
+            end
+        end
+
+        local min = 1
+        local max = #arg
+        for k, v in pairs(obj) do
+            path:push(k)
+            local err = schema.Integer(k, path)
+            if not err then
+                err = schema.CheckSchema(v, arg[k], path)
+                if err then
+                    AddError(err)
+                end
+            else
+                AddError(schema.Error("Invalid tuple key", path, err))
+            end
+            path:pop()
+        end
+        return errmsg
+    end
+    return CheckTuple
+end
+
+-- Builds a conditional type schema, i.e. a schema that depends on the value of
+-- another value. The dependence must be *local*, i.e. defined in the same
+-- table. Use as in
+--   Case("name", {"Peter", schema1}, {"Mary", schema2}, {OneOf(...), schema3})
+-- This will check the field "name" against every schema in the first component
+-- and will return the second component of the first match.
+function schema.Case(relativePath, ...)
+    if type(relativePath) ~= "table" then
+        relativePath = schema.Path("..", relativePath)
+    end
+    local cases = {...}
+    for k,v in ipairs(cases) do
+        if type(v) ~= "table" then
+            error("Cases expects inputs of the form {conditionSchema, schema}; argument "..v.." is invalid")
+        end
+    end
+
+    local function CheckCase(obj, path)
+        local condPath = path:copy()
+        for k=0, #relativePath do
+            local s = relativePath:get(k)
+            if s == ".." then
+                condPath:pop()
+            else
+                condPath:push(s)
+            end
+        end
+
+        local errmsg = nil
+        local function AddError(msg)
+            if errmsg == nil then
+                errmsg = msg
+            else
+                errmsg = errmsg:append(msg)
+            end
+        end
+
+        local anyCond = false
+        local condObj = condPath:target()
+        for k,v in ipairs(cases) do
+            local condSchema = v[1]
+            local valSchema = v[2]
+            local condErr = schema.CheckSchema(condObj, condSchema, condPath)
+            if not condErr then
+                anyCond = true
+                local err = schema.CheckSchema(obj, valSchema, path)
+                if err then
+                    AddError(schema.Error("Case failed: Condition "..k.." of '"..path.."' holds but the consequence does not", path, err))
+                end
+            end
+        end
+
+        if not anyCond then
+            AddError(schema.Error("Case failed: No condition on '"..path.."' holds"))
+        end
+
+        return errmsg
+    end
+    return CheckCase
+end
+
+function schema.Test(fn, msg)
+    local function CheckTest(obj, path)
+        local pok, ok = pcall(fn, obj)
+        if pok and ok then
+            return nil
+        else
+            return schema.Error("Invalid value: '"..path..(msg and "': "..msg or ""), path)
+        end
+    end
+    return CheckTest
+end
+
+return schema
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/source/sortbox.lua.html b/src/resources/MDK/doc/source/sortbox.lua.html new file mode 100755 index 0000000..960e2ed --- /dev/null +++ b/src/resources/MDK/doc/source/sortbox.lua.html @@ -0,0 +1,613 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

sortbox.lua

+
+---An H/VBox alternative which can be set to either vertical or horizontal, and will autosort the windows
+-- @classmod SortBox
+-- @author Damian Monogue <demonnic@gmail.com>
+-- @copyright 2020 Damian Monogue
+-- @license MIT, see LICENSE.lua
+local SortBox = Geyser.Container:new({
+  name = "SortBoxClass",
+  autoSort = true,
+  timerSort = true,
+  sortInterval = 500,
+  elastic = false,
+  maxHeight = 0,
+  maxWidth = 0,
+  boxType = "v",
+  sortFunction = "gaugeValue",
+})
+local BIGNUMBER = 999999999
+
+--- Sorting functions for spairs, should you wish
+-- @table SortFunctions
+-- @field gaugeValue sorts Geyser gauges by value, ascending
+-- @field reverseGaugeValue sorts Geyser gauges by value, descending
+-- @field timeLeft sorts TimerGauges by how much time is left, ascending
+-- @field reverseTimeLeft sorts TimerGauges by how much time is left, descending.
+-- @field name sorts Geyser objects by name, ascending
+-- @field reverseName sorts Geyser objects by name, descending
+-- @field message sorts Geyser labels and gauges by their echoed text, ascending
+-- @field reverseMessage sorts Geyser labels and gauges by their echoed text, descending
+SortBox.SortFunctions = {
+  gaugeValue = function(t, a, b)
+    local avalue = t[a].value or BIGNUMBER
+    local bvalue = t[b].value or BIGNUMBER
+    return avalue < bvalue
+  end,
+  reverseGaugeValue = function(t, a, b)
+    local avalue = t[a].value or BIGNUMBER
+    local bvalue = t[b].value or BIGNUMBER
+    return avalue > bvalue
+  end,
+  timeLeft = function(t, a, b)
+    a = t[a]
+    b = t[b]
+    local avalue = a.getTime and tonumber(a:getTime("S.mm")) or BIGNUMBER
+    local bvalue = b.getTime and tonumber(b:getTime("S.mm")) or BIGNUMBER
+    return avalue < bvalue
+  end,
+  reverseTimeLeft = function(t, a, b)
+    a = t[a]
+    b = t[b]
+    local avalue = a.getTime and tonumber(a:getTime("S.mm")) or BIGNUMBER
+    local bvalue = b.getTime and tonumber(b:getTime("S.mm")) or BIGNUMBER
+    return avalue > bvalue
+  end,
+  name = function(t, a, b)
+    return t[a].name < t[b].name
+  end,
+  reverseName = function(t, a, b)
+    return t[a].name > t[b].name
+  end,
+  message = function(t, a, b)
+    a = t[a]
+    b = t[b]
+    local avalue = a.text and a.text.message or a.message
+    local bvalue = b.text and b.text.message or b.message
+    avalue = avalue or ""
+    bvalue = bvalue or ""
+    return avalue < bvalue
+  end,
+  reverseMessage = function(t, a, b)
+    a = t[a]
+    b = t[b]
+    local avalue = a.text and a.text.message or a.message
+    local bvalue = b.text and b.text.message or b.message
+    avalue = avalue or ""
+    bvalue = bvalue or ""
+    return avalue > bvalue
+  end,
+}
+--- Creates a new SortBox
+-- @usage
+-- local SortBox = require("MDK.sortbox")
+-- mySortBox = SortBox:new({
+--   name = "mySortBox",
+--   x = 400,
+--   y = 100,
+--   height = 150,
+--   width = 300,
+--   sortFunction = "timeLeft"
+-- })
+-- @tparam table options the options to use for the SortBox. See table below for added options
+-- @param[opt] container the container to add the SortBox into
+-- <br><br>Table of new options
+-- <table class="tg">
+-- <thead>
+--   <tr>
+--     <th>option name</th>
+--     <th>description</th>
+--     <th>default</th>
+--   </tr>
+-- </thead>
+-- <tbody>
+--   <tr>
+--     <td class="tg-1">autoSort</td>
+--     <td class="tg-1">should the SortBox perform function based sorting? If false, will behave like a normal H/VBox</td>
+--     <td class="tg-1">true</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">timerSort</td>
+--     <td class="tg-2">should the SortBox automatically perform sorting on a timer?</td>
+--     <td class="tg-2">true</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">sortInterval</td>
+--     <td class="tg-1">how frequently should we sort on a timer if timerSort is true, in milliseconds</td>
+--     <td class="tg-1">500</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">boxType</td>
+--     <td class="tg-2">Should we stack like an HBox or VBox? use 'h' for hbox and 'v' for vbox</td>
+--     <td class="tg-2">v</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">sortFunction</td>
+--     <td class="tg-1">how should we sort the items in the SortBox? see setSortFunction for valid options</td>
+--     <td class="tg-1">gaugeValue</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">elastic</td>
+--     <td class="tg-2">Should this container stretch to fit its contents? boxType v stretches in height, h stretches in width.</td>
+--     <td class="tg-2">false</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">maxHeight</td>
+--     <td class="tg-1">If elastic, what's the biggest a 'v' style box should grow in height? Use 0 for unlimited</td>
+--     <td class="tg-1">0</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">maxWidth</td>
+--     <td class="tg-2">If elastic, what's the biggest a 'h' style box should grow in width? Use 0 for unlimited</td>
+--     <td class="tg-2">0</td>
+--   </tr>
+-- </tbody>
+-- </table>
+function SortBox:new(options, container)
+  options = options or {}
+  options.type = options.type or "SortBox"
+  local me = self.parent:new(options, container)
+  setmetatable(me, self)
+  self.__index = self
+  if me.timerSort then
+    me:enableTimer()
+  end
+  me:setBoxType(me.boxType)
+  return me
+end
+
+--- Iterates a key:value pair table in a sorted fashion
+-- @local
+-- I first found this on https://stackoverflow.com/questions/15706270/sort-a-table-in-lua
+-- modified slightly, as Mudlet already has table.keys to collect keys, and I don't want
+-- to sort if no function to sort with is given. In this case, I want it to work like pairs.
+local function spairs(t, order)
+  local keys = table.keys(t)
+  if order then
+    table.sort(keys, function(a, b)
+      return order(t, a, b)
+    end)
+  end
+
+  local i = 0
+  return function()
+    i = i + 1
+    if keys[i] then
+      return keys[i], t[keys[i]]
+    end
+  end
+end
+
+function SortBox:add(window, cons)
+  if self.useAdd2 then
+    Geyser.add2(self, window, cons)
+  else
+    Geyser.add(self, window, cons)
+  end
+  if not self.defer_updates then
+    self:organize()
+  end
+end
+
+function SortBox:remove(window)
+  Geyser.remove(self, window)
+  self:organize()
+end
+
+--- Calling this will cause the SortBox to reposition/resize everything
+function SortBox:organize()
+  -- make sure we don't divide by zero later
+  if self:get_width() == 0 then
+    self:resize("0.9px", nil)
+  end
+  if self:get_height() == 0 then
+    self:resize(nil, "0.9px")
+  end
+  -- handle the individual boxType organization
+  if self.boxType == "v" then
+    self:vorganize()
+  else
+    self:horganize()
+  end
+  -- shrink/grow if needed
+  self:handleElastic()
+end
+
+--- replicates Geyser.HBox functionality, but with the option of sorting
+-- @local
+function SortBox:horganize()
+  local window_width = (self:calculate_dynamic_window_size().width / self:get_width()) * 100
+  local start_x = 0
+  local sortFunction = (self.autoSort and self.sortFunction) and SortBox.SortFunctions[self.sortFunction] or nil
+  if sortFunction then
+    for _, window in spairs(self.windowList, sortFunction) do
+      start_x = start_x + self:handleWindow(window, start_x, window_width)
+    end
+  else
+    for _, window_name in ipairs(self.windows) do
+      local window = self.windowList[window_name]
+      start_x = start_x + self:handleWindow(window, start_x, window_width)
+    end
+  end
+end
+
+--- replicates Geyser.VBox functionality, but with the option of sorting
+-- @local
+function SortBox:vorganize()
+  local window_height = (self:calculate_dynamic_window_size().height / self:get_height()) * 100
+  local start_y = 0
+  local sortFunction = (self.autoSort and self.sortFunction) and SortBox.SortFunctions[self.sortFunction] or nil
+  if sortFunction then
+    for _, window in spairs(self.windowList, sortFunction) do
+      start_y = start_y + self:handleWindow(window, start_y, window_height)
+    end
+  else
+    for _, window_name in ipairs(self.windows) do
+      local window = self.windowList[window_name]
+      start_y = start_y + self:handleWindow(window, start_y, window_height)
+    end
+  end
+end
+
+--- handles a single window during the shuffle process
+-- @local
+function SortBox:handleWindow(window, start, window_dimension)
+  local width = (window:get_width() / self:get_width()) * 100
+  local height = (window:get_height() / self:get_height()) * 100
+  if window.h_policy == Geyser.Fixed or window.v_policy == Geyser.Fixed then
+    self.contains_fixed = true
+  end
+  if self.boxType == "v" then
+    window:move("0%", start .. "%")
+    if window.h_policy == Geyser.Dynamic then
+      width = 100
+      if window.width ~= width then
+        window:resize(width .. "%", nil)
+      end
+    end
+    if window.v_policy == Geyser.Dynamic then
+      height = window_dimension * window.v_stretch_factor
+      if window.height ~= height then
+        window:resize(nil, height .. "%")
+      end
+    end
+    return height
+  else
+    window:move(start .. "%", "0%")
+    if window.h_policy == Geyser.Dynamic then
+      width = window_dimension * window.h_stretch_factor
+      if window.width ~= width then
+        window:resize(width .. "%", nil)
+      end
+    end
+    if window.v_policy == Geyser.Dynamic then
+      height = 100
+      if window.height ~= height then
+        window:resize(nil, height .. "%")
+      end
+    end
+    return width
+  end
+end
+
+---handles actually resizing the window if elastic
+-- @local
+function SortBox:handleElastic()
+  if not self.elastic or table.is_empty(self.windows) then
+    return
+  end
+  if self.boxType == "v" then
+    local contentHeight, canElastic = self:getContentHeight()
+    if not canElastic then
+      debugc(string.format("SortBox named %s cannot properly elasticize, as it contains at least one item with a dynamic v_policy", self.name))
+      return
+    end
+    local currentHeight = self:get_height()
+    local maxHeight = self.maxHeight
+    if maxHeight > 0 and contentHeight > maxHeight then
+      contentHeight = maxHeight
+    end
+    if contentHeight ~= currentHeight then
+      self:resize(nil, contentHeight)
+    end
+  else
+    local contentWidth, canElastic = self:getContentWidth()
+    if not canElastic then
+      debugc(string.format("SortBox named %s cannot properly elasticize, as it contains at least one item with a dynamic h_policy", self.name))
+      return
+    end
+    local currentWidth = self:get_width()
+    local maxWidth = self.maxWidth
+    if maxWidth > 0 and contentWidth > maxWidth then
+      contentWidth = maxWidth
+    end
+    if contentWidth ~= currentWidth then
+      self:resize(contentWidth, nil)
+    end
+  end
+end
+
+---prevents gaps from forming during resize if it doesn't autoorganize on a timer.
+-- @local
+function SortBox:reposition()
+  Geyser.Container.reposition(self)
+  if self.contains_fixed then
+    self:organize()
+  end
+end
+
+--- Returns the sum of the heights of the contents, and whether this SortBox can be elastic in height
+-- @local
+function SortBox:getContentHeight()
+  if self.boxType ~= "v" then
+    return self:get_height()
+  end
+  local canElastic = true
+  local contentHeight = 0
+  for _, window in pairs(self.windowList) do
+    contentHeight = contentHeight + window:get_height()
+    if window.v_policy == Geyser.Dynamic then
+      canElastic = false
+    end
+  end
+  return contentHeight, canElastic
+end
+
+--- Returns the sum of the widths of the contents, and whether this SortBox can be elastic in width.
+-- @local
+function SortBox:getContentWidth()
+  if self.boxType == "v" then
+    return self:get_width()
+  end
+  local canElastic = true
+  local contentWidth = 0
+  for _, window in pairs(self.windowList) do
+    contentWidth = contentWidth + window:get_width()
+    if window.h_policy == Geyser.Dynamic then
+      canElastic = false
+    end
+  end
+  return contentWidth, canElastic
+end
+
+--- Enables elasticity for the SortBox.
+function SortBox:enableElastic()
+  self:setElastic(true)
+end
+
+--- Disables elasticity for the SortBox
+function SortBox:disableElastic()
+  self:setElastic(false)
+end
+
+--- Set elasticity specifically
+-- @tparam boolean enabled if true, enable elasticity. If false, disable it.
+function SortBox:setElastic(enabled)
+  self.elastic = enabled and true or false
+end
+
+--- Set the max width of the SortBox if it's elastic
+-- @tparam number maxWidth The maximum width in pixels to resize the SortBox to. Use 0 for unlimited.
+function SortBox:setMaxWidth(maxWidth)
+  local mwtype = type(maxWidth)
+  assert(mwtype == "number", string.format("SortBox:setMaxWidth(maxWidth): SortBox: %s maxWidth as number expected, got %s", self.name, mwtype))
+  assert(maxWidth >= 0, string.format("SortBox:setMaxWidth(maxWidth): SortBox: %s maxWidth must be >= 0, %d", self.name, maxWidth))
+  self.maxWidth = maxWidth
+end
+
+--- Set the max height of the SortBox if it's elastic
+-- @tparam number maxHeight The maximum height in pixels to resize the SortBox to. Use 0 for unlimited.
+function SortBox:setMaxHeight(maxHeight)
+  local mhtype = type(maxHeight)
+  assert(mhtype == "number", string.format("SortBox:setMaxHeight(maxHeight): SortBox: %s maxHeight as number expected, got %s", self.name, mhtype))
+  assert(maxHeight >= 0, string.format("SortBox:setMaxHeight(maxHeight): SortBox: %s maxHeight must be >= 0, %d", self.name, maxHeight))
+  self.maxHeight = maxHeight
+end
+
+--- Starts the SortBox sorting and organizing itself on a timer
+function SortBox:enableTimer()
+  if self.timerID then
+    self:disableTimer()
+  end
+  self.timerSort = true
+  self.timerID = tempTimer(self.sortInterval / 1000, function()
+    self:organize()
+  end, true)
+end
+
+--- Stops the SortBox from sorting and organizing itself on a timer
+function SortBox:disableTimer()
+  killTimer(self.timerID)
+  self.timerID = nil
+  self.timerSort = false
+end
+
+--- Sets the sortInterval, or amount of time in milliseconds between auto sorting on a timer if timerSort is true
+-- @tparam number sortInterval time in milliseconds between auto sorting if timerSort is true
+function SortBox:setSortInterval(sortInterval)
+  local sitype = type(sortInterval)
+  assert(sitype == "number", string.format("SortBox:setSortInterval(sortInterval): sortInterval as number expected, got %s", sitype))
+  assert(sortInterval > 0, string.format("SortBox:setSortInterval(sortInterval): sortInterval must be positive"))
+  self.sortInterval = sortInterval
+  if self.timerSort then
+    self:enableTimer()
+  end
+end
+
+--- Enables sorting when items are added/removed, or if timerSort is true, every sortInterval milliseconds
+function SortBox:enableSort()
+  self.autoSort = true
+  self:organize()
+end
+
+--- Disables sorting when items are added or removed
+function SortBox:disableSort()
+  self.autoSort = false
+end
+
+---Set whether the SortBox acts as a VBox or HBox.
+-- @tparam string boxType If you pass 'h' or 'horizontal' it will act like an HBox. Anything else it will act like a VBox.
+-- @usage mySortBox:setBoxType("v") -- behave like a VBox
+-- mySortBox:setBoxType("h") -- behave like an HBox
+-- mySortBox:setBoxType("beeblebrox") -- why?! Why would you do this? It'll behave like a VBox
+function SortBox:setBoxType(boxType)
+  boxType = boxType:lower()
+  if boxType == "h" or boxType == "horizontal" then
+    self.boxType = "h"
+  else
+    self.boxType = "v"
+  end
+end
+
+---Sets the type of sorting in use by this SortBox.
+-- <br>If an item in the box does not have the appropriate property or function, then 999999999 is used for sorting except as otherwise noted.
+-- <br>If an invalid option is given, then existing H/VBox behaviour is maintained, just like if autoSort is false.
+-- @usage mySortBox:setSortFunction("gaugeValue")
+-- @tparam string functionName what type of sorting should we use? See table below for valid options and their descriptions.
+-- <table class="tg">
+-- <thead>
+--   <tr>
+--     <th>sort type</th>
+--     <th>description</th>
+--   </tr>
+-- </thead>
+-- <tbody>
+--   <tr>
+--     <td class="tg-1">gaugeValue</td>
+--     <td class="tg-1">sort gauges based on how full the gauge is, from less full to more</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">reverseGaugeValue</td>
+--     <td class="tg-2">sort gauges based on how full the gauge is, from more full to less</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">timeLeft</td>
+--     <td class="tg-1">sort TimerGauges based on the total time left in the gauge, from less time to more</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">reverseTimeLeft</td>
+--     <td class="tg-2">sort TimerGauges based on the total time left in the gauge, from more time to less</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">name</td>
+--     <td class="tg-1">sort any item (and mixed types) by name, alphabetically.</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">reverseName</td>
+--     <td class="tg-2">sort any item (and mixed types) by name, reverse alphabetically.</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">message</td>
+--     <td class="tg-1">sorts Labels based on their echoed message, alphabetically. If not a label, the empty string will be used</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">reverseMessage</td>
+--     <td class="tg-2">sorts Labels based on their echoed message, reverse alphabetically. If not a label, the empty string will be used</td>
+--   </tr>
+-- </tbody>
+-- </table>
+
+function SortBox:setSortFunction(functionName)
+  self.sortFunction = functionName
+end
+
+SortBox.parent = Geyser.Container
+
+return SortBox
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/source/spinbox.lua.html b/src/resources/MDK/doc/source/spinbox.lua.html new file mode 100755 index 0000000..e25fc5c --- /dev/null +++ b/src/resources/MDK/doc/source/spinbox.lua.html @@ -0,0 +1,580 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

spinbox.lua

+
+--- A Geyser object to create a spinbox for adjusting a number
+-- @classmod spinbox
+-- @author Damian Monogue <demonnic@gmail.com>
+-- @copyright 2023
+-- @license MIT, see https://raw.githubusercontent.com/demonnic/MDK/main/src/scripts/LICENSE.lua
+local spinbox = {
+  parent = Geyser.Container,
+  name = 'SpinboxClass',
+  min = 0,
+  max = 10,
+  delta = 1,
+  value = 0,
+  activeButtonColor = "gray",
+  inactiveButtonColor = "DimGray",
+  integer = true,
+  upArrowLocation = "https://demonnic.github.io/image-assets/uparrow.png",
+  downArrowLocation = "https://demonnic.github.io/image-assets/downarrow.png",
+  color = "#202020"
+}
+spinbox.__index = spinbox
+setmetatable(spinbox, spinbox.parent)
+
+local gss = Geyser.StyleSheet
+local directory = getMudletHomeDir() .. "/spinbox/"
+local saveFile = directory .. "fileLocations.lua"
+if not io.exists(directory) then
+  lfs.mkdir(directory)
+end
+
+--- Creates a new spinbox.
+-- @tparam table cons a table containing the options for this spinbox.
+-- <table class="tg">
+-- <thead>
+--   <tr>
+--     <th>option name</th>
+--     <th>description</th>
+--     <th>default</th>
+--   </tr>
+-- </thead>
+-- <tbody>
+--   <tr>
+--     <td class="tg-1">min</td>
+--     <td class="tg-1">The minimum value for this spinbox</td>
+--     <td class="tg-1">0</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">max</td>
+--     <td class="tg-2">The maximum value for this spinbox</td>
+--     <td class="tg-2">10</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">activeButtonColor</td>
+--     <td class="tg-1">The color the up/down buttons should be when they are active/able to be used</td>
+--     <td class="tg-1">gray</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">inactiveButtonColor</td>
+--     <td class="tg-2">The color the up/down buttons should be when they are inactive/unable to be used</td>
+--     <td class="tg-2">dimgray</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">integer</td>
+--     <td class="tg-1">Boolean value. When true, values must always be integers (no decimal place)</td>
+--     <td class="tg-1">true</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">delta</td>
+--     <td class="tg-2">The amount to change the spinbox's value when the up or down button is pressed.</td>
+--     <td class="tg-2">1</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">upArrowLocation</td>
+--     <td class="tg-1">The location of the up arrow image. Either a web URL where it can be downloaded, or the location on disk to read it from</td>
+--     <td class="tg-1">https://demonnic.github.io/image-assets/uparrow.png</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">downArrowLocation</td>
+--     <td class="tg-2">The location of the down arrow image. Either a web URL where it can be downloaded, or the location on disk to read it from</td>
+--     <td class="tg-2">https://demonnic.github.io/image-assets/downarrow.png</td>
+--   <tr>
+--     <td class="tg-1">callBack</td>
+--     <td class="tg-1">The function to run when the spinbox's value is updated. Is called with parameters (self.name, value, oldValue)</td>
+--     <td class="tg-1">nil</td>
+--   </tr>
+--   </tr>
+--</tbody>
+--</table>
+-- @param container The Geyser container for this spinbox
+function spinbox:new(cons, container)
+  cons = cons or {}
+  local consType = type(cons)
+  if consType ~= "table" then
+    printError(f"spinbox:new(cons, container): cons as table of options expected, got {consType}!", true, true)
+  end
+  cons.name = cons.name or Geyser.nameGen("spinbox")
+  local me = self.parent:new(cons, container)
+  setmetatable(me, self)
+  me:createComponents()
+  if me.callBack then
+    me:setCallBack(me.callBack)
+  end
+  me.oldValue = me.value
+  return me
+end
+
+--- Creates the components that make up the spinbox UI.
+-- @local
+-- Obtains the up and down arrow images specified in the spinbox options.
+-- Generates styles for the spinbox.
+-- Calculates the height of the up/down buttons and any remainder space.
+-- Creates:
+--   `self.upButton` - A button with an up arrow image for incrementing the value
+--   `self.downButton` - A button with a down arrow image for decrementing the value
+--   `self.displayLabel` - A label to display the current spinbox value
+--   `self.input` - A command line input to allow directly entering a value
+-- Hides the input by default.
+-- Applies the generated styles.
+function spinbox:createComponents()
+  self:obtainImages()
+  self:generateStyles()
+  self:calculateButtonDimensions()
+
+  self.upButton = self:createButton("up")
+  self.downButton = self:createButton("down")
+
+  self.displayLabel = self:createDisplayLabel()
+
+  self.input = self:createInput()
+  self.input:hide()
+
+  self:applyStyles()
+end
+
+--- Calculates the button height. We use square buttons in this house.
+-- @local
+-- Calculates the height of the up/down buttons by dividing the spinbox height in half.
+-- Stores the remainder (if any) in self.remainder.
+-- Stores the calculated button height in self.buttonHeight.
+function spinbox:calculateButtonDimensions()
+  self.buttonHeight = math.floor(self.get_height() / 2)
+  self.remainder = self.get_height() % 2
+end
+
+--- Creates a button (up or down arrow) for the spinbox.
+-- @param type Either "up" or "down" to specify which direction the arrow should point
+-- @return The created Geyser.Label button
+-- @local
+-- Creates a Geyser.Label button with an up or down arrow image.
+-- Positions the button at the top or bottom of the spinbox respectively.
+-- Sets a click callback on the button to call increment() or decrement() depending on the type.
+-- Returns the created button.
+function spinbox:createButton(type)
+  local button = Geyser.Label:new({
+    name = self.name .. "spinbox_"..type.."Arrow",
+    height = self.buttonHeight,
+    width = self.buttonHeight,
+    x = "100%-" .. self.buttonHeight,
+    y = type == "up" and 0 or self.buttonHeight + self.remainder,
+  }, self)
+
+  button:setClickCallback(function()
+    if type == "up" then
+      self:increment()
+    else
+      self:decrement()
+    end
+  end)
+  return button
+end
+
+--- Creates the display label for the spinbox value.
+-- @return The created Geyser.Label display label
+-- @local
+-- Creates a Geyser.Label to display the current spinbox value.
+-- Centers the text in the label.
+-- Sets a double click callback on the label to show the input, put the current
+-- value in it, select the text, and hide the label.
+-- Returns the created display label.
+function spinbox:createDisplayLabel()
+  local displayLabel = Geyser.Label:new({
+    name = self.name .. "spinbox_displayLabel",
+    x = 0,
+    y = 0,
+    width = "100%-" .. self.buttonHeight,
+    height = "100%",
+    message = self.value
+  }, self)
+  displayLabel:setAlignment("center")
+  displayLabel:setDoubleClickCallback(function()
+    self.input:show()
+    self.input:print(self.value)
+    self.input:selectText()
+    displayLabel:hide()
+  end)
+  return displayLabel
+end
+
+--- Creates the input for directly entering a spinbox value.
+-- @return The created Geyser.CommandLine input
+-- @local
+-- Creates a Geyser.CommandLine input.
+-- Sets an action on the input to:
+--   - Attempt to convert the input text to a number
+--   - If successful, call setValue() with the number to set the spinbox value
+--   - Hide the input
+--   - Show the display label
+--   - Put the new spinbox value in the input
+-- Returns the created input.
+function spinbox:createInput()
+  local input = Geyser.CommandLine:new({
+    x = 0,
+    y = 0,
+    width = "100%-".. self.buttonHeight,
+    height = "100%",
+  }, self)
+  input:setAction(function(txt)
+    txt = tonumber(txt)
+    if txt then
+      self:setValue(txt)
+      input:hide()
+    end
+    self.displayLabel:show()
+    input:print(self.value)
+  end)
+  return input
+end
+
+--- Used to increment the value by the delta amount
+-- @local
+-- Increments the spinbox value by the delta amount.
+-- Checks if the new value would exceed the max, and if so sets it to the max.
+-- Updates the display label with the new value.
+-- Applies any styles that depend on the value.
+function spinbox:increment()
+  local val = self.value + self.delta
+  if val >= self.max then
+    val = self.max
+  end
+  self.oldValue = self.value
+  self.value = val
+  self.displayLabel:echo(val)
+  self:applyStyles()
+  self:handleCallBacks()
+end
+
+--- Used to decrement the value by the delta amount
+-- @local
+-- Decrements the spinbox value by the delta amount.
+-- Checks if the new value would be below the min, and if so sets it to the min.
+-- Updates the display label with the new value.
+-- Applies any styles that depend on the value.
+function spinbox:decrement()
+  local val = self.value - self.delta
+  if val <= self.min then
+    val = self.min
+  end
+  self.oldValue = self.value
+  self.value = val
+  self.displayLabel:echo(val)
+  self:applyStyles()
+  self:handleCallBacks()
+end
+
+--- Used to directly set the value of the spinbox.
+-- @param value The new value to set
+-- Rounds the value to an integer if the spinbox is integer only.
+-- Checks if the new value is within the min/max range and clamps it if not.
+-- Updates the display label with the new value.
+-- Applies any styles that depend on the value.
+function spinbox:setValue(value)
+  if self.integer then
+    value = math.floor(value)
+  end
+  if value >= self.max then
+    value = self.max
+  elseif value <= self.min then
+    value = self.min
+  end
+  self.oldValue = self.value
+  self.value = value
+  self.displayLabel:echo(value)
+  self:applyStyles()
+  self:handleCallBacks()
+end
+
+--- Obtains the up and down arrow images for the spinbox.
+-- @local
+-- Gets the previously saved file locations.
+-- Checks if the up arrow image exists at the upArrowLocation.
+-- If not, it will download the image from a URL or copy a local file. It saves
+-- the new location.
+-- Does the same for the down arrow image and downArrowLocation.
+-- Saves any new locations to the save file.
+-- Sets self.upArrowFile and self.downArrowFile to the locations of the images.
+function spinbox:obtainImages()
+  local locations = self:getFileLocs()
+  local upURL = self.upArrowLocation
+  local downURL = self.downArrowLocation
+  local upFile = locations[upURL]
+  local downFile = locations[downURL]
+  local locationsChanged = false
+  if not (upFile and io.exists(upFile)) then
+    if not upFile then
+      upFile = directory .. self.name .. "/uparrow.png"
+      locations[upURL] = upFile
+      locationsChanged = true
+    end
+    if upURL:match("^http") then
+      self:downloadFile(upURL, upFile)
+    elseif io.exists(upURL) then
+      upFile = upURL
+      locations[upURL] = upFile
+      locationsChanged = true
+    end
+  end
+  if not (downFile and io.exists(downFile)) then
+    if not downFile then
+      downFile = directory .. self.name .. "/downarrow.png"
+      locations[downURL] = downFile
+      locationsChanged = true
+    end
+    if downURL:match("^http") then
+      self:downloadFile(downURL, downFile)
+    elseif io.exists(downURL) then
+      downFile = downURL
+      locations[downURL] = downFile
+      locationsChanged = true
+    end
+  end
+  self.upArrowFile = upFile
+  self.downArrowFile = downFile
+  if locationsChanged then
+    table.save(saveFile, locations)
+  end
+end
+
+--- Handles the actual download of a file from a url
+-- @param url The url to download the file from
+-- @param fileName The location to save the downloaded file
+-- @local
+-- Creates any missing directories in the file path.
+-- Registers named event handlers to handle the download completing or erroring.
+-- The completion handler stops the error handler.
+-- The error handler prints an error message and stops the completion handler.
+-- Downloads the file from the url to the fileName location.
+function spinbox:downloadFile(url, fileName)
+  local parts = fileName:split("/")
+  parts[#parts] = nil
+  local dirName = table.concat(parts, "/") .. "/"
+  if not io.exists(dirName) then
+    lfs.mkdir(dirName)
+  end
+  local uname = "spinbox"
+  local handlerName = self.name .. url
+  local handler = function(event, ...)
+    local args = {...}
+    local file = #args == 1 and args[1] or args[2]
+    if file ~= fileName then
+      return true
+    end
+    if event == "sysDownloadDone" then
+      debugc(f"INFO:Spinbox successfully downloaded {file}")
+      stopNamedEventHandler(uname, handlerName .. "error")
+      return false
+    end
+    cecho(f"\n<red>ERROR:<reset>Spinbox had an issue downloading an image file to {file}: {args[1]}\n")
+    stopNamedEventHandler(uname, handlerName .. "done")
+  end
+  registerNamedEventHandler(uname, handlerName .. "done", "sysDownloadDone", handler, true)
+  registerNamedEventHandler(uname, handlerName .. "error", "sysDownloadError", handler, true)
+  downloadFile(fileName, url)
+end
+
+--- Responsible for reading the file locations from disk and returning them
+-- @local
+function spinbox:getFileLocs()
+  local locations = {}
+  if io.exists(saveFile) then
+    table.load(saveFile, locations)
+  end
+  return locations
+end
+
+--- (Re)generates the stylesheets for the spinbox
+-- Should not need to call but if you change something and it doesn't take effect
+-- you can try calling this followed by applyStyles
+function spinbox:generateStyles()
+  self.baseStyle = gss:new([[
+    border-radius: 2px;
+    border-color: black;
+  ]])
+  self.activeStyle = gss:new(f[[
+    background-color: {self.activeButtonColor};
+  ]], self.baseStyle)
+  self.inactiveStyle = gss:new(f[[
+    background-color: {self.inactiveButtonColor};
+  ]], self.baseStyle)
+  self.upStyle = gss:new(f[[
+    border-image: url("{self.upArrowFile}");
+  ]])
+  self.downStyle = gss:new(f[[
+    border-image: url("{self.downArrowFile}");
+  ]])
+  self.displayStyle = gss:new(f[[
+    background-color: {Geyser.Color.hex(self.color)};
+    text-align: center;
+  ]], self.baseStyle)
+end
+
+--- Applies updated stylesheets to the components of the spinbox
+-- Should not need to call this directly
+function spinbox:applyStyles()
+  if self.value >= self.max then
+    self.upStyle:setParent(self.inactiveStyle)
+  else
+    self.upStyle:setParent(self.activeStyle)
+  end
+  if self.value <= self.min then
+    self.downStyle:setParent(self.inactiveStyle)
+  else
+    self.downStyle:setParent(self.activeStyle)
+  end
+  self.upButton:setStyleSheet(self.upStyle:getCSS())
+  self.downButton:setStyleSheet(self.downStyle:getCSS())
+  self.displayLabel:setStyleSheet(self.displayStyle:getCSS())
+end
+
+--- sets the color for active buttons on the spinbox
+-- @param color any valid color formatting string, such a "red" or "#880000" or "<128,0,0>" or a table of colors, like {128, 0,0}. See Geyser.Color.parse at https://www.mudlet.org/geyser/files/geyser/GeyserColor.html#Geyser.Color.parse
+function spinbox:setActiveButtonColor(color)
+  local colorType = type(color)
+  local hex
+  if colorType == "table" then
+    hex = Geyser.Color.hex(unpack(color))
+  else
+    hex = Geyser.Color.hex(color)
+  end
+  self.activeButtonColor = hex
+  self.activeStyle:set("background-color", hex)
+  self:applyStyles()
+end
+
+--- sets the color for inactive buttons on the spinbox
+-- @param color any valid color formatting string, such a "<red>" or "red" or "<128,0,0>" or a table of colors, like {128, 0,0}. See Geyser.Color.parse at https://www.mudlet.org/geyser/files/geyser/GeyserColor.html#Geyser.Color.parse
+function spinbox:setInactiveButtonColor(color)
+  local colorType = type(color)
+  local hex
+  if colorType == "table" then
+    hex = Geyser.Color.hex(unpack(color))
+  else
+    hex = Geyser.Color.hex(color)
+  end
+  self.inactiveButtonColor = hex
+  self.inactiveStyle:set("background-color", hex)
+  self:applyStyles()
+end
+
+-- internal function that handles calling a registered callback and raising an event any time the
+-- spinbox value is changed, whether using the buttons or the :set function.
+function spinbox:handleCallBacks()
+  raiseEvent("spinbox updated", self.name, self.value, self.oldValue)
+  if self.callBack then
+    local ok, err = pcall(self.callBack, self.name, self.value, self.oldValue)
+    if not ok then
+      printError(f"Had an issue running the callback handler for spinbox named {self.name}: {err}", true, true)
+    end
+  end
+end
+
+--- Set a callback function for the spinbox to call any time the value of the spinbox is changed
+-- the function will be called as func(self.value, self.name)
+function spinbox:setCallBack(func)
+  local funcType = type(func)
+  if funcType ~= "function" then
+    printError(f"spinbox:setCallBack(func): func as function required, got {funcType}", true, true)
+  end
+  self.callBack = func
+  return true
+end
+
+return spinbox
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/source/sug.lua.html b/src/resources/MDK/doc/source/sug.lua.html new file mode 100755 index 0000000..50b2f9a --- /dev/null +++ b/src/resources/MDK/doc/source/sug.lua.html @@ -0,0 +1,354 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

sug.lua

+
+--- Self Updating Gauge, extends <a href="https://www.mudlet.org/geyser/files/geyser/GeyserGauge.html">Geyser.Gauge</a>
+-- @classmod SUG
+-- @author Damian Monogue <demonnic@gmail.com>
+-- @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,
+}
+
+-- Internal function, used to turn a string variable name into a value
+local function getValueAt(accessString)
+  local ok, err = pcall(loadstring("return " .. tostring(accessString)))
+  if ok then return err end
+  return nil, err
+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:
+-- <br>
+-- <table class="tg">
+-- <tr>
+--  <th>name</th>
+--  <th>description</th>
+--  <th>default</th>
+-- </tr>
+-- <tr>
+--  <td class="tg-1">active</td>
+--  <td class="tg-1">boolean, if true starts the timer updating</td>
+--  <td class="tg-1">true</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-2">updateTime</td>
+--  <td class="tg-2">How often should the gauge autoupdate? Milliseconds. 0 to disable the timer but still allow event updates</td>
+--  <td class="tg-2">333</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-1">currentVariable</td>
+--  <td class="tg-1">What variable will hold the 'current' value of the gauge? Pass the name as a string, IE "currentHP" or "gmcp.Char.Vitals.hp"</td>
+--  <td class="tg-1">""</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-2">maxVariable</td>
+--  <td class="tg-2">What variable will hold the 'current' value of the gauge? Pass the name as a string, IE "maxHP" or "gmcp.Char.Vitals.maxhp"</td>
+--  <td class="tg-2">""</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-1">textTemplate</td>
+--  <td class="tg-1">Template 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</td>
+--  <td class="tg-1">" |c/|m |p%"</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-2">defaultCurrent</td>
+--  <td class="tg-2">What value to use if the currentVariable points to nil or something which cannot be made a number?</td>
+--  <td class="tg-2">50</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-1">defaultMax</td>
+--  <td class="tg-1">What value to use if the maxVariable points to nil or something which cannot be made a number?</td>
+--  <td class="tg-1">100</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-2">updateEvent</td>
+--  <td class="tg-2">The 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</td>
+--  <td class="tg-2">""</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-1">updateHook</td>
+--  <td class="tg-1">A function which is run each time the gauge updates. Should take 3 arguments, the gauge itself, current value, and max value. You can return new current and max values to be used, for example `return 34, 120` would cause the gauge to use 34 for current and 120 for max regardless of what the variables it reads say.</td>
+--  <td class="tg-1"></td>
+-- </tr>
+-- </table>
+-- @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
+
+--- Set the updateHook function which is run just prior to the gauge updating
+-- @tparam function func The function which will be called when the gauge updates. It should take 3 arguments, the gauge itself, the current value, and the max value. If you wish to override the current or max values used for the gauge, you can return new current and max values, like `return newCurrent newMax`
+function SUG:setUpdateHook(func)
+  local funcType = type(func)
+  if funcType ~= "function" then
+    return nil, "setUpdateHook only takes functions, no strings or anything like that. You passed in: " .. funcType
+  end
+  self.updateHook = func
+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
+  if self.updateHook and type(self.updateHook) == "function" then
+    local ok, newcur, newmax = pcall(self.updateHook, self, current, max)
+    if ok and newcur then
+      current = newcur
+      max = newmax and newmax or self.defaultMax
+    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
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/source/textgauge.lua.html b/src/resources/MDK/doc/source/textgauge.lua.html new file mode 100755 index 0000000..b921c98 --- /dev/null +++ b/src/resources/MDK/doc/source/textgauge.lua.html @@ -0,0 +1,434 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

textgauge.lua

+
+--- Creates a text based gauge, for use in miniconsoles and the like.
+-- @classmod TextGauge
+-- @author Damian Monogue <demonnic@gmail.com>
+-- @copyright 2020 Damian Monogue
+-- @copyright 2021 Damian Monogue
+-- @license MIT, see LICENSE.lua
+local TextGauge = {width = 24, fillCharacter = ":", emptyCharacter = "-", showPercent = true, showPercentSymbol = true, format = "c", value = 50}
+
+--- Creates a new TextGauge.
+-- @tparam[opt] table options The table of options you would like the TextGauge to start with.
+-- <br><br>Table of new options
+-- <table class="tg">
+-- <thead>
+--   <tr>
+--     <th>option name</th>
+--     <th>description</th>
+--     <th>default</th>
+--   </tr>
+-- </thead>
+-- <tbody>
+--   <tr>
+--     <td class="tg-1">width</td>
+--     <td class="tg-1">How many characters wide to make the gauge</td>
+--     <td class="tg-1">24</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">fillCharacter</td>
+--     <td class="tg-2">What character to use for the 'full' part of the gauge</td>
+--     <td class="tg-2">:</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">overflowCharacter</td>
+--     <td class="tg-1">What character to use for >100% part of the gauge</td>
+--     <td class="tg-1">if not set, it uses whatever you set fillCharacter to</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">emptyCharacter</td>
+--     <td class="tg-2">What character to use for the 'empty' part of the gauge</td>
+--     <td class="tg-2">-</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">showPercentSymbol</td>
+--     <td class="tg-1">Should we show the % sign itself?</td>
+--     <td class="tg-1">true</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">showPercent</td>
+--     <td class="tg-2">Should we show what % of the gauge is filled?</td>
+--     <td class="tg-2">true</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">value</td>
+--     <td class="tg-1">How much of the gauge should be filled</td>
+--     <td class="tg-1">50</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">format</td>
+--     <td class="tg-2">What type of color formatting to use? 'c' for cecho, 'd' for decho, 'h' for hecho</td>
+--     <td class="tg-2">c</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">fillColor</td>
+--     <td class="tg-1">What color to make the full part of the bar?</td>
+--     <td class="tg-1">"DarkOrange" or equivalent for your format type</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">emptyColor</td>
+--     <td class="tg-2">what color to use for the empty part of the bar?</td>
+--     <td class="tg-2">"white" or format appropriate equivalent</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">percentColor</td>
+--     <td class="tg-1">What color to print the percentage numvers in, if shown?</td>
+--     <td class="tg-1">"white" or fortmat appropriate equivalent</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-2">percentSymbolColor</td>
+--     <td class="tg-2">What color to make the % if shown?</td>
+--     <td class="tg-2">If not set, uses what percentColor is set to.</td>
+--   </tr>
+--   <tr>
+--     <td class="tg-1">overflowColor</td>
+--     <td class="tg-1">What color to make the >100% portion of the bar?</td>
+--     <td class="tg-1">If not set, will use the same color as fillColor</td>
+--   </tr>
+-- </tbody>
+-- </table>
+-- @usage
+-- local TextGauge = require("MDK.textgauge")
+-- myTextGauge = TextGauge:new()
+-- gaugeText = myTextGauge:setValue(382, 830)
+function TextGauge:new(options)
+  options = options or {}
+  local optionsType = type(options)
+  assert(optionsType == "table" or optionsType == "nil", "TextGauge:new(options): options expected as table, got " .. optionsType)
+  local me = table.deepcopy(options)
+  setmetatable(me, self)
+  self.__index = self
+  me:setDefaultColors()
+  return me
+end
+
+--- Sets the width in characters of the gauge
+-- @tparam number width number of characters wide to make the gauge
+function TextGauge:setWidth(width)
+  local widthType = type(width)
+  assert(widthType == "number", string.format("TextGauge:setWidth(width): width as number expected, got %s", widthType))
+  self.width = width
+end
+
+function TextGauge:setFormat(format)
+  self.format = self:getColorType(format)
+  self:setDefaultColors()
+end
+
+--- Sets the character to use for the 'full' part of the gauge
+-- @tparam string character the character to use.
+function TextGauge:setFillCharacter(character)
+  assert(character ~= nil, "TextGauge:setFillCharacter(character): character required, got nil")
+  assert(utf8.len(character) == 1, "TextGauge:setFillCharacter(character): character must be a single character")
+  self.fillCharacter = character
+end
+
+--- Sets the character to use for the 'overflow' (>100%) part of the gauge
+-- @tparam string character the character to use.
+function TextGauge:setOverflowCharacter(character)
+  assert(character ~= nil, "TextGauge:setOverflowCharacter(character): character required, got nil")
+  assert(utf8.len(character) == 1, "TextGauge:setOverflowCharacter(character): character must be a single character")
+  self.overflowCharacter = character
+end
+
+--- Sets the character to use for the 'full' part of the gauge
+-- @tparam string character the character to use.
+function TextGauge:setEmptyCharacter(character)
+  assert(character ~= nil, "TextGauge:setEmptyCharacter(character): character required, got nil")
+  assert(utf8.len(character) == 1, "TextGauge:setEmptyCharacter(character): character must be a single character")
+  self.emptyCharacter = character
+end
+
+--- Sets the fill color for the gauge.
+-- @tparam string color the color to use for the full portion of the gauge. Will be run through Geyser.Golor
+function TextGauge:setFillColor(color)
+  assert(color ~= nil, "TextGauge:setFillColor(color): color required, got nil")
+  self.fillColor = color
+end
+
+--- Sets the overflow color for the gauge.
+-- @tparam string color the color to use for the full portion of the gauge. Will be run through Geyser.Golor
+function TextGauge:setOverflowColor(color)
+  assert(color ~= nil, "TextGauge:setOverflowColor(color): color required, got nil")
+  self.overflowColor = color
+end
+
+--- Sets the empty color for the gauge.
+-- @tparam string color the color to use for the empty portion of the gauge. Will be run through Geyser.Golor
+function TextGauge:setEmptyColor(color)
+  assert(color ~= nil, "TextGauge:setEmptyColor(color): color required, got nil")
+  self.emptyColor = color
+end
+
+--- Sets the fill color for the gauge.
+-- @tparam string color the color to use for the numeric value. Will be run through Geyser.Golor
+function TextGauge:setPercentColor(color)
+  assert(color ~= nil, "TextGauge:setPercentColor(color): color required, got nil")
+  self.percentColor = color
+end
+--- Sets the fill color for the gauge.
+-- @tparam string color the color to use for the numeric value. Will be run through Geyser.Golor
+function TextGauge:setPercentSymbolColor(color)
+  assert(color ~= nil, "TextGauge:setPercentSymbolColor(color): color required, got nil")
+  self.percentSymbolColor = color
+end
+
+--- Enables reversing the fill direction (right to left instead of the usual left to right)
+function TextGauge:enableReverse()
+  self.reverse = true
+end
+
+--- Disables reversing the fill direction (go back to the usual left to right)
+function TextGauge:disableReverse()
+  self.reverse = false
+end
+
+--- Enables showing the percent value of the gauge
+function TextGauge:enableShowPercent()
+  self.showPercent = true
+end
+
+--- Disables showing the percent value of the gauge
+function TextGauge:disableShowPercent()
+  self.showPercent = false
+end
+
+--- Enables showing the percent symbol (appears after the value)
+function TextGauge:enableShowPercentSymbol()
+  self.showPercentSymbol = true
+end
+
+--- Enables showing the percent symbol (appears after the value)
+function TextGauge:disableShowPercentSymbol()
+  self.showPercentSymbol = false
+end
+
+function TextGauge:getColorType(format)
+  format = format or self.format
+  local dec = {"d", "decimal", "dec", "decho"}
+  local hex = {"h", "hexidecimal", "hex", "hecho"}
+  local col = {"c", "color", "colour", "col", "name", "cecho"}
+  if table.contains(col, format) then
+    return "c"
+  elseif table.contains(dec, format) then
+    return "d"
+  elseif table.contains(hex, format) then
+    return "h"
+  else
+    return ""
+  end
+end
+
+-- internal function, used at instantiation to ensure some colors are set
+function TextGauge:setDefaultColors()
+  local colorType = self:getColorType()
+  if colorType == "c" then
+    self.percentColor = self.percentColor or "white"
+    self.percentSymbolColor = self.percentSymbolColor or self.percentColor
+    self.fillColor = self.fillColor or "DarkOrange"
+    self.emptyColor = self.emptyColor or "white"
+    self.resetColor = "<reset>"
+  elseif colorType == "d" then
+    self.percentColor = self.percentColor or "<255,255,255>"
+    self.percentSymbolColor = self.percentSymbolColor or self.percentColor
+    self.fillColor = self.fillColor or "<255,140,0>"
+    self.emptyColor = self.emptyColor or "<255,255,255>"
+    self.resetColor = "<r>"
+  elseif colorType == "h" then
+    self.percentColor = self.percentColor or "#ffffff"
+    self.percentSymbolColor = self.percentSymbolColor or self.percentColor
+    self.fillColor = self.fillColor or "#ff8c00"
+    self.emptyColor = self.emptyColor or "#ffffff"
+    self.resetColor = "#r"
+  else
+    self.percentColor = self.percentColor or ""
+    self.percentSymbolColor = self.percentSymbolColor or self.percentColor
+    self.fillColor = self.fillColor or ""
+    self.emptyColor = self.emptyColor or ""
+    self.resetColor = ""
+  end
+  self.overflowColor = self.overflowColor or self.fillColor
+end
+
+-- Internal function used to route Geyser.Color based on internally stored format
+function TextGauge:getColor(color)
+  local colorType = self:getColorType()
+  if colorType == "c" then
+    return string.format("<%s>", color) -- pass the color back in <> for cecho
+  elseif colorType == "d" then
+    return Geyser.Color.hdec(color) -- return it in decho format
+  elseif colorType == "h" then
+    return Geyser.Color.hex(color) -- return it in hex format
+  else
+    return "" -- return an empty string for noncolored output
+  end
+end
+
+--- Used to set the gauge's value and return the string representation of the gauge
+-- @tparam[opt] number current current value. If no value is passed it will use the stored value. Defaults to 50 to prevent errors.
+-- @tparam[opt] number max maximum value. If not passed, the internally stored one will be used. Defaults to 100 so that it can be used with single values as a percent
+-- @usage myGauge:setValue(55) -- sets the gauge to 55% full
+-- @usage myGauge:setValue(2345, 2780) -- will figure out what the percentage fill is based on the given current/max values
+function TextGauge:setValue(current, max)
+  current = current or self.value
+  assert(type(current) == "number", "TextGauge:setValue(current,max) current as number expected, got " .. type(current))
+  assert(max == nil or type(max) == "number", "TextGauge:setValue(current, max) option max as number expected, got " .. type(max))
+  if current < 0 then
+    current = 0
+  end
+  max = max or 100
+  local value = math.floor(current / max * 100)
+  self.value = value
+  local width = self.width
+  local percentString = ""
+  local percentSymbolString = ""
+  local fillCharacter = self.fillCharacter
+  local overflowCharacter = self.overflowCharacter or fillCharacter
+  local emptyCharacter = self.emptyCharacter
+  local fillColor = self:getColor(self.fillColor)
+  local overflowColor = self:getColor(self.overflowColor)
+  local emptyColor = self:getColor(self.emptyColor)
+  local percentColor = self:getColor(self.percentColor)
+  local percentSymbolColor = self:getColor(self.percentSymbolColor)
+  local resetColor = self.resetColor
+  if self.showPercent then
+    percentString = string.format("%s%02d%s", percentColor, value, resetColor)
+    width = width - 2
+  end
+  if self.showPercentSymbol then
+    percentSymbolString = string.format("%s%s%s", percentSymbolColor, "%", resetColor)
+    width = width - 1
+  end
+  local perc = value / 100
+  local overflow = perc - 1
+  if overflow < 0 then
+    overflow = 0
+  end
+  if overflow > 1 then
+    perc = 2
+    overflow = 1
+  end
+  local overflowWidth = math.floor(overflow * width)
+  local fillWidth = math.floor((perc - overflow) * width)
+  local emptyWidth = width - fillWidth
+  fillWidth = fillWidth - overflowWidth
+  if value >= 100 and self.showPercent then
+    fillWidth = fillWidth - 1
+  end
+  if value >= 200 and self.showPercent then
+    overflowWidth = overflowWidth - 1
+  end
+  local result = ""
+  if self.reverse then
+    result = string.format("%s%s%s%s%s%s%s%s%s%s%s", emptyColor, string.rep(emptyCharacter, emptyWidth), resetColor,fillColor, string.rep(fillCharacter, fillWidth), resetColor, overflowColor, string.rep(overflowCharacter, overflowWidth), resetColor, percentString, percentSymbolString, resetColor)
+  else
+    result = string.format("%s%s%s%s%s%s%s%s%s%s%s", overflowColor, string.rep(overflowCharacter, overflowWidth), fillColor,
+                  string.rep(fillCharacter, fillWidth), resetColor, emptyColor, string.rep(emptyCharacter, emptyWidth), resetColor,
+                  percentString, percentSymbolString, resetColor)
+  end
+  return result
+end
+
+--- Synonym for setValue
+function TextGauge:print(...)
+  self:setValue(...)
+end
+
+return TextGauge
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/doc/source/timergauge.lua.html b/src/resources/MDK/doc/source/timergauge.lua.html new file mode 100755 index 0000000..1372b2c --- /dev/null +++ b/src/resources/MDK/doc/source/timergauge.lua.html @@ -0,0 +1,628 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

timergauge.lua

+
+--- Animated countdown timer, extends <a href="https://www.mudlet.org/geyser/files/geyser/GeyserGauge.html">Geyser.Gauge</a>
+-- @classmod TimerGauge
+-- @author Damian Monogue <demonnic@gmail.com>
+-- @copyright 2020 Damian Monogue
+-- @license MIT, see LICENSE.lua
+local TimerGauge = {
+  name = "TimerGaugeClass",
+  active = true,
+  showTime = true,
+  prefix = "",
+  timeFormat = "S.t",
+  suffix = "",
+  updateTime = "10",
+  autoHide = true,
+  autoShow = true,
+  manageContainer = false,
+}
+
+function TimerGauge:setStyleSheet(cssFront, cssBack, cssText)
+  cssFront = cssFront or self.cssFront
+  cssBack = cssBack or self.cssBack
+  cssBack = cssBack or self.cssFront .. "background-color: black;"
+  cssText = cssText or self.cssText
+  if cssFront then
+    self.front:setStyleSheet(cssFront)
+  end
+  if cssBack then
+    self.back:setStyleSheet(cssBack)
+  end
+  if cssText then
+    self.text:setStyleSheet(cssText)
+  end
+  -- self.gauge:setStyleSheet(cssFront, cssBack, cssText)
+  self.cssFront = cssFront
+  self.cssBack = cssBack
+  self.cssText = cssText
+end
+
+--- Shows the TimerGauge. If the manageContainer property is true, then will add it back to its container
+function TimerGauge:show2()
+  if self.manageContainer and self.savedContainer then
+    self.savedContainer:add(self)
+    self.savedContainer = nil
+  end
+  self:show()
+end
+
+--- Hides the TimerGauge. If manageContainer property is true, then it will remove it from its container and if the container is an HBox or VBox it will initiate size/position management
+function TimerGauge:hide2()
+  if self.manageContainer and self.container.name ~= Geyser.name then
+    self.savedContainer = self.container
+    Geyser:add(self)
+    self.savedContainer:remove(self)
+    if self.savedContainer.type == "VBox" or self.savedContainer.type == "HBox" then
+      if self.savedContainer.organize then
+        self.savedContainer:organize()
+      else
+        self.savedContainer:reposition()
+      end
+    end
+  end
+  self:hide()
+end
+
+--- Starts the timergauge. Works whether the timer is stopped or not. Does not start a timer which is already at 0
+-- @tparam[opt] boolean show override the autoShow property. True will always show, false will never show.
+-- @usage myTimerGauge:start() --starts the timer, will show or not based on autoShow property
+-- myTimerGauge:start(false) --starts the timer, will not change hidden status, regardless of autoShow property
+-- myTimerGauge:start(true) --starts the timer, will show it regardless of autoShow property
+function TimerGauge:start(show)
+  if show == nil then
+    show = self.autoShow
+  end
+  self.active = true
+  if self.timer then
+    killTimer(self.timer)
+    self.timer = nil
+  end
+  startStopWatch(self.stopWatchName)
+  self:update()
+  self.timer = tempTimer(self.updateTime / 1000, function()
+    self:update()
+  end, true)
+  if show then
+    self:show2()
+  end
+end
+
+--- Stops the timergauge. Works whether the timer is started or not.
+-- @tparam[opt] boolean hide override the autoHide property. True will always hide, false will never hide.
+-- @usage myTimerGauge:stop() --stops the timer, will hide or not based on autoHide property
+-- myTimerGauge:stop(false) --stops the timer, will not change hidden status, regardless of autoHide property
+-- myTimerGauge:stop(true) --stops the timer, will hide it regardless of autoHide property
+function TimerGauge:stop(hide)
+  if hide == nil then
+    hide = self.autoHide
+  end
+  self.active = false
+  if self.timer then
+    killTimer(self.timer)
+    self.timer = nil
+  end
+  stopStopWatch(self.stopWatchName)
+  if hide then
+    self:hide2()
+  end
+end
+
+--- Alias for stop.
+-- @tparam[opt] boolean hide override the autoHide property. True will always hide, false will never hide.
+function TimerGauge:pause(hide)
+  self:stop(hide)
+end
+
+--- Resets the time on the timergauge to its original value. Does not alter the running state of the timer
+function TimerGauge:reset()
+  resetStopWatch(self.stopWatchName)
+  adjustStopWatch(self.stopWatchName, self.time * -1)
+  self:update()
+end
+
+--- Resets and starts the timergauge.
+-- @tparam[opt] boolean show override the autoShow property. true will always show, false will never show
+-- @usage myTimerGauge:restart() --restarts the timer, will show or not based on autoShow property
+-- myTimerGauge:restart(false) --restarts the timer, will not change hidden status, regardless of autoShow property
+-- myTimerGauge:restart(true) --restarts the timer, will show it regardless of autoShow property
+function TimerGauge:restart(show)
+  self:reset()
+  self:start(show)
+end
+
+--- Get the amount of time remaining on the timer, in seconds
+-- @tparam string format Format string for how to return the time. If not provided defaults to self.timeFormat(which defaults to "S.t").<br>
+--                      If "" is passed will return "" as the time. See below table for formatting codes<br>
+-- <table class="tg">
+-- <tr>
+--  <th>format code</th>
+--  <th>what it is replaced with</th>
+-- </tr>
+-- <tr>
+--  <td class="tg-1">S</td>
+--  <td class="tg-1">Time left in seconds, unbroken down. Does not include milliseconds.<br>
+--      IE a timer with 2 minutes left it would replace S with 120
+--  </td>
+-- </tr>
+-- <tr>
+--  <td class="tg-2">dd</td>
+--  <td class="tg-2">Days, with 1 leading 0 (0, 01, 02-...)</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-1">d</td>
+--  <td class="tg-1">Days, with no leading 0 (1,2,3-...)</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-2">hh</td>
+--  <td class="tg-2">hours, with leading 0 (00-24)</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-1">h</td>
+--  <td class="tg-1">hours, without leading 0 (0-24)</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-2">MM</td>
+--  <td class="tg-2">minutes, with a leading 0 (00-59)</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-1">M</td>
+--  <td class="tg-1">minutes, no leading 0 (0-59)</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-2">ss</td>
+--  <td class="tg-2">seconds, with leading 0 (00-59)</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-1">s</td>
+--  <td class="tg-1">seconds, no leading 0 (0-59)</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-2">t</td>
+--  <td class="tg-2">tenths of a second<br>
+--      timer with 12.345 seconds left, t would<br>
+--      br replaced by 3.
+--  </td>
+-- </tr>
+-- <tr>
+--  <td class="tg-1">mm</td>
+--  <td class="tg-1">milliseconds with leadings 0s (000-999)</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-2">m</td>
+--  <td class="tg-2">milliseconds with no leading 0s (0-999)</td>
+-- </tr>
+-- </table><br>
+-- @usage myTimerGauge:getTime() --returns the time using myTimerGauge.format
+-- myTimerGauge:getTime("hh:MM:ss") --returns the time as hours, minutes, and seconds, with leading 0s (01:23:04)
+-- myTimerGauge:getTime("S.mm") --returns the time as the total number of seconds, including milliseconds (114.004)
+function TimerGauge:getTime(format)
+  format = format or self.timeFormat
+  local time = getStopWatchTime(self.stopWatchName)
+  local timerTable = getStopWatchBrokenDownTime(self.stopWatchName)
+  if time > 0 then
+    self:stop(self.autoHide)
+    resetStopWatch(self.stopWatchName)
+    time = getStopWatchTime(self.stopWatchName)
+    timerTable = getStopWatchBrokenDownTime(self.stopWatchName)
+    self.active = false
+  end
+  if format == "" then
+    return format
+  end
+  local totalSeconds = string.split(math.abs(time), "%.")[1]
+  local tenths = string.sub(string.format("%03d", timerTable.milliSeconds), 1, 1)
+  format = format:gsub("S", totalSeconds)
+  format = format:gsub("t", tenths)
+  format = format:gsub("mm", string.format("%03d", timerTable.milliSeconds))
+  format = format:gsub("m", timerTable.milliSeconds)
+  format = format:gsub("MM", string.format("%02d", timerTable.minutes))
+  format = format:gsub("M", timerTable.minutes)
+  format = format:gsub("dd", string.format("%02d", timerTable.days))
+  format = format:gsub("d", timerTable.days)
+  format = format:gsub("ss", string.format("%02d", timerTable.seconds))
+  format = format:gsub("s", timerTable.seconds)
+  format = format:gsub("hh", string.format("%02d", timerTable.hours))
+  format = format:gsub("h", timerTable.hours)
+  return format
+end
+
+-- Execute the timer's hook, if there is one. Internal function
+function TimerGauge:executeHook()
+  local hook = self.hook
+  if not hook then
+    return
+  end
+  local hooktype = type(hook)
+  if hooktype == "string" then
+    local f, e = loadstring("return " .. hook)
+    if not f then
+      f, e = loadstring(hook)
+    end
+    if not f then
+      debugc(string.format("TimerGauge encountered an error while executing the hook for TimerGauge with name: %s error: %s", self.name, tostring(e)))
+      return
+    end
+    hook = f
+  end
+  hooktype = type(hook)
+  if hooktype ~= "function" then
+    debugc(string.format(
+             "TimerGauge with name: %s was given a hook which is neither a function nor a string which can be made into one. Provided type was %s",
+             self.name, hooktype))
+    return
+  end
+  local worked, err = pcall(hook)
+  if not worked then
+    debugc(string.format("TimerGauge named %s encountered the following error while executing its hook: %s", self.name, err))
+  end
+end
+
+--- Sets the timer's remaining time to 0, stops it, and executes the hook if one exists.
+-- @tparam[opt] boolean skipHook use true to have it set the timer to 0 and stop, but not execute the hook.
+-- @usage myTimerGauge:finish() --executes the hook if it has one
+-- myTimerGauge:finish(false) --will not execute the hook
+function TimerGauge:finish(skipHook)
+  resetStopWatch(self.stopWatchName)
+  self:update(skipHook)
+end
+
+-- Internal function, no ldoc
+-- Updates the gauge based on time remaining.
+-- @tparam[opt] boolean skipHook use true if you do not want to execute the hook if the timer is at 0.
+function TimerGauge:update(skipHook)
+  local time = self.showTime and self:getTime(self.timeFormat) or ""
+  local current = tonumber(self:getTime("S.mm"))
+  local suffix = self.suffix or ""
+  local prefix = self.prefix or ""
+  local text = string.format("%s%s%s", prefix, time, suffix)
+  self:setValue(current, self.time, text)
+  if current == 0 then
+    if self.timer then
+      killTimer(self.timer)
+      self.timer = nil
+    end
+    if not skipHook then
+      self:executeHook()
+    end
+  end
+end
+
+--- Sets the amount of time the timer will run for. Make sure to call :reset() or :restart()
+-- if you want to cause the timer to run for that amount of time. If you set it to a time lower
+-- than the time left on the timer currently, it will reset the current time, otherwise it is left alone
+-- @tparam number time how long in seconds the timer should run for
+-- @usage myTimerGauge:setTime(50) -- sets myTimerGauge's max time to 50.
+function TimerGauge:setTime(time)
+  local timetype = type(time)
+  if timetype ~= "number" then
+    local err = string.format("TimerGauge:setTime(time): time as number expected, got %s", timetype)
+    debugc(err)
+    return nil, err
+  end
+  time = math.abs(time)
+  if time == 0 then
+    local err = "TimerGauge:setTime(time): you cannot pass in 0 as the max time for the timer"
+    debugc(err)
+    return nil, err
+  end
+  local currentTime = tonumber(self:getTime("S.t"))
+  self.time = time
+  if time < currentTime then
+    self:reset()
+  else
+    self:update(currentTime == 0)
+  end
+end
+
+--- Changes the time between gauge updates.
+-- @tparam number updateTime amount of time in milliseconds between gauge updates. Must be a positive number.
+function TimerGauge:setUpdateTime(updateTime)
+  local updateTimeType = type(updateTime)
+  assert(updateTimeType == "number",
+         string.format("TimerGauge:setUpdateTime(updateTime): name: %s updateTime as number expected, got %s", self.name, updateTimeType))
+  assert(updateTime > 0,
+         string.format("TimerGauge:setUpdateTime(updateTime): name: %s updateTime must be a positive number. You gave %d", self.name, updateTime))
+  self.updateTime = updateTime
+  if self.timer then
+    killTimer(self.timer)
+    self.timer = nil
+  end
+  if self.active then
+    self.timer = tempTimer(updateTime / 1000, function()
+      self:update()
+    end, true)
+  end
+end
+
+TimerGauge.parent = Geyser.Gauge
+setmetatable(TimerGauge, Geyser.Gauge)
+--- Creates a new TimerGauge instance.
+-- @tparam table cons a table of options (or constraints) for how the TimerGauge will behave. Valid options include:
+-- <br>
+-- <table class="tg">
+-- <tr>
+--  <th>name</th>
+--  <th>description</th>
+--  <th>default</th>
+-- </tr>
+-- <tr>
+--  <td class="tg-1">time</td>
+--  <td class="tg-1">how long the timer should run for</td>
+--  <td class="tg-1"></td>
+-- </tr>
+-- <tr>
+--  <td class="tg-2">active</td>
+--  <td class="tg-2">whether the timer should run or not</td>
+--  <td class="tg-2">true</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-1">showTime</td>
+--  <td class="tg-1">should we show the time remaining on the gauge?</td>
+--  <td class="tg-1">true</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-2">prefix</td>
+--  <td class="tg-2">text you want shown before the time.</td>
+--  <td class="tg-2">""</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-1">suffix</td>
+--  <td class="tg-1">text you want shown after the time.</td>
+--  <td class="tg-1">""</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-2">timerCaption</td>
+--  <td class="tg-2">Alias for suffix. Deprecated and may be remove in the future</td>
+--  <td class="tg-2"/>
+-- </tr>
+-- <tr>
+--  <td class="tg-1">updateTime</td>
+--  <td class="tg-1">number of milliseconds between gauge updates.</td>
+--  <td class="tg-1">10</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-2">autoHide</td>
+--  <td class="tg-2">should the timer :hide() itself when it runs out/you stop it?</td>
+--  <td class="tg-2">true</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-1">autoShow</td>
+--  <td class="tg-1">should the timer :show() itself when you start it?</td>
+--  <td class="tg-1">true</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-2">manageContainer</td>
+--  <td class="tg-2">should the timer remove itself from its container when you call <br>:hide() and add itself back when you call :show()?</td>
+--  <td class="tg-2">false</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-1">timeFormat</td>
+--  <td class="tg-1">how should the time be displayed/returned if you call :getTime()? <br>See table below for more information</td>
+--  <td class="tg-1">"S.t"</td>
+-- </tr>
+-- </table>
+-- <br>Table of time format options
+-- <table class="tg">
+-- <tr>
+--  <th>format code</th>
+--  <th>what it is replaced with</th>
+-- </tr>
+-- <tr>
+--  <td class="tg-1">S</td>
+--  <td class="tg-1">Time left in seconds, unbroken down. Does not include milliseconds.<br>
+--      IE a timer with 2 minutes left it would replace S with 120
+--  </td>
+-- </tr>
+-- <tr>
+--  <td class="tg-2">dd</td>
+--  <td class="tg-2">Days, with 1 leading 0 (0, 01, 02-...)</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-1">d</td>
+--  <td class="tg-1">Days, with no leading 0 (1,2,3-...)</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-2">hh</td>
+--  <td class="tg-2">hours, with leading 0 (00-24)</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-1">h</td>
+--  <td class="tg-1">hours, without leading 0 (0-24)</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-2">MM</td>
+--  <td class="tg-2">minutes, with a leading 0 (00-59)</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-1">M</td>
+--  <td class="tg-1">minutes, no leading 0 (0-59)</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-2">ss</td>
+--  <td class="tg-2">seconds, with leading 0 (00-59)</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-1">s</td>
+--  <td class="tg-1">seconds, no leading 0 (0-59)</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-2">t</td>
+--  <td class="tg-2">tenths of a second<br>
+--      timer with 12.345 seconds left, t would<br>
+--      br replaced by 3.
+--  </td>
+-- </tr>
+-- <tr>
+--  <td class="tg-1">mm</td>
+--  <td class="tg-1">milliseconds with leadings 0s (000-999)</td>
+-- </tr>
+-- <tr>
+--  <td class="tg-2">m</td>
+--  <td class="tg-2">milliseconds with no leading 0s (0-999)</td>
+-- </tr>
+-- </table><br>
+-- @param parent The Geyser parent for this TimerGauge
+-- @usage
+-- local TimerGauge = require("MDK.timergauge")
+-- myTimerGauge = TimerGauge:new({
+--   name = "testGauge",
+--   x = 100,
+--   y = 100,
+--   height = 40,
+--   width = 200,
+--   time = 10
+-- })
+function TimerGauge:new(cons, parent)
+  -- type checking and error handling
+  local consType = type(cons)
+  if consType ~= "table" then
+    local err = string.format("TimerGauge:new(options, parent): options must be provided as a table, received: %s", consType)
+    debugc(err)
+    return nil, err
+  end
+  local timetype = type(cons.time)
+  local time = tonumber(cons.time)
+  if not time then
+    local err = string.format(
+                  "TimerGauge:new(options, parent): options table must include a time entry, which must be a number. We received: %s which is type: %s",
+                  cons.time or tostring(cons.time), timetype)
+    debugc(err)
+    return nil, err
+  end
+  cons.time = math.abs(time)
+  if cons.time == 0 then
+    local err = "TimerGauge:new(options, parent): time entry in options table must be non-0"
+    debugc(err)
+    return nil, err
+  end
+
+  if cons.timerCaption and (not cons.suffix) then
+    cons.suffix = cons.timerCaption
+  end
+  cons.type = cons.type or "timergauge"
+  -- call parent constructor
+  local me = self.parent:new(cons, parent)
+  -- add TimerGauge as the metatable/index
+  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
+
+  -- create and reset the driving stopwatch
+  me.stopWatchName = me.name .. "_timergauge"
+  createStopWatch(me.stopWatchName)
+  me:reset()
+
+  -- start it up?
+  if me.active then
+    me:start()
+  end
+  me:update()
+  return me
+end
+
+return TimerGauge
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2023-05-29 18:41:27 +
+
+ + diff --git a/src/resources/MDK/echofile.lua b/src/resources/MDK/echofile.lua new file mode 100755 index 0000000..dc77cd4 --- /dev/null +++ b/src/resources/MDK/echofile.lua @@ -0,0 +1,296 @@ +--- set of functions for echoing files to things. Uses a slightly hacked up version of f-strings for interpolation/templating +-- @module echofile +-- @author Damian Monogue +-- @copyright 2021 Damian Monogue +-- @copyright 2016 Hisham Muhammad (https://github.com/hishamhm/f-strings/blob/master/LICENSE) +-- @license MIT, see LICENSE.lua +local echofile = {} + +-- following functions fiddled with from https://github.com/hishamhm/f-strings/blob/master/F.lua and https://hisham.hm/2016/01/04/string-interpolation-in-lua/ +-- it seems to work :shrug: +local load = load + +if _VERSION == "Lua 5.1" then + load = function(code, name, _, env) + local fn, err = loadstring(code, name) + if fn then + setfenv(fn, env) + return fn + end + return nil, err + end +end + +local function f(str) + local outer_env = _ENV or getfenv(1) + return (str:gsub("%b{}", function(block) + local code = block:match("{(.*)}") + local exp_env = {} + setmetatable(exp_env, { + __index = function(_, k) + local stack_level = 5 + while debug.getinfo(stack_level, "") ~= nil do + local i = 1 + repeat + local name, value = debug.getlocal(stack_level, i) + if name == k then + return value + end + i = i + 1 + until name == nil + stack_level = stack_level + 1 + end + return rawget(outer_env, k) + end, + }) + local fn, err = load("return " .. code, "expression `" .. code .. "`", "t", exp_env) + if fn then + return tostring(fn()) + else + error(err, 0) + end + end)) +end + +local function xechoFile(options) + local filename = options.filename + local window = options.window + local func = options.func + local functionName = options.functionName + local fntype = type(filename) + if fntype ~= "string" then + return nil, f("{functionName}: filename as string expected, got {fnType}") + end + if not io.exists(filename) then + return nil, f("{functionName}: {filename} not found") + end + local file, err = io.open(filename, "r") + if not file then + return nil, err + end + local lines = file:read("*a") + if options.ansi then + lines = ansi2decho(lines) + end + if options.filter then + lines = f(lines) + end + return func(window, lines) +end + +local function getOptions(etype, filter, window, filename) + if filename == nil then + filename = window + window = "main" + end + local ansi = false + if etype == "a" then + etype = 'd' + ansi = true + end + local options = { + filename = filename, + window = window, + func = _G[etype .. "echo"], + functionName = etype .. "echoFile([window,] filename)", + ansi = ansi, + filter = filter, + } + return options +end + +--- Takes a string and performs interpolation +--- Uses {} as the delimiter. Expressions will be evaluated +---@param str string: The string to interpolate +---@usage echofile = require("MDK.echofile") +--- echofile.f("{1+1}") -- returns "2" +--- local x = 4 +--- echofile.f"4+3 = {x+3}" -- returns "4+3 = 7" +function echofile.f(str) + return f(str) +end + +--- reads the contents of a file, converts it to decho and then dechos it +---@param window string: Optional window to cecho to +---@param filename string: Full path to file +---@see echofile.f +---@see echofile.cechoFile +---@usage local ec = require("MDK.echofile") +--- local cechoFile,f = ec.cechoFile, ec.f +--- cechoFile("C:/path/to/file") -- windows1 +--- cechoFile("C:\\path\\to\\file") -- windows2 +--- cechoFile("/path/to/file") -- Linux/MacOS +--- cechoFile("aMiniConsole", f"{getMudletHomeDir()}/myPkgName/helpfile") -- cecho a file from your pkg to a miniconsole +function echofile.aechoFile(window, filename) + local options = getOptions("a", false, window, filename) + return xechoFile(options) +end + +--- reads the contents of a file and then cechos it +---@param window string: Optional window to cecho to +---@param filename string: Full path to file +---@see echofile.f +---@see echofile.cechoFilef +---@usage local ec = require("MDK.echofile") +--- local cechoFile,f = ec.cechoFile, ec.f +--- cechoFile("C:/path/to/file") -- windows1 +--- cechoFile("C:\\path\\to\\file") -- windows2 +--- cechoFile("/path/to/file") -- Linux/MacOS +--- cechoFile("aMiniConsole", f"{getMudletHomeDir()}/myPkgName/helpfile") -- cecho a file from your pkg to a miniconsole +function echofile.aechoFilef(window, filename) + local options = getOptions("a", true, window, filename) + return xechoFile(options) +end + +--- reads the contents of a file and then cechos it +---@param window string: Optional window to cecho to +---@param filename string: Full path to file +---@see echofile.f +---@usage local ec = require("MDK.echofile") +--- local cechoFile,f = ec.cechoFile, ec.f +--- cechoFile("C:/path/to/file") -- windows1 +--- cechoFile("C:\\path\\to\\file") -- windows2 +--- cechoFile("/path/to/file") -- Linux/MacOS +--- cechoFile("aMiniConsole", f"{getMudletHomeDir()}/myPkgName/helpfile") -- cecho a file from your pkg to a miniconsole +function echofile.cechoFile(window, filename) + local options = getOptions("c", false, window, filename) + return xechoFile(options) +end + +--- reads the contents of a file, interpolates it as per echofile.f and then cechos it +---@param window string: Optional window to cecho to +---@param filename string: Full path to file +---@see echofile.f +---@usage local ec = require("MDK.echofile") +--- local cechoFile,f = ec.cechoFile, ec.f +--- cechoFile("C:/path/to/file") -- windows1 +--- cechoFile("C:\\path\\to\\file") -- windows2 +--- cechoFile("/path/to/file") -- Linux/MacOS +--- cechoFile("aMiniConsole", f"{getMudletHomeDir()}/myPkgName/helpfile") -- cecho a file from your pkg to a miniconsole +function echofile.cechoFilef(window, filename) + local options = getOptions("c", true, window, filename) + return xechoFile(options) +end + +--- reads the contents of a file and then dechos it +---@param window string: Optional window to decho to +---@param filename string: Full path to file +---@see echofile.f +---@see echofile.cechoFile +function echofile.dechoFile(window, filename) + local options = getOptions("d", false, window, filename) + return xechoFile(options) +end + +--- reads the contents of a file, interpolates it as per echofile.f and then dechos it +---@param window string: Optional window to decho to +---@param filename string: Full path to file +---@see echofile.f +---@see echofile.cechoFile +function echofile.dechoFilef(window, filename) + local options = getOptions("d", true, window, filename) + return xechoFile(options) +end + +--- reads the contents of a file and then hechos it +---@param window string: Optional window to hecho to +---@param filename string: Full path to file +---@see echofile.f +---@see echofile.cechoFile +function echofile.hechoFile(window, filename) + local options = getOptions("h", false, window, filename) + return xechoFile(options) +end + +--- reads the contents of a file, interpolates it as per echofile.f and then hechos it +---@param window string: Optional window to hecho to +---@param filename string: Full path to file +---@see echofile.f +---@see echofile.cechoFile +function echofile.hechoFilef(window, filename) + local options = getOptions("h", true, window, filename) + return xechoFile(options) +end + +--- reads the contents of a file, interpolates it as per echofile.f and then echos it +---@param window string: Optional window to echo to +---@param filename string: Full path to file +---@see echofile.f +---@see echofile.cechoFile +function echofile.echoFile(window, filename) + local options = getOptions("", false, window, filename) + return xechoFile(options) +end + +--- reads the contents of a file, interpolates it as per echofile.f and then echos it +---@param window string: Optional window to echo to +---@param filename string: Full path to file +---@see echofile.f +---@see echofile.cechoFile +function echofile.echoFilef(window, filename) + local options = getOptions("", true, window, filename) + return xechoFile(options) +end + +--- Adds c/d/h/echoFile functions to Geyser miniconsole and userwindow objects +---@usage require("MDK.echofile").patchGeyser() +--- myMC = Geyser.MiniConsole:new({name = "myMC"}) +--- myMC:cechoFile(f"{getMudletHomeDir()}/helpfile") +function echofile.patchGeyser() + if Geyser.MiniConsole.echoFile then + return + end + function Geyser.MiniConsole:echoFile(filename) + return echofile.echoFile(self.name, filename) + end + function Geyser.MiniConsole:echoFilef(filename) + return echofile.echoFilef(self.name, filename) + end + function Geyser.MiniConsole:aechoFile(filename) + return echofile.aechoFile(self.name, filename) + end + function Geyser.MiniConsole:aechoFilef(filename) + return echofile.aechoFilef(self.name, filename) + end + function Geyser.MiniConsole:cechoFile(filename) + return echofile.cechoFile(self.name, filename) + end + function Geyser.MiniConsole:cechoFilef(filename) + return echofile.cechoFilef(self.name, filename) + end + function Geyser.MiniConsole:dechoFile(filename) + return echofile.dechoFile(self.name, filename) + end + function Geyser.MiniConsole:dechoFilef(filename) + return echofile.dechoFilef(self.name, filename) + end + function Geyser.MiniConsole:hechoFile(filename) + return echofile.hechoFile(self.name, filename) + end + function Geyser.MiniConsole:hechoFilef(filename) + return echofile.hechoFilef(self.name, filename) + end +end + +--- Installs c/d/h/echoFile and f to the global namespace, and adds functions to Geyser +---@usage require("MDK.echofile").installGlobal() +--- f"{1+2}" -- returns "2" +--- dechoFile(f"{getMudletHomeDir()}/fileWithDechoLines.txt") +--- -- reads contents of fileWithDechoLines.txt from profile directory +--- -- and dechos them to the main console +function echofile.installGlobal() + _G.f = f + _G.echoFile = echofile.echoFile + _G.echoFilef = echofile.echoFilef + _G.aechoFile = echofile.aechoFile + _G.aechoFilef = echofile.aechoFilef + _G.cechoFile = echofile.cechoFile + _G.cechoFilef = echofile.cechoFilef + _G.dechoFile = echofile.dechoFile + _G.dechoFilef = echofile.dechoFilef + _G.hechoFile = echofile.hechoFile + _G.hechoFilef = echofile.hechoFilef + echofile.patchGeyser() +end + +return echofile diff --git a/src/resources/MDK/emco.lua b/src/resources/MDK/emco.lua new file mode 100755 index 0000000..123ac13 --- /dev/null +++ b/src/resources/MDK/emco.lua @@ -0,0 +1,2353 @@ +--- 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 = {}, + logFormat = "h", + gags = {}, + notifyTabs = {}, + notifyWithFocus = false, + cmdLineStyleSheet = [[ + QPlainTextEdit { + border: 1px solid grey; + } + ]] +}) + +-- 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{}
cmdLineStyleSheetWhat stylesheet to use for the command lines."QPlainTextEdit {\n border: 1px solid grey;\n }\n"
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.{}
notifyTabsTables containing the names of all tabs you want to send notifications. IE {"Says", "Tells", "Org"}{}
notifyWithFocusIf true, EMCO will send notifications even if Mudlet has focus. If false, it will only send them when Mudlet does NOT have focus.false
+-- @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 + for _,tname in ipairs(cons.notifyTabs or {}) do + me:addNotifyTab(tname) + end + if me:fuzzyBoolean(cons.notifyWithFocus) then + self:enableNotifyWithFocus() + end + me:reset() + if me.allTab then + me:setAllTabName(me.allTabName or me.consoles[1]) + end + 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 + if self.currentTab == tabName then + if self.allTab and self.allTabName then + self:switchTab(self.allTabName) + else + self:switchTab(self.consoles[1]) + end + 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 + self.currentTab = tabName + if oldTab ~= tabName and oldTab ~= "" then + self.mc[oldTab]:hide() + self:adjustTabBackground(oldTab) + 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:adjustTabBackground(tabName) + self.tabs[tabName]:echo(tabName, self.activeTabFGColor) + -- if oldTab and self.mc[oldTab] then + -- self.mc[oldTab]:hide() + -- end + self.mc[tabName]:show() + if oldTab ~= tabName then + raiseEvent("EMCO tab change", self.name, oldTab, tabName) + end +end + +--- Cycles between the tabs in order +-- @tparam boolean reverse Defaults to false. When true, moves backward through the tab list rather than forward. +function EMCO:cycleTab(reverse) + -- add the property to demonnic.chat + local consoles = self.consoles + local cycleIndex = table.index_of(consoles, self.currentTab) + + local maxIndex = #consoles + cycleIndex = reverse and cycleIndex - 1 or cycleIndex + 1 + if cycleIndex > maxIndex then cycleIndex = 1 end + if cycleIndex < 1 then cycleIndex = maxIndex end + self:switchTab(consoles[cycleIndex]) +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:setAlignment(self.tabAlignment) + tab:setFontSize(self.tabFontSize) + tab:setItalics(self.tabItalics) + tab:setBold(self.tabBold) + tab:setUnderline(self.tabUnderline) + tab:setClickCallback(self.switchTab, self, tabName) + self.tabs[tabName] = tab + self:adjustTabBackground(tabName) + tab:echo(tabName, self.inactiveTabFGColor) + 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, + cmdLineStyleSheet = self.cmdLineStyleSheet, + path = self:processTemplate(self.path, tabName), + fileName = self:processTemplate(self.fileName, tabName), + logFormat = self.logFormat + } + 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, nil) + end + window:hide() + self:processImage(tabName) + self:switchTab(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) + local safeName = self.name:gsub("[<>:'\"?*]", "_") + local safeTabName = tabName and tabName:gsub("[<>:'\"?*]", "_") or "" + str = str:gsub("|E", safeName) + str = str:gsub("|N", safeTabName) + 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 stylesheet for command lines in this EMCO +-- @tparam string styleSheet the stylesheet to use for the command line. See https://wiki.mudlet.org/w/Manual:Lua_Functions#setCmdLineStyleSheet for examples +function EMCO:setCmdLineStyleSheet(styleSheet) + self.cmdLineStyleSheet = styleSheet + if not styleSheet then + return + end + for _, window in pairs(self.mc) do + window:setCmdLineStyleSheet(styleSheet) + end +end +--- Enables the commandLine on the specified tab. +-- @tparam string tabName the name of the tab to turn the commandLine on for +-- @param template the template for the commandline to use, or the function to run when enter is hit. +-- @usage myEMCO:enableCmdLine(tabName, template) +function EMCO:enableCmdLine(tabName, template) + if not table.contains(self.consoles, tabName) then + return nil, f"{self.name}:enableCmdLine(tabName,template) tabName is not in the console list. Valid options are {table.concat(self.consoles, 'm')}" + end + local window = self.mc[tabName] + window:enableCommandLine() + if self.cmdLineStyleSheet then + window:setCmdLineStyleSheet(self.cmdLineStyleSheet) + end + self:setCmdAction(tabName, template) +end + +--- Enables all command lines, using whatever template they may currently have set +function EMCO:enableAllCmdLines() + for _, tabName in ipairs(self.consoles) do + self:enableCmdLine(tabName, self.cmdActions[tabName]) + end +end + +--- Disables all commands line, but does not change their template +function EMCO:disableAllCmdLines() + for _, tabName in ipairs(self.consoles) do + self:disableCmdLine(tabName) + end +end + +--- Disables the command line for a particular tab +-- @tparam string tabName the name of the tab to disable the command line of. +function EMCO:disableCmdLine(tabName) + if not table.contains(self.consoles, tabName) then + return nil, f"{self.name}:disableCmdLine(tabName,template) tabName is not in the console list. Valid options are {table.concat(self.consoles, 'm')}" + end + local window = self.mc[tabName] + window:disableCommandLine() +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 = self.allTabName or self.consoles[1] + 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), '[ThHmMszZaApPdy0-9%-%+:. ]', '') +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.consoles, ",")) + 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:adjustTabBackground(console) + local tab = self.tabs[console] + local activeTabCSS = self.activeTabCSS + local inactiveTabCSS = self.inactiveTabCSS + local activeTabBGColor = self.activeTabBGColor + local inactiveTabBGColor = self.inactiveTabBGColor + if console == self.currentTab then + if activeTabCSS and activeTabCSS ~= "" then + tab:setStyleSheet(activeTabCSS) + elseif activeTabBGColor then + tab:setColor(activeTabBGColor) + end + else + if inactiveTabCSS and inactiveTabCSS ~= "" then + tab:setStyleSheet(inactiveTabCSS) + elseif inactiveTabBGColor then + tab:setColor(inactiveTabBGColor) + end + end +end + +function EMCO:adjustTabBackgrounds() + for _, console in ipairs(self.consoles) do + self:adjustTabBackground(console) + end +end + +--- Sets the inactiveTabCSS +-- @tparam string stylesheet the stylesheet to use for inactive tabs. +function EMCO:setInactiveTabCSS(stylesheet) + self.inactiveTabCSS = stylesheet + self:adjustTabBackgrounds() +end + +--- Sets the activeTabCSS +-- @tparam string stylesheet the stylesheet to use for active tab. +function EMCO:setActiveTabCSS(stylesheet) + self.activeTabCSS = stylesheet + self:adjustTabBackgrounds() +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 tab to the list of tabs to send OS toast/popup notifications for +--@tparam string tabName the name of a tab to enable notifications for +--@return true if it was added, false if it was already included, nil if the tab does not exist. +function EMCO:addNotifyTab(tabName) + if not table.contains(self.consoles, tabName) then + return nil, "Tab does not exist" + end + if self.notifyTabs[tabName] then + return false + end + self.notifyTabs[tabName] = true + return true +end + +--- Removes a tab from the list of tabs to send OS toast/popup notifications for +--@tparam string tabName the name of a tab to disable notifications for +--@return true if it was removed, false if it wasn't enabled to begin with, nil if the tab does not exist. +function EMCO:removeNotifyTab(tabName) + if not table.contains(self.consoles, tabName) then + return nil, "Tab does not exist" + end + if not self.notifyTabs[tabName] then + return false + end + self.notifyTabs[tabName] = nil + return true +end + +--- Adds a pattern to the gag list for the EMCO +--@tparam string pattern 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 string pattern 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 string str 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 + +--- Enables sending OS notifications even if Mudlet has focus +function EMCO:enableNotifyWithFocus() + self.notifyWithFocus = true +end + +--- Disables sending OS notifications if Mudlet has focus +function EMCO:disableNotifyWithFocus() + self.notifyWithFocus = false +end + +function EMCO:strip(message, xtype) + local strippers = { + a = function(msg) return msg end, + echo = function(msg) return msg end, + cecho = cecho2string, + decho = decho2string, + hecho = hecho2string, + } + local result = strippers[xtype](message) + return result +end + +function EMCO:sendNotification(tabName, msg) + if self.notifyWithFocus or not hasFocus() then + if self.notifyTabs[tabName] then + showNotification(f'{self.name}:{tabName}', msg) + end + end +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() + local txt = self:strip(getCurrentLine(), xtype) + self:sendNotification(tabName, txt) + 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) + self:sendNotification(tabName, self:strip(message, xtype)) + 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 + +--- 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, + timestampFGColor = self.timestampFGColor, + 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, + notifyTabs = self.notifyTabs, + notifyWithFocus = self.notifyWithFocus, + cmdLineStyleSheet = self.cmdLineStyleSheet, + } + local dirname = getMudletHomeDir() .. "/EMCO/" + local filename = dirname .. self.name:gsub("[<>:'\"/\\|?*]", "_") .. ".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)) + return + 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.timestampFGColor = configTable.timestampFGColor + 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.notifyTabs = configTable.notifyTabs + self.notifyWithFocus = configTable.notifyWithFocus + self.cmdLineStyleSheet = configTable.cmdLineStyleSheet + 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/src/resources/MDK/figlet.lua b/src/resources/MDK/figlet.lua new file mode 100755 index 0000000..d07b4cf --- /dev/null +++ b/src/resources/MDK/figlet.lua @@ -0,0 +1,267 @@ +--- Figlet +-- A module to read figlet fonts and produce figlet ascii art from text +-- @module figlet +-- @copyright 2010,2011 Nick Gammon +-- @copyright 2022 Damian Monogue +local Figlet = {} + +--[[ + Based on figlet. + + FIGlet Copyright 1991, 1993, 1994 Glenn Chappell and Ian Chai + FIGlet Copyright 1996, 1997 John Cowan + Portions written by Paul Burton + Internet: + FIGlet, along with the various FIGlet fonts and documentation, is + copyrighted under the provisions of the Artistic License (as listed + in the file "artistic.license" which is included in this package. + +--]] + +--[[ + Latin-1 codes for German letters, respectively: + LATIN CAPITAL LETTER A WITH DIAERESIS = A-umlaut + LATIN CAPITAL LETTER O WITH DIAERESIS = O-umlaut + LATIN CAPITAL LETTER U WITH DIAERESIS = U-umlaut + LATIN SMALL LETTER A WITH DIAERESIS = a-umlaut + LATIN SMALL LETTER O WITH DIAERESIS = o-umlaut + LATIN SMALL LETTER U WITH DIAERESIS = u-umlaut + LATIN SMALL LETTER SHARP S = ess-zed +--]] + +local deutsch = {196, 214, 220, 228, 246, 252, 223} +local fcharlist = {} +local magic, hardblank, charheight, maxlen, smush, cmtlines, ffright2left, smush2 + +local function readfontchar(fontfile, theord) + + local t = {} + fcharlist[theord] = t + + -- read each character line + + --[[ + + eg. + + __ __ @ + | \/ |@ + | \ / |@ + | |\/| |@ + | | | |@ + |_| |_|@ + @ + @@ +--]] + + for i = 1, charheight do + local line = assert(fontfile:read("*l"), "Not enough character lines for character " .. theord) + local line = string.gsub(line, "%s+$", "") -- remove trailing spaces + assert(line ~= "", "Unexpected empty line") + + -- find the last character (eg. @) + local endchar = line:sub(-1) -- last character + + -- trim one or more of the last character from the end + while line:sub(-1) == endchar do + line = line:sub(1, #line - 1) + end -- while line ends with endchar + + table.insert(t, line) + + end -- for each line + +end -- readfontchar + +--- Reads a figlet font file (.flf) into memory and readies it for use by the next figlet +-- These files are cached in memory so that future calls to load a font just read from there. +-- @param filename the full path to the file to read the font from +function Figlet.readfont(filename) + local fontfile = assert(io.open(filename, "r")) + local s + + fcharlist = {} + + -- header line + s = assert(fontfile:read("*l"), "Empty FIGlet file") + + -- eg. flf2a$ 8 6 59 15 10 0 24463 153 + -- magic charheight maxlen smush cmtlines ffright2left smush2 ?? + + -- configuration line + magic, hardblank, charheight, maxlen, smush, cmtlines, ffright2left, smush2 = string.match(s, + "^(flf2).(.) (%d+) %d+ (%d+) (%-?%d+) (%d+) ?(%d*) ?(%d*) ?(%-?%d*)") + + assert(magic, "Not a FIGlet 2 font file") + + -- convert to numbers + charheight = tonumber(charheight) + maxlen = tonumber(maxlen) + smush = tonumber(smush) + cmtlines = tonumber(cmtlines) + + -- sanity check + if charheight < 1 then + charheight = 1 + end -- if + + -- skip comment lines + for i = 1, cmtlines do + assert(fontfile:read("*l"), "Not enough comment lines") + end -- for + + -- get characters space to tilde + for theord = string.byte(' '), string.byte('~') do + readfontchar(fontfile, theord) + end -- for + + -- get 7 German characters + for theord = 1, 7 do + readfontchar(fontfile, deutsch[theord]) + end -- for + + -- get extra ones like: + -- 0x0395 GREEK CAPITAL LETTER EPSILON + -- 246 LATIN SMALL LETTER O WITH DIAERESIS + + repeat + local extra = fontfile:read("*l") + if not extra then + break + end -- if eof + + local negative, theord = string.match(extra, "^(%-?)0[xX](%x+)") + if theord then + theord = tonumber(theord, 16) + if negative == "-" then + theord = -theord + end -- if negative + else + theord = string.match(extra, "^%d+") + assert(theord, "Unexpected line:" .. extra) + theord = tonumber(theord) + end -- if + + readfontchar(fontfile, theord) + + until false + + fontfile:close() + + -- remove leading/trailing spaces + + for k, v in pairs(fcharlist) do + + -- first see if all lines have a leading space or a trailing space + local leading_space = true + local trailing_space = true + for _, line in ipairs(v) do + if line:sub(1, 1) ~= " " then + leading_space = false + end -- if + if line:sub(-1, -1) ~= " " then + trailing_space = false + end -- if + end -- for each line + + -- now remove them if necessary + for i, line in ipairs(v) do + if leading_space then + v[i] = line:sub(2) + end -- removing leading space + if trailing_space then + v[i] = line:sub(1, -2) + end -- removing trailing space + end -- for each line + end -- for each character +end -- readfont + +-- add one character to output lines +local function addchar(which, output, kern, smush) + local c = fcharlist[string.byte(which)] + if not c then + return + end -- if doesn't exist + + for i = 1, charheight do + + if smush and output[i] ~= "" and which ~= " " then + local lhc = output[i]:sub(-1) + local rhc = c[i]:sub(1, 1) + output[i] = output[i]:sub(1, -2) -- remove last character + if rhc ~= " " then + output[i] = output[i] .. rhc + else + output[i] = output[i] .. lhc + end + output[i] = output[i] .. c[i]:sub(2) + + else + output[i] = output[i] .. c[i] + end -- if + + if not (kern or smush) or which == " " then + output[i] = output[i] .. " " + end -- if + end -- for + +end -- addchar + +--- Returns a table of lines representing a string as figlet +-- @tparam string s the text to make into a figlet +-- @tparam boolean kern should we reduce spacing +-- @tparam boolean smush causes the letters to share edges, condensing it even further +function Figlet.ascii_art(s, kern, smush) + assert(fcharlist) + assert(charheight > 0) + + -- make table of output lines + local output = {} + for i = 1, charheight do + output[i] = "" + end -- for + + for i = 1, #s do + local c = s:sub(i, i) + + if c >= " " and c < "\127" then + addchar(c, output, kern, smush) + end -- if in range + + end -- for + + -- fix up blank character so we can do a string.gsub on it + local fixedblank = string.gsub(hardblank, "[%%%]%^%-$().[*+?]", "%%%1") + + for i, line in ipairs(output) do + output[i] = string.gsub(line, fixedblank, " ") + end -- for + + return output +end -- function ascii_art + +--- Returns the figlet as a string, rather than a table +-- @tparam string str the string the make into a figlet +-- @tparam boolean kern should we reduce the space between letters? +-- @tparam boolean smush should the letters share edges, further condensing the output? +-- @see ascii_art +function Figlet.getString(str, kern, smush) + local tbl = Figlet.ascii_art(str, kern, smush) + return table.concat(tbl, "\n") +end + +--- Returns a figlet as a string, with kern set to true. +-- @tparam string str The string to turn into a figlet +-- @see getString +function Figlet.getKern(str) + return Figlet.getString(str, true) +end + +--- Returns a figlet as a string, with smush set to true. +-- @tparam string str The string to turn into a figlet +-- @see getString +function Figlet.getSmush(str) + return Figlet.getString(str, true, true) +end + +return Figlet diff --git a/src/resources/MDK/ftext.lua b/src/resources/MDK/ftext.lua new file mode 100755 index 0000000..6a1cbe8 --- /dev/null +++ b/src/resources/MDK/ftext.lua @@ -0,0 +1,1697 @@ +--- ftext +-- functions to format and print text, and the objects which use them +-- @module ftext +-- @author Damian Monogue +-- @copyright 2020 Damian Monogue +-- @copyright 2021 Damian Monogue +-- @copyright 2022 Damian Monogue +-- @license MIT, see LICENSE.lua +local ftext = {} +local dec = {"d", "decimal", "dec"} +local hex = {"h", "hexidecimal", "hex"} +local col = {"c", "color", "colour", "col", "name"} + +--- Performs wordwrapping on a string, given a length limit. Does not understand colour tags and will count them as characters in the string +-- @tparam string str the string to wordwrap +-- @tparam number limit the line length to wrap at +function ftext.wordWrap(str, limit, indent, indent1) + -- pulled from http://lua-users.org/wiki/StringRecipes + indent = indent or "" + indent1 = indent1 or indent + limit = limit or 72 + local here = 1 - #indent1 + local function check(sp, st, word, fi) + if fi - here > limit then + here = st - #indent + return "\n" .. indent .. word + end + end + return indent1 .. str:gsub("(%s+)()(%S+)()", check) +end + +--- Performs wordwrapping on a string, while ignoring color tags of a given type. +-- @tparam string text the string you are wordwrapping +-- @tparam number limit the line length to wrap at +-- @tparam string type What type of color codes to ignore. 'c' for cecho, 'd' for decho, 'h' for hecho, and anything else or nil to pass the string on to wordWrap +function ftext.xwrap(text, limit, type) + local colorPattern + if table.contains(dec, type) then + colorPattern = _Echos.Patterns.Decimal[1] + elseif table.contains(hex, type) then + colorPattern = _Echos.Patterns.Hex[1] + elseif table.contains(col, type) then + colorPattern = _Echos.Patterns.Color[1] + else + return ftext.wordWrap(text, limit) + end + local strippedString = rex.gsub(text, colorPattern, "") + local strippedLines = ftext.wordWrap(strippedString, limit):split("\n") + local lineIndex = 1 + local line = "" + local strLine = "" + local lines = {} + local strLines = {} + local workingLine = strippedLines[lineIndex]:split("") + local workingLineLength = #workingLine + local lineColumn = 0 + for str, color, res in rex.split(text, colorPattern) do + if res then + if type == "Hex" then + color = "#r" + elseif type == "Dec" then + color = "" + elseif type == "Color" then + color = "" + end + end + color = color or "" + local strLen = str:len() + if lineColumn + strLen <= workingLineLength then + strLine = strLine .. str + line = line .. str .. color + lineColumn = lineColumn + strLen + else + local neededChars = workingLineLength - lineColumn + local take = str:sub(1, neededChars) + local leave = str:sub(neededChars + 1, -1) + strLine = strLine .. take + line = line .. take + table.insert(lines, line) + table.insert(strLines, strLine) + line = "" + strLine = "" + lineIndex = lineIndex + 1 + workingLine = strippedLines[lineIndex]:split("") + workingLineLength = #workingLine + lineColumn = 0 + if leave:sub(1, 1) == " " then + leave = leave:sub(2, -1) + end + while leave ~= "" do + take = leave:sub(1, workingLineLength) + leave = leave:sub(workingLineLength + 1, -1) + if leave:sub(1, 1) == " " then + leave = leave:sub(2, -1) + end + if take:len() < workingLineLength then + lineColumn = take:len() + line = line .. take .. color + strLine = strLine .. take + else + lineIndex = lineIndex + 1 + workingLine = strippedLines[lineIndex] + if workingLine then + workingLine = strippedLines[lineIndex]:split("") + workingLineLength = #workingLine + end + table.insert(lines, take) + table.insert(strLines, take) + end + if leave == "\n" then + table.insert(lines, leave) + table.insert(strLines, leave) + leave = "" + end + end + end + end + if line ~= "" then + table.insert(lines, line) + end + return table.concat(lines, "\n") +end + +--- The main course, this function returns a formatted string, based on a table of options +-- @tparam string str the string to format +-- @tparam table opts the table of options which control the formatting +--

Table of options +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
option namedescriptiondefault
wrapShould it wordwrap to multiple lines?true
formatTypeDetermines how it formats for color. 'c' for cecho, 'd' for decho, 'h' for hecho, and anything else for no colors""
widthHow wide should we format the text?80
capwhat characters to use for the endcap.""
capColorwhat color to make the endcap?the correct 'white' for your formatType
spacerWhat character to use for empty space. Must be a single character" "
spacerColorwhat color should the spacer be?the correct 'white' for your formatType
textColorwhat color should the text itself be?the correct 'white' for your formatType
alignmentHow should the text be aligned within the width. "center", "left", or "right""center"
nogapShould we put a literal space between the spacer character and the text?false
insidePut the spacers inside the caps?false
mirrorShould the endcap be reversed on the right? IE [[ becomes ]]true
truncateCut the string to width. Is superceded by wrap being true.false
+function ftext.fText(str, opts) + local options = ftext.fixFormatOptions(str, opts) + if options.wrap and (options.strLen > options.effWidth) then + local wrapped = "" + if str:find("\n") then + for _,line in ipairs(str:split("\n")) do + local newline = "\n" + if _ == 1 then newline = "" end + wrapped = wrapped .. newline .. ftext.xwrap(line, options.effWidth, options.formatType) + end + else + wrapped = ftext.xwrap(str, options.effWidth, options.formatType) + end + local lines = wrapped:split("\n") + local formatted = {} + options.fixed = false + for _, line in ipairs(lines) do + table.insert(formatted, ftext.fLine(line, options)) + end + return table.concat(formatted, "\n") + else + return ftext.fLine(str, options) + end +end + +-- internal function, used to set defaults and type correct the options table +function ftext.fixFormatOptions(str, opts) + if opts.fixed then + return table.deepcopy(opts) + end + -- Set up all the things we might call the different echo types + if opts == nil then + opts = {} + end -- don't overwrite options if they passed them + -- but if they passed something other than a table as the options than oopsie! + if type(opts) ~= "table" then + error("Improper argument: options expected to be passed as table") + end + -- now we make a copy of the table, so we don't edit the original during all this + local options = table.deepcopy(opts) + if options.wrap == nil then + options.wrap = true + end -- wrap by default. + if options.truncate == nil then + options.truncate = false + end -- do not truncate by default + options.formatType = options.formatType or "" -- by default, no color formatting. + options.width = options.width or 80 -- default 80 width + options.cap = options.cap or "" -- no cap by default + options.spacer = options.spacer or " " -- default spacer is the space character + options.alignment = options.alignment or "center" -- default alignment is centered + if options.nogap == nil then + options.nogap = false + end + if options.inside == nil then + options.inside = false + end -- by default, we don't put the spacer inside + if not options.mirror == false then + options.mirror = options.mirror or true + end -- by default, we do want to use mirroring for the caps + -- setup default options for colors based on the color formatting type + if table.contains(dec, options.formatType) then + options.capColor = options.capColor or "<255,255,255>" + options.spacerColor = options.spacerColor or "<255,255,255>" + options.textColor = options.textColor or "<255,255,255>" + options.colorReset = "" + options.colorPattern = _Echos.Patterns.Decimal[1] + elseif table.contains(hex, options.formatType) then + options.capColor = options.capColor or "#FFFFFF" + options.spacerColor = options.spacerColor or "#FFFFFF" + options.textColor = options.textColor or "#FFFFFF" + options.colorReset = "#r" + options.colorPattern = _Echos.Patterns.Hex[1] + elseif table.contains(col, options.formatType) then + options.capColor = options.capColor or "" + options.spacerColor = options.spacerColor or "" + options.textColor = options.textColor or "" + options.colorReset = "" + options.colorPattern = _Echos.Patterns.Color[1] + else + options.capColor = "" + options.spacerColor = "" + options.textColor = "" + options.colorReset = "" + options.colorPattern = "" + end + options.originalString = str + options.strippedString = rex.gsub(tostring(str), options.colorPattern, "") + options.strLen = string.len(options.strippedString) + options.leftCap = options.cap + options.rightCap = options.cap + options.capLen = string.len(options.cap) + local gapSpaces = 0 + if not options.nogap then + if options.alignment == "center" then + gapSpaces = 2 + else + gapSpaces = 1 + end + end + options.nontextlength = options.width - options.strLen - gapSpaces + options.leftPadLen = math.floor(options.nontextlength / 2) + options.rightPadLen = options.nontextlength - options.leftPadLen + options.effWidth = options.width - ((options.capLen * gapSpaces) + gapSpaces) + if options.capLen > options.leftPadLen then + options.cap = options.cap:sub(1, options.leftPadLen) + options.capLen = string.len(options.cap) + end + options.fixed = true + return options +end + +-- internal function, processes a single line of the wrapped string. +function ftext.fLine(str, opts) + local options = ftext.fixFormatOptions(str, opts) + local truncate, strLen, width = options.truncate, options.strLen, options.width + if truncate and strLen > width then + local wrapped = ftext.xwrap(str, options.effWidth, options.formatType) + local lines = wrapped:split("\n") + str = lines[1] + end + local leftCap = options.leftCap + local rightCap = options.rightCap + local leftPadLen = options.leftPadLen + local rightPadLen = options.rightPadLen + local capLen = options.capLen + + if options.alignment == "center" then -- we're going to center something + if options.mirror then -- if we're reversing the left cap and the right cap (IE {{[[ turns into ]]}} ) + rightCap = string.gsub(rightCap, "<", ">") + rightCap = string.gsub(rightCap, "%[", "%]") + rightCap = string.gsub(rightCap, "{", "}") + rightCap = string.gsub(rightCap, "%(", "%)") + rightCap = string.reverse(rightCap) + end -- otherwise, they'll be the same, so don't do anything + if not options.nogap then + str = string.format(" %s ", str) + end + + elseif options.alignment == "right" then -- we'll right-align the text + leftPadLen = leftPadLen + rightPadLen + rightPadLen = 0 + rightCap = "" + if not options.nogap then + str = string.format(" %s", str) + end + + else -- Ok, so if it's not center or right, we assume it's left. We don't do justified. Sorry. + rightPadLen = rightPadLen + leftPadLen + leftPadLen = 0 + leftCap = "" + if not options.nogap then + str = string.format("%s ", str) + end + end -- that's it, took care of both left, right, and center formattings, now to output the durn thing. + local fullLeftCap = string.format("%s%s%s", options.capColor, leftCap, options.colorReset) + local fullLeftSpacer = string.format("%s%s%s", options.spacerColor, string.rep(options.spacer, (leftPadLen - capLen)), options.colorReset) + local fullText = string.format("%s%s%s", options.textColor, str, options.colorReset) + local fullRightSpacer = string.format("%s%s%s", options.spacerColor, string.rep(options.spacer, (rightPadLen - capLen)), options.colorReset) + local fullRightCap = string.format("%s%s%s", options.capColor, rightCap, options.colorReset) + + if options.inside then + -- "endcap===== some text =====endcap" + -- "endcap===== some text =====pacdne" + -- "endcap================= some text" + -- "some text =================endcap" + local finalString = string.format("%s%s%s%s%s", fullLeftCap, fullLeftSpacer, fullText, fullRightSpacer, fullRightCap) + return finalString + else + -- "=====endcap some text endcap=====" + -- "=====endcap some text pacdne=====" + -- "=================endcap some text" + -- "some text endcap=================" + + local finalString = string.format("%s%s%s%s%s", fullLeftSpacer, fullLeftCap, fullText, fullRightCap, fullRightSpacer) + return finalString + end +end + +-- Functions below here are honestly for backwards compatibility and subject to removal soon. +-- They just force some options table overrides for the most part. + +-- no colors, no wrap +function ftext.align(str, opts) + local options = {} + if opts == nil then + opts = {} + end + if type(opts) == "table" then + options = table.deepcopy(opts) + options.formatType = "" + options.wrap = false + else + error("Improper argument: options expected to be passed as table") + end + options = ftext.fixFormatOptions(str, options) + return ftext.fLine(str, options) +end + +-- decho formatting, no wrap +function ftext.dalign(str, opts) + local options = {} + if opts == nil then + opts = {} + end + if type(opts) == "table" then + options = table.deepcopy(opts) + options.formatType = "d" + options.wrap = false + else + error("Improper argument: options expected to be passed as table") + end + options = ftext.fixFormatOptions(str, options) + return ftext.fLine(str, options) +end + +-- cecho formatting, no wrap +function ftext.calign(str, opts) + local options = {} + if opts == nil then + opts = {} + end + if type(opts) == "table" then + options = table.deepcopy(opts) + options.formatType = "c" + options.wrap = false + else + error("Improper argument: options expected to be passed as table") + end + options = ftext.fixFormatOptions(str, options) + return ftext.fLine(str, options) +end + +-- hecho formatting, no wrap +function ftext.halign(str, opts) + local options = {} + if opts == nil then + opts = {} + end + if type(opts) == "table" then + options = table.deepcopy(opts) + options.formatType = "h" + options.wrap = false + else + error("Improper argument: options expected to be passed as table") + end + options = ftext.fixFormatOptions(str, options) + return ftext.fLine(str, options) +end + +-- literally just fText but forces cecho formatting +function ftext.cfText(str, opts) + local options = {} + if opts == nil then + opts = {} + end + if type(opts) == "table" then + options = table.deepcopy(opts) + options.formatType = "c" + else + error("Improper argument: options expected to be passed as table") + end + options = ftext.fixFormatOptions(str, options) + return ftext.fText(str, options) +end + +-- fText but forces decho formatting +function ftext.dfText(str, opts) + local options = {} + if opts == nil then + opts = {} + end + if type(opts) == "table" then + options = table.deepcopy(opts) + options.formatType = "d" + else + error("Improper argument: options expected to be passed as table") + end + options = ftext.fixFormatOptions(str, options) + return ftext.fText(str, options) +end + +-- fText but forces hecho formatting +function ftext.hfText(str, opts) + local options = {} + if opts == nil then + opts = {} + end + if type(opts) == "table" then + options = table.deepcopy(opts) + options.formatType = "h" + else + error("Improper argument: options expected to be passed as table") + end + options = ftext.fixFormatOptions(str, options) + return ftext.fText(str, options) +end + +--- Stand alone text formatter object. Remembers the options you set and can be adjusted as needed +-- @type ftext.TextFormatter +-- @author Damian Monogue +-- @copyright 2020 Damian Monogue +-- @license MIT, see LICENSE.lua + +local TextFormatter = {} +TextFormatter.validFormatTypes = {'d', 'dec', 'decimal', 'h', 'hex', 'hexidecimal', 'c', 'color', 'colour', 'col', 'name', 'none', 'e', 'plain', ''} + +--- Set's the formatting type whether it's for cecho, decho, or hecho +-- @tparam string typeToSet What type of formatter is this? Valid options are { 'd', 'dec', 'decimal', 'h', 'hex', 'hexidecimal', 'c', 'color', 'colour', 'col', 'name'} +function TextFormatter:setType(typeToSet) + local isNotValid = not table.contains(self.validFormatTypes, typeToSet) + if isNotValid then + error("TextFormatter:setType: Invalid argument, valid types are:" .. table.concat(self.validFormatTypes, ", ")) + end + self.options.formatType = typeToSet +end + +function TextFormatter:toBoolean(thing) + if type(thing) ~= "boolean" then + if thing == "true" then + thing = true + elseif thing == "false" then + thing = false + else + return nil + end + end + return thing +end + +function TextFormatter:checkString(str) + if type(str) ~= "string" then + if tostring(str) then + str = tostring(str) + else + return nil + end + end + return str +end + +--- Sets whether or not we should do word wrapping. +-- @tparam boolean shouldWrap should we do wordwrapping? +function TextFormatter:setWrap(shouldWrap) + local argumentType = type(shouldWrap) + shouldWrap = self:toBoolean(shouldWrap) + if shouldWrap == nil then + error("TextFormatter:setWrap(shouldWrap) Argument error, boolean expected, got " .. argumentType .. + ", if you want to set the number of characters wide to format for, use setWidth()") + end + self.options.wrap = shouldWrap +end + +--- Sets the width we should format for +-- @tparam number width the width we should format for +function TextFormatter:setWidth(width) + if type(width) ~= "number" then + if tonumber(width) then + width = tonumber(width) + else + error("TextFormatter:setWidth(width): Argument error, number expected, got " .. type(width)) + end + end + self.options.width = width +end + +--- Sets the cap for the formatter +-- @tparam string cap the string to use for capping the formatted string. +function TextFormatter:setCap(cap) + local argumentType = type(cap) + local cap = self:checkString(cap) + if cap == nil then + error("TextFormatter:setCap(cap): Argument error, string expect, got " .. argumentType) + end + self.options.cap = cap +end + +--- Sets the color for the format cap +-- @tparam string capColor Color which can be formatted via Geyser.Color.parse() +function TextFormatter:setCapColor(capColor) + local argumentType = type(capColor) + local capColor = self:checkString(capColor) + if capColor == nil then + error("TextFormatter:setCapColor(capColor): Argument error, string expected, got " .. argumentType) + end + self.options.capColor = capColor +end + +--- Sets the color for spacing character +-- @tparam string spacerColor Color which can be formatted via Geyser.Color.parse() +function TextFormatter:setSpacerColor(spacerColor) + local argumentType = type(spacerColor) + local spacerColor = self:checkString(spacerColor) + if spacerColor == nil then + error("TextFormatter:setSpacerColor(spacerColor): Argument error, string expected, got " .. argumentType) + end + self.options.spacerColor = spacerColor +end + +--- Sets the color for formatted text +-- @tparam string textColor Color which can be formatted via Geyser.Color.parse() +function TextFormatter:setTextColor(textColor) + local argumentType = type(textColor) + local textColor = self:checkString(textColor) + if textColor == nil then + error("TextFormatter:setTextColor(textColor): Argument error, string expected, got " .. argumentType) + end + self.options.textColor = textColor +end + +--- Sets the spacing character to use. Should be a single character +-- @tparam string spacer the character to use for spacing +function TextFormatter:setSpacer(spacer) + local argumentType = type(spacer) + local spacer = self:checkString(spacer) + if spacer == nil then + error("TextFormatter:setSpacer(spacer): Argument error, string expect, got " .. argumentType) + end + self.options.spacer = spacer +end + +--- Set the alignment to format for +-- @tparam string alignment How to align the formatted string. Valid options are 'left', 'right', or 'center' +function TextFormatter:setAlignment(alignment) + local validAlignments = {"left", "right", "center"} + if not table.contains(validAlignments, alignment) then + error("TextFormatter:setAlignment(alignment): Argument error: Only valid arguments for setAlignment are 'left', 'right', or 'center'. You sent" .. + alignment) + end + self.options.alignment = alignment +end + +--- Set whether the the spacer should go inside the the cap or outside of it +-- @tparam boolean spacerInside +function TextFormatter:setInside(spacerInside) + local argumentType = type(spacerInside) + spacerInside = self:toBoolean(spacerInside) + if spacerInside == nil then + error("TextFormatter:setInside(spacerInside) Argument error, boolean expected, got " .. argumentType) + end + self.options.inside = spacerInside +end + +--- Set whether we should mirror/reverse the caps. IE << becomes >> if set to true +-- @tparam boolean shouldMirror +function TextFormatter:setMirror(shouldMirror) + local argumentType = type(shouldMirror) + shouldMirror = self:toBoolean(shouldMirror) + if shouldMirror == nil then + error("TextFormatter:setMirror(shouldMirror): Argument error, boolean expected, got " .. argumentType) + end + self.options.mirror = shouldMirror +end + +--- Set whether we should remove the gap spaces between the text and spacer characters. "===some text===" if set to true, "== some text ==" if set to false +-- @tparam boolean noGap +function TextFormatter:setNoGap(noGap) + local argumentType = type(noGap) + noGap = self:toBoolean(noGap) + if noGap == nil then + error("TextFormatter:setNoGap(noGap): Argument error, boolean expected, got " .. argumentType) + end + self.options.noGap = noGap +end + +--- Enables truncation (cutting to length). You still need to ensure wrap is disabled, as it supercedes. +function TextFormatter:enableTruncate() + self.options.truncate = true +end + +--- Disables truncation (cutting to length). You still need to ensure wrap is enabled if you want it to wrap. +function TextFormatter:disableTruncate() + self.options.truncate = false +end + +--- Format a string based on the stored options +-- @tparam string str The string to format +function TextFormatter:format(str) + return ftext.fText(str, self.options) +end + +--- Creates and returns a new TextFormatter. +-- @tparam table options the options for the text formatter to use when running format() +--

Table of options +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
option namedescriptiondefault
wrapShould it wordwrap to multiple lines?true
formatTypeDetermines how it formats for color. 'c' for cecho, 'd' for decho, 'h' for hecho, and anything else for no colors"c"
widthHow wide should we format the text?80
capwhat characters to use for the endcap.""
capColorwhat color to make the endcap?the correct 'white' for your formatType
spacerWhat character to use for empty space. Must be a single character" "
spacerColorwhat color should the spacer be?the correct 'white' for your formatType
textColorwhat color should the text itself be?the correct 'white' for your formatType
alignmentHow should the text be aligned within the width. "center", "left", or "right""center"
nogapShould we put a literal space between the spacer character and the text?false
insidePut the spacers inside the caps?false
mirrorShould the endcap be reversed on the right? IE [[ becomes ]]true
truncateCut the string to width. Is superceded by wrap being true.false
+-- @usage +-- local TextFormatter = require("MDK.ftext").TextFormatter +-- myFormatter = TextFormatter:new( { +-- width = 40, +-- cap = "[CAP]", +-- capColor = "", +-- textColor = "" +-- }) +-- myMessage = "This is a test of the emergency broadcasting system. This is only a test" +-- cecho(myFormatter:format(myMessage)) + +function TextFormatter:new(options) + if options == nil then + options = {} + end + if options and type(options) ~= "table" then + error("TextFormatter:new(options): Argument error, table expected, got " .. type(options)) + end + local me = {} + me.options = {formatType = "c", wrap = true, width = 80, cap = "", spacer = " ", alignment = "center", inside = true, mirror = false} + for option, value in pairs(options) do + me.options[option] = value + end + setmetatable(me, self) + self.__index = self + return me +end +ftext.TextFormatter = TextFormatter + +--- Easy formatting for text tables +-- @type ftext.TableMaker +-- @author Damian Monogue +-- @copyright 2020 Damian Monogue +-- @license MIT, see LICENSE.lua + +local TableMaker = { + headCharacter = "*", + footCharacter = "*", + edgeCharacter = "*", + rowSeparator = "-", + separator = "|", + separateRows = true, + colorReset = "", + formatType = "c", + printHeaders = true, + autoEcho = false, + title = "", + printTitle = false, + headerTitle = false, + forceHeaderSeparator = false, + autoEchoConsole = "main", +} + +function TableMaker:checkPosition(position, func) + if position == nil then + position = 0 + end + if type(position) ~= "number" then + if tonumber(position) then + position = tonumber(position) + else + error(func .. ": Argument error: position expected as number, got " .. type(position)) + end + end + return position +end + +function TableMaker:insert(tbl, pos, item) + if pos ~= 0 then + table.insert(tbl, pos, item) + else + table.insert(tbl, item) + end +end + +--- Get the TextFormatter which defines the format of a specific column +-- @tparam number position The position of the column you're getting, counting from the left. If not provided will return the last column. +function TableMaker:getColumn(position) + position = position or #self.columns + position = self:checkPosition(position, "TableMaker:getColumn(position)") + return self.columns[position] +end + +--- Adds a column definition for the table. +-- @tparam table options Table of options suitable for a TextFormatter object. See https://github.com/demonnic/fText/wiki/fText +-- @tparam number position The position of the column you're adding, counting from the left. If not provided will add it as the last column +function TableMaker:addColumn(options, position) + if options == nil then + options = {} + end + if not type(options) == "table" then + error("TableMaker:addColumn(options, position): Argument error: options expected as table, got " .. type(options)) + end + local options = table.deepcopy(options) + position = self:checkPosition(position, "TableMaker:addColumn(options, position)") + options.width = options.width or 20 + options.name = options.name or "" + local formatter = TextFormatter:new(options) + self:insert(self.columns, position, formatter) +end + +--- Deletes a column at the given position +-- @tparam number position the column you wish to delete +function TableMaker:deleteColumn(position) + if position == nil then + error("TableMaker:deleteColumn(position): Argument Error: position as number expected, got nil") + end + position = self:checkPosition(position) + local maxColumn = #self.columns + if position > maxColumn then + error( + "TableMaker:deleteColumn(position): Argument Error: position provided was larger than number of columns in the table. Number of columns: " .. + #self.columns) + end + table.remove(self.columns, position) +end + +--- Replaces a column at a specific position with the newly provided formatting +-- @tparam table options table of options suitable for a TextFormatter object. See https://github.com/demonnic/fText/wiki/fText +-- @tparam number position which column you are replacing, counting from the left. +function TableMaker:replaceColumn(options, position) + if position == nil then + error("TableMaker:replaceColumn(options, position): Argument error: position as number expected, got nil") + end + position = self:checkPosition(position) + if type(options) ~= "table" then + error("TableMaker:replaceColumn(options, position): Argument error: options as table expected, got " .. type(options)) + end + if #self.columns < position then + error( + "TableMaker:replaceColumn(options, position): you cannot specify a position higher than the number of columns currently in the TableMaker. You sent:" .. + position .. " and there are: " .. #self.columns .. "columns in the TableMaker") + end + options.width = options.width or 20 + options.name = options.name or "" + local formatter = TextFormatter:new(options) + self.columns[position] = formatter +end + +--- Gets the row of output at a specific position +-- @tparam number position The position of the row you want to get, coutning from the top down. If not provided defaults to the last row in the table +-- @return table of entries in the specified row +function TableMaker:getRow(position) + position = position or #self.rows + position = self:checkPosition(position, "TableMaker:getRow(position)") + return self.rows[position] +end + +--- Adds a row of output to the table +-- @tparam table columnEntries This indexed table contains an entry for each column in the table. Entries in the table must be strings, a table of options for insertPopup or insertLink, or a function which returns one of these things +-- @tparam number position position for the row you want to add, counting from the top down. If not provided defaults to the last line in the table. +function TableMaker:addRow(columnEntries, position) + local columnEntriesType = type(columnEntries) + if columnEntriesType ~= "table" then + error("TableMaker:addRow(columnEntries, position): Argument error, columnEntries expected as table, got " .. columnEntriesType) + end + for _, entry in ipairs(columnEntries) do + local entryCheck = self:checkEntry(entry) + if entryCheck == 0 then + if type(entry) == "function" then + error( + "TableMaker:addRow(columnEntries, position): Argument Error, you provided a function for a columnEntry but it does not return a string. We need a string. It was entry number " .. + _ .. "in columnEntries") + else + error("TableMaker:addRow(columnEntries, position): Argument error, columnEntries items expected as string, got:" .. type(entry)) + end + end + end + position = self:checkPosition(position, "TableMaker:addRow(columnEntries, position)") + self:insert(self.rows, position, columnEntries) +end + +--- Deletes the row at the given position +-- @tparam number position the row to delete +function TableMaker:deleteRow(position) + if position == nil then + error("TableMaker:deleteRow(position): Argument Error: position as number expected, got nil") + end + position = self:checkPosition(position, "TableMaker:deleteRow(position)") + local maxRow = #self.rows + if position > maxRow then + error("TableMaker:deleteRow(position): Argument Error: position given was > the number of rows we have # of rows is:" .. maxRow) + end + table.remove(self.rows, position) +end + +--- Replaces a row of output in the table +-- @tparam table columnEntries This indexed table contains an entry for each column in the table. Entries in the table must be strings, a table of options for insertPopup or insertLink, or a function which returns one of these things +-- @tparam number position position for the row you want to add, counting from the top down. +function TableMaker:replaceRow(columnEntries, position) + if position == nil then + error("TableMaker:replaceRow(columnEntries, position): ArgumentError: position expected as number, received nil") + end + position = self:checkPosition(position, "TableMaker:replaceRow(columnEntries, position)") + if #self.rows < position then + error( + "TableMaker:replaceRow(columnEntries, position): position cannot be greater than the number of rows already in the tablemaker. You provided: " .. + position .. " and there are " .. #self.rows .. "rows in the TableMaker") + end + for _, entry in ipairs(columnEntries) do + local entryCheck = self:checkEntry(entry) + if entryCheck == 0 then + if type(entry) == "function" then + error( + "TableMaker:replaceRow(columnEntries, position): Argument Error: you provided a function for a columnEntry but it does not return a string. We need a string. It was entry number " .. + _ .. "in columnEntries") + else + error("TableMaker:replaceRow(columnEntries, position): Argument error: columnEntries items expected as string, got:" .. type(entry)) + end + end + end + self.rows[position] = columnEntries +end + +function TableMaker:checkEntry(entry) + local allowedTypes = {"string"} + if self.allowPopups then + table.insert(allowedTypes, "table") + end + local entryType = type(entry) + if entryType == "function" then + entryType = type(entry()) + end + if table.contains(allowedTypes, entryType) then + return entry + else + return 0 + end +end + +function TableMaker:checkNumber(num) + if num == nil then + num = 0 + end + if not tonumber(num) then + num = 0 + end + return tonumber(num) +end + +--- Get the contents and formatter for a specific cell +-- @tparam number row the row number of the cell, counted top down. +-- @tparam number column the column number of the cell, counted from the left. +-- @return the base text and TextFormatter for the cell at the specific row and column number +function TableMaker:getCell(row, column) + local rowType = type(row) + local columnType = type(column) + local maxRow = #self.rows + local maxColumn = #self.columns + local ae = "TableMaker:getCell(row, column): Argument error:" + row = self:checkNumber(row) + column = self:checkNumber(column) + if row == 0 then + if rowType ~= "number" then + printError(f"{ae} row as number expected, got {rowType}", true, true) + else + printError(f"{ae} rows start at 1, and you asked for row 0", true, true) + end + elseif column == 0 then + if columnType ~= "number" then + printError(f"{ae} column as number expected, got {columnType}", true, true) + else + printError(f"{ae} columns start at 1, and you asked for column 0", true, true) + end + elseif row > maxRow then + printError(f"{ae} row exceeds number of rows in table ({maxRow})") + elseif column > maxColumn then + printError(f"{ae} column exceeds number of columns in table ({maxColumn})", true, true) + end + return self.rows[row][column], self.columns[column] +end + +--- Sets a specific cell's display information +-- @tparam number row the row number of the cell, counted from the top down +-- @tparam number column the column number of the cell, counted from the left +-- @param entry What to set the entry to. Must be a string, or a table of options for insertLink/insertPopup if allowPopups is set. Or a function which returns one of these things +function TableMaker:setCell(row, column, entry) + local maxRow = #self.rows + local maxColumn = #self.columns + local ae = "TableMaker:setCell(row, column, entry): Argument Error:" + row = self:checkNumber(row) + if row == 0 then + error(ae .. " row must be a number, you provided " .. type(row)) + end + column = self:checkNumber(column) + if column == 0 then + error(ae .. " column must be a number, you provided " .. type(column)) + end + if row > maxRow then + error(ae .. " row is higher than the number of rows in the table. Highest row:" .. maxRow) + end + if column > maxColumn then + error(ae .. " column is higher than the number of columns in the table. Highest column:" .. maxColumn) + end + local entryType = type(entry) + entry = self:checkEntry(entry) + if entry == 0 then + if entryType == "function" then + error(ae .. " entry was provided as a function, but does not return a string. We need a string in the end") + else + error("TableMaker:setCell(row, column, entry): Argument Error: entry must be a string, or a function which returns a string. You provided a " .. entryType) + end + end + self.rows[row][column] = entry +end + +function TableMaker:totalWidth() + local width = 0 + local numberOfColumns = #self.columns + local separatorWidth = string.len(self.separator) + local edgeWidth = string.len(self.edgeCharacter) * 2 + for _, column in ipairs(self.columns) do + width = width + column.options.width + end + separatorWidth = separatorWidth * (numberOfColumns - 1) + width = width + edgeWidth + separatorWidth + return width +end + +function TableMaker:getType() + local dec = {"d", "decimal", "dec"} + local hex = {"h", "hexidecimal", "hex"} + local col = {"c", "color", "colour", "col", "name"} + if table.contains(dec, self.formatType) then + return 'd' + elseif table.contains(hex, self.formatType) then + return 'h' + elseif table.contains(col, self.formatType) then + return 'c' + else + return '' + end +end + +function TableMaker:echo(message, echoType, ...) + local fType = self:getType() + local consoleType = type(self.autoEchoConsole) + local console = "" + if echoType == nil then + echoType = "" + end + if consoleType == "string" then + console = self.autoEchoConsole + elseif consoleType == "nil" then + console = "main" + else + console = self.autoEchoConsole.name + end + local functionName = string.format("%secho%s", fType, echoType) + local func = _G[functionName] + if echoType == "" then + func(console, message) + else + func(console, message, ...) + end +end + +function TableMaker:scanRow(rowToScan) + local row = table.deepcopy(rowToScan) + local rowEntries = #row + local numberOfColumns = #self.columns + local columns = {} + local linesInRow = 0 + local rowText = "" + local ec = self.frameColor .. self.edgeCharacter .. self.colorReset + local sep = self.separatorColor .. self.separator .. self.colorReset + + if rowEntries < numberOfColumns then + local entriesNeeded = numberOfColumns - rowEntries + for i = 1, entriesNeeded do + table.insert(row, "") + end + end + for index, formatter in ipairs(self.columns) do + local str = row[index] + local column = "" + if type(str) == "function" then + str = str() + end + column = formatter:format(str) + table.insert(columns, column:split("\n")) + end + for _, rowLines in ipairs(columns) do + if linesInRow < #rowLines then + linesInRow = #rowLines + end + end + for index, rowLines in ipairs(columns) do + if #rowLines < linesInRow then + local neededLines = linesInRow - #rowLines + for i = 1, neededLines do + table.insert(rowLines, self.columns[index]:format("")) + end + end + end + for i = 1, linesInRow do + local thisLine = ec + for index, column in ipairs(columns) do + if index == 1 then + thisLine = string.format("%s%s", thisLine, column[i]) + else + thisLine = string.format("%s%s%s", thisLine, sep, column[i]) + end + end + thisLine = string.format("%s%s", thisLine, ec) + if rowText == "" then + rowText = thisLine + else + rowText = string.format("%s\n%s", rowText, thisLine) + end + end + return rowText +end + +function TableMaker:echoRow(rowToScan) + local row = table.deepcopy(rowToScan) + local rowEntries = #row + local numberOfColumns = #self.columns + local columns = {} + local linesInRow = 0 + local ec = self.frameColor .. self.edgeCharacter .. self.colorReset + local sep = self.separatorColor .. self.separator .. self.colorReset + if rowEntries < numberOfColumns then + local entriesNeeded = numberOfColumns - rowEntries + for i = 1, entriesNeeded do + table.insert(row, "") + end + end + for index, formatter in ipairs(self.columns) do + local str = row[index] + local column = "" + if type(str) == "function" then + str = str() + end + if type(str) == "table" then + str = str[1] + end + column = formatter:format(str) + table.insert(columns, column:split("\n")) + end + for _, rowLines in ipairs(columns) do + if linesInRow < #rowLines then + linesInRow = #rowLines + end + end + for index, rowLines in ipairs(columns) do + if #rowLines < linesInRow then + local neededLines = linesInRow - #rowLines + for i = 1, neededLines do + table.insert(rowLines, self.columns[index]:format("")) + end + end + end + for i = 1, linesInRow do + self:echo(ec) + for index, column in ipairs(columns) do + local message = column[i] + if index ~= 1 then + self:echo(sep) + end + if type(row[index]) == "string" then + self:echo(message) + elseif type(row[index]) == "table" then + local rowEntry = row[index] + local echoType = "" + if type(rowEntry[2]) == "string" then + echoType = "Link" + elseif type(rowEntry[2]) == "table" then + echoType = "Popup" + end + self:echo(message, echoType, rowEntry[2], rowEntry[3], rowEntry[4] or true) + end + end + self:echo(ec) + self:echo("\n") + end +end + +function TableMaker:makeHeader() + local totalWidth = self:totalWidth() + local ec = self.frameColor .. self.edgeCharacter .. self.colorReset + local sep = self.separatorColor .. self.separator .. self.colorReset + local header = self.frameColor .. string.rep(self.headCharacter, totalWidth) .. self.colorReset + local columnHeaders = "" + if self.printHeaders then + local columnEntries = {} + for _, v in ipairs(self.columns) do + table.insert(columnEntries, v:format(v.options.name)) + end + local divWithNewlines = self.headerTitle and header or self:createRowDivider() + divWithNewlines = "\n" .. divWithNewlines + columnHeaders = string.format("\n%s%s%s%s", ec, table.concat(columnEntries, sep), ec, (self.separateRows or self.forceHeaderSeparator) and divWithNewlines or '') + end + local title = self:makeTitle(totalWidth, header) + header = string.format("%s%s%s", header, title, columnHeaders) + return header +end + +function TableMaker:makeTitle(totalWidth, header) + if not self.printTitle then + return "" + end + local title = ftext.fText(self.title, {width = totalWidth, alignment = "center", cap = self.headCharacter, capColor = self.frameColor, inside = true, textColor = self.titleColor, formatType = self.formatType}) + title = string.format("\n%s\n%s", title, header) + return title +end + +function TableMaker:createRowDivider() + local columnPieces = {} + for _, v in ipairs(self.columns) do + local piece = string.format("%s%s%s", self.separatorColor, string.rep(self.rowSeparator, v.options.width), self.colorReset) + table.insert(columnPieces, piece) + end + local ec = self.frameColor .. self.edgeCharacter .. self.colorReset + local sep = self.separatorColor .. self.separator .. self.colorReset + return string.format("%s%s%s", ec, table.concat(columnPieces, sep), ec) +end + +--- set the title of the table +-- @tparam string title The title of the table. +function TableMaker:setTitle(title) + self.title = title + if self.autoEcho then self:assemble() end +end + +--- set the rowSeparator for the table +-- @tparam string char The row separator to use +function TableMaker:setRowSeparator(char) + self.rowSeparator = char + if self.autoEcho then self:assemble() end +end + +--- set the edgeCharacter for the table +-- @tparam string char The edge character to use +function TableMaker:setEdgeCharacter(char) + self.edgeCharacter = char + if self.autoEcho then self:assemble() end +end + +--- set the foot character for the table +-- @tparam string char The foot character to use +function TableMaker:setFootCharacter(char) + self.footCharacter = char + if self.autoEcho then self:assemble() end +end + +--- set the head character for the table +-- @tparam string char The head character to use +function TableMaker:setHeadCharacter(char) + self.headCharacter = char + if self.autoEcho then self:assemble() end +end + +--- set the column separator character for the table +-- @tparam string char The separator character to use +function TableMaker:setSeparator(char) + self.separator = char + if self.autoEcho then self:assemble() end +end + +--- set the title color for the table +-- @tparam string color The title color to use. Should match the color type of the tablemaker (cecho by default) +function TableMaker:setTitleColor(color) + self.titleColor = color + if self.autoEcho then self:assemble() end +end + +--- set the title color for the table +-- @tparam string color The separator color to use. Should match the color type of the tablemaker (cecho by default) +function TableMaker:setSeparatorColor(color) + self.separatorColor = color + if self.autoEcho then self:assemble() end +end + +--- set the title color for the table +-- @tparam string color The frame color to use. Should match the color type of the tablemaker (cecho by default) +function TableMaker:setFrameColor(color) + self.frameColor = color + if self.autoEcho then self:assemble() end +end + +--- Force a separator between the header and first row, even if the row separator is disabled for the overall table +function TableMaker:enableForceHeaderSeparator() + self.forceHeaderSeparator = true + if self.autoEcho then self:assemble() end +end + +--- Do not force a separator between the header and first row, even if the row separator is disabled for the overall table +function TableMaker:disableForceHeaderSeparator() + self.forceHeaderSeparator = false + if self.autoEcho then self:assemble() end +end + +--- Enable using the title separator for the column headers as well +function TableMaker:enableHeaderTitle() + self.headerTitle = true + if self.autoEcho then self:assemble() end +end + +--- Disable using the title separator for the column headers as well +function TableMaker:disableHeaderTitle() + self.headerTitle = false + if self.autoEcho then self:assemble() end +end + +--- enable printing the title of the table +function TableMaker:enablePrintTitle() + self.printTitle = true + if self.autoEcho then self:assemble() end +end + +--- disable printing the title of the table +function TableMaker:disablePrintTitle() + self.printTitle = false + if self.autoEcho then self:assemble() end +end + +--- enable printing of the column headers +function TableMaker:enablePrintHeaders() + self.printHeaders = true + if self.autoEcho then self:assemble() end +end + +--- disable printing of the column headers +function TableMaker:disablePrintHeaders() + self.printHeaders = false + if self.autoEcho then self:assemble() end +end + +--- enable printing the separator line between rows +function TableMaker:enableRowSeparator() + self.separateRows = true + if self.autoEcho then self:assemble() end +end + +--- enable printing the separator line between rows +function TableMaker:disableRowSeparator() + self.separateRows = false + if self.autoEcho then self:assemble() end +end + +--- enables making cells which incorporate insertLink/insertPopup +function TableMaker:enablePopups() + self.autoEcho = true + self.allowPopups = true + if self.autoEcho then self:assemble() end +end + +--- enables autoEcho so that when assemble is called it echos automatically +function TableMaker:enableAutoEcho() + self.autoEcho = true + self:assemble() +end + +--- disables autoecho. Cannot be used if allowPopups is set +function TableMaker:disableAutoEcho() + if self.allowPopups then + error("TableMaker:disableAutoEcho(): you cannot disable autoEcho once you have enabled popups.") + else + self.autoEcho = false + end +end + +--- Enables automatically clearing the miniconsole we echo to +function TableMaker:enableAutoClear() + self.autoClear = true + if self.autoEcho then self:assemble() end +end + +--- Disables automatically clearing the miniconsole we echo to +function TableMaker:disableAutoClear() + self.autoClear = false +end + +--- Set the miniconsole to echo to +-- @param console The miniconsole to autoecho to. Set to "main" or do not pass the parameter to autoecho to the main console. Can be a string name of the console, or a Geyser MiniConsole +function TableMaker:setAutoEchoConsole(console) + local funcName = "TableMaker:setAutoEchoConsole(console)" + if console == nil then + console = "main" + end + local consoleType = type(console) + if consoleType ~= "string" and consoleType ~= "table" then + error(funcName .. " ArgumentError: console as string or a Geyser MiniConsole or UserWindow expected, got " .. consoleType) + elseif consoleType == "table" and not (console.type == "miniConsole" or console.type == "userwindow") then + error(funcName .. " ArgumentError: console received was a table and may be a Geyser object, but console.type is not miniConsole, it is " .. + console.type) + end + self.autoEchoConsole = console + if self.autoEcho then self:assemble() end +end + +--- Assemble the table. If autoEcho is enabled/set to true, will automatically echo. Otherwise, returns the formatted string to echo the table +function TableMaker:assemble() + if self.allowPopups and self.autoEcho then + self:popupAssemble() + else + return self:textAssemble() + end +end + +function TableMaker:popupAssemble() + if self.autoClear then + local console = self.autoEchoConsole + if console and console ~= "main" then + if type(console) == "table" then + console = console.name + end + clearWindow(console) + end + end + local divWithNewLines = string.format("%s\n", self:createRowDivider()) + local header = self:makeHeader() .. "\n" + local footer = string.format("%s%s%s\n", self.frameColor, string.rep(self.footCharacter, self:totalWidth()), self.colorReset) + self:echo(header) + for _, row in ipairs(self.rows) do + if _ ~= 1 and self.separateRows then + self:echo(divWithNewLines) + end + self:echoRow(row) + end + self:echo(footer) +end + +function TableMaker:textAssemble() + local sheet = "" + local rows = {} + for _, row in ipairs(self.rows) do + table.insert(rows, self:scanRow(row)) + end + local divWithNewlines = string.format("\n%s\n", self:createRowDivider()) + local footer = string.format("%s%s%s", self.frameColor, string.rep(self.footCharacter, self:totalWidth()), self.colorReset) + sheet = string.format("%s\n%s\n%s\n", self:makeHeader(), table.concat(rows, self.separateRows and divWithNewlines or "\n"), footer) + if self.autoEcho then + local console = self.autoEchoConsole or "main" + if type(console) == "table" then + console = console.name + end + if self.autoClear and console ~= "main" then + clearWindow(console) + end + self:echo(sheet) + end + return sheet +end + +--- Creates and returns a new TableMaker. +-- see https://github.com/demonnic/MDK/wiki/fText%3A-TableMaker%3A-Examples for usage +-- @tparam table options table of options for the TableMaker object +--

Table of new options +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
option namedescriptiondefault
formatTypeDetermines how it formats for color. 'c' for cecho, 'd' for decho, 'h' for hecho, and anything else for no colorsc
printHeadersprint top row as headertrue
headCharacterThe character used to construct the very top of the table. A solid line of these characters is used"*"
footCharacterThe character used to construct the very bottom of the table. A solid line of these characters is used"*"
edgeCharacterthe character used to form the left and right edges of the table. There is one on either side of every line"*"
frameColorThe color to use for the frame. The frame is the border around the outside edge of the table (headCharacter, footCharacter, and edgeCharacters will all be this color).the correct 'white' for your formatType
rowSeparatorthe character used to form the lines between rows of text"-"
separatorCharacter used between columns."|"
separatorColorthe color used for the separators, the things which divide the rows and columns internally. (separator and rowSeparator will be this color)frameColor
autoEchoecho the table automatically in addition to returning the string representation?false
autoEchoConsoleMiniConsole to autoEcho to"main"
autoClearIf autoEchoing, and not echoing to the main console, should we clear the console before outputting?false
allowPopupssetting this to true allows you to make cells in the table clickable, as well as give them right-click menus.
+-- Please see Clickable Tables HERE
false
separateRowsWhen false, will not print the separator line between rowstrue
titleThe overall title of the table. Displayed at the top""
titleColorWhat color to use for the title textformatColor
printTitleShould we print the title of the table at the very tip-top?false
headerTitleUse the same separator for the column headers as the title/top, rather than the row separatorformatColor
forceHeaderSeparatorForce a separator between the column headers and the first row, even if rowSeparator is false.false
+function TableMaker:new(options) + local funcName = "TableMaker:new(options)" + local me = {} + setmetatable(me, self) + self.__index = self + if options == nil then + options = {} + end + if type(options) ~= "table" then + error("TableMaker:new(options): ArgumentError: options expected as table, got " .. type(options)) + end + local options = table.deepcopy(options) + if options.allowPopups == true then + options.autoEcho = true + else + options.allowPopups = false + end + local columns = false + if options.columns then + if type(options.columns) ~= "table" then + error("TableMaker:new(options): option error: You provided an options.columns entry of type " .. type(options.columns) .. + " and columns must a table with entries suitable for TableFormatter:addColumn().") + end + columns = table.deepcopy(options.columns) + options.columns = nil + end + local rows = false + if options.rows then + if type(options.rows) ~= "table" then + error("TableMaker:new(options): option error: You provided an options.rows entry of type " .. type(options.rows) .. + " and rows must be a table with entrys suitable for TableFormatter:addRow()") + end + rows = table.deepcopy(options.rows) + options.rows = nil + end + for option, value in pairs(options) do + me[option] = value + end + local dec = {"d", "decimal", "dec"} + local hex = {"h", "hexidecimal", "hex"} + local col = {"c", "color", "colour", "col", "name"} + if table.contains(dec, me.formatType) then + me.frameColor = me.frameColor or "<255,255,255>" + me.separatorColor = me.separatorColor or me.frameColor + me.titleColor = me.titleColor or me.frameColor + me.colorReset = "" + elseif table.contains(hex, me.formatType) then + me.frameColor = me.frameColor or "#ffffff" + me.separatorColor = me.separatorColor or me.frameColor + me.titleColor = me.titleColor or me.frameColor + me.colorReset = "#r" + elseif table.contains(col, me.formatType) then + me.frameColor = me.frameColor or "" + me.separatorColor = me.separatorColor or me.frameColor + me.titleColor = me.titleColor or me.frameColor + me.colorReset = "" + else + me.frameColor = "" + me.separatorColor = "" + me.titleColor = "" + me.colorReset = "" + end + me.columns = {} + me.rows = {} + if columns then + for _, column in ipairs(columns) do + me:addColumn(column) + end + end + if rows then + for _, row in ipairs(rows) do + me:addRow(row) + end + end + return me +end +ftext.TableMaker = TableMaker + +return ftext diff --git a/src/resources/MDK/ftext_spec.lua b/src/resources/MDK/ftext_spec.lua new file mode 100755 index 0000000..fdd3c15 --- /dev/null +++ b/src/resources/MDK/ftext_spec.lua @@ -0,0 +1,447 @@ +local ftext = require("MDK.ftext") + +describe("ftext:", function() + describe("ftext.fText:", function() + local fText = ftext.fText + + it("Should properly center text", function() + local expected = " some text " + local actual = fText("some text", {width = 20}) + assert.equals(expected, actual) + assert.equals(20, actual:len()) + end) + + it("Should properly pad left aligned text", function() + local expected = "some text " + local actual = fText("some text", {width = 20, alignment = "left"}) + assert.equals(expected, actual) + assert.equals(20, actual:len()) + end) + + it("Should properly pad right aligned text", function() + local expected = " some text" + local actual = fText("some text", {width = 20, alignment = "right"}) + assert.equals(expected, actual) + assert.equals(20, actual:len()) + end) + + it("Should wrap lines to the correct length", function() + local str = "This is a test of the emergency broadcast system. This is only a test" + local options = {width = 10, alignment = "centered"} + local actual = fText(str, options) + for _, line in ipairs(actual:split("\n")) do + assert.equals(line:len(), 10) + end + options.width = 15 + actual = fText(str, options) + for _, line in ipairs(actual:split("\n")) do + assert.equals(line:len(), 15) + end + end) + + describe("non-space spacer character:", function() + local str = "some text" + local options = {width = "20", alignment = "left", spacer = "="} + it("Should work with left align", function() + local expected = "some text ==========" + local actual = fText(str, options) + assert.equals(expected, actual) + assert.equals(20, actual:len()) + end) + + it("Should work with right align", function() + local expected = "========== some text" + options.alignment = "right" + local actual = fText(str, options) + assert.equals(expected, actual) + assert.equals(20, actual:len()) + end) + + it("Should work with center align", function() + local expected = ("==== some text =====") + options.alignment = "center" + local actual = fText(str, options) + assert.equals(expected, actual) + assert.equals(20, actual:len()) + end) + end) + + describe("nogap option:", function() + local str = "some text" + local options = {width = "20", alignment = "left", spacer = "=", nogap = true} + + it("Should work with left align", function() + local expected = "some text===========" + local actual = fText(str, options) + assert.equals(expected, actual) + assert.equals(20, actual:len()) + end) + + it("Should work with right align", function() + local expected = "===========some text" + options.alignment = "right" + local actual = fText(str, options) + assert.equals(expected, actual) + assert.equals(20, actual:len()) + end) + + it("Should work with center align", function() + local expected = "=====some text======" + options.alignment = "center" + local actual = fText(str, options) + assert.equals(expected, actual) + assert.equals(20, actual:len()) + end) + end) + + describe("cap functionality", function() + local str = "some text" + local options = {width = 20, spacer = "=", cap = "|"} + + it("Should place the spacer outside the cap by default", function() + local expected = "===| some text |====" + local actual = fText(str, options) + assert.equals(expected, actual) + assert.equals(20, actual:len()) + end) + + it("Should place it inside the cap if inside option is true", function() + local expected = "|=== some text ====|" + options.inside = true + local actual = fText(str, options) + options.inside = nil + assert.equals(expected, actual) + assert.equals(20, actual:len()) + end) + + it("Should mirror certain characters with their opposites", function() + local expected = "===[ some text ]====" + options.mirror = true + options.cap = "[" + local actual = fText(str, options) + assert.equals(expected, actual) + options.inside = true + expected = "[=== some text ====]" + actual = fText(str, options) + assert.equals(expected, actual) + options.inside = nil + options.cap = "<" + expected = "===< some text >====" + actual = fText(str, options) + assert.equals(expected, actual) + options.cap = "{" + expected = "==={ some text }====" + actual = fText(str, options) + assert.equals(expected, actual) + options.cap = "(" + expected = "===( some text )====" + actual = fText(str, options) + assert.equals(expected, actual) + options.cap = "|" + expected = "===| some text |====" + actual = fText(str, options) + assert.equals(expected, actual) + end) + end) + end) + + describe("ftext.cfText", function() + local cfText = ftext.cfText + local str = "some text" + local options = { + width = 20, + spacer = "=", + cap = "[", + inside = true, + mirror = true, + capColor = "", + spacerColor = "", + textColor = "", + } + it("Should handle cecho colored text", function() + local expectedStripped = "[=== some text ====]" + local expected = "[=== some text ====]" + local actual = cfText(str, options) + local actualStripped = cecho2string(actual) + assert.equals(expected, actual) + assert.equals(expectedStripped, actualStripped) + assert.equals(20, actualStripped:len()) + expectedStripped = "===[ some text ]====" + expected = "===[ some text ]====" + options.inside = false + actual = cfText(str, options) + actualStripped = cecho2string(actual) + assert.equals(expected, actual) + assert.equals(expectedStripped, actualStripped) + assert.equals(20, actualStripped:len()) + end) + + it("Should wrap cecho lines to the correct length", function() + local str = "This is a test of the emergency broadcast system. This is only a test" + local options = {width = 10, alignment = "centered"} + local actual = cfText(str, options) + for _, line in ipairs(actual:split("\n")) do + assert.equals(cecho2string(line):len(), 10) + end + options.width = 15 + actual = cfText(str, options) + for _, line in ipairs(actual:split("\n")) do + assert.equals(cecho2string(line):len(), 15) + end + end) + + end) + + describe("ftext.dfText", function() + local dfText = ftext.dfText + local str = "some text" + local options = { + width = 20, + spacer = "=", + cap = "[", + inside = true, + mirror = true, + capColor = "<160,32,240>", + spacerColor = "<0,255,0>", + textColor = "<255,0,0>", + } + it("Should handle decho colored text", function() + local expectedStripped = "[=== some text ====]" + local expected = "<160,32,240>[<0,255,0>===<255,0,0> some text <0,255,0>====<160,32,240>]" + local actual = dfText(str, options) + local actualStripped = decho2string(actual) + assert.equals(expected, actual) + assert.equals(expectedStripped, actualStripped) + assert.equals(20, actualStripped:len()) + expectedStripped = "===[ some text ]====" + expected = "<0,255,0>===<160,32,240>[<255,0,0> some text <160,32,240>]<0,255,0>====" + options.inside = false + actual = dfText(str, options) + actualStripped = decho2string(actual) + assert.equals(expected, actual) + assert.equals(expectedStripped, actualStripped) + assert.equals(20, actualStripped:len()) + end) + + it("Should wrap decho lines to the correct length", function() + local str = "This is a test of the emergency broadcast system. This is only a test" + local options = {width = 10, alignment = "centered"} + local actual = dfText(str, options) + for _, line in ipairs(actual:split("\n")) do + assert.equals(decho2string(line):len(), 10) + end + options.width = 15 + actual = dfText(str, options) + for _, line in ipairs(actual:split("\n")) do + assert.equals(decho2string(line):len(), 15) + end + end) + end) + + describe("ftext.hfText", function() + local hfText = ftext.hfText + local str = "some text" + local options = { + width = 20, + spacer = "=", + cap = "[", + inside = true, + mirror = true, + capColor = "#a020f0", + spacerColor = "#00ff00", + textColor = "#ff0000", + } + it("Should handle hecho colored text", function() + local expectedStripped = "[=== some text ====]" + local expected = "#a020f0[#r#00ff00===#r#ff0000 some text #r#00ff00====#r#a020f0]#r" + local actual = hfText(str, options) + local actualStripped = hecho2string(actual) + assert.equals(expected, actual) + assert.equals(expectedStripped, actualStripped) + assert.equals(20, actualStripped:len()) + expectedStripped = "===[ some text ]====" + expected = "#00ff00===#r#a020f0[#r#ff0000 some text #r#a020f0]#r#00ff00====#r" + options.inside = false + actual = hfText(str, options) + actualStripped = hecho2string(actual) + assert.equals(expected, actual) + assert.equals(expectedStripped, actualStripped) + assert.equals(20, actualStripped:len()) + end) + + it("Should wrap hecho lines to the correct length", function() + local str = "This is a test of the emergency broadcast system. This is only a test" + local options = {width = 10, alignment = "centered"} + local actual = hfText(str, options) + for _, line in ipairs(actual:split("\n")) do + assert.equals(hecho2string(line):len(), 10) + end + options.width = 15 + actual = hfText(str, options) + for _, line in ipairs(actual:split("\n")) do + assert.equals(hecho2string(line):len(), 15) + end + end) + end) + + describe("ftext.TextFormatter", function() + local tf = ftext.TextFormatter + local str = "some text" + local formatter + + before_each(function() + formatter = tf:new({width = 20}) + end) + + it("Should let you change width using :setWidth", function() + formatter:setWidth(80) + local expected = + " some text " + local actual = formatter:format(str) + assert.equals(expected, actual) + assert.equals(80, cecho2string(actual):len()) + end) + + it("Should format for cecho by default", function() + local expected = " some text " + local expectedStripped = " some text " + local actual = formatter:format(str) + local actualStripped = cecho2string(actual) + assert.equals(expected, actual) + assert.equals(expectedStripped, actualStripped) + assert.equals(20, actualStripped:len()) + end) + + it("Should produce the same line as cfText given the same options", function() + local expected = ftext.cfText(str, formatter.options) + local actual = formatter:format(str) + assert.equals(expected, actual) + end) + + it("Should let you change type using :setType", function() + formatter:setType("h") + local expected = ftext.hfText(str, formatter.options) + local actual = formatter:format(str) + assert.equals(expected, actual) + formatter:setType("d") + expected = ftext.dfText(str, formatter.options) + actual = formatter:format(str) + assert.equals(expected, actual) + formatter:setType("") + expected = ftext.fText(str, formatter.options) + actual = formatter:format(str) + assert.equals(expected, actual) + end) + + it("Should default to word wrapping, and let you change it with :setWrap", function() + formatter:setWidth(10) + local expected = + " some \n text " + local actual = formatter:format(str) + assert.equals(expected, actual) + expected = " some text " + formatter:setWrap(false) + actual = formatter:format(str) + assert.equals(expected, actual) + end) + + it("Should allow you to change the cap using :setCap", function() + formatter:setCap('|') + local expected = "| some text |" + local actual = formatter:format(str) + assert.equals(expected, actual) + end) + + it("Should allow you to change the capColor using :setCapColor", function() + formatter:setCapColor('') + local expected = " some text " + local actual = formatter:format(str) + assert.equals(expected, actual) + end) + + it("Should allow you to change the spacer color using :setSpacerColor", function() + formatter:setSpacerColor("") + local expected = " some text " + local actual = formatter:format(str) + assert.equals(expected, actual) + end) + + it("Should allow you to change the text color using :setTextColor", function() + formatter:setTextColor("") + local expected = " some text " + local actual = formatter:format(str) + assert.equals(expected, actual) + end) + + it("Should allow you to change the spacer using :setSpacer", function() + formatter:setSpacer("=") + -- local expected = " some text " + local expected = "==== some text =====" + local actual = formatter:format(str) + assert.equals(expected, actual) + end) + + it("Should allow you to set the alignment using :setAlignment", function() + formatter:setAlignment("left") + local expected = "some text " + local actual = formatter:format(str) + assert.equals(expected, actual) + formatter:setAlignment("right") + expected = " some text" + actual = formatter:format(str) + assert.equals(expected, actual) + end) + + it("Should allow you to change the 'inside' option using :setInside", function() + formatter:setInside(false) + local expected = " some text " + local actual = formatter:format(str) + assert.equals(expected, actual) + end) + + it("Should allow you to change the mirror option using :setMirror", function() + formatter:setCap('<') + formatter:setMirror(true) + local expected = "< some text >" + local actual = formatter:format(str) + assert.equal(expected, actual) + end) + end) + + describe("ftext.TableMaker", function() + local TableMaker = ftext.TableMaker + local tm + before_each(function() + tm = TableMaker:new() + tm:addColumn({name = "col1", width = 15, textColor = ""}) + tm:addColumn({name = "col2", width = 15, textColor = ""}) + tm:addColumn({name = "col3", width = 15, textColor = ""}) + tm:addRow({"some text", "more text", "other text"}) + tm:addRow({"little text", "bigger text", "text"}) + end) + + it("Should assemble a formatted table given default options", function() + local expected = [[************************************************* +* col1 | col2 | col3 * +*---------------|---------------|---------------* +* some text | more text | other text * +*---------------|---------------|---------------* +* little text | bigger text | text * +************************************************* +]] + local actual = tm:assemble() + assert.equals(expected, actual) + end) + + it("TableMaker:getCell should return the text and formatter for a specific cell", function() + local expectedText = "more text" + local expectedFormatter = tm.columns[2] + local actualText, actualFormatter = tm:getCell(1, 2) + assert.equals(expectedText, actualText) + assert.equals(expectedFormatter, actualFormatter) + local expectedFormatted = " more text " + local actualFormatted = actualFormatter:format(actualText) + assert.equals(expectedFormatted, actualFormatted) + end) + end) +end) diff --git a/src/resources/MDK/gradientmaker.lua b/src/resources/MDK/gradientmaker.lua new file mode 100755 index 0000000..b93e0ea --- /dev/null +++ b/src/resources/MDK/gradientmaker.lua @@ -0,0 +1,342 @@ +--- Module which provides for creating color gradients for your text. +-- Original functions found on the Lusternia Forums +--
I added functions to work with hecho. +--
I also made performance enhancements by storing already calculated gradients after first use for the session and only including the colorcode in the returned string if the color changed. +-- @module GradientMaker +-- @author Sylphas on the Lusternia forums +-- @author Damian Monogue +-- @copyright 2018 Sylphas +-- @copyright 2020 Damian Monogue +local GradientMaker = {} +local gradient_table = {} + +local function _clamp(num1, num2, num3) + local smaller = math.min(num2, num3) + local larger = math.max(num2, num3) + local minimum = math.max(0, smaller) + local maximum = math.min(255, larger) + return math.min(maximum, math.max(minimum, num1)) +end + +local function _gradient(length, rgb1, rgb2) + assert(length > 0) + if length == 1 then + return {rgb1} + elseif length == 2 then + return {rgb1, rgb2} + else + local step = {} + for color = 1, 3 do + step[color] = (rgb2[color] - rgb1[color]) / (length - 2) + end + local gradient = {rgb1} + for iter = 1, length - 2 do + gradient[iter + 1] = {} + for color = 1, 3 do + gradient[iter + 1][color] = math.ceil(rgb1[color] + (iter * step[color])) + end + end + gradient[length] = rgb2 + for index, color in ipairs(gradient) do + for iter = 1, 3 do + gradient[index][iter] = _clamp(color[iter], rgb1[iter], rgb2[iter]) + end + end + return gradient + end +end + +local function gradient_to_string(gradient) + local gradstring = "" + for _, grad in ipairs(gradient) do + local nodestring = "" + for _, col in ipairs(grad) do + nodestring = string.format("%s%03d", nodestring, col) + end + if _ == 1 then + gradstring = nodestring + else + gradstring = gradstring .. "|" .. nodestring + end + end + return gradstring +end + +local function _gradients(length, ...) + local arg = {...} + local argkey = gradient_to_string(arg) + local gradients_for_length = gradient_table[length] + if not gradients_for_length then + gradient_table[length] = {} + gradients_for_length = gradient_table[length] + end + local grads = gradients_for_length[argkey] + if grads then + return grads + end + if #arg == 0 then + gradients_for_length[argkey] = {} + return {} + elseif #arg == 1 then + gradients_for_length[argkey] = arg[1] + return arg[1] + elseif #arg == 2 then + gradients_for_length[argkey] = _gradient(length, arg[1], arg[2]) + return gradients_for_length[argkey] + else + local quotient = math.floor(length / (#arg - 1)) + local remainder = length % (#arg - 1) + local gradients = {} + for section = 1, #arg - 1 do + local slength = quotient + if section <= remainder then + slength = slength + 1 + end + local gradient = _gradient(slength, arg[section], arg[section + 1]) + for _, rgb in ipairs(gradient) do + table.insert(gradients, rgb) + end + end + gradients_for_length[argkey] = gradients + return gradients + end +end + +local function _color_name(rgb) + 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 + return cname +end + +local function errorIfEmpty(text, funcName) + assert(#text > 0, string.format("%s: you passed in an empty string, and I cannot make a gradient out of an empty string", funcName)) +end + +local function dgradient_table(text, ...) + errorIfEmpty(text, "dgradient_table") + local gradients = _gradients(#text, ...) + local dgrads = {} + for character = 1, #text do + table.insert(dgrads, {gradients[character], text:sub(character, character)}) + end + return dgrads +end + +local function dgradient(text, ...) + errorIfEmpty(text, "dgradient") + local gradients = _gradients(#text, ...) + local dgrad = "" + local current_color = "" + for character = 1, #text do + local new_color = "<" .. table.concat(gradients[character], ",") .. ">" + local char = text:sub(character, character) + if new_color == current_color then + dgrad = dgrad .. char + else + dgrad = dgrad .. new_color .. char + current_color = new_color + end + end + return dgrad +end + +local function cgradient_table(text, ...) + errorIfEmpty(text, "cgradient_table") + local gradients = _gradients(#text, ...) + local cgrads = {} + for character = 1, #text do + table.insert(cgrads, {_color_name(gradients[character]), text:sub(character, character)}) + end + return cgrads +end + +local function cgradient(text, ...) + errorIfEmpty(text, "cgradient") + local gradients = _gradients(#text, ...) + local cgrad = "" + local current_color = "" + for character = 1, #text do + local new_color = "<" .. _color_name(gradients[character]) .. ">" + local char = text:sub(character, character) + if new_color == current_color then + cgrad = cgrad .. char + else + cgrad = cgrad .. new_color .. char + current_color = new_color + end + end + return cgrad +end + +local hex = Geyser.Color.hex + +local function hgradient_table(text, ...) + errorIfEmpty(text, "hgradient_table") + local grads = _gradients(#text, ...) + local hgrads = {} + for character = 1, #text do + table.insert(hgrads, {hex(unpack(grads[character])):sub(2, -1), text:sub(character, character)}) + end + return hgrads +end + +local function hgradient(text, ...) + errorIfEmpty(text, "hgradient") + local grads = _gradients(#text, ...) + local hgrads = "" + local current_color = "" + for character = 1, #text do + local new_color = hex(unpack(grads[character])) + local char = text:sub(character, character) + if new_color == current_color then + hgrads = hgrads .. char + else + hgrads = hgrads .. new_color .. char + current_color = new_color + end + end + return hgrads +end + +local function color_name(...) + local arg = {...} + if #arg == 1 then + return _color_name(arg[1]) + elseif #arg == 3 then + return _color_name(arg) + else + local errmsg = + "color_name: You must pass either a table of r,g,b values: color_name({r,g,b})\nor the three r,g,b values separately: color_name(r,g,b)" + error(errmsg) + end +end + +--- Returns the closest color name to a given r,g,b color +-- @param r The red component. Can also pass the full color as a table, IE { 255, 0, 0 } +-- @param g The green component. If you pass the color as a table as noted above, this param should be empty +-- @param b the blue components. If you pass the color as a table as noted above, this param should be empty +-- @usage +-- closest_color = GradientMaker.color_name(128,200,30) -- returns "ansi_149" +-- closest_color = GradientMaker.color_name({128, 200, 30}) -- this is functionally equivalent to the first one +function GradientMaker.color_name(...) + return color_name(...) +end + +--- Returns the text, with the defined color gradients applied and formatted for us with decho. Usage example below produces the following text +--
dgradient example +-- @tparam string text The text you want to apply the color gradients to +-- @param first_color The color you want it to start at. Table of colors in { r, g, b } format +-- @param second_color The color you want the gradient to transition to first. Table of colors in { r, g, b } format +-- @param next_color Keep repeating if you want it to transition from the second color to a third, then a third to a fourth, etc +-- @see cgradient +-- @see hgradient +-- @usage +-- decho(GradientMaker.dgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {255,0,0}, {255,128,0}, {255,255,0}, {0,255,0}, {0,255,255}, {0,128,255}, {128,0,255})) +-- decho(GradientMaker.dgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {255,0,0}, {0,0,255})) +-- decho(GradientMaker.dgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {50,50,50}, {0,255,0}, {50,50,50})) +function GradientMaker.dgradient(text, ...) + return dgradient(text, ...) +end + +--- Returns the text, with the defined color gradients applied and formatted for us with cecho. Usage example below produces the following text +--
cgradient example +-- @tparam string text The text you want to apply the color gradients to +-- @param first_color The color you want it to start at. Table of colors in { r, g, b } format +-- @param second_color The color you want the gradient to transition to first. Table of colors in { r, g, b } format +-- @param next_color Keep repeating if you want it to transition from the second color to a third, then a third to a fourth, etc +-- @see dgradient +-- @see hgradient +-- @usage +-- cecho(GradientMaker.cgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {255,0,0}, {255,128,0}, {255,255,0}, {0,255,0}, {0,255,255}, {0,128,255}, {128,0,255})) +-- cecho(GradientMaker.cgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {255,0,0}, {0,0,255})) +-- cecho(GradientMaker.cgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {50,50,50}, {0,255,0}, {50,50,50})) +function GradientMaker.cgradient(text, ...) + return cgradient(text, ...) +end + +--- Returns the text, with the defined color gradients applied and formatted for us with hecho. Usage example below produces the following text +--
hgradient example +-- @tparam string text The text you want to apply the color gradients to +-- @param first_color The color you want it to start at. Table of colors in { r, g, b } format +-- @param second_color The color you want the gradient to transition to first. Table of colors in { r, g, b } format +-- @param next_color Keep repeating if you want it to transition from the second color to a third, then a third to a fourth, etc +-- @see cgradient +-- @see dgradient +-- @usage +-- hecho(GradientMaker.hgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {255,0,0}, {255,128,0}, {255,255,0}, {0,255,0}, {0,255,255}, {0,128,255}, {128,0,255})) +-- hecho(GradientMaker.hgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {255,0,0}, {0,0,255})) +-- hecho(GradientMaker.hgradient("a luminescent butterly floats about lazily on brillant blue and lilac wings\n", {50,50,50}, {0,255,0}, {50,50,50})) +function GradientMaker.hgradient(text, ...) + return hgradient(text, ...) +end + +--- Returns a table, each element of which is a table, the first element of which is the color name to use and the character which should be that color +-- @tparam string text The text you want to apply the color gradients to +-- @param first_color The color you want it to start at. Table of colors in { r, g, b } format +-- @param second_color The color you want the gradient to transition to first. Table of colors in { r, g, b } format +-- @param next_color Keep repeating if you want it to transition from the second color to a third, then a third to a fourth, etc +-- @see cgradient +function GradientMaker.cgradient_table(text, ...) + return cgradient_table(text, ...) +end + +--- Returns a table, each element of which is a table, the first element of which is the color({r,g,b} format) to use and the character which should be that color +-- @tparam string text The text you want to apply the color gradients to +-- @param first_color The color you want it to start at. Table of colors in { r, g, b } format +-- @param second_color The color you want the gradient to transition to first. Table of colors in { r, g, b } format +-- @param next_color Keep repeating if you want it to transition from the second color to a third, then a third to a fourth, etc +-- @see dgradient +function GradientMaker.dgradient_table(text, ...) + return dgradient_table(text, ...) +end + +--- Returns a table, each element of which is a table, the first element of which is the color(in hex) to use and the second element of which is the character which should be that color +-- @tparam string text The text you want to apply the color gradients to +-- @param first_color The color you want it to start at. Table of colors in { r, g, b } format +-- @param second_color The color you want the gradient to transition to first. Table of colors in { r, g, b } format +-- @param next_color Keep repeating if you want it to transition from the second color to a third, then a third to a fourth, etc +-- @see hgradient +function GradientMaker.hgradient_table(text, ...) + return hgradient_table(text, ...) +end + +--- Creates global copies of the c/d/hgradient(_table) functions and color_name for use without accessing the module table +-- @usage +-- GradientMaker.install_global() +-- cecho(cgradient(...)) -- use cgradient directly now +function GradientMaker.install_global() + _G["hgradient"] = function(...) + return hgradient(...) + end + _G["dgradient"] = function(...) + return dgradient(...) + end + _G["cgradient"] = function(...) + return cgradient(...) + end + _G["hgradient_table"] = function(...) + return hgradient_table(...) + end + _G["dgradient_table"] = function(...) + return dgradient_table(...) + end + _G["cgradient_table"] = function(...) + return cgradient_table(...) + end + _G["color_name"] = function(...) + return color_name(...) + end +end + +-- function GradientMaker.getGrads() +-- return gradient_table +-- end + +return GradientMaker diff --git a/src/resources/MDK/loggingconsole.lua b/src/resources/MDK/loggingconsole.lua new file mode 100755 index 0000000..cbc812a --- /dev/null +++ b/src/resources/MDK/loggingconsole.lua @@ -0,0 +1,461 @@ +--- 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) + fileName = fileName:gsub("[<>:'\"/\\?*]", "_") + 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 + fullPath = fullPath:gsub("|", "_") + 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" + elseif logType ~= "t" then + 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/src/resources/MDK/loginator.lua b/src/resources/MDK/loginator.lua new file mode 100755 index 0000000..3c5c093 --- /dev/null +++ b/src/resources/MDK/loginator.lua @@ -0,0 +1,456 @@ +--- Loginator creates an object which allows you to log things to file at +-- various severity levels, with the ability to only log items above a specific +-- severity to file. +-- @classmod Loginator +-- @author Damian Monogue +-- @copyright 2021 Damian Monogue +-- @license MIT, see LICENSE.lua +local Loginator = { + format = "h", + name = "logname", + fileNameTemplate = "|p/log/Loginator/|y-|M-|d-|n.|e", + entryTemplate = "|y-|M-|d |h:|m:|s.|x [|c|l|r] |t", + level = "warn", + bgColor = "black", + fontSize = 12, + fgColor = "white", +} + +local levelColors = {error = "red", warn = "DarkOrange", info = "ForestGreen", debug = "ansi_yellow"} +local loggerLevels = {error = 1, warn = 2, info = 3, debug = 4} + +local function exists(path) + local ok, err, code = os.rename(path, path) + if not ok and code == 13 then + return true + end + return ok, err +end + +local function isWindows() + return package.config:sub(1, 1) == [[\]] +end + +local function mkdir_p(path) + path = path:gsub("\\", "/") + local pathTbl = path:split("/") + local cwd = "/" + if isWindows() then + cwd = "" + end + for index, dirName in ipairs(pathTbl) do + if index == 1 then + cwd = cwd .. dirName + else + cwd = cwd .. "/" .. dirName + cwd = cwd:gsub("//", "/") + end + if not table.contains({"/", "C:"}, cwd) and not exists(cwd) then + local ok, err = lfs.mkdir(cwd) + if not ok then + return ok, err + end + end + end + return true +end + +local htmlHeaderTemplate = [=[ + + + + + + + +]=] + +--- Creates a new Loginator object +--@tparam table options table of options for the logger +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
option namedescriptiondefault
formatWhat format to log in? "h" for html, "a" for ansi, anything else for plaintext."h"
nameWhat is the name of the logger? Will replace |n in templateslogname
levelWhat level should the logger operate at? This will control what level the log function defaults to, as well as what logs will actually be written
+-- Only items of an equal or higher severity to this will be written to the log file.
"info"
bgColorWhat background color to use for html logs"black"
fgColorWhat color to use for the main text in html logs"white"
fontSizeWhat font size to use in html logs12
levelColorsTable with the log level as the key, and the color which corresponds to it as the value{ error = "red", warn = "DarkOrange", info = "ForestGreen", debug = "ansi_yellow" }
fileNameTemplateA template which will be transformed into the full filename, with path. See template options below for replacements"|p/log/Loginator/|y-|M-|d-|n.|e"
entryTemplateThe template which controls the look of each log entry. See template options below for replacements"|y-|M-|d |h:|m:|s.|x [|c|l|r] |t"

+-- Table of template options +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
template codewhat it is replaced withexample
|ythe year in 4 digits2021
|pgetMudletHomeDir()/home/demonnic/.config/mudlet/profiles/testprofile
|MMonth as 2 digits05
|dday, as 2 digits23
|hhour in 24hr time format, 2 digits03
|mminute as 2 digits42
|sseconds as 2 digits34
|xmilliseconds as 3 digits194
|eFilename extension expected. "html" for html format, "log" for everything elsehtml
|lThe logging level of the entry, in ALLCAPSWARN
|cThe color which corresponds with the logging level. Set via the levelColors table in the options. Example not included.
|rReset back to standard color. Used to close |c. Example not included
|nThe name of the logger, set via the options when you have Loginator create it.CoolPackageLog
+--@return newly created logger object +function Loginator:new(options) + options = options or {} + local optionsType = type(options) + if optionsType ~= "table" then + return nil, f "Loginator:new(options) options as table expected, got {optionsType}" + end + local me = table.deepcopy(options) + me.levelColors = me.levelColors or {} + local lcType = type(me.levelColors) + if lcType ~= "table" then + return nil, f "Loginator:new(options) provided options.levelColors must be a table, but you provided a {lcType}" + end + for lvl,clr in pairs(levelColors) do + me.levelColors[lvl] = me.levelColors[lvl] or clr + end + setmetatable(me, self) + self.__index = self + return me +end + +---@local +function Loginator:processTemplate(str, level) + local lvl = level or self.level + local timeTable = getTime() + for what, with in pairs({ + ["|y"] = function() + return timeTable.year + end, + ["|p"] = getMudletHomeDir, + ["|M"] = function() + return string.format("%02d", timeTable.month) + end, + ["|d"] = function() + return string.format("%02d", timeTable.day) + end, + ["|h"] = function() + return string.format("%02d", timeTable.hour) + end, + ["|m"] = function() + return string.format("%02d", timeTable.min) + end, + ["|s"] = function() + return string.format("%02d", timeTable.sec) + end, + ["|x"] = function() + return string.format("%03d", timeTable.msec) + end, + ["|e"] = function() + return (self.format:starts("h") and "html" or "log") + end, + ["|l"] = function() + return lvl:upper() + end, + ["|c"] = function() + return self:getColor(lvl) + end, + ["|r"] = function() + return self:getReset() + end, + ["|n"] = function() + return self.name + end, + }) do + if str:find(what) then + str = str:gsub(what, with()) + end + end + return str +end + +--- Set the color to associate with a logging level post-creation +--@param color The color to set for the level, as a string. Can be any valid color string for cecho, decho, or hecho. +--@param level The level to set the color for. Must be one of 'error', 'warn', 'info', or 'debug' +--@return true if the color is updated, or nil+error if it could not be updated for some reason. +function Loginator:setColorForLevel(color, level) + if not color then + return nil, "You must provide a color to set" + end + if not level then + return nil, "You must provide a level to set the color for" + end + if not loggerLevels[level] then + return nil, "Invalid level. Valid levels are 'error', 'warn', 'info', or 'debug'" + end + if not Geyser.Color.parse(color) then + return nil, "You must provide a color which can be parsed by Geyser.Color.parse. Examples are 'blue' (cecho), '<128,0,0>' (decho), '#aa3388' (hecho), or {128,0,0} (table of r,g,b values)" + end + self.levelColors[level] = color + return true +end + +---@local +function Loginator:getColor(level) + if self.format == "t" then + return "" + end + local r, g, b = Geyser.Color.parse((self.levelColors[level] or {128, 128, 128})) + if self.format == "h" then + return string.format("", r, g, b) + elseif self.format == "a" then + return string.format("\27[38:2::%d:%d:%dm", r, g, b) + end + return "" +end + +---@local +function Loginator:getReset() + if self.format == "t" then + return "" + elseif self.format == "h" then + return "" + elseif self.format == "a" then + return "\27[39;49m" + end + return "" +end + +--- Returns the full path and filename to the logfile +function Loginator:getFullFilename() + return self:processTemplate(self.fileNameTemplate) +end + +--- Write an error level message to the logfile. Error level messages are always written. +--@param msg the message to log +--@return true if msg written, nil+error if error +function Loginator:error(msg) + return self:log(msg, "error") +end + +--- Write a warn level message to the logfile. +-- Msg is only written if the logger level is <= warn +-- From most to least severe the levels are: +-- error > warn > info > debug +--@param msg the message to log +--@return true if msg written, false if skipped due to level, nil+error if error +function Loginator:warn(msg) + return self:log(msg, "warn") +end + +--- Write an info level message to the logfile. +-- Msg is only written if the logger level is <= info +-- From most to least severe the levels are: +-- error > warn > info > debug +--@param msg the message to log +--@return true if msg written, false if skipped due to level, nil+error if error +function Loginator:info(msg) + return self:log(msg, "info") +end + +--- Write a debug level message to the logfile. +-- Msg is only written if the logger level is debug +-- From most to least severe the levels are: +-- error > warn > info > debug +--@param msg the message to log +--@return true if msg written, false if skipped due to level, nil+error if error +function Loginator:debug(msg) + return self:log(msg, "debug") +end + +--- Write a message to the log file and optionally specify the level +--@param msg the message to log +--@param level the level to log the message at. Defaults to the level of the logger itself if not provided. +--@return true if msg written, false if skipped due to level, nil+error if error +function Loginator:log(msg, level) + level = level or self.level + local levelNumber = loggerLevels[level] + if not levelNumber then + return nil, f"Unknown logging level: {level}. Valid levels are 'error', 'warn', 'info', and 'debug'" + end + local displayLevelNumber = loggerLevels[self.level] + if levelNumber > displayLevelNumber then + return false + end + local filename = self:getFullFilename() + local filteredMsg = self:processTemplate(self.entryTemplate, level):gsub("|t", msg) + local ok, err = self:createPathIfNotExists(filename) + if err then + debugc(err) + return ok, err + end + if self.format == "h" and not io.exists(filename) then + filteredMsg = self:getHtmlHeader() .. filteredMsg + end + local file, err = io.open(filename, "a") + if not file then + err = string.format("Logger %s failed to open %s because: %s\n", self.name, filename, err) + debugc(err) + return nil, err + end + file:write(filteredMsg .. "\n") + file:close() + return true +end + +--- Uses openUrl() to request your OS open the logfile in the appropriate application. Usually your web browser for html and text editor for all others. +function Loginator:open() + openUrl(self:getFullFilename()) +end + +--- Uses openUrl() to request your OS open the directory the logfile resides in. This allows for easier browsing if you have more than one file. +function Loginator:openDir() + openUrl(self:getPath()) +end + +--- Returns the path to the log file (directory in which the file resides) as a string +--@param filename optional filename to return the path of. If not supplied, with use the logger's current filename +function Loginator:getPath(filename) + filename = filename or self:getFullFilename() + filename = filename:gsub([[\]], "/") + local filenameTable = filename:split("/") + filenameTable[#filenameTable] = nil + local path = table.concat(filenameTable, "/") + return path +end + +---@local +function Loginator:createPathIfNotExists(filename) + if exists(filename) then + return false + end + filename = filename:gsub([[\]], "/") + local path = self:getPath(filename) + if exists(path) then + return false + end + local ok, err = mkdir_p(path) + if not ok then + err = string.format("Could not create directory for log files: %s\n Reason was: %s", path, err) + return nil, err + end + return true +end + +---@local +function Loginator:getHtmlHeader() + local header = htmlHeaderTemplate + header = header:gsub("|b", self.bgColor) + header = header:gsub("|c", self.fgColor) + header = header:gsub("|f", self.fontSize) + return header +end + +return Loginator diff --git a/src/resources/MDK/mastermindsolver.lua b/src/resources/MDK/mastermindsolver.lua new file mode 100755 index 0000000..0fc34a5 --- /dev/null +++ b/src/resources/MDK/mastermindsolver.lua @@ -0,0 +1,254 @@ +--- Interactive object which helps you solve a Master Mind puzzle. +-- @classmod MasterMindSolver +-- @author Damian Monogue +-- @copyright 2021 Damian Monogue +-- @copyright 2008,2009 Konstantinos Asimakis for code used to turn an index number into a guess (indexToGuess method) +local MasterMindSolver = { + places = 4, + items = {"red", "orange", "yellow", "green", "blue", "purple"}, + template = "|t", + autoSend = false, + singleCommand = false, + separator = " ", + allowDuplicates = true, +} +local mod, floor, random, randomseed = math.mod, math.floor, math.random, math.randomseed +local initialGuess = {{1}, {1, 2}, {1, 1, 2}, {1, 1, 2, 2}, {1, 1, 1, 2, 2}, {1, 1, 1, 2, 2, 2}, {1, 1, 1, 1, 2, 2, 2}, {1, 1, 1, 1, 2, 2, 2, 2}} + +--- Removes duplicate elements from a list +-- @param tbl the table you want to remove dupes from +-- @local +local function tableUnique(tbl) + local used = {} + local result = {} + for _, item in ipairs(tbl) do + if not used[item] then + result[#result + 1] = item + used[item] = true + end + end + return result +end + +--- Creates a new Master Mind solver +-- @tparam table options table of configuration options for the solver +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
option namedescriptiondefault
placesHow many spots in the code we're breaking?4
itemsThe table of colors/gemstones/whatever which can be part of the code{"red", "orange", "yellow", "green", "blue", "purple"}
templateThe string template to use for the guess. Within the template, |t is replaced by the item. Used as the command if autoSend is true"|t"
autoSendShould we send the guess directly to the server?false
allowDuplicatesCan the same item be used more than once in a code?true
singleCommandIf true, combines the guess into a single command, with each one separated by the separatorfalse
separatorIf sending the guess as a single command, what should we put between the guesses to separate them?" "
+function MasterMindSolver:new(options) + if options == nil then + options = {} + end + local optionsType = type(options) + if optionsType ~= "table" then + error(f "MasterMindSolver:new(options): options as table expected, got {tostring(options)} of type: {optionsType}") + end + local me = options + setmetatable(me, self) + self.__index = self + me:populateInitialSet() + if not me.allowDuplicates then + me.initialGuessMade = true -- skip the preset initial guess, they assume duplicates + end + return me +end + +--- Takes a guess number (4, or 1829, or any number from 1 - ) and returns the +-- actual guess. +-- @tparam number index which guess to generate +-- @local +function MasterMindSolver:indexToGuess(index) + local guess = {} + local options = #self.items + for place = 1, self.places do + guess[place] = mod(floor((index - 1) / options ^ (place - 1)), options) + 1 + end + return guess +end + +--- Compares a guess with the solution and returns the answer +-- @tparam table guess The guess you are checking, as numbers. { 1 , 1, 2, 2 } as an example +-- @tparam table solution the solution you are checking against, as numbers. { 3, 4, 1, 6 } as an example. +-- @local +function MasterMindSolver:compare(guess, solution) + local coloredPins = 0 + local whitePins = 0 + local usedGuessPlace = {} + local usedSolutionPlace = {} + local places = self.places + for place = 1, places do + if guess[place] == solution[place] then + coloredPins = coloredPins + 1 + usedGuessPlace[place] = true + usedSolutionPlace[place] = true + end + end + for guessPlace = 1, places do + if not usedGuessPlace[guessPlace] then + for solutionPlace = 1, places do + if not usedSolutionPlace[solutionPlace] then + if guess[guessPlace] == solution[solutionPlace] then + whitePins = whitePins + 1 + usedSolutionPlace[solutionPlace] = true + break + end + end + end + end + end + return coloredPins, whitePins +end + +--- Generates an initial table of all guesses from 1 to that are valid. +-- If allowDuplicates is false, will filter out any of the possible combinations which contain duplicates +-- @local +function MasterMindSolver:populateInitialSet() + local possible = {} + local allowDuplicates = self.allowDuplicates + local places = self.places + local numberOfItems = #self.items + local totalCombos = numberOfItems ^ places + local numberRemaining = 0 + for entry = 1, totalCombos do + local useItem = true + if not allowDuplicates then + local guess = self:indexToGuess(entry) + local guessUnique = tableUnique(guess) + if #guessUnique ~= self.places then + useItem = false + end + end + if useItem then + possible[entry] = true + numberRemaining = numberRemaining + 1 + end + end + self.possible = possible + self.numberRemaining = numberRemaining +end + +--- Function used to reduce the remaining possible answers, given a guess and the answer to that guess. This is not undoable. +-- @tparam table guess guess which the answer belongs to. Uses numbers, rather than item names. IE { 1, 1, 2, 2} rather than { "blue", "blue", "green", "green" } +-- @tparam number coloredPins how many parts of the guess are both the right color and the right place +-- @tparam number whitePins how many parts of the guess are the right color, but in the wrong place +-- @return true if you solved the puzzle (coloredPins == number of positions in the code), or false otherwise +function MasterMindSolver:reducePossible(guess, coloredPins, whitePins) + if coloredPins == #guess then + return true + end + local possible = self.possible + local numberRemaining = 0 + for entry, _ in pairs(possible) do + local testColor, testWhite = self:compare(guess, self:indexToGuess(entry)) + if testColor ~= coloredPins or testWhite ~= whitePins then + possible[entry] = nil + else + numberRemaining = numberRemaining + 1 + end + end + self.possible = possible + self.numberRemaining = numberRemaining + return false +end + +--- Function which assumes you used the last suggested guess from the solver, and reduces the number of possible correct solutions based on the answer given +-- @see MasterMindSolver:reducePossible +-- @tparam number coloredPins how many parts of the guess are both the right color and the right place +-- @tparam number whitePins how many parts of the guess are the right color, but in the wrong place +-- @return true if you solved the puzzle (coloredPins == number of positions in the code), or false otherwise +function MasterMindSolver:checkLastSuggestion(coloredPins, whitePins) + return self:reducePossible(self.guess, coloredPins, whitePins) +end + +--- Used to get one of the remaining valid possible guesses +-- @tparam boolean useActions if true, will return the guess as the commands which would be sent, rather than the numbered items +function MasterMindSolver:getValidGuess(useActions) + local guess + if not self.initialGuessMade then + self.initialGuessMade = true + guess = initialGuess[self.places] + end + if not guess then + local possible = self.possible + local keys = table.keys(possible) + randomseed(os.time()) + guess = self:indexToGuess(keys[random(#keys)]) + end + self.guess = guess + if self.autoSend then + self:sendGuess(guess) + end + if useActions then + return self:guessToActions(guess) + end + return guess +end + +--- Takes a guess and converts the numbers to commands/actions. IE guessToActions({1, 1, 2, 2}) might return { "blue", "blue", "green", "green" } +-- @tparam table guess the guess to convert as numbers. IE { 1, 1, 2, 2} +-- @return table of commands/actions correlating to the numbers in the guess. +-- @local +function MasterMindSolver:guessToActions(guess) + local actions = {} + for index, itemNumber in ipairs(guess) do + local item = self.items[itemNumber] + actions[index] = self.template:gsub("|t", item) + end + return actions +end + +--- Handles sending the commands to the game for a guess +-- @local +function MasterMindSolver:sendGuess(guess) + local actions = self:guessToActions(guess) + if self.singleCommand then + send(table.concat(actions, self.separator)) + else + sendAll(unpack(actions)) + end +end + +return MasterMindSolver diff --git a/src/resources/MDK/mdkversion.txt b/src/resources/MDK/mdkversion.txt new file mode 100755 index 0000000..10c2c0c --- /dev/null +++ b/src/resources/MDK/mdkversion.txt @@ -0,0 +1 @@ +2.10.0 diff --git a/src/resources/MDK/revisionator.lua b/src/resources/MDK/revisionator.lua new file mode 100755 index 0000000..4d5ec0d --- /dev/null +++ b/src/resources/MDK/revisionator.lua @@ -0,0 +1,141 @@ +--- The revisionator provides a standardized way of migrating configurations between revisions +-- for instance, it will track what the currently applied revision number is, and when you tell +-- tell it to migrate, it will apply every individual migration between the currently applied +-- revision and the latest/current revision. This should allow for more seamlessly moving from +-- an older version of a package to a new one. +-- @classmod revisionator +-- @author Damian Monogue +-- @copyright 2023 +-- @license MIT, see https://raw.githubusercontent.com/demonnic/MDK/main/src/scripts/LICENSE.lua +local revisionator = { + name = "Revisionator", + patches = {}, +} +revisionator.__index = revisionator +local dataDir = getMudletHomeDir() .. "/revisionator" +revisionator.dataDir = dataDir +if not io.exists(dataDir) then + local ok,err = lfs.mkdir(dataDir) + if not ok then + printDebug(f"Error creating the directory for storing applied revisions: {err}", true) + end +end + +--- Creates a new revisionator +-- @tparam table options the options to create the revisionator with. +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
option namedescriptiondefault
nameThe name of the revisionator. This is absolutely required, as the name is used for tracking the currently applied patch levelraises an error if not provided
patchesA table of patch functions. It is traversed using ipairs, so must be in the form of {function1, function2, function3} etc. If you do not provide it, you can add the patches by calling :addPatch for each patch in order.{}
+function revisionator:new(options) + options = options or {} + local optionsType = type(options) + if optionsType ~= "table" then + printError(f"revisionator:new bad argument #1 type, options as table expected, got {optionsType}", true, true) + end + if not options.name then + printError("revisionator:new(options) options must include a 'name' key as this is used as part of tracking the applied patch level.", true, true) + end + local me = table.deepcopy(options) + setmetatable(me, self) + return me +end + +--- Get the currently applied revision from file +--- @treturn[1] number the revision number currently applied, or 0 if it can't read a current version +--- @treturn[2] nil nil +--- @treturn[2] string error message +function revisionator:getAppliedPatch() + local fileName = f"{self.dataDir}/{self.name}.txt" + debugc(fileName) + local revision = 0 + if io.exists(fileName) then + local file = io.open(fileName, "r") + local fileContents = file:read("*a") + file:close() + local revNumber = tonumber(fileContents) + if revNumber then + revision = revNumber + else + return nil, f"Error while attempting to read current patch version from file: {fileName}\nThe contents of the file are {fileContents} and it was unable to be converted to a revision number" + end + end + return revision +end + +--- go through all the patches in order and apply any which are still necessary +--- @treturn boolean true if it successfully applied patches, false if it was already at the latest patch level +--- @error error message +function revisionator:migrate() + local applied,err = self:getAppliedPatch() + if not applied then + printError(err, true, true) + end + local patches = self.patches + if applied >= #patches then + return false + end + for revision, patch in ipairs(patches) do + if applied < revision then + local ok, err = pcall(patch) + if not ok then + self:setAppliedPatch(revision - 1) + return nil, f"Error while running patch #{revision}: {err}" + end + end + end + self:setAppliedPatch(#patches) + return true +end + +--- add a patch to the table of patches +--- @tparam function func the function to run as the patch +--- @number[opt] position which patch to insert it as? If not supplied, inserts it as the last patch. Which is usually what you want. +function revisionator:addPatch(func, position) + if position then + table.insert(self.patches, position, func) + else + table.insert(self.patches, func) + end +end + +--- Remove a patch from the table of patches +--- this is primarily used for testing +--- @local +--- @number[opt] patchNumber the patch number to remove. Will remove the last item if not provided. +function revisionator:removePatch(patchNumber) + table.remove(self.patches, patchNumber) +end + +--- set the currently applied patch number +-- only directly called for testing +--- @local +--- @number patchNumber the patch number to set as the currently applied patch +function revisionator:setAppliedPatch(patchNumber) + local fileName = f"{self.dataDir}/{self.name}.txt" + local revFile, err = io.open(fileName, "w+") + if not revFile then + printError(err, true, true) + end + revFile:write(patchNumber) + revFile:close() +end + +return revisionator \ No newline at end of file diff --git a/src/resources/MDK/schema.lua b/src/resources/MDK/schema.lua new file mode 100755 index 0000000..a680236 --- /dev/null +++ b/src/resources/MDK/schema.lua @@ -0,0 +1,644 @@ +--[[ +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. +]] + +local schema = {} + +-- Checks an object against a schema. +function schema.CheckSchema(obj, schem, path) + if path == nil then + path = schema.Path.new() + path:setBase(obj) + end + if type(schem) == "function" then + return schem(obj, path) + else -- attempt to simply compare the values + if schem == obj then + return nil + end + return schema.Error("Invalid value: "..path.." should be "..tostring(schem), path) + end +end + +function schema.FormatOutput(output) + local format = schema.List() + for k,v in ipairs(output) do + format:append(v:format()) + end + return table.concat(format, "\n") +end + +-- +-- Infrastructure +-- + +-- Path class. Represents paths to values in a table (the path's *base*). +local Path = {} +function Path.new(...) + local arg = {...} + local self = setmetatable({}, Path) + self.p = {} + for k,v in ipairs(arg) do + self.p[k] = v + end + return self +end + +-- Sets the base of the path, i.e. the table to which the path is relative. +-- Note that this is the actual *table*, not the table's name. +function Path:setBase(base) + self.base = base +end + +-- Gets the base of the path. +function Path:getBase() + return self.base +end + +-- Returns the target of the path or 'nil' if the path is invalid. +function Path:target() + if self.base == nil then + error("Path:target() called on a path without a base!") + end + local current = self.base + for k,v in ipairs(self.p) do + current = current[v] + if current == nil then + return nil + end + end + return current +end + +-- Pushes an entry to the end of the path. +function Path:push(obj) + self.p[#self.p + 1] = obj + return self +end + +-- Pops an entry from the end of the path. +function Path:pop() + local tmp = self.p[#self.p] + self.p[#self.p] = nil + return tmp +end + +-- Returns the topmost entry of the end of the path. +function Path:top() + return self.p[#self.p] +end + +-- Returns the length of the path. +function Path:length() + return #self.p +end + +-- Returns the element at the specified index. +function Path:get(index) + return self.p[index] +end + +-- Copies the path. +function Path:copy() + local cp = Path.new() + cp.base = self.base + for k,v in ipairs(self) do + cp.p[k] = v + end + return cp +end + +Path.__index = Path +Path.__tostring = function(tbl) + if #tbl.p == 0 then + return '' + end + return table.concat(tbl.p,".") +end +Path.__concat = function(lhs, rhs) + if type(lhs) == "table" then + return tostring(lhs)..rhs + elseif type(rhs) == "table" then + return lhs..tostring(rhs) + end +end +Path.__len = function(self) + return #self.p +end + +setmetatable(Path, { + __call = function (cls, ...) + return Path.new(...) + end +}) +schema.Path = Path + +-- List class +local List = {} +function List.new(...) + local self = setmetatable({}, List) + local arg = {...} + for k,v in ipairs(arg) do + self[k] = v + end + return self +end + +function List:add(obj) + self[#self+1] = obj + return self +end + +function List:append(list) + for k,v in ipairs(list) do + self[#self+k] = v + end + return self +end + +List.__index = List +List.__tostring = function(self) + local tmp = {} + for k,v in ipairs(self) do + tmp[k] = tostring(v) + end + return table.concat(tmp, "\n") +end +setmetatable(List, { + __call = function(cls, ...) + return List.new(...) + end +}) +schema.List = List + +-- Error class. Describes mismatches that occured during the schema-checking. +local Error = {} +function Error.new(msg, path, suberrors) + local self = setmetatable({}, Error) + self.message = msg + self.path = path:copy() + self.suberrors = suberrors + return self +end + +-- Returns a list of strings which represent the error (with indenttation for +-- suberrors). +function Error:format() + local output = List.new(self.message) + if self.suberrors ~= nil then + for k,sub in pairs(self.suberrors) do + local subout = sub:format() + for k1,msg in pairs(subout) do + output = output:add(" "..msg) + end + end + end + return output +end + +Error.__tostring = function(self) + return table.concat(self:format(), "\n") +end +Error.__index = Error +setmetatable(Error, { + __call = function(cls, ...) + return List(Error.new(...)) + end +}) +schema.Error = Error + +-- +-- Schema Building Blocks +-- A schema is a function taking the object to be checked and the path to the +-- current value in the environment. +-- It returns either 'true' if the schema accepted the object or an Error +-- object which describes why it was rejected. +-- The schemata below are just some basic building blocks. Expand them to your +-- liking. +-- + +-- Always accepts. +function schema.Any(obj, path) + return nil +end + +-- Always fails. +function schema.Nothing(obj, path) + return schema.Error("Failure: '"..path.."' will always fail.", path) +end + +-- Checks a value against a specific type. +local function TypeSchema(obj, path, typeId) + if type(obj) ~= typeId then + return schema.Error("Type mismatch: '"..path.."' should be "..typeId..", is "..type(obj), path) + else + return nil + end +end + +function schema.Boolean (obj, path) return TypeSchema(obj, path, "boolean") end +function schema.Function(obj, path) return TypeSchema(obj, path, "function") end +function schema.Nil (obj, path) return TypeSchema(obj, path, "nil") end +function schema.Number (obj, path) return TypeSchema(obj, path, "number") end +function schema.String (obj, path) return TypeSchema(obj, path, "string") end +function schema.Table (obj, path) return TypeSchema(obj, path, "table") end +function schema.UserData(obj, path) return TypeSchema(obj, path, "userdata") end + +-- Checks that some value is a string matching a given pattern. +function schema.Pattern(pattern) + local userPattern = pattern + if not pattern:match("^^") then + pattern = "^" .. pattern + end + if not pattern:match("$$") then + pattern = pattern .. "$" + end + local function CheckPattern(obj, path) + local err = schema.String(obj, path) + if err then + return err + end + if string.match(obj, pattern) then + return nil + else + return schema.Error("Invalid value: '"..path.."' must match pattern '"..userPattern.."'", path) + end + end + return CheckPattern +end + +-- Checks that some number is an integer. +function schema.Integer(obj, path) + local err = schema.Number(obj, path) + if err then + return err + end + if math.floor(obj) == obj then + return nil + end + return schema.Error("Invalid value: '"..path.."' must be an integral number", path) +end + +-- Checks that some number is >= 0. +function schema.NonNegativeNumber(obj, path) + local err = schema.Number(obj, path) + if err then + return err + end + if obj >= 0 then + return nil + end + return schema.Error("Invalid value: '"..path.."' must be >= 0", path) +end + +-- Checks that some number is > 0. +function schema.PositiveNumber(obj, path) + local err = schema.Number(obj, path) + if err then + return err + end + if obj > 0 then + return nil + end + return schema.Error("Invalid value: '"..path.."' must be > 0", path) +end + +-- Checks that some value is a number from the interval [lower, upper]. +function schema.NumberFrom(lower, upper) + local function CheckNumberFrom(obj, path) + local err = schema.Number(obj, path) + if err then + return err + end + if lower <= obj and upper >= obj then + return nil + else + return schema.Error("Invalid value: '"..path.."' must be between "..lower.." and "..upper, path) + end + end + return CheckNumberFrom +end + +-- Takes schemata and accepts their disjunction. +function schema.OneOf(...) + local arg = {...} + local function CheckOneOf(obj, path) + for k,v in ipairs(arg) do + local err = schema.CheckSchema(obj, v, path) + if not err then return nil end + end + return schema.Error("No suitable alternative: No schema matches '"..path.."'", path) + end + return CheckOneOf +end + +-- Takes a schema and returns an optional schema. +function schema.Optional(s) + return schema.OneOf(s, schema.Nil) +end + +-- Takes schemata and accepts their conjuction. +function schema.AllOf(...) + local arg = {...} + local function CheckAllOf(obj, path) + local errmsg = nil + for k,v in ipairs(arg) do + local err = schema.CheckSchema(obj, v, path) + if err then + if errmsg == nil then + errmsg = err + else + errmsg = errmsg:append(err) + end + end + end + return errmsg + end + return CheckAllOf +end + +-- Builds a record type schema, i.e. a table with a fixed set of keys (strings) +-- with corresponding values. Use as in +-- Record({ +-- name = schema, +-- name2 = schema2 +-- }) +function schema.Record(recordschema, additionalValues) + if additionalValues == nil then + additionalValues = false + end + local function CheckRecord(obj, path) + if type(obj) ~= "table" then + return schema.Error("Type mismatch: '"..path.."' should be a record (table), is "..type(obj), path) + end + + local errmsg = nil + local function AddError(msg) + if errmsg == nil then + errmsg = msg + else + errmsg = errmsg:append(msg) + end + end + + for k,v in pairs(recordschema) do + path:push(k) + local err = schema.CheckSchema(obj[k], v, path) + if err then + AddError(err) + end + path:pop() + end + + for k, v in pairs(obj) do + path:push(k) + if type(k) ~= "string" then + AddError(schema.Error("Invalid key: '"..path.."' must be of type 'string'", path)) + end + if recordschema[k] == nil and not additionalValues then + AddError(schema.Error("Superfluous value: '"..path.."' does not appear in the record schema", path)) + end + path:pop() + end + return errmsg + end + return CheckRecord +end + +function schema.MixedTable(t_schema, additional_values) + local function CheckMixedTable(obj, path) + local obj_t = type(obj) + if obj_t ~= "table" then + local msg = ("Type mismatch: '%s' should be a table, is %s"):format(path, obj_t) + return schema.Error(msg, path) + end + + local errmsg = nil + local function AddError(msg) + if errmsg == nil then + errmsg = msg + else + errmsg = errmsg:append(msg) + end + end + + local checked_keys = {} + for k, v in pairs(t_schema) do + path:push(k) + local err = schema.CheckSchema(obj[k], v, path) + if err then + AddError(err) + end + checked_keys[k] = true + path:pop() + end + + for k, v in pairs(obj) do + if not checked_keys[k] then + path:push(k) + local k_type = type(k) + if k_type ~= "string" and k_type ~= "number" then + local msg = ("Invalid key: '%s' must be of type 'string' or 'number'"):format(k_type) + AddError(schema.Error(msg, path)) + end + + local t_schema_v = t_schema[k] + if t_schema_v then + local err = schema.CheckSchema(v, t_schema_v, path) + if err then + AddError(err) + end + else + if not additional_values then + local msg = ("Superfluous value: '%s' does not appear in the table schema") + :format(path) + AddError(schema.Error(msg, path)) + end + end + path:pop() + end + end + return errmsg + end + return CheckMixedTable +end + +-- Builds a map type schema, i.e. a table with an arbitraty number of +-- entries where both all keys (and all vaules) fit a common schema. +function schema.Map(keyschema, valschema) + local function CheckMap(obj, path) + if type(obj) ~= "table" then + return schema.Error("Type mismatch: '"..path.."' should be a map (table), is "..type(obj), path) + end + + local errmsg = nil + local function AddError(msg) + if errmsg == nil then + errmsg = msg + else + errmsg = errmsg:append(msg) + end + end + + -- aggregate error message + for k, v in pairs(obj) do + path:push(k) + local keyErr = schema.CheckSchema(k, keyschema, path) + if keyErr then + AddError(schema.Error("Invalid map key", path, keyErr)) + end + + local valErr = schema.CheckSchema(v, valschema, path) + if valErr then + AddError(valErr) + end + path:pop() + end + return errmsg + end + return CheckMap +end + +-- Builds a collection type schema, i.e. a table with an arbitrary number of +-- entries where we only care about the type of the values. +function schema.Collection(valschema) + return schema.Map(schema.Any, valschema) +end + +-- Builds a tuple type schema, i.e. a table with a fixed number of entries, +-- each indexed by a number and with a fixed type. +function schema.Tuple(...) + local arg = {...} + + local function CheckTuple(obj, path) + if type(obj) ~= "table" then + return schema.Error("Type mismatch: '"..path.."' should be a map (tuple), is "..type(obj), path) + end + + if #obj ~= #arg then + return schema.Error("Invalid length: '"..path.." should have exactly "..#arg.." elements", path) + end + + local errmsg = nil + local function AddError(msg) + if errmsg == nil then + errmsg = msg + else + errmsg = errmsg:append(msg) + end + end + + local min = 1 + local max = #arg + for k, v in pairs(obj) do + path:push(k) + local err = schema.Integer(k, path) + if not err then + err = schema.CheckSchema(v, arg[k], path) + if err then + AddError(err) + end + else + AddError(schema.Error("Invalid tuple key", path, err)) + end + path:pop() + end + return errmsg + end + return CheckTuple +end + +-- Builds a conditional type schema, i.e. a schema that depends on the value of +-- another value. The dependence must be *local*, i.e. defined in the same +-- table. Use as in +-- Case("name", {"Peter", schema1}, {"Mary", schema2}, {OneOf(...), schema3}) +-- This will check the field "name" against every schema in the first component +-- and will return the second component of the first match. +function schema.Case(relativePath, ...) + if type(relativePath) ~= "table" then + relativePath = schema.Path("..", relativePath) + end + local cases = {...} + for k,v in ipairs(cases) do + if type(v) ~= "table" then + error("Cases expects inputs of the form {conditionSchema, schema}; argument "..v.." is invalid") + end + end + + local function CheckCase(obj, path) + local condPath = path:copy() + for k=0, #relativePath do + local s = relativePath:get(k) + if s == ".." then + condPath:pop() + else + condPath:push(s) + end + end + + local errmsg = nil + local function AddError(msg) + if errmsg == nil then + errmsg = msg + else + errmsg = errmsg:append(msg) + end + end + + local anyCond = false + local condObj = condPath:target() + for k,v in ipairs(cases) do + local condSchema = v[1] + local valSchema = v[2] + local condErr = schema.CheckSchema(condObj, condSchema, condPath) + if not condErr then + anyCond = true + local err = schema.CheckSchema(obj, valSchema, path) + if err then + AddError(schema.Error("Case failed: Condition "..k.." of '"..path.."' holds but the consequence does not", path, err)) + end + end + end + + if not anyCond then + AddError(schema.Error("Case failed: No condition on '"..path.."' holds")) + end + + return errmsg + end + return CheckCase +end + +function schema.Test(fn, msg) + local function CheckTest(obj, path) + local pok, ok = pcall(fn, obj) + if pok and ok then + return nil + else + return schema.Error("Invalid value: '"..path..(msg and "': "..msg or ""), path) + end + end + return CheckTest +end + +return schema diff --git a/src/resources/MDK/sortbox.lua b/src/resources/MDK/sortbox.lua new file mode 100755 index 0000000..85376d7 --- /dev/null +++ b/src/resources/MDK/sortbox.lua @@ -0,0 +1,514 @@ +---An H/VBox alternative which can be set to either vertical or horizontal, and will autosort the windows +-- @classmod SortBox +-- @author Damian Monogue +-- @copyright 2020 Damian Monogue +-- @license MIT, see LICENSE.lua +local SortBox = Geyser.Container:new({ + name = "SortBoxClass", + autoSort = true, + timerSort = true, + sortInterval = 500, + elastic = false, + maxHeight = 0, + maxWidth = 0, + boxType = "v", + sortFunction = "gaugeValue", +}) +local BIGNUMBER = 999999999 + +--- Sorting functions for spairs, should you wish +-- @table SortFunctions +-- @field gaugeValue sorts Geyser gauges by value, ascending +-- @field reverseGaugeValue sorts Geyser gauges by value, descending +-- @field timeLeft sorts TimerGauges by how much time is left, ascending +-- @field reverseTimeLeft sorts TimerGauges by how much time is left, descending. +-- @field name sorts Geyser objects by name, ascending +-- @field reverseName sorts Geyser objects by name, descending +-- @field message sorts Geyser labels and gauges by their echoed text, ascending +-- @field reverseMessage sorts Geyser labels and gauges by their echoed text, descending +SortBox.SortFunctions = { + gaugeValue = function(t, a, b) + local avalue = t[a].value or BIGNUMBER + local bvalue = t[b].value or BIGNUMBER + return avalue < bvalue + end, + reverseGaugeValue = function(t, a, b) + local avalue = t[a].value or BIGNUMBER + local bvalue = t[b].value or BIGNUMBER + return avalue > bvalue + end, + timeLeft = function(t, a, b) + a = t[a] + b = t[b] + local avalue = a.getTime and tonumber(a:getTime("S.mm")) or BIGNUMBER + local bvalue = b.getTime and tonumber(b:getTime("S.mm")) or BIGNUMBER + return avalue < bvalue + end, + reverseTimeLeft = function(t, a, b) + a = t[a] + b = t[b] + local avalue = a.getTime and tonumber(a:getTime("S.mm")) or BIGNUMBER + local bvalue = b.getTime and tonumber(b:getTime("S.mm")) or BIGNUMBER + return avalue > bvalue + end, + name = function(t, a, b) + return t[a].name < t[b].name + end, + reverseName = function(t, a, b) + return t[a].name > t[b].name + end, + message = function(t, a, b) + a = t[a] + b = t[b] + local avalue = a.text and a.text.message or a.message + local bvalue = b.text and b.text.message or b.message + avalue = avalue or "" + bvalue = bvalue or "" + return avalue < bvalue + end, + reverseMessage = function(t, a, b) + a = t[a] + b = t[b] + local avalue = a.text and a.text.message or a.message + local bvalue = b.text and b.text.message or b.message + avalue = avalue or "" + bvalue = bvalue or "" + return avalue > bvalue + end, +} +--- Creates a new SortBox +-- @usage +-- local SortBox = require("MDK.sortbox") +-- mySortBox = SortBox:new({ +-- name = "mySortBox", +-- x = 400, +-- y = 100, +-- height = 150, +-- width = 300, +-- sortFunction = "timeLeft" +-- }) +-- @tparam table options the options to use for the SortBox. See table below for added options +-- @param[opt] container the container to add the SortBox into +--

Table of new options +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
option namedescriptiondefault
autoSortshould the SortBox perform function based sorting? If false, will behave like a normal H/VBoxtrue
timerSortshould the SortBox automatically perform sorting on a timer?true
sortIntervalhow frequently should we sort on a timer if timerSort is true, in milliseconds500
boxTypeShould we stack like an HBox or VBox? use 'h' for hbox and 'v' for vboxv
sortFunctionhow should we sort the items in the SortBox? see setSortFunction for valid optionsgaugeValue
elasticShould this container stretch to fit its contents? boxType v stretches in height, h stretches in width.false
maxHeightIf elastic, what's the biggest a 'v' style box should grow in height? Use 0 for unlimited0
maxWidthIf elastic, what's the biggest a 'h' style box should grow in width? Use 0 for unlimited0
+function SortBox:new(options, container) + options = options or {} + options.type = options.type or "SortBox" + local me = self.parent:new(options, container) + setmetatable(me, self) + self.__index = self + if me.timerSort then + me:enableTimer() + end + me:setBoxType(me.boxType) + return me +end + +--- Iterates a key:value pair table in a sorted fashion +-- @local +-- I first found this on https://stackoverflow.com/questions/15706270/sort-a-table-in-lua +-- modified slightly, as Mudlet already has table.keys to collect keys, and I don't want +-- to sort if no function to sort with is given. In this case, I want it to work like pairs. +local function spairs(t, order) + local keys = table.keys(t) + if order then + table.sort(keys, function(a, b) + return order(t, a, b) + end) + end + + local i = 0 + return function() + i = i + 1 + if keys[i] then + return keys[i], t[keys[i]] + end + end +end + +function SortBox:add(window, cons) + if self.useAdd2 then + Geyser.add2(self, window, cons) + else + Geyser.add(self, window, cons) + end + if not self.defer_updates then + self:organize() + end +end + +function SortBox:remove(window) + Geyser.remove(self, window) + self:organize() +end + +--- Calling this will cause the SortBox to reposition/resize everything +function SortBox:organize() + -- make sure we don't divide by zero later + if self:get_width() == 0 then + self:resize("0.9px", nil) + end + if self:get_height() == 0 then + self:resize(nil, "0.9px") + end + -- handle the individual boxType organization + if self.boxType == "v" then + self:vorganize() + else + self:horganize() + end + -- shrink/grow if needed + self:handleElastic() +end + +--- replicates Geyser.HBox functionality, but with the option of sorting +-- @local +function SortBox:horganize() + local window_width = (self:calculate_dynamic_window_size().width / self:get_width()) * 100 + local start_x = 0 + local sortFunction = (self.autoSort and self.sortFunction) and SortBox.SortFunctions[self.sortFunction] or nil + if sortFunction then + for _, window in spairs(self.windowList, sortFunction) do + start_x = start_x + self:handleWindow(window, start_x, window_width) + end + else + for _, window_name in ipairs(self.windows) do + local window = self.windowList[window_name] + start_x = start_x + self:handleWindow(window, start_x, window_width) + end + end +end + +--- replicates Geyser.VBox functionality, but with the option of sorting +-- @local +function SortBox:vorganize() + local window_height = (self:calculate_dynamic_window_size().height / self:get_height()) * 100 + local start_y = 0 + local sortFunction = (self.autoSort and self.sortFunction) and SortBox.SortFunctions[self.sortFunction] or nil + if sortFunction then + for _, window in spairs(self.windowList, sortFunction) do + start_y = start_y + self:handleWindow(window, start_y, window_height) + end + else + for _, window_name in ipairs(self.windows) do + local window = self.windowList[window_name] + start_y = start_y + self:handleWindow(window, start_y, window_height) + end + end +end + +--- handles a single window during the shuffle process +-- @local +function SortBox:handleWindow(window, start, window_dimension) + local width = (window:get_width() / self:get_width()) * 100 + local height = (window:get_height() / self:get_height()) * 100 + if window.h_policy == Geyser.Fixed or window.v_policy == Geyser.Fixed then + self.contains_fixed = true + end + if self.boxType == "v" then + window:move("0%", start .. "%") + if window.h_policy == Geyser.Dynamic then + width = 100 + if window.width ~= width then + window:resize(width .. "%", nil) + end + end + if window.v_policy == Geyser.Dynamic then + height = window_dimension * window.v_stretch_factor + if window.height ~= height then + window:resize(nil, height .. "%") + end + end + return height + else + window:move(start .. "%", "0%") + if window.h_policy == Geyser.Dynamic then + width = window_dimension * window.h_stretch_factor + if window.width ~= width then + window:resize(width .. "%", nil) + end + end + if window.v_policy == Geyser.Dynamic then + height = 100 + if window.height ~= height then + window:resize(nil, height .. "%") + end + end + return width + end +end + +---handles actually resizing the window if elastic +-- @local +function SortBox:handleElastic() + if not self.elastic or table.is_empty(self.windows) then + return + end + if self.boxType == "v" then + local contentHeight, canElastic = self:getContentHeight() + if not canElastic then + debugc(string.format("SortBox named %s cannot properly elasticize, as it contains at least one item with a dynamic v_policy", self.name)) + return + end + local currentHeight = self:get_height() + local maxHeight = self.maxHeight + if maxHeight > 0 and contentHeight > maxHeight then + contentHeight = maxHeight + end + if contentHeight ~= currentHeight then + self:resize(nil, contentHeight) + end + else + local contentWidth, canElastic = self:getContentWidth() + if not canElastic then + debugc(string.format("SortBox named %s cannot properly elasticize, as it contains at least one item with a dynamic h_policy", self.name)) + return + end + local currentWidth = self:get_width() + local maxWidth = self.maxWidth + if maxWidth > 0 and contentWidth > maxWidth then + contentWidth = maxWidth + end + if contentWidth ~= currentWidth then + self:resize(contentWidth, nil) + end + end +end + +---prevents gaps from forming during resize if it doesn't autoorganize on a timer. +-- @local +function SortBox:reposition() + Geyser.Container.reposition(self) + if self.contains_fixed then + self:organize() + end +end + +--- Returns the sum of the heights of the contents, and whether this SortBox can be elastic in height +-- @local +function SortBox:getContentHeight() + if self.boxType ~= "v" then + return self:get_height() + end + local canElastic = true + local contentHeight = 0 + for _, window in pairs(self.windowList) do + contentHeight = contentHeight + window:get_height() + if window.v_policy == Geyser.Dynamic then + canElastic = false + end + end + return contentHeight, canElastic +end + +--- Returns the sum of the widths of the contents, and whether this SortBox can be elastic in width. +-- @local +function SortBox:getContentWidth() + if self.boxType == "v" then + return self:get_width() + end + local canElastic = true + local contentWidth = 0 + for _, window in pairs(self.windowList) do + contentWidth = contentWidth + window:get_width() + if window.h_policy == Geyser.Dynamic then + canElastic = false + end + end + return contentWidth, canElastic +end + +--- Enables elasticity for the SortBox. +function SortBox:enableElastic() + self:setElastic(true) +end + +--- Disables elasticity for the SortBox +function SortBox:disableElastic() + self:setElastic(false) +end + +--- Set elasticity specifically +-- @tparam boolean enabled if true, enable elasticity. If false, disable it. +function SortBox:setElastic(enabled) + self.elastic = enabled and true or false +end + +--- Set the max width of the SortBox if it's elastic +-- @tparam number maxWidth The maximum width in pixels to resize the SortBox to. Use 0 for unlimited. +function SortBox:setMaxWidth(maxWidth) + local mwtype = type(maxWidth) + assert(mwtype == "number", string.format("SortBox:setMaxWidth(maxWidth): SortBox: %s maxWidth as number expected, got %s", self.name, mwtype)) + assert(maxWidth >= 0, string.format("SortBox:setMaxWidth(maxWidth): SortBox: %s maxWidth must be >= 0, %d", self.name, maxWidth)) + self.maxWidth = maxWidth +end + +--- Set the max height of the SortBox if it's elastic +-- @tparam number maxHeight The maximum height in pixels to resize the SortBox to. Use 0 for unlimited. +function SortBox:setMaxHeight(maxHeight) + local mhtype = type(maxHeight) + assert(mhtype == "number", string.format("SortBox:setMaxHeight(maxHeight): SortBox: %s maxHeight as number expected, got %s", self.name, mhtype)) + assert(maxHeight >= 0, string.format("SortBox:setMaxHeight(maxHeight): SortBox: %s maxHeight must be >= 0, %d", self.name, maxHeight)) + self.maxHeight = maxHeight +end + +--- Starts the SortBox sorting and organizing itself on a timer +function SortBox:enableTimer() + if self.timerID then + self:disableTimer() + end + self.timerSort = true + self.timerID = tempTimer(self.sortInterval / 1000, function() + self:organize() + end, true) +end + +--- Stops the SortBox from sorting and organizing itself on a timer +function SortBox:disableTimer() + killTimer(self.timerID) + self.timerID = nil + self.timerSort = false +end + +--- Sets the sortInterval, or amount of time in milliseconds between auto sorting on a timer if timerSort is true +-- @tparam number sortInterval time in milliseconds between auto sorting if timerSort is true +function SortBox:setSortInterval(sortInterval) + local sitype = type(sortInterval) + assert(sitype == "number", string.format("SortBox:setSortInterval(sortInterval): sortInterval as number expected, got %s", sitype)) + assert(sortInterval > 0, string.format("SortBox:setSortInterval(sortInterval): sortInterval must be positive")) + self.sortInterval = sortInterval + if self.timerSort then + self:enableTimer() + end +end + +--- Enables sorting when items are added/removed, or if timerSort is true, every sortInterval milliseconds +function SortBox:enableSort() + self.autoSort = true + self:organize() +end + +--- Disables sorting when items are added or removed +function SortBox:disableSort() + self.autoSort = false +end + +---Set whether the SortBox acts as a VBox or HBox. +-- @tparam string boxType If you pass 'h' or 'horizontal' it will act like an HBox. Anything else it will act like a VBox. +-- @usage mySortBox:setBoxType("v") -- behave like a VBox +-- mySortBox:setBoxType("h") -- behave like an HBox +-- mySortBox:setBoxType("beeblebrox") -- why?! Why would you do this? It'll behave like a VBox +function SortBox:setBoxType(boxType) + boxType = boxType:lower() + if boxType == "h" or boxType == "horizontal" then + self.boxType = "h" + else + self.boxType = "v" + end +end + +---Sets the type of sorting in use by this SortBox. +--
If an item in the box does not have the appropriate property or function, then 999999999 is used for sorting except as otherwise noted. +--
If an invalid option is given, then existing H/VBox behaviour is maintained, just like if autoSort is false. +-- @usage mySortBox:setSortFunction("gaugeValue") +-- @tparam string functionName what type of sorting should we use? See table below for valid options and their descriptions. +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
sort typedescription
gaugeValuesort gauges based on how full the gauge is, from less full to more
reverseGaugeValuesort gauges based on how full the gauge is, from more full to less
timeLeftsort TimerGauges based on the total time left in the gauge, from less time to more
reverseTimeLeftsort TimerGauges based on the total time left in the gauge, from more time to less
namesort any item (and mixed types) by name, alphabetically.
reverseNamesort any item (and mixed types) by name, reverse alphabetically.
messagesorts Labels based on their echoed message, alphabetically. If not a label, the empty string will be used
reverseMessagesorts Labels based on their echoed message, reverse alphabetically. If not a label, the empty string will be used
+ +function SortBox:setSortFunction(functionName) + self.sortFunction = functionName +end + +SortBox.parent = Geyser.Container + +return SortBox diff --git a/src/resources/MDK/spinbox.lua b/src/resources/MDK/spinbox.lua new file mode 100755 index 0000000..784c88c --- /dev/null +++ b/src/resources/MDK/spinbox.lua @@ -0,0 +1,481 @@ +--- A Geyser object to create a spinbox for adjusting a number +-- @classmod spinbox +-- @author Damian Monogue +-- @copyright 2023 +-- @license MIT, see https://raw.githubusercontent.com/demonnic/MDK/main/src/scripts/LICENSE.lua +local spinbox = { + parent = Geyser.Container, + name = 'SpinboxClass', + min = 0, + max = 10, + delta = 1, + value = 0, + activeButtonColor = "gray", + inactiveButtonColor = "DimGray", + integer = true, + upArrowLocation = "https://demonnic.github.io/image-assets/uparrow.png", + downArrowLocation = "https://demonnic.github.io/image-assets/downarrow.png", + color = "#202020" +} +spinbox.__index = spinbox +setmetatable(spinbox, spinbox.parent) + +local gss = Geyser.StyleSheet +local directory = getMudletHomeDir() .. "/spinbox/" +local saveFile = directory .. "fileLocations.lua" +if not io.exists(directory) then + lfs.mkdir(directory) +end + +--- Creates a new spinbox. +-- @tparam table cons a table containing the options for this spinbox. +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
option namedescriptiondefault
minThe minimum value for this spinbox0
maxThe maximum value for this spinbox10
activeButtonColorThe color the up/down buttons should be when they are active/able to be usedgray
inactiveButtonColorThe color the up/down buttons should be when they are inactive/unable to be useddimgray
integerBoolean value. When true, values must always be integers (no decimal place)true
deltaThe amount to change the spinbox's value when the up or down button is pressed.1
upArrowLocationThe location of the up arrow image. Either a web URL where it can be downloaded, or the location on disk to read it fromhttps://demonnic.github.io/image-assets/uparrow.png
downArrowLocationThe location of the down arrow image. Either a web URL where it can be downloaded, or the location on disk to read it fromhttps://demonnic.github.io/image-assets/downarrow.png
callBackThe function to run when the spinbox's value is updated. Is called with parameters (self.name, value, oldValue)nil
+-- @param container The Geyser container for this spinbox +function spinbox:new(cons, container) + cons = cons or {} + local consType = type(cons) + if consType ~= "table" then + printError(f"spinbox:new(cons, container): cons as table of options expected, got {consType}!", true, true) + end + cons.name = cons.name or Geyser.nameGen("spinbox") + local me = self.parent:new(cons, container) + setmetatable(me, self) + me:createComponents() + if me.callBack then + me:setCallBack(me.callBack) + end + me.oldValue = me.value + return me +end + +--- Creates the components that make up the spinbox UI. +-- @local +-- Obtains the up and down arrow images specified in the spinbox options. +-- Generates styles for the spinbox. +-- Calculates the height of the up/down buttons and any remainder space. +-- Creates: +-- `self.upButton` - A button with an up arrow image for incrementing the value +-- `self.downButton` - A button with a down arrow image for decrementing the value +-- `self.displayLabel` - A label to display the current spinbox value +-- `self.input` - A command line input to allow directly entering a value +-- Hides the input by default. +-- Applies the generated styles. +function spinbox:createComponents() + self:obtainImages() + self:generateStyles() + self:calculateButtonDimensions() + + self.upButton = self:createButton("up") + self.downButton = self:createButton("down") + + self.displayLabel = self:createDisplayLabel() + + self.input = self:createInput() + self.input:hide() + + self:applyStyles() +end + +--- Calculates the button height. We use square buttons in this house. +-- @local +-- Calculates the height of the up/down buttons by dividing the spinbox height in half. +-- Stores the remainder (if any) in self.remainder. +-- Stores the calculated button height in self.buttonHeight. +function spinbox:calculateButtonDimensions() + self.buttonHeight = math.floor(self.get_height() / 2) + self.remainder = self.get_height() % 2 +end + +--- Creates a button (up or down arrow) for the spinbox. +-- @param type Either "up" or "down" to specify which direction the arrow should point +-- @return The created Geyser.Label button +-- @local +-- Creates a Geyser.Label button with an up or down arrow image. +-- Positions the button at the top or bottom of the spinbox respectively. +-- Sets a click callback on the button to call increment() or decrement() depending on the type. +-- Returns the created button. +function spinbox:createButton(type) + local button = Geyser.Label:new({ + name = self.name .. "spinbox_"..type.."Arrow", + height = self.buttonHeight, + width = self.buttonHeight, + x = "100%-" .. self.buttonHeight, + y = type == "up" and 0 or self.buttonHeight + self.remainder, + }, self) + + button:setClickCallback(function() + if type == "up" then + self:increment() + else + self:decrement() + end + end) + return button +end + +--- Creates the display label for the spinbox value. +-- @return The created Geyser.Label display label +-- @local +-- Creates a Geyser.Label to display the current spinbox value. +-- Centers the text in the label. +-- Sets a double click callback on the label to show the input, put the current +-- value in it, select the text, and hide the label. +-- Returns the created display label. +function spinbox:createDisplayLabel() + local displayLabel = Geyser.Label:new({ + name = self.name .. "spinbox_displayLabel", + x = 0, + y = 0, + width = "100%-" .. self.buttonHeight, + height = "100%", + message = self.value + }, self) + displayLabel:setAlignment("center") + displayLabel:setDoubleClickCallback(function() + self.input:show() + self.input:print(self.value) + self.input:selectText() + displayLabel:hide() + end) + return displayLabel +end + +--- Creates the input for directly entering a spinbox value. +-- @return The created Geyser.CommandLine input +-- @local +-- Creates a Geyser.CommandLine input. +-- Sets an action on the input to: +-- - Attempt to convert the input text to a number +-- - If successful, call setValue() with the number to set the spinbox value +-- - Hide the input +-- - Show the display label +-- - Put the new spinbox value in the input +-- Returns the created input. +function spinbox:createInput() + local input = Geyser.CommandLine:new({ + x = 0, + y = 0, + width = "100%-".. self.buttonHeight, + height = "100%", + }, self) + input:setAction(function(txt) + txt = tonumber(txt) + if txt then + self:setValue(txt) + input:hide() + end + self.displayLabel:show() + input:print(self.value) + end) + return input +end + +--- Used to increment the value by the delta amount +-- @local +-- Increments the spinbox value by the delta amount. +-- Checks if the new value would exceed the max, and if so sets it to the max. +-- Updates the display label with the new value. +-- Applies any styles that depend on the value. +function spinbox:increment() + local val = self.value + self.delta + if val >= self.max then + val = self.max + end + self.oldValue = self.value + self.value = val + self.displayLabel:echo(val) + self:applyStyles() + self:handleCallBacks() +end + +--- Used to decrement the value by the delta amount +-- @local +-- Decrements the spinbox value by the delta amount. +-- Checks if the new value would be below the min, and if so sets it to the min. +-- Updates the display label with the new value. +-- Applies any styles that depend on the value. +function spinbox:decrement() + local val = self.value - self.delta + if val <= self.min then + val = self.min + end + self.oldValue = self.value + self.value = val + self.displayLabel:echo(val) + self:applyStyles() + self:handleCallBacks() +end + +--- Used to directly set the value of the spinbox. +-- @param value The new value to set +-- Rounds the value to an integer if the spinbox is integer only. +-- Checks if the new value is within the min/max range and clamps it if not. +-- Updates the display label with the new value. +-- Applies any styles that depend on the value. +function spinbox:setValue(value) + if self.integer then + value = math.floor(value) + end + if value >= self.max then + value = self.max + elseif value <= self.min then + value = self.min + end + self.oldValue = self.value + self.value = value + self.displayLabel:echo(value) + self:applyStyles() + self:handleCallBacks() +end + +--- Obtains the up and down arrow images for the spinbox. +-- @local +-- Gets the previously saved file locations. +-- Checks if the up arrow image exists at the upArrowLocation. +-- If not, it will download the image from a URL or copy a local file. It saves +-- the new location. +-- Does the same for the down arrow image and downArrowLocation. +-- Saves any new locations to the save file. +-- Sets self.upArrowFile and self.downArrowFile to the locations of the images. +function spinbox:obtainImages() + local locations = self:getFileLocs() + local upURL = self.upArrowLocation + local downURL = self.downArrowLocation + local upFile = locations[upURL] + local downFile = locations[downURL] + local locationsChanged = false + if not (upFile and io.exists(upFile)) then + if not upFile then + upFile = directory .. self.name .. "/uparrow.png" + locations[upURL] = upFile + locationsChanged = true + end + if upURL:match("^http") then + self:downloadFile(upURL, upFile) + elseif io.exists(upURL) then + upFile = upURL + locations[upURL] = upFile + locationsChanged = true + end + end + if not (downFile and io.exists(downFile)) then + if not downFile then + downFile = directory .. self.name .. "/downarrow.png" + locations[downURL] = downFile + locationsChanged = true + end + if downURL:match("^http") then + self:downloadFile(downURL, downFile) + elseif io.exists(downURL) then + downFile = downURL + locations[downURL] = downFile + locationsChanged = true + end + end + self.upArrowFile = upFile + self.downArrowFile = downFile + if locationsChanged then + table.save(saveFile, locations) + end +end + +--- Handles the actual download of a file from a url +-- @param url The url to download the file from +-- @param fileName The location to save the downloaded file +-- @local +-- Creates any missing directories in the file path. +-- Registers named event handlers to handle the download completing or erroring. +-- The completion handler stops the error handler. +-- The error handler prints an error message and stops the completion handler. +-- Downloads the file from the url to the fileName location. +function spinbox:downloadFile(url, fileName) + local parts = fileName:split("/") + parts[#parts] = nil + local dirName = table.concat(parts, "/") .. "/" + if not io.exists(dirName) then + lfs.mkdir(dirName) + end + local uname = "spinbox" + local handlerName = self.name .. url + local handler = function(event, ...) + local args = {...} + local file = #args == 1 and args[1] or args[2] + if file ~= fileName then + return true + end + if event == "sysDownloadDone" then + debugc(f"INFO:Spinbox successfully downloaded {file}") + stopNamedEventHandler(uname, handlerName .. "error") + return false + end + cecho(f"\nERROR:Spinbox had an issue downloading an image file to {file}: {args[1]}\n") + stopNamedEventHandler(uname, handlerName .. "done") + end + registerNamedEventHandler(uname, handlerName .. "done", "sysDownloadDone", handler, true) + registerNamedEventHandler(uname, handlerName .. "error", "sysDownloadError", handler, true) + downloadFile(fileName, url) +end + +--- Responsible for reading the file locations from disk and returning them +-- @local +function spinbox:getFileLocs() + local locations = {} + if io.exists(saveFile) then + table.load(saveFile, locations) + end + return locations +end + +--- (Re)generates the stylesheets for the spinbox +-- Should not need to call but if you change something and it doesn't take effect +-- you can try calling this followed by applyStyles +function spinbox:generateStyles() + self.baseStyle = gss:new([[ + border-radius: 2px; + border-color: black; + ]]) + self.activeStyle = gss:new(f[[ + background-color: {self.activeButtonColor}; + ]], self.baseStyle) + self.inactiveStyle = gss:new(f[[ + background-color: {self.inactiveButtonColor}; + ]], self.baseStyle) + self.upStyle = gss:new(f[[ + border-image: url("{self.upArrowFile}"); + ]]) + self.downStyle = gss:new(f[[ + border-image: url("{self.downArrowFile}"); + ]]) + self.displayStyle = gss:new(f[[ + background-color: {Geyser.Color.hex(self.color)}; + text-align: center; + ]], self.baseStyle) +end + +--- Applies updated stylesheets to the components of the spinbox +-- Should not need to call this directly +function spinbox:applyStyles() + if self.value >= self.max then + self.upStyle:setParent(self.inactiveStyle) + else + self.upStyle:setParent(self.activeStyle) + end + if self.value <= self.min then + self.downStyle:setParent(self.inactiveStyle) + else + self.downStyle:setParent(self.activeStyle) + end + self.upButton:setStyleSheet(self.upStyle:getCSS()) + self.downButton:setStyleSheet(self.downStyle:getCSS()) + self.displayLabel:setStyleSheet(self.displayStyle:getCSS()) +end + +--- sets the color for active buttons on the spinbox +-- @param color any valid color formatting string, such a "red" or "#880000" or "<128,0,0>" or a table of colors, like {128, 0,0}. See Geyser.Color.parse at https://www.mudlet.org/geyser/files/geyser/GeyserColor.html#Geyser.Color.parse +function spinbox:setActiveButtonColor(color) + local colorType = type(color) + local hex + if colorType == "table" then + hex = Geyser.Color.hex(unpack(color)) + else + hex = Geyser.Color.hex(color) + end + self.activeButtonColor = hex + self.activeStyle:set("background-color", hex) + self:applyStyles() +end + +--- sets the color for inactive buttons on the spinbox +-- @param color any valid color formatting string, such a "" or "red" or "<128,0,0>" or a table of colors, like {128, 0,0}. See Geyser.Color.parse at https://www.mudlet.org/geyser/files/geyser/GeyserColor.html#Geyser.Color.parse +function spinbox:setInactiveButtonColor(color) + local colorType = type(color) + local hex + if colorType == "table" then + hex = Geyser.Color.hex(unpack(color)) + else + hex = Geyser.Color.hex(color) + end + self.inactiveButtonColor = hex + self.inactiveStyle:set("background-color", hex) + self:applyStyles() +end + +-- internal function that handles calling a registered callback and raising an event any time the +-- spinbox value is changed, whether using the buttons or the :set function. +function spinbox:handleCallBacks() + raiseEvent("spinbox updated", self.name, self.value, self.oldValue) + if self.callBack then + local ok, err = pcall(self.callBack, self.name, self.value, self.oldValue) + if not ok then + printError(f"Had an issue running the callback handler for spinbox named {self.name}: {err}", true, true) + end + end +end + +--- Set a callback function for the spinbox to call any time the value of the spinbox is changed +-- the function will be called as func(self.value, self.name) +function spinbox:setCallBack(func) + local funcType = type(func) + if funcType ~= "function" then + printError(f"spinbox:setCallBack(func): func as function required, got {funcType}", true, true) + end + self.callBack = func + return true +end + +return spinbox \ No newline at end of file diff --git a/src/resources/MDK/sug.lua b/src/resources/MDK/sug.lua new file mode 100755 index 0000000..ea99c90 --- /dev/null +++ b/src/resources/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, +} + +-- Internal function, used to turn a string variable name into a value +local function getValueAt(accessString) + local ok, err = pcall(loadstring("return " .. tostring(accessString))) + if ok then return err end + return nil, err +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""
updateHookA function which is run each time the gauge updates. Should take 3 arguments, the gauge itself, current value, and max value. You can return new current and max values to be used, for example `return 34, 120` would cause the gauge to use 34 for current and 120 for max regardless of what the variables it reads say.
+-- @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 + +--- Set the updateHook function which is run just prior to the gauge updating +-- @tparam function func The function which will be called when the gauge updates. It should take 3 arguments, the gauge itself, the current value, and the max value. If you wish to override the current or max values used for the gauge, you can return new current and max values, like `return newCurrent newMax` +function SUG:setUpdateHook(func) + local funcType = type(func) + if funcType ~= "function" then + return nil, "setUpdateHook only takes functions, no strings or anything like that. You passed in: " .. funcType + end + self.updateHook = func +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 + if self.updateHook and type(self.updateHook) == "function" then + local ok, newcur, newmax = pcall(self.updateHook, self, current, max) + if ok and newcur then + current = newcur + max = newmax and newmax or self.defaultMax + 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 diff --git a/src/resources/MDK/textgauge.lua b/src/resources/MDK/textgauge.lua new file mode 100755 index 0000000..05c34c3 --- /dev/null +++ b/src/resources/MDK/textgauge.lua @@ -0,0 +1,335 @@ +--- Creates a text based gauge, for use in miniconsoles and the like. +-- @classmod TextGauge +-- @author Damian Monogue +-- @copyright 2020 Damian Monogue +-- @copyright 2021 Damian Monogue +-- @license MIT, see LICENSE.lua +local TextGauge = {width = 24, fillCharacter = ":", emptyCharacter = "-", showPercent = true, showPercentSymbol = true, format = "c", value = 50} + +--- Creates a new TextGauge. +-- @tparam[opt] table options The table of options you would like the TextGauge to start with. +--

Table of new options +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
option namedescriptiondefault
widthHow many characters wide to make the gauge24
fillCharacterWhat character to use for the 'full' part of the gauge:
overflowCharacterWhat character to use for >100% part of the gaugeif not set, it uses whatever you set fillCharacter to
emptyCharacterWhat character to use for the 'empty' part of the gauge-
showPercentSymbolShould we show the % sign itself?true
showPercentShould we show what % of the gauge is filled?true
valueHow much of the gauge should be filled50
formatWhat type of color formatting to use? 'c' for cecho, 'd' for decho, 'h' for hechoc
fillColorWhat color to make the full part of the bar?"DarkOrange" or equivalent for your format type
emptyColorwhat color to use for the empty part of the bar?"white" or format appropriate equivalent
percentColorWhat color to print the percentage numvers in, if shown?"white" or fortmat appropriate equivalent
percentSymbolColorWhat color to make the % if shown?If not set, uses what percentColor is set to.
overflowColorWhat color to make the >100% portion of the bar?If not set, will use the same color as fillColor
+-- @usage +-- local TextGauge = require("MDK.textgauge") +-- myTextGauge = TextGauge:new() +-- gaugeText = myTextGauge:setValue(382, 830) +function TextGauge:new(options) + options = options or {} + local optionsType = type(options) + assert(optionsType == "table" or optionsType == "nil", "TextGauge:new(options): options expected as table, got " .. optionsType) + local me = table.deepcopy(options) + setmetatable(me, self) + self.__index = self + me:setDefaultColors() + return me +end + +--- Sets the width in characters of the gauge +-- @tparam number width number of characters wide to make the gauge +function TextGauge:setWidth(width) + local widthType = type(width) + assert(widthType == "number", string.format("TextGauge:setWidth(width): width as number expected, got %s", widthType)) + self.width = width +end + +function TextGauge:setFormat(format) + self.format = self:getColorType(format) + self:setDefaultColors() +end + +--- Sets the character to use for the 'full' part of the gauge +-- @tparam string character the character to use. +function TextGauge:setFillCharacter(character) + assert(character ~= nil, "TextGauge:setFillCharacter(character): character required, got nil") + assert(utf8.len(character) == 1, "TextGauge:setFillCharacter(character): character must be a single character") + self.fillCharacter = character +end + +--- Sets the character to use for the 'overflow' (>100%) part of the gauge +-- @tparam string character the character to use. +function TextGauge:setOverflowCharacter(character) + assert(character ~= nil, "TextGauge:setOverflowCharacter(character): character required, got nil") + assert(utf8.len(character) == 1, "TextGauge:setOverflowCharacter(character): character must be a single character") + self.overflowCharacter = character +end + +--- Sets the character to use for the 'full' part of the gauge +-- @tparam string character the character to use. +function TextGauge:setEmptyCharacter(character) + assert(character ~= nil, "TextGauge:setEmptyCharacter(character): character required, got nil") + assert(utf8.len(character) == 1, "TextGauge:setEmptyCharacter(character): character must be a single character") + self.emptyCharacter = character +end + +--- Sets the fill color for the gauge. +-- @tparam string color the color to use for the full portion of the gauge. Will be run through Geyser.Golor +function TextGauge:setFillColor(color) + assert(color ~= nil, "TextGauge:setFillColor(color): color required, got nil") + self.fillColor = color +end + +--- Sets the overflow color for the gauge. +-- @tparam string color the color to use for the full portion of the gauge. Will be run through Geyser.Golor +function TextGauge:setOverflowColor(color) + assert(color ~= nil, "TextGauge:setOverflowColor(color): color required, got nil") + self.overflowColor = color +end + +--- Sets the empty color for the gauge. +-- @tparam string color the color to use for the empty portion of the gauge. Will be run through Geyser.Golor +function TextGauge:setEmptyColor(color) + assert(color ~= nil, "TextGauge:setEmptyColor(color): color required, got nil") + self.emptyColor = color +end + +--- Sets the fill color for the gauge. +-- @tparam string color the color to use for the numeric value. Will be run through Geyser.Golor +function TextGauge:setPercentColor(color) + assert(color ~= nil, "TextGauge:setPercentColor(color): color required, got nil") + self.percentColor = color +end +--- Sets the fill color for the gauge. +-- @tparam string color the color to use for the numeric value. Will be run through Geyser.Golor +function TextGauge:setPercentSymbolColor(color) + assert(color ~= nil, "TextGauge:setPercentSymbolColor(color): color required, got nil") + self.percentSymbolColor = color +end + +--- Enables reversing the fill direction (right to left instead of the usual left to right) +function TextGauge:enableReverse() + self.reverse = true +end + +--- Disables reversing the fill direction (go back to the usual left to right) +function TextGauge:disableReverse() + self.reverse = false +end + +--- Enables showing the percent value of the gauge +function TextGauge:enableShowPercent() + self.showPercent = true +end + +--- Disables showing the percent value of the gauge +function TextGauge:disableShowPercent() + self.showPercent = false +end + +--- Enables showing the percent symbol (appears after the value) +function TextGauge:enableShowPercentSymbol() + self.showPercentSymbol = true +end + +--- Enables showing the percent symbol (appears after the value) +function TextGauge:disableShowPercentSymbol() + self.showPercentSymbol = false +end + +function TextGauge:getColorType(format) + format = format or self.format + local dec = {"d", "decimal", "dec", "decho"} + local hex = {"h", "hexidecimal", "hex", "hecho"} + local col = {"c", "color", "colour", "col", "name", "cecho"} + if table.contains(col, format) then + return "c" + elseif table.contains(dec, format) then + return "d" + elseif table.contains(hex, format) then + return "h" + else + return "" + end +end + +-- internal function, used at instantiation to ensure some colors are set +function TextGauge:setDefaultColors() + local colorType = self:getColorType() + if colorType == "c" then + self.percentColor = self.percentColor or "white" + self.percentSymbolColor = self.percentSymbolColor or self.percentColor + self.fillColor = self.fillColor or "DarkOrange" + self.emptyColor = self.emptyColor or "white" + self.resetColor = "" + elseif colorType == "d" then + self.percentColor = self.percentColor or "<255,255,255>" + self.percentSymbolColor = self.percentSymbolColor or self.percentColor + self.fillColor = self.fillColor or "<255,140,0>" + self.emptyColor = self.emptyColor or "<255,255,255>" + self.resetColor = "" + elseif colorType == "h" then + self.percentColor = self.percentColor or "#ffffff" + self.percentSymbolColor = self.percentSymbolColor or self.percentColor + self.fillColor = self.fillColor or "#ff8c00" + self.emptyColor = self.emptyColor or "#ffffff" + self.resetColor = "#r" + else + self.percentColor = self.percentColor or "" + self.percentSymbolColor = self.percentSymbolColor or self.percentColor + self.fillColor = self.fillColor or "" + self.emptyColor = self.emptyColor or "" + self.resetColor = "" + end + self.overflowColor = self.overflowColor or self.fillColor +end + +-- Internal function used to route Geyser.Color based on internally stored format +function TextGauge:getColor(color) + local colorType = self:getColorType() + if colorType == "c" then + return string.format("<%s>", color) -- pass the color back in <> for cecho + elseif colorType == "d" then + return Geyser.Color.hdec(color) -- return it in decho format + elseif colorType == "h" then + return Geyser.Color.hex(color) -- return it in hex format + else + return "" -- return an empty string for noncolored output + end +end + +--- Used to set the gauge's value and return the string representation of the gauge +-- @tparam[opt] number current current value. If no value is passed it will use the stored value. Defaults to 50 to prevent errors. +-- @tparam[opt] number max maximum value. If not passed, the internally stored one will be used. Defaults to 100 so that it can be used with single values as a percent +-- @usage myGauge:setValue(55) -- sets the gauge to 55% full +-- @usage myGauge:setValue(2345, 2780) -- will figure out what the percentage fill is based on the given current/max values +function TextGauge:setValue(current, max) + current = current or self.value + assert(type(current) == "number", "TextGauge:setValue(current,max) current as number expected, got " .. type(current)) + assert(max == nil or type(max) == "number", "TextGauge:setValue(current, max) option max as number expected, got " .. type(max)) + if current < 0 then + current = 0 + end + max = max or 100 + local value = math.floor(current / max * 100) + self.value = value + local width = self.width + local percentString = "" + local percentSymbolString = "" + local fillCharacter = self.fillCharacter + local overflowCharacter = self.overflowCharacter or fillCharacter + local emptyCharacter = self.emptyCharacter + local fillColor = self:getColor(self.fillColor) + local overflowColor = self:getColor(self.overflowColor) + local emptyColor = self:getColor(self.emptyColor) + local percentColor = self:getColor(self.percentColor) + local percentSymbolColor = self:getColor(self.percentSymbolColor) + local resetColor = self.resetColor + if self.showPercent then + percentString = string.format("%s%02d%s", percentColor, value, resetColor) + width = width - 2 + end + if self.showPercentSymbol then + percentSymbolString = string.format("%s%s%s", percentSymbolColor, "%", resetColor) + width = width - 1 + end + local perc = value / 100 + local overflow = perc - 1 + if overflow < 0 then + overflow = 0 + end + if overflow > 1 then + perc = 2 + overflow = 1 + end + local overflowWidth = math.floor(overflow * width) + local fillWidth = math.floor((perc - overflow) * width) + local emptyWidth = width - fillWidth + fillWidth = fillWidth - overflowWidth + if value >= 100 and self.showPercent then + fillWidth = fillWidth - 1 + end + if value >= 200 and self.showPercent then + overflowWidth = overflowWidth - 1 + end + local result = "" + if self.reverse then + result = string.format("%s%s%s%s%s%s%s%s%s%s%s", emptyColor, string.rep(emptyCharacter, emptyWidth), resetColor,fillColor, string.rep(fillCharacter, fillWidth), resetColor, overflowColor, string.rep(overflowCharacter, overflowWidth), resetColor, percentString, percentSymbolString, resetColor) + else + result = string.format("%s%s%s%s%s%s%s%s%s%s%s", overflowColor, string.rep(overflowCharacter, overflowWidth), fillColor, + string.rep(fillCharacter, fillWidth), resetColor, emptyColor, string.rep(emptyCharacter, emptyWidth), resetColor, + percentString, percentSymbolString, resetColor) + end + return result +end + +--- Synonym for setValue +function TextGauge:print(...) + self:setValue(...) +end + +return TextGauge diff --git a/src/resources/MDK/timergauge.lua b/src/resources/MDK/timergauge.lua new file mode 100755 index 0000000..c25e307 --- /dev/null +++ b/src/resources/MDK/timergauge.lua @@ -0,0 +1,529 @@ +--- Animated countdown timer, extends Geyser.Gauge +-- @classmod TimerGauge +-- @author Damian Monogue +-- @copyright 2020 Damian Monogue +-- @license MIT, see LICENSE.lua +local TimerGauge = { + name = "TimerGaugeClass", + active = true, + showTime = true, + prefix = "", + timeFormat = "S.t", + suffix = "", + updateTime = "10", + autoHide = true, + autoShow = true, + manageContainer = false, +} + +function TimerGauge:setStyleSheet(cssFront, cssBack, cssText) + cssFront = cssFront or self.cssFront + cssBack = cssBack or self.cssBack + cssBack = cssBack or self.cssFront .. "background-color: black;" + cssText = cssText or self.cssText + if cssFront then + self.front:setStyleSheet(cssFront) + end + if cssBack then + self.back:setStyleSheet(cssBack) + end + if cssText then + self.text:setStyleSheet(cssText) + end + -- self.gauge:setStyleSheet(cssFront, cssBack, cssText) + self.cssFront = cssFront + self.cssBack = cssBack + self.cssText = cssText +end + +--- Shows the TimerGauge. If the manageContainer property is true, then will add it back to its container +function TimerGauge:show2() + if self.manageContainer and self.savedContainer then + self.savedContainer:add(self) + self.savedContainer = nil + end + self:show() +end + +--- Hides the TimerGauge. If manageContainer property is true, then it will remove it from its container and if the container is an HBox or VBox it will initiate size/position management +function TimerGauge:hide2() + if self.manageContainer and self.container.name ~= Geyser.name then + self.savedContainer = self.container + Geyser:add(self) + self.savedContainer:remove(self) + if self.savedContainer.type == "VBox" or self.savedContainer.type == "HBox" then + if self.savedContainer.organize then + self.savedContainer:organize() + else + self.savedContainer:reposition() + end + end + end + self:hide() +end + +--- Starts the timergauge. Works whether the timer is stopped or not. Does not start a timer which is already at 0 +-- @tparam[opt] boolean show override the autoShow property. True will always show, false will never show. +-- @usage myTimerGauge:start() --starts the timer, will show or not based on autoShow property +-- myTimerGauge:start(false) --starts the timer, will not change hidden status, regardless of autoShow property +-- myTimerGauge:start(true) --starts the timer, will show it regardless of autoShow property +function TimerGauge:start(show) + if show == nil then + show = self.autoShow + end + self.active = true + if self.timer then + killTimer(self.timer) + self.timer = nil + end + startStopWatch(self.stopWatchName) + self:update() + self.timer = tempTimer(self.updateTime / 1000, function() + self:update() + end, true) + if show then + self:show2() + end +end + +--- Stops the timergauge. Works whether the timer is started or not. +-- @tparam[opt] boolean hide override the autoHide property. True will always hide, false will never hide. +-- @usage myTimerGauge:stop() --stops the timer, will hide or not based on autoHide property +-- myTimerGauge:stop(false) --stops the timer, will not change hidden status, regardless of autoHide property +-- myTimerGauge:stop(true) --stops the timer, will hide it regardless of autoHide property +function TimerGauge:stop(hide) + if hide == nil then + hide = self.autoHide + end + self.active = false + if self.timer then + killTimer(self.timer) + self.timer = nil + end + stopStopWatch(self.stopWatchName) + if hide then + self:hide2() + end +end + +--- Alias for stop. +-- @tparam[opt] boolean hide override the autoHide property. True will always hide, false will never hide. +function TimerGauge:pause(hide) + self:stop(hide) +end + +--- Resets the time on the timergauge to its original value. Does not alter the running state of the timer +function TimerGauge:reset() + resetStopWatch(self.stopWatchName) + adjustStopWatch(self.stopWatchName, self.time * -1) + self:update() +end + +--- Resets and starts the timergauge. +-- @tparam[opt] boolean show override the autoShow property. true will always show, false will never show +-- @usage myTimerGauge:restart() --restarts the timer, will show or not based on autoShow property +-- myTimerGauge:restart(false) --restarts the timer, will not change hidden status, regardless of autoShow property +-- myTimerGauge:restart(true) --restarts the timer, will show it regardless of autoShow property +function TimerGauge:restart(show) + self:reset() + self:start(show) +end + +--- Get the amount of time remaining on the timer, in seconds +-- @tparam string format Format string for how to return the time. If not provided defaults to self.timeFormat(which defaults to "S.t").
+-- If "" is passed will return "" as the time. See below table for formatting codes
+-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
format codewhat it is replaced with
STime left in seconds, unbroken down. Does not include milliseconds.
+-- IE a timer with 2 minutes left it would replace S with 120 +--
ddDays, with 1 leading 0 (0, 01, 02-...)
dDays, with no leading 0 (1,2,3-...)
hhhours, with leading 0 (00-24)
hhours, without leading 0 (0-24)
MMminutes, with a leading 0 (00-59)
Mminutes, no leading 0 (0-59)
ssseconds, with leading 0 (00-59)
sseconds, no leading 0 (0-59)
ttenths of a second
+-- timer with 12.345 seconds left, t would
+-- br replaced by 3. +--
mmmilliseconds with leadings 0s (000-999)
mmilliseconds with no leading 0s (0-999)

+-- @usage myTimerGauge:getTime() --returns the time using myTimerGauge.format +-- myTimerGauge:getTime("hh:MM:ss") --returns the time as hours, minutes, and seconds, with leading 0s (01:23:04) +-- myTimerGauge:getTime("S.mm") --returns the time as the total number of seconds, including milliseconds (114.004) +function TimerGauge:getTime(format) + format = format or self.timeFormat + local time = getStopWatchTime(self.stopWatchName) + local timerTable = getStopWatchBrokenDownTime(self.stopWatchName) + if time > 0 then + self:stop(self.autoHide) + resetStopWatch(self.stopWatchName) + time = getStopWatchTime(self.stopWatchName) + timerTable = getStopWatchBrokenDownTime(self.stopWatchName) + self.active = false + end + if format == "" then + return format + end + local totalSeconds = string.split(math.abs(time), "%.")[1] + local tenths = string.sub(string.format("%03d", timerTable.milliSeconds), 1, 1) + format = format:gsub("S", totalSeconds) + format = format:gsub("t", tenths) + format = format:gsub("mm", string.format("%03d", timerTable.milliSeconds)) + format = format:gsub("m", timerTable.milliSeconds) + format = format:gsub("MM", string.format("%02d", timerTable.minutes)) + format = format:gsub("M", timerTable.minutes) + format = format:gsub("dd", string.format("%02d", timerTable.days)) + format = format:gsub("d", timerTable.days) + format = format:gsub("ss", string.format("%02d", timerTable.seconds)) + format = format:gsub("s", timerTable.seconds) + format = format:gsub("hh", string.format("%02d", timerTable.hours)) + format = format:gsub("h", timerTable.hours) + return format +end + +-- Execute the timer's hook, if there is one. Internal function +function TimerGauge:executeHook() + local hook = self.hook + if not hook then + return + end + local hooktype = type(hook) + if hooktype == "string" then + local f, e = loadstring("return " .. hook) + if not f then + f, e = loadstring(hook) + end + if not f then + debugc(string.format("TimerGauge encountered an error while executing the hook for TimerGauge with name: %s error: %s", self.name, tostring(e))) + return + end + hook = f + end + hooktype = type(hook) + if hooktype ~= "function" then + debugc(string.format( + "TimerGauge with name: %s was given a hook which is neither a function nor a string which can be made into one. Provided type was %s", + self.name, hooktype)) + return + end + local worked, err = pcall(hook) + if not worked then + debugc(string.format("TimerGauge named %s encountered the following error while executing its hook: %s", self.name, err)) + end +end + +--- Sets the timer's remaining time to 0, stops it, and executes the hook if one exists. +-- @tparam[opt] boolean skipHook use true to have it set the timer to 0 and stop, but not execute the hook. +-- @usage myTimerGauge:finish() --executes the hook if it has one +-- myTimerGauge:finish(false) --will not execute the hook +function TimerGauge:finish(skipHook) + resetStopWatch(self.stopWatchName) + self:update(skipHook) +end + +-- Internal function, no ldoc +-- Updates the gauge based on time remaining. +-- @tparam[opt] boolean skipHook use true if you do not want to execute the hook if the timer is at 0. +function TimerGauge:update(skipHook) + local time = self.showTime and self:getTime(self.timeFormat) or "" + local current = tonumber(self:getTime("S.mm")) + local suffix = self.suffix or "" + local prefix = self.prefix or "" + local text = string.format("%s%s%s", prefix, time, suffix) + self:setValue(current, self.time, text) + if current == 0 then + if self.timer then + killTimer(self.timer) + self.timer = nil + end + if not skipHook then + self:executeHook() + end + end +end + +--- Sets the amount of time the timer will run for. Make sure to call :reset() or :restart() +-- if you want to cause the timer to run for that amount of time. If you set it to a time lower +-- than the time left on the timer currently, it will reset the current time, otherwise it is left alone +-- @tparam number time how long in seconds the timer should run for +-- @usage myTimerGauge:setTime(50) -- sets myTimerGauge's max time to 50. +function TimerGauge:setTime(time) + local timetype = type(time) + if timetype ~= "number" then + local err = string.format("TimerGauge:setTime(time): time as number expected, got %s", timetype) + debugc(err) + return nil, err + end + time = math.abs(time) + if time == 0 then + local err = "TimerGauge:setTime(time): you cannot pass in 0 as the max time for the timer" + debugc(err) + return nil, err + end + local currentTime = tonumber(self:getTime("S.t")) + self.time = time + if time < currentTime then + self:reset() + else + self:update(currentTime == 0) + end +end + +--- Changes the time between gauge updates. +-- @tparam number updateTime amount of time in milliseconds between gauge updates. Must be a positive number. +function TimerGauge:setUpdateTime(updateTime) + local updateTimeType = type(updateTime) + assert(updateTimeType == "number", + string.format("TimerGauge:setUpdateTime(updateTime): name: %s updateTime as number expected, got %s", self.name, updateTimeType)) + assert(updateTime > 0, + string.format("TimerGauge:setUpdateTime(updateTime): name: %s updateTime must be a positive number. You gave %d", self.name, updateTime)) + self.updateTime = updateTime + if self.timer then + killTimer(self.timer) + self.timer = nil + end + if self.active then + self.timer = tempTimer(updateTime / 1000, function() + self:update() + end, true) + end +end + +TimerGauge.parent = Geyser.Gauge +setmetatable(TimerGauge, Geyser.Gauge) +--- Creates a new TimerGauge instance. +-- @tparam table cons a table of options (or constraints) for how the TimerGauge will behave. Valid options include: +--
+-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
namedescriptiondefault
timehow long the timer should run for
activewhether the timer should run or nottrue
showTimeshould we show the time remaining on the gauge?true
prefixtext you want shown before the time.""
suffixtext you want shown after the time.""
timerCaptionAlias for suffix. Deprecated and may be remove in the future +--
updateTimenumber of milliseconds between gauge updates.10
autoHideshould the timer :hide() itself when it runs out/you stop it?true
autoShowshould the timer :show() itself when you start it?true
manageContainershould the timer remove itself from its container when you call
:hide() and add itself back when you call :show()?
false
timeFormathow should the time be displayed/returned if you call :getTime()?
See table below for more information
"S.t"
+--
Table of time format options +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
format codewhat it is replaced with
STime left in seconds, unbroken down. Does not include milliseconds.
+-- IE a timer with 2 minutes left it would replace S with 120 +--
ddDays, with 1 leading 0 (0, 01, 02-...)
dDays, with no leading 0 (1,2,3-...)
hhhours, with leading 0 (00-24)
hhours, without leading 0 (0-24)
MMminutes, with a leading 0 (00-59)
Mminutes, no leading 0 (0-59)
ssseconds, with leading 0 (00-59)
sseconds, no leading 0 (0-59)
ttenths of a second
+-- timer with 12.345 seconds left, t would
+-- br replaced by 3. +--
mmmilliseconds with leadings 0s (000-999)
mmilliseconds with no leading 0s (0-999)

+-- @param parent The Geyser parent for this TimerGauge +-- @usage +-- local TimerGauge = require("MDK.timergauge") +-- myTimerGauge = TimerGauge:new({ +-- name = "testGauge", +-- x = 100, +-- y = 100, +-- height = 40, +-- width = 200, +-- time = 10 +-- }) +function TimerGauge:new(cons, parent) + -- type checking and error handling + local consType = type(cons) + if consType ~= "table" then + local err = string.format("TimerGauge:new(options, parent): options must be provided as a table, received: %s", consType) + debugc(err) + return nil, err + end + local timetype = type(cons.time) + local time = tonumber(cons.time) + if not time then + local err = string.format( + "TimerGauge:new(options, parent): options table must include a time entry, which must be a number. We received: %s which is type: %s", + cons.time or tostring(cons.time), timetype) + debugc(err) + return nil, err + end + cons.time = math.abs(time) + if cons.time == 0 then + local err = "TimerGauge:new(options, parent): time entry in options table must be non-0" + debugc(err) + return nil, err + end + + if cons.timerCaption and (not cons.suffix) then + cons.suffix = cons.timerCaption + end + cons.type = cons.type or "timergauge" + -- call parent constructor + local me = self.parent:new(cons, parent) + -- add TimerGauge as the metatable/index + 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 + + -- create and reset the driving stopwatch + me.stopWatchName = me.name .. "_timergauge" + createStopWatch(me.stopWatchName) + me:reset() + + -- start it up? + if me.active then + me:start() + end + me:update() + return me +end + +return TimerGauge diff --git a/src/scripts/autostudy/autostudy.lua b/src/scripts/autostudy/autostudy.lua new file mode 100644 index 0000000..de9859e --- /dev/null +++ b/src/scripts/autostudy/autostudy.lua @@ -0,0 +1,33 @@ +function autoStudyStartup() + cecho("\nAutostudy loaded. 'studyhelp' for more info.") +end + +function autostudy.checkNextMove(index) + if studyIndex <= 4 then + return "w" + elseif studyIndex == 5 then + return "s" + elseif studyIndex <= 7 then + return "se" + elseif studyIndex == 8 then + return "e" + elseif studyIndex == 9 then + return "n" + elseif studyIndex == 10 then + return "ne" + elseif studyIndex == 11 then + return "se" + elseif studyIndex == 12 then + return "s" + elseif studyIndex == 13 then + return "e" + elseif studyIndex <= 15 then + return "ne" + elseif studyIndex == 16 then + return "n" + else + return "w" + end +end + +registerAnonymousEventHandler("sysLoadEvent", "autoStudyStartup") \ No newline at end of file diff --git a/src/scripts/autostudy/scripts.json b/src/scripts/autostudy/scripts.json new file mode 100644 index 0000000..822e2f6 --- /dev/null +++ b/src/scripts/autostudy/scripts.json @@ -0,0 +1,5 @@ +[ + { + "name": "autostudy" + } +] \ No newline at end of file diff --git a/src/triggers/autoresearch/triggers.json b/src/triggers/autoresearch/triggers.json index 2dc72ae..61f4182 100644 --- a/src/triggers/autoresearch/triggers.json +++ b/src/triggers/autoresearch/triggers.json @@ -33,8 +33,8 @@ "name": "autoresearch.grabSkills.featsLine", "patterns": [ { - "pattern": "^-+ Feats -+$", - "type": "regex" + "pattern": "To see a shorter practice list, type PRACTICE .", + "type": "startOfLine" } ], "script": "lotj.autoResearch.startOnPracticeEnd = true; disableTrigger(\"autoresearch.grabSkills\")" diff --git a/src/triggers/autostudy/study.botStart.lua b/src/triggers/autostudy/study.botStart.lua new file mode 100644 index 0000000..4b773e2 --- /dev/null +++ b/src/triggers/autostudy/study.botStart.lua @@ -0,0 +1,3 @@ +send("afk") +send("bot start") +send("study " .. studyList[studyIndex]) \ No newline at end of file diff --git a/src/triggers/autostudy/study.copyOver.lua b/src/triggers/autostudy/study.copyOver.lua new file mode 100644 index 0000000..e9661c8 --- /dev/null +++ b/src/triggers/autostudy/study.copyOver.lua @@ -0,0 +1 @@ +send("study " .. studyList[studyIndex]) \ No newline at end of file diff --git a/src/triggers/autostudy/study.lua b/src/triggers/autostudy/study.lua new file mode 100644 index 0000000..e9661c8 --- /dev/null +++ b/src/triggers/autostudy/study.lua @@ -0,0 +1 @@ +send("study " .. studyList[studyIndex]) \ No newline at end of file diff --git a/src/triggers/autostudy/study.next.lua b/src/triggers/autostudy/study.next.lua new file mode 100644 index 0000000..bd7454a --- /dev/null +++ b/src/triggers/autostudy/study.next.lua @@ -0,0 +1,4 @@ +studyIndex = studyIndex + 1 + +send(autostudy.checkNextMove(studyIndex)) +send("study " .. studyList[studyIndex]) \ No newline at end of file diff --git a/src/triggers/autostudy/study.nextMove.lua b/src/triggers/autostudy/study.nextMove.lua new file mode 100644 index 0000000..cf98b96 --- /dev/null +++ b/src/triggers/autostudy/study.nextMove.lua @@ -0,0 +1 @@ +send(autostudy.checkNextMove(studyIndex)) \ No newline at end of file diff --git a/src/triggers/autostudy/triggers.json b/src/triggers/autostudy/triggers.json new file mode 100644 index 0000000..acf81c8 --- /dev/null +++ b/src/triggers/autostudy/triggers.json @@ -0,0 +1,56 @@ +[ + { + "name": "study", + "isActive": "yes", + "patterns": [ + { + "pattern": "You study it for some time,", + "type": "startOfLine" + }, + { + "pattern": "After some time studying", + "type": "startOfLine" + } + ] + }, + { + "name": "study.next", + "isActive": "yes", + "patterns": [ + { + "pattern": "^You are now an adept of (?!study)", + "type": "regex" + } + ] + }, + { + "name": "study.botStart", + "isActive": "yes", + "patterns": [ + { + "pattern": "You may now bot again.", + "type": "exactMatch" + } + ] + }, + { + "name": "study.nextMove", + "isActive": "yes", + "patterns": [ + { + "pattern": "You don't see anything like that nearby to study.", + "type": "exactMatch" + } + ] + }, + { + "name": "study.copyOver", + "isActive": "yes", + "patterns": [ + { + "pattern": "Copyover recovery complete.", + "type": "exactMatch" + } + ] + } +] \ No newline at end of file