view Devian.lua @ 111:5c591d9b4029 tip

Added tag v8.0.1-20181807-1 for changeset 930922e1ec5b
author Nenue
date Wed, 18 Jul 2018 15:02:50 -0400
parents 7a36f5a92f0a
children
line wrap: on
line source
--- Devian - Devian.lua
-- @file-author@
-- @project-revision@ @project-hash@
-- @file-revision@ @file-hash@

--GLOBALS: Devian, DevianLoadMessage, DEVIAN_WORKSPACE, DEVIAN_PNAME, DEVIAN_PID

SLASH_RL1 = "/rl"
SlashCmdList.RL = function() ReloadUI() end
DEVIAN_WORKSPACE = false
DEVIAN_PNAME = 'Dvn'
DEVIAN_PID = 0

local ADDON, D = ...
local MAJOR, MINOR = 'Devian-2.0', 'r@project-revision@'
local D_INITIALIZED
local next = next
local sub, GetTime, print, _G = string.sub, GetTime, print, _G
local format, setmetatable, getprinthandler, setprinthandler = string.format, setmetatable, getprinthandler, setprinthandler
local tinsert, tremove, rawset = tinsert, tremove, rawset
local currentProfile
local playerName = UnitName("player")
local playerRealm = playerName .. '-' .. GetRealmName()
local num_dock_tabs = 0
local charStates ={}

local channels_report = {}
local registeredTags = {}


DevianCore = {}

function DevianCore:OnLoad ()
  self:RegisterEvent('ADDON_LOADED')
  self:RegisterEvent('PLAYER_LOGIN')

  self:SetShown(true)
end

function DevianCore:OnEvent(event, arg)
  if event == 'ADDON_LOADED' or event == 'PLAYER_LOGIN' then
    --print(event, arg, DevianDB)
    if (arg == 'Devian') and not D_INITIALIZED then
      D_INITIALIZED = true
      self:Initialize()
      self:UnregisterAllEvents()
    end
  end
end

DevianLoadMessage = setmetatable({}, {
  __call = function(t, msg)
    rawset(t, #t+1, msg)
  end,
  __index = function(t)
    return #t
  end
})

--@debug@
D.debugmode = true
--@end-debug@
D.print = function(...)
  if currentProfile and not currentProfile.workspace then
    return nop
  end

  if D.debugmode then
    return print('Dvn', ...)
  else
    return nop
  end
end
local print = D.print

D.L = setmetatable({}, {
  __index= function(t,k)
    return tostring(k)
  end,
  __call = function(t,k,...)
    return format((t[k]) , ...)
  end
})
D.oldprint = getprinthandler()
if not _G.oldprint then _G.oldprint = D.oldprint end

local pairs, tostring, tonumber, ipairs, type = pairs, tostring, tonumber, ipairs, type
local max, rand, format, print = max, math.random, string.format, print
local insert, wipe, concat = table.insert, table.wipe, table.concat
local select, unpack = select, unpack
local GetNumAddOns, GetAddOnInfo, GetAddOnEnableState, EnableAddOn = GetNumAddOns, GetAddOnInfo, GetAddOnEnableState, EnableAddOn
local UnitName, DisableAddOn = UnitName, DisableAddOn

local db, L
local defaults = {
  global = {{}, {}},
  default_channel = {
    signature = 'Main',
    x = 100, y = -200,
    height = 500, width = 600,
    enabled = true},
  current_profile = 1,
  main_profile = 1,
  last_profile = 1,
  profilesName = {},
  profiles = {
  },
  font = [[Interface\Addons\Devian\font\SourceCodePro-Regular.ttf]], -- font info
  fontsize = 13,
  fontoutline = 'NONE',

  headergrad = {'VERTICAL', 0, 0, 0, 1,
                            1, 0.1, 0.1, 1}, -- header info
  headerdrop = {1,1,1,1},
  headerblend = 'BLEND',
  headeralpha = 1,
  headerfontcolor = {1,1,1,1},

  backdrop = {1,1,1,1},                                      -- background frame info
  backgrad = {'VERTICAL', 0, 0, 0, .75, 0,0,0, .65},
  backblend = 'BLEND',
  backalpha = 1,
  backborder = {.5,.5,.5,1},
  backheader = {.25,.25,.25,1},

  frontdrop = {1,1,1,1},                                     -- foreground frame info
  frontgrad = {'VERTICAL', 0, 0, 0, 1, 0,0,0,  0.95},
  frontblend = 'BLEND',
  frontalpha = 1,
  frontborder = {.07,.47,1,1},
  frontheader = {1,1,1,1},
  tagcolor = {},   -- tag color repository
  workspace = 2,   -- current profile
  last_workspace = 2, -- default workspace to alternate with when just "/dvn" is issued

  dock_onshow_fade_time = 2.5,
  dock_onshow_fade_from = 1,
  dock_onshow_fade_to = 0.2,

  dock_alpha_on = 1,
  dock_alpha_off = 0.2,
  dock_fade_in = 0.15,
  dock_fade_out = 0.45,
  dock_button_alpha_on = 1,
  dock_button_alpha_off = 0.2,
  dock_button_fade_in = 0.075,
  dock_button_fade_out = 0.075,

  movement_fade = true,
  movement_fade_time = 0.15,
  movement_fade_from = 1,
  movement_fade_to  = 0,
  movement_translation_x = 25,
  movement_translation_y = 25,
}

D.console = {}
D.max_channel = 0

D.InWorkspace = function ()
  return db.profiles[db.current_profile].workspace
end

local profileTemplate = {
  name = function(id, name) return name end,
  workspace = function(id, name) return (id ~= 1) end,
  current_channel = 1,
  default_channel = 1,
  num_channels = 1,
  max_channel = 1, -- the highest created channel id
  enabled = true, -- allow enabled consoles to appear
  channels = {
    {
      index = 1,
      signature = 'Main',
      x = 100, y = -200,
      height = 500, width = 600,
      enabled = true
    }
  },
  loadouts = {},
  global = {},
  tags = {},
  char = {
    [playerRealm] = {}
  },
  unlisted = {}
}

--- Applies complex template tables
-- If he value is a function, then it will invoke f(...) and use whatever gets returned
function D.DeepCopy(src, dest, ...)

  for k,v in pairs(src) do
    if not dest[k] then
      --oldprint('Rebuilding conf value', k)
      if type(v) == 'table' then
        dest[k] = {}
        D.DeepCopy(v, dest[k], ...)

      else
        if type(v) == 'function' then
          v = v(...)
        end
        dest[k] = v
      end
    end
  end
end

D.FixProfile = function(forced)

  local numChannels = 0
  local minChannel = 400
  local sortedChannels = {}
  local sortedTags = {}
  local maxChannel = 0
  for k,v in pairs(currentProfile.channels) do
    numChannels = numChannels + 1
    maxChannel = max(tonumber(k), maxChannel)
    minChannel = min(tonumber(k), minChannel)
    tinsert(sortedChannels, v)
  end
  if (maxChannel > numChannels) or forced then
    oldprint('fixing channels data')
    table.sort(sortedChannels, function(a,b)
      return (b.index > a.index)
    end)
    for i, info in ipairs(sortedChannels) do

      info.tags = info.tags or {}
      for tag, tagSet in pairs(currentProfile.tags) do
        for _, index in pairs(tagSet) do
          if index == info.index then
            sortedTags[tag] = sortedTags[tag] or {}
            sortedTags[tag][i] = i
            tinsert(info.tags, tag)
          end
        end
      end
      print('Set tags:', table.concat(info.tags, ', '))

      info.index = i
    end
    currentProfile.channels = sortedChannels
    currentProfile.tags = sortedTags
  else
    minChannel = 2
  end
  currentProfile.lastUpdateFix = MINOR
end

D.Profile = function (id, name)

  if name and not id and db.profilesName[name] then
    id = db.profilesName[name]
    print('ID located by name, |cFF00FF00'..name..'|r is |cFFFFFF00'.. id..'|r')
  end

  if not id or not db.profiles[id] then
    if not id then
      id = #db.profiles+1
      print('Generated profile ID: |cFFFFFF00'.. id .. '|r')
    end

    if not name or db.profilesName[name] then
      local newName = name or (id == 1 and 'Main' or 'Profile')
      local prefix = newName
      local i = 2
      while db.profilesName[newName] do
        i = i + 1
        newName = prefix .. i
      end
      name = newName
      print('Generated profile name: |cFF00FF00'..newName..'|r')
    end



    print('Creating profile')
    db.profilesName[name] = id
    db.profiles[id] = {}
  end



  D.currentProfile = db.profiles[id]
  currentProfile = D.currentProfile

  D.DeepCopy(profileTemplate, currentProfile, id, name)


  currentProfile.char[playerRealm] = currentProfile.char[playerRealm] or {}
  if currentProfile.workspace then
    DEVIAN_WORKSPACE = true
    DEVIAN_PNAME = currentProfile.name
    DEVIAN_PID = id
    setprinthandler(D.Message)
  else
    DEVIAN_WORKSPACE = false
    DEVIAN_PNAME = nil
    print = nop
  end
  DEVIAN_PID =id


  -- Attempt to fix bad data
  --@debug@
  MINOR = 70100
  --@end-debug@
  if (currentProfile.lastUpdateFix or 0) < MINOR then
    D.FixProfile(true)
  end


  D.unlisted = currentProfile.unlisted
  D.channels = currentProfile.channels
  D.tags = currentProfile.tags
  D.channelinfo = currentProfile.channels
  D.char = currentProfile.char[playerRealm]
  D.global = currentProfile.global
  D.num_channels = currentProfile.num_channels
  D.enabled = currentProfile.enabled
  D.sig = {}
  D.sigID = {}
  D.IDsig = {}
  D.dock = _G.DevianDock
  D.dock.buttons = D.dock.buttons or {}



  return id, name
end

local targetGlobal, targetChar
D.Command = function (cmd)
  local list_id, scan_func, reload

  local args = {}
  if cmd then
    local i, j = 0, 0
    repeat
      i, j = cmd:find("%S+", j+1)
      if i and j then
        tinsert(args, cmd:sub(i, j))
      end

    until not(i or j)
  end
  local mode, tag, dest = unpack(args)


  -- no args, toggle ui
  if mode == 'rc' then
    return D:ResetChannels(tag)
  elseif  mode == 'stack' then
    return D:StackFrames()
  elseif mode == 'grid' then
    return D:DistributeFrames()
  elseif mode == 'tag' then -- tagging
    return D:Tag(tag, dest)
  elseif mode == 'new' then
    return D:New(tag)
  elseif mode == 'dock' then
    D.db.dockPoint = tag
    return D:UpdateDock()
  elseif mode == 'remove' then
    return D:Remove(tag)
  elseif mode ~= nil then
    -- profile selector or save command
    if mode == 'save' then
      list_id = tonumber(tag)
    else
      list_id = tonumber(mode)
    end

    if not list_id then
      if db.profilesName[tostring(list_id)] then
        list_id = db.profilesName[tostring(list_id)]
      else

        D:Print(L('Unable to resolve profile ID/name', list_id, dest))
        return
      end
    end


    if mode == 'save' then
      D.Profile(list_id, dest)
      scan_func = D.Save


      local name = currentProfile.name
      if dest then
        dest = dest:gsub("$%s+", ''):gsub("%s+^", '')
        if dest then
          if name then
            db.profilesName[name] = nil
          end
          db.profiles[list_id].name = dest
          db.profilesName[dest] = list_id

          name = dest
        end
      end


      D:Print("Profile |cFFFFFF00".. list_id .."|r:|cFF00FFFF".. name .."|r saved.")
    else

      if db.profiles[list_id] then
        D.LoadMessage ("Switched profiles.")
        if list_id ~= db.main_profile then
          db.last_profile = list_id
        end
        db.current_profile = list_id
        scan_func = D.Load
      else
        return D:PrintHelp()
      end

    end
  elseif mode == nil then
    list_id = (db.current_profile ~= db.main_profile) and db.main_profile or db.last_profile
    D.LoadMessage ("Switched between main and recent profile ("..db.current_profile..' and '..list_id..')')
    db.current_profile = list_id
    scan_func = D.Load
  else


    return D:PrintHelp()
  end

  if not db.profiles[list_id] then
    db.profiles[list_id] = {global = {}, char = {} }
    D.LoadMessage ("Starting profile #|cFF00FFFF".. list_id..'|r')
  end
  if not db.profiles[list_id].char[playerRealm] then
    db.profiles[list_id].char[playerRealm] = {}
  end

  targetGlobal = db.profiles[list_id].global
  targetChar = db.profiles[list_id].char[playerRealm]


  if scan_func then
    wipe(charStates)
    for id, name, enableState, globalState in D.Addons() do
      scan_func(id, name, enableState, globalState)
    end
  end

  if scan_func == D.Load then
    _G.ReloadUI()
    if AddonList_Update then
      AddonList_Update()
    end
  elseif (scan_func == D.Save) then
    print('reckoning')
    local updated = {}
    for addon, newState in pairs(charStates) do
      for character, addons in pairs(db.profiles[list_id].char) do
        if addons[addon] then
          print(addon, addons[addon], '::', newState)
          if (addons[addon] ~= newState) then
            addons[addon] = newState
            updated[character] = (updated[character] or 0) + 1
          end

        end

      end
    end
    for character, numAddons in pairs(updated) do
      print(character, numAddons, 'settings')
    end

  end


  D.Profile(db.current_profile)
end

D.Addons = function()
  local playername = UnitName("player")
  return function(n, i)
    if i >= n then
      return nil
    end

    i = i + 1
    local name = GetAddOnInfo(i)
    local enableState, globalState = GetAddOnEnableState(playername, i), GetAddOnEnableState(nil, i)
    return i, name, enableState, globalState
  end, GetNumAddOns(), 0
end

D.Load = function(id, name, charState, globalState)
  print('load', tostring(name), tostring(charState), tostring(globalState))
  if targetGlobal[name] == 2 then
    EnableAddOn(id, true)
  elseif targetChar[name] == 2 then
    EnableAddOn(id, playerName)
  elseif targetGlobal[name] == 0 then
    DisableAddOn(id, true)
  elseif targetChar[name] == 0 then
    DisableAddOn(id, playerName)
  end

  if not (targetChar[name] or targetGlobal[name]) then
    tinsert(D.unlisted, name)
  end

end


D.Save = function(id, name, charState, globalState)
  if (charState ~= 0) or (globalState ~= 0) then
    print('save', id, name, playerRealm .. ': '.. charState, 'All: '.. globalState)
  end

  targetGlobal[name] = globalState
  targetChar[name] = charState

  -- if enabling/disabling globally
  if globalState ~= 1 then
    charStates[name] = globalState
  end
end

D.UpdateTags = function()
  wipe(registeredTags)
  for index, channel in ipairs(D.channels) do
    for _, tag in ipairs(channel.tags) do
      registeredTags[tag] = registeredTags[tag] or {}
      tinsert(registeredTags[tag], D.console[index])
    end
  end
end

D.Tag = function(self, tag, id)
  local sig
  if tag and id then
    --@debug@
    --print(tag, dest)
    --@end-debug@

    -- convert to ID
    local channel, sig
    if tonumber(id) == nil then
      sig = id
      if D.sigID[id] then
        id = D.sigID[id]
        channel = D.channels[id]
      end
    else
      id = tonumber(id)
      channel = D.channels[id]
    end

    -- if channel is still nil, create one
    if not channel then
      id = #D.channels + 1
      D:Print(L('New channel created', (sig and (id..':'..sig)) or id))
      channel = D:GetOrCreateChannel(id, sig)
    else
      sig = channel.signature
    end
    --@debug@
    --print('3 tag,dest,channel.sig=',tag, dest, channel.signature)--@end-debug@

    if not currentProfile.tags[tag] then -- no tag table?
      currentProfile.tags[tag] = {}
    end

    local existingTag = tContains(channel.tags, tag)
    if existingTag then -- is tag set?

      for i, tag in ipairs(channel.tags) do
        if tag == tag then
          tremove(channel.tags, i)
          D:Print(L('Tag removed from channel', tag, channel.index, channel.signature))
          break
        end
      end
    else
      tinsert(channel.tags, tag)
      D:Print(L('Tag added to channel', tag, channel.index, channel.signature))
    end
    D.UpdateTags()
    DevianDock:Update()
  else
    D:Print(L['Command tag help'])
  end
end

D.ResetChannels = function(self, profile)
  currentProfile.current_channel = 1
  currentProfile.primary_channel = 1
  currentProfile.channels = {}
  D.DeepCopy(profileTemplate.channels, currentProfile.channels)
  currentProfile.tags = {}
  D.LoadMessage('Profile reset.')
  ReloadUI()
end

D.New = function(self, tag)
  if tag and not self.sigID[tag] then
    local id = D.max_channel + 1
    D.SetChannel(tag, id)
  end
end

D.Remove = function(self, dest)
  dest = D.sigID[dest] or tonumber(dest)
  if D.console[dest] and D.channels[dest] then
    for tag, tagDest in pairs(D.tags) do
      for i = #tagDest, 0 do
        -- work downward so we aren't skipping entries
        if tagDest[i] == dest then
          tremove(tagDest, i)
        end
      end
    end
    D.console[dest]:Hide()
    D.channels[dest] = nil
    tremove(D.console, dest)
    D.dock.buttons[dest]:SetShown(false)
    D.dock:Update()
    D:Print('Removed channel #'..dest)
  end
end

--- Queue up a message to appear after UI reload
function D.LoadMessage(msg)

  tinsert(_G.DevianLoadMessage, msg)
end

--- Creates a Devian-style output.
-- The first argument describes the channel to output on, and the remaining arguments are concatenated in a manner similar to default print()
-- This becomes the print handler when development mode is active. The original print() function is assigned to oldprint().
-- @param Tag, signature, or numeric index of the channel to output on. Defaults to primary channel.
-- @param ... Output contents.
local default_sendq = {}
function D.Message(prefix, ...)
  if not currentProfile.workspace then
    return D.oldprint(prefix, ...)
  end
  local print = D.oldprint
  prefix =  tostring(prefix)
  if prefix == nil then
    prefix = 'nil*'
  end

  local sendq = default_sendq
  local tag, id, tagged
  local byName = true

  if registeredTags[prefix] then
    sendq = registeredTags[prefix]
  else
    if D.sig[prefix] then
      sendq[D.sig[prefix].index] = D.sig[prefix]
    elseif not tagged then
      sendq[1] = D.console[D.primary_channel]
    end
  end



  -- color me timbers
  local pcolor
  if (not db.tagcolor[prefix]) and byName then
    -- numbers, use white
    if prefix:match('^%d+%.%d+') then
      pcolor = 'FFFFFF'
    else
      local c = {
        rand(64,255), rand(64,255), rand(64,255)
      }
      if c[1] > 223 and c[2] > 223 and c[3] > 223 then
        c[rand(1,3)] = rand(64,223)
      end
      db.tagcolor[prefix] = format('%02X%02X%02X', unpack(c))
      pcolor = db.tagcolor[prefix]
    end
  else
    pcolor = db.tagcolor[prefix]
  end

  local buffer = {}
  for i = 1, select('#',...) do
    local var = select(i, ...)

    if type(var) == 'table' then
      if type(var.GetName) == 'function' then
        var = '[table:'..tostring(var:GetName())..']'
      else
        var = '<'..tostring(var)..'>'
      end

    elseif type(var) == 'boolean' then
      var = var and 'true' or 'false'
    elseif type(var) == 'function' then
      var = '['..tostring(var)..']'
    elseif type(var) == 'nil' then
      var = 'nil'
    else
      var = tostring(var)
    end

    insert(buffer, var)
  end
  local message = concat(buffer, ' ')
  for id, channel in pairs(sendq) do
    if channel.width < 250 then
      prefix = sub(prefix, 0,2)
    end
    channel.out:AddMessage('|cFF'.. pcolor..prefix ..'|r ' .. message, 0.8, 0.8, 0.8)
    if not channel.newMessage then
      channel.newMessage = true
      if channel.Dock then
        channel.Dock:Update()
      end
    end


  end
  wipe(buffer)
end




function D:PrintHelp()
  D:Print("|cFFFFFF00/dvn|r",
    "\n |cFFFFFF00<number>|r - Loads a saved addon list. List 1 is treated as a gameplay profile and consoles will be disabled by default.")
  D:Print("|cFFFFFF00/dvc|r [<key>, ...]", "- Hides and show consoles. A list of channel keys can be passed to specify which ones get toggled.")

  D:Print("|cFFFFFF00/resetdvn|r", "- Resets all but profile data SavedVariables.")
  D:Print("|cFFFFFF00/cleandvn|r", "- Fully resets SavedVariables, profiles and all.")
end

local blocked = {profiles = true, debugmode = true}
D.SetDefaults = function()
  local DevianDB = _G.DevianDB
  for k,v in pairs(DevianDB) do
    if not blocked[k] then
      DevianDB[k] = nil
    end
  end
  for k,v in pairs(defaults) do
    if not blocked[k] then
      DevianDB[k] = v
    end
  end

  D.LoadMessage "Non-user SavedVars have been reset."
  _G.ReloadUI()
end
D.SetDefaultsAll = function ()
  _G.DevianDB = nil
  D.LoadMessage "All SavedVars wiped."
  _G.ReloadUI()
end

D.UnsetColors = function()
  db.tagcolor = {}
  D:Print('Tag color cache cleared.')
end


function D.ConsoleCommand (cmd)
  DevianDock:ToggleAll()
end


function DevianCore:Initialize()
  L = D.L

  -- pull defaults
  if not DevianDB then
    DevianDB = defaults
  end
  D.db = _G.DevianDB
  db = _G.DevianDB

  ---
  if #_G.DevianLoadMessage >= 1 then
    for i, msg in ipairs(_G.DevianLoadMessage) do
    D:Print(msg)
    end

    table.wipe(_G.DevianLoadMessage)
  end


  -- commands
  local cmdlist = {
    ['dvn'] = "Command",
    ['devian'] = "Command",
    ['dvc'] = "ConsoleCommand",
    ['dvncolors'] = "UnsetColors",
    ['cleandvn'] = "SetDefaultsAll",
    ['resetdvn'] = "SetDefaults",
  }
  for cmd, func in pairs(cmdlist) do
    local CMD = cmd:upper()
    _G['SLASH_' .. CMD .. '1'] = "/"..cmd

    if type(func == 'string') then
      --print('SLASH_' .. CMD .. '1','/'.. cmd, func)
      SlashCmdList[CMD] = D[func]
    else
      --print('SLASH_' .. CMD .. '1','/'.. cmd, func)
      SlashCmdList[CMD] = func
    end
  end

  --- initialize the current profile
  local id, name = D.Profile(db.current_profile or 1)
  if  currentProfile.workspace then
    tinsert(channels_report, 'Profile |cFFFFFF00'.. id ..'|r: |cFF00FF00'..currentProfile.name.. '|r')
    if D.channels[currentProfile.default_channel] then
      tinsert(channels_report, 'Primary: |cFFFFFF00#'..currentProfile.default_channel..'|r |cFF00FFFF'.. D.channels[currentProfile.default_channel].signature..'|r')
    end
  end



  for index, cinfo in ipairs(D.channels) do
    --oldprint(index, cinfo.signature)
    if not D.primary_channel then
      D.primary_channel = index
    end
    D:GetOrCreateChannel(index)
    D.num_channels = #D.channels
  end
  if #channels_report >= 1 then
    D:Print(concat(channels_report, ', '))
  end

  D.primary_channel = D.primary_channel or 1
  D.max_channel = max(D.max_channel, currentProfile.max_channel)
  if currentProfile.max_channel < D.max_channel then
    for i = currentProfile.max_channel, D.max_channel do
      D.console[i]:Hide()
    end
  end

  D.UpdateTags()

  if currentProfile.workspace then
    if D.console[currentProfile.current_channel] then
      --print('bringing', D.console[currentProfile.current_channel].signature, 'to the front')
      D.console[currentProfile.current_channel]:ToFront()
      -- bring the current channel to the front
    end
    DevianDock:Update()
  end
end

function D:Print (...)
  local msg = '|cFF00FF44Devian|r:'
  for i = 1, select('#', ...) do
    msg = msg .. ' ' .. tostring(select(i, ...))
  end
  DEFAULT_CHAT_FRAME:AddMessage(msg)
end

function D:GetActiveChannel()
  return D.console[currentProfile.current_channel]
end

function D:GetOrCreateChannel(id, name)
  id = id or (#D.channels + 1)

  local info = D.channels[id]
  if not info then
    --print('new channel')
    name = name or ('Channel ' .. id)
    info = {
      index = id,
      signature = name,
      tags = {}
    }
    D.DeepCopy(defaults.default_channel, info)
    D.channels[id] = info
  elseif not info.tags then
    -- fix old data?
    info.tags = {info.signature}
    oldprint(D.db)
    for tag, tagSet in pairs(D.tags) do
      for _, index in pairs(tagSet) do
        if index == id then
          tinsert(info.tags, tag)
        end
      end
    end
  end

  local frame = D.console[id]

  if not frame then

    if DEVIAN_WORKSPACE then
      tinsert(channels_report, (info.index) .. ':' .. (info.signature) .. ' (|cFF00FFFF'.. concat(info.tags, '|r; |cFF00FFFF')..'|r)')
    end
    frame = CreateFrame('Frame', 'DevianConsole'..id, Devian, 'DevianConsoleTemplate')
    frame:SetID(id)
    D.console[id] = frame
    D.sigID[info.signature] = id
  end

  frame:Setup(info)
  return frame
end