emco.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;
    }
  ]]
})
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
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
    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
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
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
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
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
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
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)
        self.mc[tabName]:show()
  if oldTab ~= tabName then
    raiseEvent("EMCO tab change", self.name, oldTab, tabName)
  end
end
function EMCO:cycleTab(reverse)
    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
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
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
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
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
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
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
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
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
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
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
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
function EMCO:enableAllCmdLines()
  for _, tabName in ipairs(self.consoles) do
    self:enableCmdLine(tabName, self.cmdActions[tabName])
  end
end
function EMCO:disableAllCmdLines()
  for _, tabName in ipairs(self.consoles) do
    self:disableCmdLine(tabName)
  end
end
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
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
function EMCO:resetCmdAction(tabName)
  self.cmdActions[tabName] = nil
  self.mc[tabName]:resetCmdAction()
end
function EMCO:getCmdLine(tabName)
  return self.mc[tabName]:getCmdLine()
end
function EMCO:printCmd(tabName, txt)
  return self.mc[tabName]:printCmd(txt)
end
function EMCO:clearCmd(tabName)
  return self.mc[tabName]:clearCmd()
end
function EMCO:appendCmd(tabName, txt)
  return self.mc[tabName]:appendCmd(txt)
end
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
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
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
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
function EMCO:setTabFont(font)
  self.tabFont = font
  for _, tab in pairs(self.tabs) do
    tab:setFont(font)
  end
end
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
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
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
function EMCO:setTabFontSize(fontSize)
  self.tabFontSize = fontSize
  for _, tab in pairs(self.tabs) do
    tab:setFontSize(fontSize)
  end
end
function EMCO:setTabAlignment(alignment)
  self.tabAlignment = alignment
  for _, tab in pairs(self.tabs) do
    tab:setAlignment(self.tabAlignment)
  end
end
function EMCO:enableTabUnderline()
  self.tabUnderline = true
  for _, tab in pairs(self.tabs) do
    tab:setUnderline(self.tabUnderline)
  end
end
function EMCO:disableTabUnderline()
  self.tabUnderline = false
  for _, tab in pairs(self.tabs) do
    tab:setUnderline(self.tabUnderline)
  end
end
function EMCO:enableTabItalics()
  self.tabItalics = true
  for _, tab in pairs(self.tabs) do
    tab:setItalics(self.tabItalics)
  end
end
function EMCO:disableTabItalics()
  self.tabItalics = false
  for _, tab in pairs(self.tabs) do
    tab:setItalics(self.tabItalics)
  end
end
function EMCO:enableTabBold()
  self.tabBold = true
  for _, tab in pairs(self.tabs) do
    tab:setBold(self.tabBold)
  end
end
function EMCO:disableTabBold()
  self.tabBold = false
  for _, tab in pairs(self.tabs) do
    tab:setBold(self.tabBold)
  end
end
function EMCO:enableCustomTimestampColor()
  self.customTimestampColor = true
end
function EMCO:disableCustomTimestampColor()
  self.customTimestampColor = false
end
function EMCO:enableTimestamp()
  self.timestamp = true
end
function EMCO:disableTimestamp()
  self.timestamp = false
end
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
function EMCO:setTimestampBGColor(color)
  self.timestampBGColor = color
end
function EMCO:setTimestampFGColor(color)
  self.timestampFGColor = color
end
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
function EMCO:enableAllTab()
  self.allTab = true
end
function EMCO:disableAllTab()
  self.allTab = false
end
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
function EMCO:disableMapTab()
  self.mapTab = false
end
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
function EMCO:enableBlinkFromAll()
  self.enableBlinkFromAll = true
end
function EMCO:disableBlinkFromAll()
  self.enableBlinkFromAll = false
end
function EMCO:enableGag()
  self.gag = true
end
function EMCO:disableGag()
  self.gag = false
end
function EMCO:enableBlink()
  self.blink = true
  if not self.blinkTimerID then
    self.blinkTimerID = tempTimer(self.blinkTime, function()
      self:doBlink()
    end, true)
  end
end
function EMCO:disableBlink()
  self.blink = false
  if self.blinkTimerID then
    killTimer(self.blinkTimerID)
    self.blinkTimerID = nil
  end
end
function EMCO:enablePreserveBackground()
  self.preserveBackground = true
end
function EMCO:disablePreserveBackground()
  self.preserveBackground = false
end
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
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
              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
function EMCO:setInactiveTabCSS(stylesheet)
  self.inactiveTabCSS = stylesheet
  self:adjustTabBackgrounds()
end
function EMCO:setActiveTabCSS(stylesheet)
  self.activeTabCSS = stylesheet
  self:adjustTabBackgrounds()
end
function EMCO:setActiveTabFGColor(color)
  self.activeTabFGColor = color
  self:adjustTabNames()
end
function EMCO:setInactiveTabFGColor(color)
  self.inactiveTabFGColor = color
  self:adjustTabNames()
end
function EMCO:setActiveTabBGColor(color)
  self.activeTabBGColor = color
  self:adjustTabBackgrounds()
end
function EMCO:setInactiveTabBGColor(color)
  self.inactiveTabBGColor = color
  self:adjustTabBackgrounds()
end
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
          else
      self.mc[console]:setColor(self.consoleColor)
    end
  end
end
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
function EMCO:setTabBoxColor(color)
  self.tabBoxColor = color
  self:adjustTabBoxBackground()
end
function EMCO:adjustTabBoxBackground()
  self.tabBoxLabel:setStyleSheet(self.tabBoxCSS)
  self.tabBoxLabel:setColor(self.tabBoxColor)
end
function EMCO:setConsoleContainerColor(color)
  self.consoleContainerColor = color
  self:adjustConsoleContainerBackground()
end
function EMCO:setConsoleContainerCSS(css)
  self.consoleContainerCSS = css
  self:adjustConsoleContainerBackground()
end
function EMCO:adjustConsoleContainerBackground()
  self.consoleContainer:setStyleSheet(self.consoleContainerCSS)
  self.consoleContainer:setColor(self.consoleContainerColor)
end
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
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
function EMCO:enableAutoWrap()
  self.autoWrap = true
  for _, console in ipairs(self.consoles) do
    if self.mapTab and console == self.mapTabName then
          else
      self.mc[console]:enableAutoWrap()
    end
  end
end
function EMCO:disableAutoWrap()
  self.autoWrap = false
  for _, console in ipairs(self.consoles) do
    if self.mapTab and self.mapTabName == console then
          else
      self.mc[console]:disableAutoWrap()
    end
  end
end
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
              else
        self.mc[console]:setWrap(wrapAtNumber)
      end
    end
  end
end
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
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
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
function EMCO:addGag(pattern)
  if self.gags[pattern] then
    return false
  end
  self.gags[pattern] = true
  return true
end
function EMCO:removeGag(pattern)
  if self.gags[pattern] then
    self.gags[pattern] = nil
    return true
  end
  return false
end
function EMCO:matchesGag(str)
  for pattern,_ in pairs(self.gags) do
    if str:match(pattern) then
      return true, pattern
    end
  end
  return false
end
function EMCO:enableNotifyWithFocus()
  self.notifyWithFocus = true
end
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
function EMCO:enableBlankLine()
  self.blankLine = true
end
function EMCO:disableBlankLine()
  self.blankLine = false
end
function EMCO:enableScrollbars()
  self.scrollbars = true
  self:adjustScrollbars()
end
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
          else
      if self.scrollbars then
        self.mc[console]:enableScrollBar()
      else
        self.mc[console]:disableScrollBar()
      end
    end
  end
end
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
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
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
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
function EMCO:enableAllLogging()
  for _,console in pairs(self.mc) do
    console.log = true
  end
  self.logExclusions = {}
end
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