<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE MudletPackage>
<!--
  Paquete de Mudlet de Galaxia Mud. Generado por herramientas/mudlet/montar.py
  a partir del channel_history de Readan y de las piezas de
  herramientas/mudlet/ (enter_repite, teclado, salida_rapida): no editar a mano.
  salida_rapida está adaptada de QuickOutput, de ironcross32 (licencia MIT;
  el texto de la licencia va en herramientas/mudlet/salida_rapida.xml).
-->
<!-- Autor (adaptacion Mudlet): Readan -->
<!--
  Plugin "channel_history" portado de MUSHclient a Mudlet.
  Original: Tyler S, Oriol G y Weyoun (2010, MUSHclient 4.51).
  Adaptado para Mudlet: la lectura del texto se hace solo con cecho
  (es tu lector de pantalla el que lee la ventana), y el "notepad" guarda
  un .txt en el directorio de Mudlet y lo abre con el visor predeterminado.

  Como importar:
    Settings > Package Manager > Install > selecciona este XML.

  Aliases del usuario:
    history_add categoria=mensaje    Anyade un mensaje a la categoria (esto lo
                                     llaman normalmente triggers que detectan
                                     mensajes de canales: gtell, say, etc).
    history_delete                   Borra la categoria actual.
    history_move right | left        Mueve la categoria actual en el orden.
    history_get N                    Lee el mensaje N de la categoria actual.
    history_catprev / history_catnext   Cambia de categoria.
    history_msgprev N / history_msgnext N   Salta N mensajes atras / adelante.
    history_msgcopy                  Copia el mensaje actual al portapapeles.
    history_notepad                  Vuelca la categoria a un .txt y lo abre.
    timestamps  (o history_timestamps)   Toggle del anuncio de tiempo.
    history_pos                      Toggle del anuncio de posicion (N de M).

  Atajos de teclado (los importantes para accesibilidad):
    Alt+1..9, Alt+0           Lee mensaje 1..10. Doble pulsacion en < 0.5s
                              copia al portapapeles, triple lo pega en el
                              cuadro de comandos.
    Alt+Left / Alt+Right      Categoria anterior / siguiente.
    Alt+Up / Alt+Down         Mensaje anterior / siguiente.
    Alt+PageUp / Alt+PageDown Salta 10 mensajes.
    Alt+Home / Alt+End        Va al primer / ultimo mensaje.
    Shift+Alt+Left/Right      Mueve la categoria en el orden.
    Alt+Delete                Borra la categoria actual.
    Alt+Space                 Repite el mensaje actual.
    Alt+Shift+Space           Copia al portapapeles.
    Alt+Shift+Enter           Abre la categoria como .txt.
    Alt+Shift+T               Toggle de timestamps.
-->
<MudletPackage version="1.001">
    <TriggerPackage>
    </TriggerPackage>
    <TimerPackage>
    </TimerPackage>
    <AliasPackage>
        <AliasGroup isActive="yes" isFolder="yes">
            <name>ChannelHistory</name>
            <packageName></packageName>
            <script></script>
            <eventHandlerList />
            <Alias isActive="yes" isFolder="no">
                <name>history_add</name>
                <script><![CDATA[channelhist.history_add(matches[2], matches[3])]]></script>
                <command></command>
                <packageName></packageName>
                <regex>^history_add (\w[\w ]*)=(.*)$</regex>
            </Alias>
            <Alias isActive="yes" isFolder="no">
                <name>history_delete</name>
                <script><![CDATA[channelhist.history_delete()]]></script>
                <command></command>
                <packageName></packageName>
                <regex>^history_delete$</regex>
            </Alias>
            <Alias isActive="yes" isFolder="no">
                <name>history_move</name>
                <script><![CDATA[channelhist.category_move(matches[2])]]></script>
                <command></command>
                <packageName></packageName>
                <regex>^history_move (\w+)$</regex>
            </Alias>
            <Alias isActive="yes" isFolder="no">
                <name>history_get</name>
                <script><![CDATA[channelhist.history_get(tonumber(matches[2]))]]></script>
                <command></command>
                <packageName></packageName>
                <regex>^history_get (\d+)$</regex>
            </Alias>
            <Alias isActive="yes" isFolder="no">
                <name>history_catprev</name>
                <script><![CDATA[channelhist.history_catprev()]]></script>
                <command></command>
                <packageName></packageName>
                <regex>^history_catprev$</regex>
            </Alias>
            <Alias isActive="yes" isFolder="no">
                <name>history_catnext</name>
                <script><![CDATA[channelhist.history_catnext()]]></script>
                <command></command>
                <packageName></packageName>
                <regex>^history_catnext$</regex>
            </Alias>
            <Alias isActive="yes" isFolder="no">
                <name>history_msgprev</name>
                <script><![CDATA[channelhist.history_msgprev(tonumber(matches[2]))]]></script>
                <command></command>
                <packageName></packageName>
                <regex>^history_msgprev (\d+)$</regex>
            </Alias>
            <Alias isActive="yes" isFolder="no">
                <name>history_msgnext</name>
                <script><![CDATA[channelhist.history_msgnext(tonumber(matches[2]))]]></script>
                <command></command>
                <packageName></packageName>
                <regex>^history_msgnext (\d+)$</regex>
            </Alias>
            <Alias isActive="yes" isFolder="no">
                <name>history_msgcopy</name>
                <script><![CDATA[channelhist.history_msgcopy()]]></script>
                <command></command>
                <packageName></packageName>
                <regex>^history_msgcopy$</regex>
            </Alias>
            <Alias isActive="yes" isFolder="no">
                <name>history_notepad</name>
                <script><![CDATA[channelhist.history_notepad()]]></script>
                <command></command>
                <packageName></packageName>
                <regex>^history_notepad$</regex>
            </Alias>
            <Alias isActive="yes" isFolder="no">
                <name>timestamps</name>
                <script><![CDATA[channelhist.toggle_timestamps()]]></script>
                <command></command>
                <packageName></packageName>
                <regex>^(?:history_)?timestamps?$</regex>
            </Alias>
            <Alias isActive="yes" isFolder="no">
                <name>history_pos</name>
                <script><![CDATA[channelhist.toggle_pos()]]></script>
                <command></command>
                <packageName></packageName>
                <regex>^history_pos$</regex>
            </Alias>
        </AliasGroup>
        <AliasGroup isActive="yes" isFolder="yes">
            <name>Enter_Repite_aliases</name>
            <packageName></packageName>
            <script></script>
            <eventHandlerList />

            <Alias isActive="yes" isFolder="no">
                <name>repiteon</name>
                <script><![CDATA[enter_repite.on()]]></script>
                <command></command><packageName></packageName>
                <regex>^repiteon$</regex>
            </Alias>

            <Alias isActive="yes" isFolder="no">
                <name>repiteoff</name>
                <script><![CDATA[enter_repite.off()]]></script>
                <command></command><packageName></packageName>
                <regex>^repiteoff$</regex>
            </Alias>

            <Alias isActive="yes" isFolder="no">
                <name>repitedebug arg</name>
                <script><![CDATA[enter_repite.debug_toggle(matches[2])]]></script>
                <command></command><packageName></packageName>
                <regex>^repitedebug\s+(on|off)$</regex>
            </Alias>

            <Alias isActive="yes" isFolder="no">
                <name>repitedebug toggle</name>
                <script><![CDATA[enter_repite.debug_toggle()]]></script>
                <command></command><packageName></packageName>
                <regex>^repitedebug$</regex>
            </Alias>

            <Alias isActive="yes" isFolder="no">
                <name>repitehelp</name>
                <script><![CDATA[enter_repite.help()]]></script>
                <command></command><packageName></packageName>
                <regex>^repitehelp$</regex>
            </Alias>
        </AliasGroup>
    
        <AliasGroup isActive="yes" isFolder="yes">
            <name>Keypad</name>
            <packageName></packageName>
            <script></script>
            <eventHandlerList />

            <Alias isActive="yes" isFolder="no">
                <name>definetecla</name>
                <script><![CDATA[
local args = matches[2]
if args then args = args:gsub("%s+$", "") end

-- Sin argumentos: listado completo.
if not args or args == "" then
    cecho("\n<yellow>[keypad] Asignaciones actuales:<reset>\n")
    for _, t in ipairs(keypad.ORDEN) do
        local marca = keypad.mapa[t] and "  (personalizada)" or ""
        cecho("  Num " .. t .. ": " .. keypad.comando(t) .. marca .. "\n")
    end
    cecho("Uso: definetecla <tecla> <comando> para redefinir, " ..
          "definetecla <tecla> defecto para restaurar una, " ..
          "definetecla defecto para restaurar todas. " ..
          "Ayuda completa: keypadhelp.\n")
    return
end

local tecla, resto = args:match("^(%S+)%s*(.*)$")

-- definetecla defecto: restaurar todo.
if tecla == "defecto" and resto == "" then
    keypad.mapa = {}
    keypad.guardar()
    cecho("\n<green>[keypad] Todas las teclas devueltas a su valor por defecto.<reset>\n")
    return
end

if not keypad.DEFAULTS[tecla] then
    cecho("\n<red>[keypad] Tecla '" .. tecla .. "' no valida. " ..
          "Teclas: 8 2 6 4 9 7 3 1 5 0 - + . / *<reset>\n")
    return
end

-- Solo la tecla: consultar.
if resto == "" then
    local marca = keypad.mapa[tecla] and " (personalizada)" or " (por defecto)"
    cecho("\n<yellow>[keypad] Num " .. tecla .. " envia: " ..
          keypad.comando(tecla) .. marca .. "<reset>\n")
    return
end

-- Tecla + "defecto": restaurar esa tecla.
if resto == "defecto" then
    keypad.mapa[tecla] = nil
    keypad.guardar()
    cecho("\n<green>[keypad] Num " .. tecla .. " devuelta a su valor por defecto: " ..
          keypad.DEFAULTS[tecla] .. "<reset>\n")
    return
end

-- Tecla + comando: redefinir.
keypad.mapa[tecla] = resto
keypad.guardar()
cecho("\n<green>[keypad] Hecho. Num " .. tecla .. " ahora envia: " .. resto .. "<reset>\n")
]]></script>
                <command></command><packageName></packageName>
                <regex>^definetecla(?:\s+(.+))?\s*$</regex>
            </Alias>

            <Alias isActive="yes" isFolder="no">
                <name>keypadhelp</name>
                <script><![CDATA[
cecho("\n<yellow>[keypad] Ayuda:<reset>\n")
cecho("  Movimiento por el teclado numerico. Requiere NumLock activado;\n")
cecho("  sin NumLock las teclas no disparan.\n")
cecho("  Teclas por defecto: 8 norte, 2 sur, 6 este, 4 oeste, 9 noreste,\n")
cecho("  7 noroeste, 3 sureste, 1 suroeste, menos arriba, mas abajo,\n")
cecho("  5 quien, 0 mirar, punto esconderse, barra inventario,\n")
cecho("  asterisco ficha.\n")
cecho("  Comandos:\n")
cecho("    definetecla                    Lista lo que envia cada tecla.\n")
cecho("    definetecla <tecla>            Dice lo que envia esa tecla.\n")
cecho("    definetecla <tecla> <comando>  Redefine la tecla. El comando\n")
cecho("                                   admite espacios. Ej: definetecla / equipo\n")
cecho("    definetecla <tecla> defecto    Devuelve esa tecla a su valor original.\n")
cecho("    definetecla defecto            Devuelve todas a su valor original.\n")
cecho("  La tecla se escribe tal cual: 8 2 6 4 9 7 3 1 5 0 - + . / *\n")
cecho("  Los cambios se guardan en keypad_config.lua (perfil de Mudlet) y\n")
cecho("  sobreviven a reinicios y a reinstalar el paquete.\n")
cecho("  Limitacion: no se puede asignar el comando literal 'defecto'.\n")
]]></script>
                <command></command><packageName></packageName>
                <regex>^keypad ?help$</regex>
            </Alias>
        </AliasGroup>
    
        <AliasGroup isActive="yes" isFolder="yes">
            <name>Salida_Rapida</name>
            <packageName></packageName>
            <script></script>
            <eventHandlerList />

            <Alias isActive="yes" isFolder="no">
                <name>salidarapida</name>
                <script><![CDATA[
cecho("\n<yellow>[salida rapida] Ayuda:<reset>\n")
cecho("  Lee las diez ultimas lineas de la pantalla sin mover el cursor.\n")
cecho("  Ctrl+1 lee la ultima linea, Ctrl+2 la anterior, y asi hasta Ctrl+9.\n")
cecho("  Ctrl+0 lee la decima.\n")
cecho("  Pulsa la misma tecla dos veces seguidas, rapido, para copiar esa\n")
cecho("  linea al portapapeles.\n")
cecho("  Para repasar los canales usa Alt con los numeros.\n")
]]></script>
                <command></command><packageName></packageName>
                <regex>^salida ?rapida$</regex>
            </Alias>
        </AliasGroup>
    
    </AliasPackage>
    <ScriptPackage>
        <ScriptGroup isActive="yes" isFolder="yes">
            <name>ChannelHistory</name>
            <packageName></packageName>
            <Script isActive="yes" isFolder="no">
                <name>ChannelHistory_Core</name>
                <packageName></packageName>
                <eventHandlerList />
                <script><![CDATA[
-- ================================================================
--  channel_history (Mudlet port) - logica central
-- ================================================================

channelhist = channelhist or {}

-- Estado en memoria (no se persiste entre sesiones, igual que el original)
channelhist.modes            = {}
channelhist.messages         = {}
channelhist.indices          = {}
channelhist.categories       = {}
channelhist.current_category = 0

-- Configuracion persistente
channelhist.timestamps  = (channelhist.timestamps  ~= nil) and channelhist.timestamps  or true
channelhist.anuncia_pos = (channelhist.anuncia_pos ~= nil) and channelhist.anuncia_pos or false

-- ----------------------------------------------------------------
--  Helpers
-- ----------------------------------------------------------------

-- Anuncia un mensaje al usuario.
-- Si el paquete `reader` esta cargado, va por TTS y NO ensucia el output.
-- Si no, cae al cecho de toda la vida en la ventana principal.
function channelhist.say(text)
    if reader and reader.say then
        reader.say(text, true)  -- urgente: el usuario espera la respuesta YA
    else
        cecho("\n<cyan>" .. text .. "\n")
    end
end

function channelhist.calculateRelativeTime(t)
    if t < 1 then return "(justo ahora)" end
    if t == 1 then return "(hace 1 segundo)" end
    if t > 1 and t < 60 then
        return "(hace " .. math.ceil(t) .. " segundos)"
    end
    if t >= 60 and t < 3600 then
        local min = math.floor(t / 60)
        local sec = math.ceil(t % 60)
        local mtxt = (min == 1) and " minuto y " or " minutos y "
        local stxt = (sec == 1) and " segundo)" or " segundos)"
        return "(hace " .. min .. mtxt .. sec .. stxt
    end
    if t >= 3600 and t < 86400 then
        local hr = math.floor(t / 3600)
        local min = math.floor((t % 3600) / 60)
        local htxt = (hr == 1) and " hora y " or " horas y "
        local mtxt = (min == 1) and " minuto)" or " minutos)"
        return "(hace " .. hr .. htxt .. min .. mtxt
    end
    if t > 86400 then
        local days = math.floor(t / 86400)
        local hr   = math.floor((t - (86400 * days)) / 3600)
        local min  = math.floor((t - ((86400 * days) + (3600 * hr))) / 60)
        local dtxt = (days == 1) and " dia, "    or " dias, "
        local htxt = (hr == 1)   and " hora, y " or " horas, y "
        local mtxt = (min == 1)  and " minuto)"  or " minutos)"
        return "(hace " .. days .. dtxt .. hr .. htxt .. min .. mtxt
    end
    return ""
end

-- Escribe el texto en el cuadro de comandos de Mudlet. La API ha cambiado
-- entre versiones: `setCmdLineText` (4.13+) o `printCmdLine` (anterior).
-- Devuelve true/false y un mensaje de error en caso de fallo.
function channelhist.paste_to_cmdline(text)
    if type(setCmdLineText) == "function" then
        local ok, err = pcall(setCmdLineText, text)
        if ok then return true end
        if type(printCmdLine) == "function" then
            local ok2, err2 = pcall(printCmdLine, text)
            if ok2 then return true end
            return false, tostring(err2)
        end
        return false, tostring(err)
    elseif type(printCmdLine) == "function" then
        local ok, err = pcall(printCmdLine, text)
        if ok then return true end
        return false, tostring(err)
    end
    return false, "no hay funcion compatible (setCmdLineText/printCmdLine)"
end

function channelhist.sayMsg(prefix, m)
    if not m then return end
    local txt = (prefix or "") .. " " .. m.message
    if channelhist.timestamps then
        local last = string.byte(m.message:sub(-1)) or 0
        -- Si la frase no acaba en signo de puntuacion, le metemos un punto
        -- para que el TTS no entone como pregunta el tiempo relativo.
        if (last >= 49 and last <= 57) or
           (last >= 65 and last <= 91) or
           (last >= 97 and last <= 123) then
            txt = txt .. "."
        end
        txt = txt .. " " .. channelhist.calculateRelativeTime(os.clock() - m.time)
    end
    channelhist.say(txt)
end

-- ----------------------------------------------------------------
--  Operaciones del historial
-- ----------------------------------------------------------------

function channelhist.history_add(cat, msg)
    if channelhist.messages[cat] == nil then
        channelhist.messages[cat] = {}
        channelhist.indices[cat]  = 0
        table.insert(channelhist.categories, cat)
    end
    table.insert(channelhist.messages[cat], { message = msg, time = os.clock() })
    -- Limite de 1000 mensajes por categoria (igual que el original)
    if #channelhist.messages[cat] > 1000 then
        table.remove(channelhist.messages[cat], 1)
        if channelhist.indices[cat] > 1 then
            channelhist.indices[cat] = channelhist.indices[cat] - 1
        end
    end
end

function channelhist.history_delete()
    if #channelhist.categories == 0 then
        channelhist.say("No hay categorias.")
        return
    end
    if channelhist.current_category == 0 then
        channelhist.say("No hay ninguna categoria seleccionada.")
        return
    end
    local cat = channelhist.categories[channelhist.current_category]
    channelhist.messages[cat] = nil
    channelhist.indices[cat]  = nil
    table.remove(channelhist.categories, channelhist.current_category)
    if channelhist.current_category > #channelhist.categories then
        channelhist.current_category = 1
    end
    if #channelhist.categories > 0 then
        channelhist.say(channelhist.categories[channelhist.current_category])
    else
        channelhist.say("No hay mas categorias.")
        channelhist.categories       = {}
        channelhist.current_category = 0
    end
end

function channelhist.category_move(direction)
    if #channelhist.categories == 0 then
        channelhist.say("No hay categorias.")
        return
    end
    if channelhist.current_category == 0 then
        channelhist.say("No hay ninguna categoria seleccionada.")
        return
    end
    local name = channelhist.categories[channelhist.current_category]
    table.remove(channelhist.categories, channelhist.current_category)
    if direction == "right" then
        channelhist.current_category = channelhist.current_category + 1
        if channelhist.current_category > #channelhist.categories + 1 then
            channelhist.current_category = 2
        end
    else
        channelhist.current_category = channelhist.current_category - 1
        if channelhist.current_category < 1 then
            channelhist.current_category = #channelhist.categories
        end
    end
    table.insert(channelhist.categories, channelhist.current_category, name)
    channelhist.say("Movida.")
end

function channelhist.history_get(item)
    if not item then return end
    local str = ""

    if channelhist.current_category == 0 and #channelhist.categories == 0 then
        channelhist.say("No hay categorias.")
        return
    elseif channelhist.current_category == 0 and #channelhist.categories > 0 then
        for i, _ in ipairs(channelhist.categories) do
            if channelhist.categories[i] == "all" then
                channelhist.current_category = i
            end
        end
        if channelhist.current_category > 0 then
            str = str .. "Cambiando a todas."
        else
            str = str .. "Cambiando a " .. channelhist.categories[1] ..
                  ". Presiona alt flecha izquierda o flecha derecha para cambiar de categoria."
            channelhist.current_category = 1
        end
    end

    local cat = channelhist.categories[channelhist.current_category]
    if #channelhist.messages[cat] < item then
        channelhist.say("No hay mensaje.")
        return
    end

    -- Lógica de pulsacion 1/2/3 veces (leer / copiar / pegar)
    local timeout = 0.5
    if channelhist.modes[item] == nil then
        channelhist.modes[item] = { os.clock(), 1 }
    elseif os.clock() - channelhist.modes[item][1] >= timeout then
        channelhist.modes[item][1] = os.clock()
        channelhist.modes[item][2] = 1
    else
        channelhist.modes[item][1] = os.clock()
        channelhist.modes[item][2] = channelhist.modes[item][2] + 1
    end
    if channelhist.modes[item][2] > 3 then
        channelhist.modes[item][2] = 3
    end

    local real_item = #channelhist.messages[cat] + 1 - item
    local m = channelhist.messages[cat][real_item]

    if channelhist.modes[item][2] == 1 then
        channelhist.sayMsg(str, m)
    elseif channelhist.modes[item][2] == 2 then
        setClipboardText(m.message)
        channelhist.say("Copiado")
    elseif channelhist.modes[item][2] == 3 then
        local ok, err = channelhist.paste_to_cmdline(m.message)
        if ok then
            channelhist.say("Pegado")
        else
            setClipboardText(m.message)
            channelhist.say("No se pudo pegar (" .. tostring(err) ..
                            "). Lo dejo en el portapapeles.")
        end
    end
end

function channelhist.history_catnext()
    if #channelhist.categories == 0 then
        channelhist.say("No hay categorias.")
        return
    end
    channelhist.current_category = channelhist.current_category + 1
    if channelhist.current_category > #channelhist.categories then
        channelhist.current_category = 1
    end
    local cat = channelhist.categories[channelhist.current_category]
    if channelhist.indices[cat] == 0 then channelhist.indices[cat] = 1 end
    if channelhist.anuncia_pos then
        channelhist.say(cat .. ": " .. channelhist.indices[cat] ..
                        " de " .. #channelhist.messages[cat])
    else
        channelhist.say(cat)
    end
end

function channelhist.history_catprev()
    if #channelhist.categories == 0 then
        channelhist.say("No hay categorias.")
        return
    end
    channelhist.current_category = channelhist.current_category - 1
    if channelhist.current_category < 1 then
        channelhist.current_category = #channelhist.categories
    end
    local cat = channelhist.categories[channelhist.current_category]
    if channelhist.indices[cat] == 0 then channelhist.indices[cat] = 1 end
    if channelhist.anuncia_pos then
        channelhist.say(cat .. ": " .. channelhist.indices[cat] ..
                        " de " .. #channelhist.messages[cat])
    else
        channelhist.say(cat)
    end
end

function channelhist.history_msgnext(skip)
    skip = tonumber(skip) or 1
    local str = ""
    if #channelhist.categories == 0 then
        channelhist.say("No hay categorias.")
        return
    end
    if channelhist.current_category == 0 then
        channelhist.say("No hay ninguna categoria seleccionada.")
        return
    end
    local cat = channelhist.categories[channelhist.current_category]
    if channelhist.indices[cat] == 0 then
        channelhist.indices[cat] = #channelhist.messages[cat]
    end
    if channelhist.indices[cat] + skip > #channelhist.messages[cat] then
        channelhist.indices[cat] = #channelhist.messages[cat]
        str = str .. "Fin: "
    else
        channelhist.indices[cat] = channelhist.indices[cat] + skip
    end
    channelhist.sayMsg(str, channelhist.messages[cat][channelhist.indices[cat]])
end

function channelhist.history_msgprev(skip)
    skip = tonumber(skip) or 1
    local str = ""
    if #channelhist.categories == 0 then
        channelhist.say("No hay categorias.")
        return
    end
    if channelhist.current_category == 0 then
        channelhist.say("No hay ninguna categoria seleccionada.")
        return
    end
    local cat = channelhist.categories[channelhist.current_category]
    if channelhist.indices[cat] == 0 then
        channelhist.indices[cat] = #channelhist.messages[cat]
    end
    if channelhist.indices[cat] - skip < 1 then
        channelhist.indices[cat] = 1
        str = str .. "Inicio: "
    else
        channelhist.indices[cat] = channelhist.indices[cat] - skip
    end
    channelhist.sayMsg(str, channelhist.messages[cat][channelhist.indices[cat]])
end

function channelhist.history_msgcopy()
    if #channelhist.categories == 0 then
        channelhist.say("No hay categorias.")
        return
    end
    if channelhist.current_category == 0 then
        channelhist.say("No hay ninguna categoria seleccionada.")
        return
    end
    local cat = channelhist.categories[channelhist.current_category]
    if channelhist.indices[cat] == 0 then
        channelhist.indices[cat] = #channelhist.messages[cat]
    end
    setClipboardText(channelhist.messages[cat][channelhist.indices[cat]].message)
    channelhist.say("Copiado")
end

-- Vuelca la categoria actual a un .txt en el directorio de Mudlet y lo abre
-- con el visor predeterminado del sistema.
function channelhist.history_notepad()
    if #channelhist.categories == 0 then
        channelhist.say("No hay categorias.")
        return
    end
    if channelhist.current_category == 0 then
        channelhist.say("No hay ninguna categoria seleccionada.")
        return
    end
    local cat = channelhist.categories[channelhist.current_category]
    local mensajes = channelhist.messages[cat]
    local safe = cat:gsub("[^%w]", "_")
    local home = getMudletHomeDir()
    local path = home .. "/channel_history_" .. safe .. ".txt"

    local f, err = io.open(path, "w")
    if not f then
        channelhist.say("Error al abrir archivo: " .. tostring(err))
        return
    end
    f:write("Mostrando " .. #mensajes .. " mensajes:\r\n")
    for i = 1, #mensajes do
        if channelhist.timestamps then
            local stamp = os.date("%c", os.time() - os.clock() + mensajes[i].time)
            f:write("[" .. stamp .. "] " .. mensajes[i].message .. "\r\n")
        else
            f:write(mensajes[i].message .. "\r\n")
        end
    end
    f:close()

    -- Abrir con el visor del sistema operativo
    if package.config:sub(1, 1) == "\\" then
        -- Windows
        os.execute('start "" "' .. path:gsub("/", "\\") .. '"')
    else
        -- Linux / macOS
        os.execute('xdg-open "' .. path .. '" >/dev/null 2>&1 || open "' .. path .. '" &')
    end
    channelhist.say("Abriendo " .. cat .. " en el editor.")
end

function channelhist.toggle_timestamps()
    channelhist.timestamps = not channelhist.timestamps
    if channelhist.timestamps then
        channelhist.say("Se anunciara el tiempo luego de los mensajes.")
    else
        channelhist.say("No se anunciara el tiempo luego de los mensajes.")
    end
    channelhist.save_config()
end

function channelhist.toggle_pos()
    channelhist.anuncia_pos = not channelhist.anuncia_pos
    if channelhist.anuncia_pos then
        channelhist.say("Se anunciara la posicion del mensaje en la categoria.")
    else
        channelhist.say("No se anunciara la posicion del mensaje en la categoria.")
    end
    channelhist.save_config()
end

-- ----------------------------------------------------------------
--  Persistencia de la configuracion (timestamps, anuncia_pos)
-- ----------------------------------------------------------------

function channelhist.save_config()
    local path = getMudletHomeDir() .. "/channel_history_config.lua"
    local data = {
        timestamps  = channelhist.timestamps,
        anuncia_pos = channelhist.anuncia_pos,
    }
    table.save(path, data)
end

function channelhist.load_config()
    local path = getMudletHomeDir() .. "/channel_history_config.lua"
    if io.exists and io.exists(path) then
        local data = {}
        table.load(path, data)
        if data.timestamps  ~= nil then channelhist.timestamps  = data.timestamps  end
        if data.anuncia_pos ~= nil then channelhist.anuncia_pos = data.anuncia_pos end
    end
end

-- ----------------------------------------------------------------
--  Inicializacion al cargar el paquete
-- ----------------------------------------------------------------

channelhist.load_config()

if not channelhist._exit_handler then
    channelhist._exit_handler = registerAnonymousEventHandler("sysExitEvent", function()
        pcall(channelhist.save_config)
    end)
end

cecho("\n<green>Plugin channel_history cargado [build 2026-10-12, paquete GMud]. Timestamps: " ..
      tostring(channelhist.timestamps) .. ". Anuncia posicion: " ..
      tostring(channelhist.anuncia_pos) .. ".\n")
]]></script>
            </Script>
            <Script isActive="yes" isFolder="no">
                <name>GMud_GMCP</name>
                <packageName></packageName>
                <eventHandlerList />
                <script><![CDATA[
-- ================================================================
--  GMud: los canales llegan por GMCP y van al historial
-- ================================================================

gmud = gmud or {}
gmud.build = "2026-10-12"

-- Mudlet no pide el modulo de canales por su cuenta: hay que pedirselo al
-- MUD al conectar, y tambien ahora por si el paquete se instala ya conectado.
function gmud.pedir_canales()
    if sendGMCP then
        sendGMCP('Core.Supports.Add ["Comm.Channel 1"]')
    end
end

-- Cada linea de canal que manda el MUD: gmcp.Comm.Channel.Text trae el
-- canal, quien habla y el texto tal como sale en pantalla, sin colores.
function gmud.canal()
    local t = gmcp and gmcp.Comm and gmcp.Comm.Channel and gmcp.Comm.Channel.Text
    if not t or not t.text or t.text == "" then
        return
    end
    local canal = (t.channel and t.channel ~= "") and t.channel or "otros"
    local texto = string.gsub(t.text, "[\r\n]+$", "")
    if channelhist and channelhist.history_add then
        channelhist.history_add(canal, texto)
    end
end

for _, id in ipairs(gmud.manejadores or {}) do
    killAnonymousEventHandler(id)
end
gmud.manejadores = {
    registerAnonymousEventHandler("gmcp.Comm.Channel.Text", gmud.canal),
    registerAnonymousEventHandler("sysProtocolEnabled", function(_, protocolo)
        if protocolo == "GMCP" then
            gmud.pedir_canales()
        end
    end),
}
gmud.pedir_canales()

cecho("\n<green>Paquete GMud cargado [build " .. gmud.build .. "]: los canales entran solos en el historial.\n")
]]></script>
            </Script>
        </ScriptGroup>
        <ScriptGroup isActive="yes" isFolder="yes">
            <name>Enter_Repite</name>
            <packageName></packageName>
            <Script isActive="yes" isFolder="no">
                <name>Enter_Repite_Core</name>
                <packageName></packageName>
                <eventHandlerList />
                <script><![CDATA[
-- ================================================================
--  enter_repite - reenvio del ultimo comando al pulsar Enter en
--  una linea vacia.
-- ================================================================

enter_repite = enter_repite or {}
enter_repite.ultimo              = enter_repite.ultimo              or nil
enter_repite.enabled             = (enter_repite.enabled ~= false) -- true por defecto
enter_repite.debug               = enter_repite.debug == true       -- false por defecto
enter_repite.en_juego            = enter_repite.en_juego == true    -- hasta Char.Name
enter_repite.en_paginador        = enter_repite.en_paginador == true
enter_repite._envios_vacios      = enter_repite._envios_vacios      or 0
enter_repite._lineas_borradas    = enter_repite._lineas_borradas    or 0
enter_repite._triggers_omitidos  = enter_repite._triggers_omitidos  or 0
enter_repite._trigger_pendiente  = enter_repite._trigger_pendiente  or false
enter_repite._cola               = enter_repite._cola               or 0
enter_repite._grabados_user      = enter_repite._grabados_user      or 0
enter_repite._ignorados_script   = enter_repite._ignorados_script   or 0

local function dbg(msg)
    if enter_repite.debug then
        cecho("\n<grey>[repite-debug] " .. tostring(msg) .. "<reset>\n")
    end
end

-- Comandos internos del propio paquete que NO queremos grabar
-- como "ultimo comando" si el usuario los teclea.
local IGNORAR = {
    ["repiteon"]      = true,
    ["repiteoff"]     = true,
    ["repitehelp"]    = true,
    ["repitedebug"]   = true,
    ["repitedebug on"]  = true,
    ["repitedebug off"] = true,
}

-- Tiempos (segundos) de la espera a que llegue lo que contesta GMud al "".
-- Mudlet suelta la ultima linea del prompt (la que no lleva salto) a los
-- 300 ms sin datos; las demas lineas de la respuesta llegan juntas.
local PAUSA         = 0.25 -- una linea que llega tras esta pausa es esa ultima
local SILENCIO      = 0.4  -- sin lineas nuevas en este rato, ya llego todo
local SIN_RESPUESTA = 1.5  -- si no llega ninguna linea, reenviar igualmente
local MAX_LINEAS    = 5    -- blanco + un prompt de hasta cuatro lineas

-- Reloj en segundos con milesimas (getEpoch es de Mudlet).
local function reloj()
    if getEpoch then return getEpoch() end
    return os.time()
end

-- Comandos de GMud que llevan una contrasena en la linea. El MUD los
-- acepta abreviados, asi que se compara por prefijo (desde 3 letras).
local CON_CLAVE = { "password", "newpassword", "transmitircontrasenya" }

local function lleva_clave(text)
    local palabra = text:match("^%s*(%S+)")
    if not palabra or #palabra < 3 then
        return false
    end
    palabra = palabra:lower()
    for _, orden in ipairs(CON_CLAVE) do
        if orden:sub(1, #palabra) == palabra then
            return true
        end
    end
    return false
end

-- Fuera del juego (login, menu de la cuenta) no se graba ni se repite
-- nada, y el ultimo comando se olvida.
local function fuera_del_juego(motivo)
    enter_repite.en_juego = false
    enter_repite.en_paginador = false
    enter_repite.ultimo = nil
    dbg("fuera del juego (" .. motivo .. "), ultimo olvidado")
end

-- ----------------------------------------------------------
--  Handler de envio.
--    - texto vacio + tenemos ultimo -> activamos flag para
--      tragarnos el proximo prompt y reenviamos ultimo.
--    - texto con contenido          -> graba como nuevo ultimo
--      (excepto nuestros comandos internos).
-- ----------------------------------------------------------
local function on_send(_, text)
    if type(text) ~= "string" then return end

    -- La variable global `command` de Mudlet NO sirve para distinguir
    -- origen (probado: solo se actualiza al matchear un alias; para
    -- inputs directos `oeste`, `este`, etc. queda el residuo del alias
    -- previo, asi que `is_user` se quedaba siempre en true).
    --
    -- En su lugar inspeccionamos el call stack con debug.traceback().
    -- Heuristica de origen segun el call stack:
    --   alias:    presente -> user input (incluso si tambien hay
    --             script:, porque el script lo llamo el alias del
    --             user al teclear).
    --   trigger:  presente -> auto (script de trigger XML/temp).
    --   key:      presente -> auto (script de un Key bind, teclado).
    --   script:   presente -> auto (script de un Script de paquete
    --             llamado desde un tempTrigger o un temporizador).
    --   nada     -> input directo de cmdline.
    local in_script = false
    if type(debug) == "table" and debug.traceback then
        local tb = debug.traceback("", 2):lower()
        if tb:find("alias:") then
            in_script = false
        elseif tb:find("trigger") or tb:find("key:") or tb:find("script:") then
            in_script = true
        end
    end
    local is_user = not in_script

    dbg("on_send: text='" .. text .. "' len=" .. #text ..
        " is_user=" .. tostring(is_user))

    if not enter_repite.en_juego then
        dbg("  fuera del juego, ni grabo ni repito")
        return
    end

    -- Lo que se manda con el paginador abierto es su respuesta: el
    -- Enter en vacio continua y una letra elige. Ni se repite ni se
    -- graba. El siguiente "(C)ontinuar..." vuelve a abrirlo.
    if enter_repite.en_paginador and is_user then
        enter_repite.en_paginador = false
        dbg("  respuesta al paginador, ni grabo ni repito")
        return
    end

    if text == "" then
        enter_repite._envios_vacios = enter_repite._envios_vacios + 1
        if not enter_repite.enabled then
            dbg("  enter vacio pero paquete deshabilitado, paso")
            return
        end
        if not is_user then
            -- Un script mando "" — rarisimo, ignoramos sin hacer nada.
            dbg("  enter vacio de script (no del usuario), ignoro")
            return
        end
        if not enter_repite.ultimo or enter_repite.ultimo == "" then
            dbg("  enter vacio pero no hay ultimo, dejo pasar el ''")
            return
        end

        -- Un solo reenvio pendiente a la vez. Si ya hay uno (has pulsado
        -- varios Enter rapidos sin esperar respuesta del MUD), este Enter
        -- solo suma uno a la cola: se reenviara igualmente, pero NO se
        -- monta otro trigger. Si montaramos varios, todos dispararian
        -- para las primeras lineas entrantes y borrariamos lineas utiles
        -- del combate. En spam rapido algunos prompts vacios quedaran
        -- visibles, pero a cambio NUNCA se pierde una linea util.
        if enter_repite._trigger_pendiente then
            enter_repite._cola = enter_repite._cola + 1
            enter_repite._triggers_omitidos =
                enter_repite._triggers_omitidos + 1
            dbg("  reenvio pendiente del enter anterior, a la cola " ..
                "(cola=" .. enter_repite._cola .. ", omitidos=" ..
                enter_repite._triggers_omitidos .. ")")
            return
        end

        -- El comando NO se reenvia ya: primero se deja llegar (y se
        -- borra) lo que contesta GMud al "", y despues se manda. GMud
        -- contesta al "" de una sola vez: una linea en blanco
        -- (configurar +blank) y el prompt, que puede ocupar varias
        -- lineas (el de quien tiene la Fuerza ocupa dos) y cuya ultima
        -- linea llega sin salto: Mudlet la suelta pasados 300 ms sin
        -- datos. La respuesta al comando llega un pulso (250 ms) despues
        -- de la del "", asi que si se mandaran juntos Mudlet pegaria esa
        -- ultima linea a la primera de la respuesta y se borrarian juntas.
        --
        -- Asi que se borra todo lo que llega seguido y se reenvia en
        -- cuanto llega una linea tras una pausa (es la ultima del prompt,
        -- la que Mudlet solto por tiempo) o tras un rato sin que llegue
        -- nada mas (prompt quitado, o de una sola linea sin blank).
        local cmd = enter_repite.ultimo
        local hecho, id, lineas = false, nil, 0
        local anterior, turno = nil, 0
        enter_repite._trigger_pendiente = true
        enter_repite._cola = 1

        local function reenviar(motivo)
            if hecho then return end
            hecho = true
            if id then pcall(killTrigger, id) end
            local veces = enter_repite._cola
            enter_repite._cola = 0
            enter_repite._trigger_pendiente = false
            if not enter_repite.en_juego then
                dbg("salio del juego mientras esperaba, no reenvio")
                return
            end
            dbg("reenvio '" .. cmd .. "' x" .. veces .. " (" .. motivo .. ")")
            for _ = 1, veces do send(cmd) end
        end

        dbg("  montando tempLineTrigger one-shot, reenviare '" .. cmd ..
            "' cuando llegue el prompt")
        -- Como mucho MAX_LINEAS: si llega mas, es otra cosa (combate,
        -- canales) y no se borra.
        id = tempLineTrigger(0, MAX_LINEAS, function()
            if hecho then return end
            local t = reloj()
            local tras_pausa = anterior ~= nil and (t - anterior) >= PAUSA
            anterior = t
            lineas = lineas + 1
            enter_repite._lineas_borradas =
                enter_repite._lineas_borradas + 1
            dbg("tempLineTrigger one-shot disparado, deleteLine() (#"
                .. enter_repite._lineas_borradas .. ", tras pausa="
                .. tostring(tras_pausa) .. ")")
            deleteLine()
            if tras_pausa then
                reenviar("ultima linea del prompt")
            elseif lineas >= MAX_LINEAS then
                reenviar("tope de lineas")
            else
                -- Si no llega nada mas, era todo: reenviar. Cada linea
                -- nueva anula la espera de la anterior.
                turno = turno + 1
                local mio = turno
                tempTimer(SILENCIO, function()
                    if turno == mio then reenviar("silencio") end
                end)
            end
        end)
        -- Sin blank ni prompt no llega nada: no esperar mas.
        tempTimer(SIN_RESPUESTA, function()
            if lineas == 0 then reenviar("no llego nada") end
        end)
        return
    end

    if IGNORAR[text] then
        dbg("  comando interno '" .. text .. "', no lo grabo como ultimo")
        return
    end

    -- NUNCA grabar una contrasena como "ultimo comando": un Enter en
    -- vacio la reenviaria al MUD como si fuera un comando.
    if lleva_clave(text) then
        dbg("  comando con contrasena, no lo grabo como ultimo")
        return
    end

    -- Solo grabamos como "ultimo" si el envio vino del usuario.
    -- Asi NO grabamos lo que mandan triggers automaticos, que es lo
    -- que el usuario espera (igual que el historial flecha-arriba).
    if is_user then
        enter_repite.ultimo = text
        enter_repite._grabados_user = enter_repite._grabados_user + 1
        dbg("  grabado como ultimo (USER): '" .. text .. "'")
    else
        enter_repite._ignorados_script = enter_repite._ignorados_script + 1
        dbg("  NO grabado, vino de script/trigger: '" .. text .. "'")
    end
end

-- Quitamos los handlers y triggers previos (si recargamos el paquete)
-- y volvemos a registrar unos limpios.
for _, id in ipairs(enter_repite._handlers or {}) do
    pcall(killAnonymousEventHandler, id)
end
for _, id in ipairs(enter_repite._triggers or {}) do
    pcall(killTrigger, id)
end
if enter_repite._send_handler then
    pcall(killAnonymousEventHandler, enter_repite._send_handler)
    enter_repite._send_handler = nil
end

enter_repite._handlers = {
    registerAnonymousEventHandler("sysDataSendRequest", on_send),
    -- El MUD manda Char.Name al entrar el personaje en el juego.
    registerAnonymousEventHandler("gmcp.Char.Name", function()
        if not enter_repite.en_juego then
            enter_repite.en_juego = true
            dbg("dentro del juego (Char.Name)")
        end
    end),
    registerAnonymousEventHandler("sysConnectionEvent", function()
        fuera_del_juego("conexion")
    end),
    registerAnonymousEventHandler("sysDisconnectionEvent", function()
        fuera_del_juego("desconexion")
    end),
}

enter_repite._triggers = {
    -- Paginador de GMud (comm.c, pager_output).
    tempTrigger("(C)ontinuar, (R)epetir, (A)tr", function()
        enter_repite.en_paginador = true
        dbg("paginador abierto")
    end),
    -- Menu de la cuenta (cuenta.c): se ha salido del personaje.
    tempTrigger("Órdenes de la cuenta:", function()
        fuera_del_juego("menu de la cuenta")
    end),
}

-- Si el paquete se instala con el personaje ya dentro, Char.Name ya llego.
if gmcp and gmcp.Char and gmcp.Char.Name then
    enter_repite.en_juego = true
end

-- ----------------------------------------------------------
--  Comandos on / off / help.
-- ----------------------------------------------------------
function enter_repite.on()
    enter_repite.enabled = true
    cecho("\n<green>enter_repite ON.<reset> ")
    cecho("Enter sobre linea vacia reenvia el ultimo comando.\n")
    if enter_repite.ultimo then
        cecho("  Ultimo grabado: <yellow>" .. enter_repite.ultimo .. "<reset>\n")
    else
        cecho("  Aun no hay ultimo comando grabado en esta sesion.\n")
    end
end

function enter_repite.off()
    enter_repite.enabled = false
    cecho("\n<yellow>enter_repite OFF.<reset> ")
    cecho("Enter sobre linea vacia no hace nada.\n")
end

function enter_repite.debug_toggle(arg)
    if arg == "on"  then enter_repite.debug = true
    elseif arg == "off" then enter_repite.debug = false
    else                 enter_repite.debug = not enter_repite.debug
    end
    cecho("\n<cyan>enter_repite debug:<reset> ")
    cecho(enter_repite.debug and "<green>ON<reset>" or "<yellow>OFF<reset>")
    cecho(string.format(
        "\n  envios del usuario grabados:        <yellow>%d<reset>\n",
        enter_repite._grabados_user))
    cecho(string.format(
        "  envios de scripts/triggers ignor.:  <yellow>%d<reset>\n",
        enter_repite._ignorados_script))
    cecho(string.format(
        "  envios vacios detectados:           <yellow>%d<reset>\n",
        enter_repite._envios_vacios))
    cecho(string.format(
        "  lineas borradas (prompts):          <yellow>%d<reset>\n",
        enter_repite._lineas_borradas))
    cecho(string.format(
        "  triggers omitidos (spam rapido):    <yellow>%d<reset>\n",
        enter_repite._triggers_omitidos))
    cecho(string.format(
        "  trigger pendiente ahora mismo:      <yellow>%s<reset>\n",
        tostring(enter_repite._trigger_pendiente)))
    cecho(string.format(
        "  dentro del juego:                   <yellow>%s<reset>\n",
        tostring(enter_repite.en_juego)))
    cecho(string.format(
        "  paginador abierto:                  <yellow>%s<reset>\n",
        tostring(enter_repite.en_paginador)))
    cecho(string.format(
        "  ultimo grabado:                     <yellow>%s<reset>\n",
        tostring(enter_repite.ultimo)))
    cecho("\n  Solo se graba lo que tecleas tu (incluido lo que mandan tus\n")
    cecho("  aliases). Lo que mandan los triggers automaticos NO cuenta\n")
    cecho("  como ultimo. La distincion se hace inspeccionando el call\n")
    cecho("  stack (debug.traceback): si menciona 'Trigger', es automatico.\n\n")
end

function enter_repite.help()
    cecho("\n<cyan>=== enter_repite ===<reset>\n")
    cecho("  Pulsa <yellow>Enter<reset> sobre una linea de input VACIA\n")
    cecho("  para reenviar el ultimo comando que mandaste al MUD.\n")
    cecho("  Solo dentro del juego; con el paginador abierto, Enter continua.\n\n")
    cecho("  <yellow>repiteon<reset>          Activa (por defecto).\n")
    cecho("  <yellow>repiteoff<reset>         Desactiva.\n")
    cecho("  <yellow>repitedebug<reset>       Toggle del modo debug + estadisticas.\n")
    cecho("  <yellow>repitedebug on/off<reset> Fija el modo debug.\n")
    cecho("  <yellow>repitehelp<reset>        Esta ayuda.\n\n")
    cecho("  Estado: ")
    if enter_repite.enabled then
        cecho("<green>ON<reset>\n")
    else
        cecho("<yellow>OFF<reset>\n")
    end
    if enter_repite.ultimo then
        cecho("  Ultimo grabado: <yellow>" .. enter_repite.ultimo .. "<reset>\n")
    else
        cecho("  Aun no hay ultimo comando grabado en esta sesion.\n")
    end
end

cecho("\n<green>enter_repite cargado.<reset> ")
cecho("Enter sobre vacio = ultimo comando. Comandos: ")
cecho("<yellow>repiteon<reset>, <yellow>repiteoff<reset>, <yellow>repitehelp<reset>.\n")
]]></script>
            </Script>
        </ScriptGroup>
    
        <ScriptGroup isActive="yes" isFolder="yes">
            <name>Keypad</name>
            <packageName></packageName>
            <Script isActive="yes" isFolder="no">
                <name>Keypad_Core</name>
                <packageName></packageName>
                <eventHandlerList />
                <script><![CDATA[
-- ================================================================
--  keypad - nucleo: asignaciones por defecto, personalizadas y
--  persistencia en keypad_config.lua (fuera del XML).
-- ================================================================

keypad = keypad or {}

keypad.DEFAULTS = {
    ["8"] = "norte",      ["2"] = "sur",
    ["6"] = "este",       ["4"] = "oeste",
    ["9"] = "noreste",    ["7"] = "noroeste",
    ["3"] = "sureste",    ["1"] = "suroeste",
    ["-"] = "arriba",     ["+"] = "abajo",
    ["5"] = "quien",      ["0"] = "mirar",
    ["."] = "esconderse", ["/"] = "inventario",
    ["*"] = "ficha",
}

-- Orden de listado en definetecla (mismo orden que la ayuda).
keypad.ORDEN = { "8", "2", "6", "4", "9", "7", "3", "1",
                 "-", "+", "5", "0", ".", "/", "*" }

keypad.config_file = getMudletHomeDir() .. "/keypad_config.lua"

-- Solo las teclas personalizadas; lo demas cae a DEFAULTS.
keypad.mapa = keypad.mapa or {}

function keypad.cargar()
    if io.exists(keypad.config_file) then
        local tmp = {}
        local ok = pcall(table.load, keypad.config_file, tmp)
        if ok then
            keypad.mapa = tmp
        else
            cecho("\n<red>[keypad] No pude leer " .. keypad.config_file ..
                  "; sigo con los valores por defecto.<reset>\n")
        end
    end
end

function keypad.guardar()
    local ok, err = pcall(table.save, keypad.config_file, keypad.mapa)
    if not ok then
        cecho("\n<red>[keypad] No pude guardar la config: " ..
              tostring(err) .. "<reset>\n")
    end
end

function keypad.comando(tecla)
    return keypad.mapa[tecla] or keypad.DEFAULTS[tecla]
end

function keypad.pulsar(tecla)
    local cmd = keypad.comando(tecla)
    if cmd then send(cmd) end
end

keypad.cargar()

local n_pers = 0
for _ in pairs(keypad.mapa) do n_pers = n_pers + 1 end
local nota_pers = (n_pers > 0)
    and (" Teclas personalizadas: " .. n_pers .. ".")
    or ""
cecho("\n<green>keypad cargado<reset><green>.<reset>" ..
      nota_pers ..
      " Para personalizar las teclas: <yellow>definetecla<reset>." ..
      " Ayuda completa: <yellow>keypadhelp<reset>.\n")
]]></script>
            </Script>
        </ScriptGroup>
    
        <ScriptGroup isActive="yes" isFolder="yes">
            <name>Salida_Rapida</name>
            <packageName></packageName>
            <Script isActive="yes" isFolder="no">
                <name>Salida_Rapida_Core</name>
                <packageName></packageName>
                <eventHandlerList />
                <script><![CDATA[
-- ================================================================
--  Salida rapida: Ctrl+1 ... Ctrl+0 leen las diez ultimas lineas
--  Adaptado de QuickOutput, de ironcross32 (licencia MIT):
--  https://github.com/ironcross32/QuickOutput-for-Mudlet
-- ================================================================
--
--  Copyright (c) 2023 ironcross32
--
--  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.
--

salida_rapida = salida_rapida or {}
salida_rapida.espera = 0.3        -- segundos para contar una segunda pulsacion
salida_rapida.reloj = salida_rapida.reloj or 0
salida_rapida.ultima = salida_rapida.ultima or 0   -- numero de la ultima linea pedida

-- Dice un texto por el lector de pantalla sin escribirlo en la ventana:
-- si se escribiera, pasaria a ser la ultima linea y descolocaria la cuenta.
function salida_rapida.decir(texto)
    if getOS and getOS() == "windows" and announce then
        announce(texto)
    elseif ttsQueue then
        ttsQueue(texto)
    elseif announce then
        announce(texto)
    end
end

-- El texto de la linea numero n empezando por el final (1 = la ultima).
function salida_rapida.texto(n)
    local linea = ""
    if n == 1 then
        moveCursorEnd()
        linea = getCurrentLine()
        -- La ultima suele ser la linea vacia donde esta el cursor.
        if linea == "" then
            moveCursorUp()
            linea = getCurrentLine()
        end
    elseif moveCursor(0, getLastLineNumber() - n) then
        linea = getCurrentLine()
    else
        linea = "no hay tantas lineas"
    end
    moveCursorEnd()
    if not linea or linea == "" then
        linea = "en blanco"
    end
    return linea
end

-- Lo que hace cada tecla: leer la linea, o copiarla si es la segunda
-- pulsacion seguida de la misma tecla.
function salida_rapida.linea(n)
    local texto = salida_rapida.texto(n)
    local ahora = os.clock()
    if salida_rapida.ultima == n and ahora - salida_rapida.reloj <= salida_rapida.espera then
        salida_rapida.ultima = 0
        if setClipboardText(texto) then
            salida_rapida.decir("copiado")
        end
        return
    end
    salida_rapida.ultima = n
    salida_rapida.reloj = ahora
    salida_rapida.decir(texto)
end

cecho("\n<green>salida rapida cargada<reset><green>.<reset>" ..
      " Ctrl con los numeros lee las ultimas lineas." ..
      " Ayuda: <yellow>salidarapida<reset>.\n")
]]></script>
            </Script>
        </ScriptGroup>
    
    </ScriptPackage>
    <KeyPackage>
        <KeyGroup isActive="yes" isFolder="yes">
            <name>ChannelHistory</name>
            <packageName></packageName>
            <script></script>
            <eventHandlerList />
            <Key isActive="yes" isFolder="no">
                <name>Alt+1</name>
                <packageName></packageName>
                <script><![CDATA[channelhist.history_get(1)]]></script>
                <command></command>
                <keyModifier>134217728</keyModifier>
                <keyCode>49</keyCode>
            </Key>
            <Key isActive="yes" isFolder="no">
                <name>Alt+2</name>
                <packageName></packageName>
                <script><![CDATA[channelhist.history_get(2)]]></script>
                <command></command>
                <keyModifier>134217728</keyModifier>
                <keyCode>50</keyCode>
            </Key>
            <Key isActive="yes" isFolder="no">
                <name>Alt+3</name>
                <packageName></packageName>
                <script><![CDATA[channelhist.history_get(3)]]></script>
                <command></command>
                <keyModifier>134217728</keyModifier>
                <keyCode>51</keyCode>
            </Key>
            <Key isActive="yes" isFolder="no">
                <name>Alt+4</name>
                <packageName></packageName>
                <script><![CDATA[channelhist.history_get(4)]]></script>
                <command></command>
                <keyModifier>134217728</keyModifier>
                <keyCode>52</keyCode>
            </Key>
            <Key isActive="yes" isFolder="no">
                <name>Alt+5</name>
                <packageName></packageName>
                <script><![CDATA[channelhist.history_get(5)]]></script>
                <command></command>
                <keyModifier>134217728</keyModifier>
                <keyCode>53</keyCode>
            </Key>
            <Key isActive="yes" isFolder="no">
                <name>Alt+6</name>
                <packageName></packageName>
                <script><![CDATA[channelhist.history_get(6)]]></script>
                <command></command>
                <keyModifier>134217728</keyModifier>
                <keyCode>54</keyCode>
            </Key>
            <Key isActive="yes" isFolder="no">
                <name>Alt+7</name>
                <packageName></packageName>
                <script><![CDATA[channelhist.history_get(7)]]></script>
                <command></command>
                <keyModifier>134217728</keyModifier>
                <keyCode>55</keyCode>
            </Key>
            <Key isActive="yes" isFolder="no">
                <name>Alt+8</name>
                <packageName></packageName>
                <script><![CDATA[channelhist.history_get(8)]]></script>
                <command></command>
                <keyModifier>134217728</keyModifier>
                <keyCode>56</keyCode>
            </Key>
            <Key isActive="yes" isFolder="no">
                <name>Alt+9</name>
                <packageName></packageName>
                <script><![CDATA[channelhist.history_get(9)]]></script>
                <command></command>
                <keyModifier>134217728</keyModifier>
                <keyCode>57</keyCode>
            </Key>
            <Key isActive="yes" isFolder="no">
                <name>Alt+0</name>
                <packageName></packageName>
                <script><![CDATA[channelhist.history_get(10)]]></script>
                <command></command>
                <keyModifier>134217728</keyModifier>
                <keyCode>48</keyCode>
            </Key>
            <Key isActive="yes" isFolder="no">
                <name>Alt+Left</name>
                <packageName></packageName>
                <script><![CDATA[channelhist.history_catprev()]]></script>
                <command></command>
                <keyModifier>134217728</keyModifier>
                <keyCode>16777234</keyCode>
            </Key>
            <Key isActive="yes" isFolder="no">
                <name>Alt+Right</name>
                <packageName></packageName>
                <script><![CDATA[channelhist.history_catnext()]]></script>
                <command></command>
                <keyModifier>134217728</keyModifier>
                <keyCode>16777236</keyCode>
            </Key>
            <Key isActive="yes" isFolder="no">
                <name>Alt+Up</name>
                <packageName></packageName>
                <script><![CDATA[channelhist.history_msgprev(1)]]></script>
                <command></command>
                <keyModifier>134217728</keyModifier>
                <keyCode>16777235</keyCode>
            </Key>
            <Key isActive="yes" isFolder="no">
                <name>Alt+Down</name>
                <packageName></packageName>
                <script><![CDATA[channelhist.history_msgnext(1)]]></script>
                <command></command>
                <keyModifier>134217728</keyModifier>
                <keyCode>16777237</keyCode>
            </Key>
            <Key isActive="yes" isFolder="no">
                <name>Alt+PageUp</name>
                <packageName></packageName>
                <script><![CDATA[channelhist.history_msgprev(10)]]></script>
                <command></command>
                <keyModifier>134217728</keyModifier>
                <keyCode>16777238</keyCode>
            </Key>
            <Key isActive="yes" isFolder="no">
                <name>Alt+PageDown</name>
                <packageName></packageName>
                <script><![CDATA[channelhist.history_msgnext(10)]]></script>
                <command></command>
                <keyModifier>134217728</keyModifier>
                <keyCode>16777239</keyCode>
            </Key>
            <Key isActive="yes" isFolder="no">
                <name>Alt+Home</name>
                <packageName></packageName>
                <script><![CDATA[channelhist.history_msgprev(2000)]]></script>
                <command></command>
                <keyModifier>134217728</keyModifier>
                <keyCode>16777232</keyCode>
            </Key>
            <Key isActive="yes" isFolder="no">
                <name>Alt+End</name>
                <packageName></packageName>
                <script><![CDATA[channelhist.history_msgnext(2000)]]></script>
                <command></command>
                <keyModifier>134217728</keyModifier>
                <keyCode>16777233</keyCode>
            </Key>
            <Key isActive="yes" isFolder="no">
                <name>Shift+Alt+Left</name>
                <packageName></packageName>
                <script><![CDATA[channelhist.category_move("left")]]></script>
                <command></command>
                <keyModifier>167772160</keyModifier>
                <keyCode>16777234</keyCode>
            </Key>
            <Key isActive="yes" isFolder="no">
                <name>Shift+Alt+Right</name>
                <packageName></packageName>
                <script><![CDATA[channelhist.category_move("right")]]></script>
                <command></command>
                <keyModifier>167772160</keyModifier>
                <keyCode>16777236</keyCode>
            </Key>
            <Key isActive="yes" isFolder="no">
                <name>Alt+Delete</name>
                <packageName></packageName>
                <script><![CDATA[channelhist.history_delete()]]></script>
                <command></command>
                <keyModifier>134217728</keyModifier>
                <keyCode>16777223</keyCode>
            </Key>
            <Key isActive="yes" isFolder="no">
                <name>Alt+Space</name>
                <packageName></packageName>
                <script><![CDATA[channelhist.history_msgprev(0)]]></script>
                <command></command>
                <keyModifier>134217728</keyModifier>
                <keyCode>32</keyCode>
            </Key>
            <Key isActive="yes" isFolder="no">
                <name>Alt+Shift+Space</name>
                <packageName></packageName>
                <script><![CDATA[channelhist.history_msgcopy()]]></script>
                <command></command>
                <keyModifier>167772160</keyModifier>
                <keyCode>32</keyCode>
            </Key>
            <Key isActive="yes" isFolder="no">
                <name>Alt+Shift+Enter</name>
                <packageName></packageName>
                <script><![CDATA[channelhist.history_notepad()]]></script>
                <command></command>
                <keyModifier>167772160</keyModifier>
                <keyCode>16777220</keyCode>
            </Key>
            <Key isActive="yes" isFolder="no">
                <name>Alt+Shift+T</name>
                <packageName></packageName>
                <script><![CDATA[channelhist.toggle_timestamps()]]></script>
                <command></command>
                <keyModifier>167772160</keyModifier>
                <keyCode>84</keyCode>
            </Key>
        </KeyGroup>
        <KeyGroup isActive="yes" isFolder="yes">
            <name>Keypad</name>
            <packageName></packageName>
            <script></script>
            <eventHandlerList />

            <Key isActive="yes" isFolder="no"><name>Num 8 (defecto: norte)</name>
                <script><![CDATA[keypad.pulsar("8")]]></script>
                <command></command><packageName></packageName>
                <keyModifier>536870912</keyModifier><keyCode>56</keyCode></Key>

            <Key isActive="yes" isFolder="no"><name>Num 2 (defecto: sur)</name>
                <script><![CDATA[keypad.pulsar("2")]]></script>
                <command></command><packageName></packageName>
                <keyModifier>536870912</keyModifier><keyCode>50</keyCode></Key>

            <Key isActive="yes" isFolder="no"><name>Num 6 (defecto: este)</name>
                <script><![CDATA[keypad.pulsar("6")]]></script>
                <command></command><packageName></packageName>
                <keyModifier>536870912</keyModifier><keyCode>54</keyCode></Key>

            <Key isActive="yes" isFolder="no"><name>Num 4 (defecto: oeste)</name>
                <script><![CDATA[keypad.pulsar("4")]]></script>
                <command></command><packageName></packageName>
                <keyModifier>536870912</keyModifier><keyCode>52</keyCode></Key>

            <Key isActive="yes" isFolder="no"><name>Num 9 (defecto: noreste)</name>
                <script><![CDATA[keypad.pulsar("9")]]></script>
                <command></command><packageName></packageName>
                <keyModifier>536870912</keyModifier><keyCode>57</keyCode></Key>

            <Key isActive="yes" isFolder="no"><name>Num 7 (defecto: noroeste)</name>
                <script><![CDATA[keypad.pulsar("7")]]></script>
                <command></command><packageName></packageName>
                <keyModifier>536870912</keyModifier><keyCode>55</keyCode></Key>

            <Key isActive="yes" isFolder="no"><name>Num 3 (defecto: sureste)</name>
                <script><![CDATA[keypad.pulsar("3")]]></script>
                <command></command><packageName></packageName>
                <keyModifier>536870912</keyModifier><keyCode>51</keyCode></Key>

            <Key isActive="yes" isFolder="no"><name>Num 1 (defecto: suroeste)</name>
                <script><![CDATA[keypad.pulsar("1")]]></script>
                <command></command><packageName></packageName>
                <keyModifier>536870912</keyModifier><keyCode>49</keyCode></Key>

            <Key isActive="yes" isFolder="no"><name>Num 5 (defecto: quien)</name>
                <script><![CDATA[keypad.pulsar("5")]]></script>
                <command></command><packageName></packageName>
                <keyModifier>536870912</keyModifier><keyCode>53</keyCode></Key>

            <Key isActive="yes" isFolder="no"><name>Num 0 (defecto: mirar)</name>
                <script><![CDATA[keypad.pulsar("0")]]></script>
                <command></command><packageName></packageName>
                <keyModifier>536870912</keyModifier><keyCode>48</keyCode></Key>

            <Key isActive="yes" isFolder="no"><name>Num - (defecto: arriba)</name>
                <script><![CDATA[keypad.pulsar("-")]]></script>
                <command></command><packageName></packageName>
                <keyModifier>536870912</keyModifier><keyCode>45</keyCode></Key>

            <Key isActive="yes" isFolder="no"><name>Num + (defecto: abajo)</name>
                <script><![CDATA[keypad.pulsar("+")]]></script>
                <command></command><packageName></packageName>
                <keyModifier>536870912</keyModifier><keyCode>43</keyCode></Key>

            <Key isActive="yes" isFolder="no"><name>Num . (defecto: esconderse)</name>
                <script><![CDATA[keypad.pulsar(".")]]></script>
                <command></command><packageName></packageName>
                <keyModifier>536870912</keyModifier><keyCode>46</keyCode></Key>

            <Key isActive="yes" isFolder="no"><name>Num / (defecto: inventario)</name>
                <script><![CDATA[keypad.pulsar("/")]]></script>
                <command></command><packageName></packageName>
                <keyModifier>536870912</keyModifier><keyCode>47</keyCode></Key>

            <Key isActive="yes" isFolder="no"><name>Num * (defecto: ficha)</name>
                <script><![CDATA[keypad.pulsar("*")]]></script>
                <command></command><packageName></packageName>
                <keyModifier>536870912</keyModifier><keyCode>42</keyCode></Key>
        </KeyGroup>
    
        <KeyGroup isActive="yes" isFolder="yes">
            <name>Salida_Rapida</name>
            <packageName></packageName>
            <script></script>
            <eventHandlerList />

            <Key isActive="yes" isFolder="no"><name>Ctrl+1 (ultima linea)</name>
                <script><![CDATA[salida_rapida.linea(1)]]></script>
                <command></command><packageName></packageName>
                <keyModifier>67108864</keyModifier><keyCode>49</keyCode></Key>

            <Key isActive="yes" isFolder="no"><name>Ctrl+2</name>
                <script><![CDATA[salida_rapida.linea(2)]]></script>
                <command></command><packageName></packageName>
                <keyModifier>67108864</keyModifier><keyCode>50</keyCode></Key>

            <Key isActive="yes" isFolder="no"><name>Ctrl+3</name>
                <script><![CDATA[salida_rapida.linea(3)]]></script>
                <command></command><packageName></packageName>
                <keyModifier>67108864</keyModifier><keyCode>51</keyCode></Key>

            <Key isActive="yes" isFolder="no"><name>Ctrl+4</name>
                <script><![CDATA[salida_rapida.linea(4)]]></script>
                <command></command><packageName></packageName>
                <keyModifier>67108864</keyModifier><keyCode>52</keyCode></Key>

            <Key isActive="yes" isFolder="no"><name>Ctrl+5</name>
                <script><![CDATA[salida_rapida.linea(5)]]></script>
                <command></command><packageName></packageName>
                <keyModifier>67108864</keyModifier><keyCode>53</keyCode></Key>

            <Key isActive="yes" isFolder="no"><name>Ctrl+6</name>
                <script><![CDATA[salida_rapida.linea(6)]]></script>
                <command></command><packageName></packageName>
                <keyModifier>67108864</keyModifier><keyCode>54</keyCode></Key>

            <Key isActive="yes" isFolder="no"><name>Ctrl+7</name>
                <script><![CDATA[salida_rapida.linea(7)]]></script>
                <command></command><packageName></packageName>
                <keyModifier>67108864</keyModifier><keyCode>55</keyCode></Key>

            <Key isActive="yes" isFolder="no"><name>Ctrl+8</name>
                <script><![CDATA[salida_rapida.linea(8)]]></script>
                <command></command><packageName></packageName>
                <keyModifier>67108864</keyModifier><keyCode>56</keyCode></Key>

            <Key isActive="yes" isFolder="no"><name>Ctrl+9</name>
                <script><![CDATA[salida_rapida.linea(9)]]></script>
                <command></command><packageName></packageName>
                <keyModifier>67108864</keyModifier><keyCode>57</keyCode></Key>

            <Key isActive="yes" isFolder="no"><name>Ctrl+0 (decima linea)</name>
                <script><![CDATA[salida_rapida.linea(10)]]></script>
                <command></command><packageName></packageName>
                <keyModifier>67108864</keyModifier><keyCode>48</keyCode></Key>
        </KeyGroup>
    
    </KeyPackage>
</MudletPackage>
