[Home] [Downloads] [Search] [Help/forum]


Register forum user name Search FAQ

Gammon Forum

Notice: Any messages purporting to come from this site telling you that your password has expired, or that you need to "verify" your details, making threats, or asking for money, are spam. We do not email users with any such messages. If you have lost your password you can obtain a new one by using the password reset link.
[Folder]  Entire forum
-> [Folder]  MUSHclient
. -> [Folder]  Tips and tricks
. . -> [Subject]  Reflex - A table-based alias/trigger/timer interface

Reflex - A table-based alias/trigger/timer interface

It is now over 60 days since the last post. This thread is closed.     [Refresh] Refresh page


Posted by Twisol   USA  (2,257 posts)  [Biography] bio
Date Sat 13 Feb 2010 09:12 AM (UTC)

Amended on Sun 14 Feb 2010 07:35 AM (UTC) by Twisol

Message
This is a decently feature-complete working draft of my Reflex library, which borrows ideas from the var.lua module as well as techniques from the addxml.lua library. Below is the library source; I'll post some examples afterwards.

-- Package table
local Reflex = {}

-- Caches
local caches = {}

-- Pluralize wordS: "alias" -> "aliases", "trigger" -> "triggers"
local plural = function(word)
  if word:sub(#word) == "s" then
    return word .. "es"
  elseif word:sub(#word) == "y" then
    return word:sub(1, #word-1) .. "ies"
  else
    return word .. "s"
  end
end

-- Proper-case: "fOoObaR" -> "Foobar", "trigger" -> "Trigger"
local proper = function(word)
  return word:sub(1,1):upper() .. word:sub(2):lower()
end

-- Converts certain characters into XML entities
local xml_escape ={
  ["<"] = "&lt;",
  [">"] = "&gt;",
  ["&"] = "&amp;",
  ['"'] = "&quot;",
  
  __call = function(tbl, line)
    return (string.gsub(line, '[<>&"]', tbl))
  end,
}
setmetatable(xml_escape, xml_escape)

-- Converts a table of details into importable XML
local table_to_xml = function(tag, details)
  local xml = {}
  table.insert(xml, "<" .. plural(tag) .. ">")
  
  -- store and remove send from the list
  local send = "<send>" .. (details.send or "") .. "</send>"
  details.send = nil
  
  table.insert(xml, "<" .. tag)
  for k,v in pairs(details) do
    table.insert(xml, ([[ %s="%s"]]):format(k, xml_escape(v)))
  end
  table.insert(xml, ">")
  
  table.insert(xml, send)
  table.insert(xml, "</" .. tag .. "></" .. plural(tag) .. ">")
  
  return table.concat(xml, "")
end


local new_reflex_proxy = function(item_type, opts)
  -- Dynamically obtain these functions.
  local GetXList   = _G["Get" .. proper(item_type) .. "List"]
  local GetXOption = _G["Get" .. proper(item_type) .. "Option"]
  local SetXOption = _G["Set" .. proper(item_type) .. "Option"]
  local DeleteX    = _G["Delete" .. proper(item_type)]
  local IsX        = _G["Is" .. proper(item_type)]

  -- Stores previously-created reflex proxies
  local cache = {}
  caches[item_type] = cache
  
  -- built-in default/required checking for options
  local opt_meta = {
    __metatable = false,
    __call = function(tbl, item, arg, mode)
      if arg == nil then
        assert(not tbl.required, "'" .. tbl.name .. "' must be supplied.")
        arg = tbl.default
      end
      return tbl[1](item, arg, mode)
    end,
  }
  
  -- copy the options and give each a metatable
  do
    local _opts = opts
    opts = {}
    for k,v in pairs(_opts) do
      if type(v) == "function" then
        opts[k] = setmetatable({v, name=k}, opt_meta)
      elseif type(v) == "table" then
        v = {
          [1] = v[1],
          default = v.default,
          required = v.required,
          name = k,
        }
        opts[k] = setmetatable(v, opt_meta)
      end
    end
  end
  
  local item_meta = {
    __metatable = false,
    
    __index = function(tbl, idx)
      assert(IsX(cache[tbl].name) == 0, proper(item_type) .. " does not exist")
      
      idx = string.lower(idx)
      
      -- retrieve the name now if requested
      if idx == "name" then
        return cache[tbl].name
      end
      
      -- check if it's a valid option
      assert(opts[idx], "Option type is invalid")
      
      -- get the value and translate it
      local val = GetXOption(cache[tbl].name, idx)
      return opts[idx](cache[tbl], val, "get")
    end,
    
    __newindex = function(tbl, idx, val)
      assert(IsX(cache[tbl].name) == 0, proper(item_type) .. " does not exist")
      
      -- check if it's a valid option
      idx = string.lower(idx)
      assert(opts[idx], "Option type is invalid")
      
      -- translate the value and set it
      val = opts[idx](cache[tbl], val, "set")
      check(SetXOption(cache[tbl].name, idx, val))
    end,
    
    __call = function(tbl, val)
      assert(type(val) == "table", item_type .. " details must be a table")
      
      -- copy the details
      local details = {}
      for k,v in pairs(val) do
        details[k] = v
      end
      details.name = cache[tbl].name
      
      -- translate the details
      for k,v in pairs(opts) do
        details[k] = opts[k](cache[tbl], details[k], "xml") or nil
      end
      
      -- convert the details to XML
      val = table_to_xml(item_type, details)
      
      -- If the reflex is unnamed, get a list of all current objects
      local names
      if cache[tbl].name == "" then
        names = GetXList() or {}
        if names then
          for _,v in ipairs(names) do
            names[v] = true
          end
        end
      else
        -- delete the previous one of this name
        DeleteX(cache[tbl].name)
      end
      
      -- import!
      ImportXML(val)
      
      -- Now compare the previous list to the new one to
      -- discover an unnamed reflex's name.
      if names then
        local name
        local reflexes = GetXList()
        if reflexes then
          for _,v in ipairs(reflexes) do
            if not names[v] then
              name = v
              break
            end
          end
        end
        
        -- use a new proxy instead of the unnamed one
        tbl = (name and Reflex[item_type][name] or nil)
      end
      
      -- Force script resolution
      tbl.script = tbl.script
      
      return tbl
    end,
  }
  
  Reflex[item_type] = setmetatable({}, {
    __metatable = false,
    
    __index = function(tbl, idx)
      assert(type(idx) == "string", item_type .. " name must be a string")
      
      local item = rawget(cache, string.lower(idx))
      if not item then
        item = {setmetatable({}, item_meta), name=idx}
        cache[string.lower(idx)] = item
        cache[item[1]] = item
      end
      
      return item[1]
    end,
    
    __newindex = function(tbl, idx, val)
      assert(type(idx) == "string", item_type .. " name must be a string")
      assert(type(val) == "nil", "Cannot assign  directly to the " .. item_type .. " object.")
      
      DeleteX(idx)
    end,
    
    __call = function(tbl, ...)
      return tbl[""](...)
    end,
  })
  
  return 
end


-- Option translation methods
local no_change = function(record, arg, mode)
  return arg
end

local translate_bool = function(record, arg, mode)
  if mode == "set" then
    return (arg == true) and "1" or "0"
  elseif mode == "get" then
    return (arg == 1) and true or false
  elseif mode == "xml" then
    return (arg == true) and "y" or "n"
  else
    return ""
  end
end

new_reflex_proxy("trigger", {
  clipboard_arg      =  no_change,
  colour_change_type =  no_change,
  custom_colour      =  no_change,
  enabled            = {translate_bool, default=true},
  expand_variables   = {translate_bool, default=false},
  group              =  no_change,
  ignore_case        = {translate_bool, default=false},
  inverse            = {translate_bool, default=false},
  italic             = {translate_bool, default=false},
  keep_evaluating    = {translate_bool, default=true},
  lines_to_match     =  no_change,
  lowercase_wildcard = {translate_bool, default=false},
  match              = {no_change,      required=true},
  match_style        =  no_change,
  multi_line         = {translate_bool, default=false},
  new_style          =  no_change,
  omit_from_log      = {translate_bool, default=false},
  omit_from_output   = {translate_bool, default=false},
  one_shot           = {translate_bool, default=false},
  other_back_color   =  no_change,
  other_text_color   =  no_change,
  regexp             = {translate_bool, default=false},
  ["repeat"]         = {translate_bool, default=false},
  script             =  no_change,
  send               =  no_change,
  send_to            = {no_change,      default="0"},
  sequence           = {no_change,      default="100"},
  sound              =  no_change,
  sound_if_inactive  = {translate_bool, default="false"},
  user               =  no_change,
  variable           =  no_change,
})

new_reflex_proxy("alias", {
  echo_alias                = {translate_bool, default=false},
  enabled                   = {translate_bool, default=true},
  expand_variables          = {translate_bool, default=false},
  group                     =  no_change,
  ignore_case               = {translate_bool, default=true},
  keep_evaluating           = {translate_bool, default=false},
  match                     = {no_change,      required=true},
  menu                      = {translate_bool, default=false},
  omit_from_command_history = {translate_bool, default=false},
  omit_from_log             = {translate_bool, default=false},
  omit_from_output          = {translate_bool, default=false},
  one_shot                  = {translate_bool, default=false},
  regexp                    = {translate_bool, default=false},
  script                    =  no_change,
  send                      =  no_change,
  send_to                   = {no_change,      default="0"},
  sequence                  = {no_change,      default="100"},
  user                      =  no_change,
  variable                  =  no_change,
})

new_reflex_proxy("timer", {
  active_closed    = {translate_bool, default=false},
  at_time          = {translate_bool, default=false},
  enabled          = {translate_bool, default=true},
  group            =  no_change,
  hour             = {no_change,      default="0"},
  minute           = {no_change,      default="0"},
  offset_hour      = {no_change,      default="0"},
  offset_minute    = {no_change,      default="0"},
  offset_second    = {no_change,      default="0.00"},
  omit_from_log    = {translate_bool, default=false},
  omit_from_output = {translate_bool, default=false},
  one_shot         = {translate_bool, default=false},
  script           =  no_change,
  second           = {no_change,      required=true},
  send             =  no_change,
  send_to          = {no_change,      default="0"},
  user             =  no_change,
  variable         =  no_change,
})

-- return Reflex
return Reflex

'Soludra' on Achaea

Blog: http://jonathan.com/
GitHub: http://github.com/Twisol
[Go to top] top

Posted by Twisol   USA  (2,257 posts)  [Biography] bio
Date Reply #1 on Sat 13 Feb 2010 09:12 AM (UTC)
Message
The module is returned dofile/require when loading the script:
Reflex = require("reflex")


The interfaces themselves are stored as Reflex.alias, etc. but you can assign them to shorter variables:
alias, trigger, timer =
  Reflex.alias, Reflex.trigger, Reflex.timer


I'll assume using the 'alias' interface for the rest of this example. You create new (or overwrite existing) objects like so:
foo = alias["name"] {
  -- options
}

-- unnamed alias
foo = alias {
  -- options
}

The returned object is a proxy table for the created trigger, which can be ignored if you don't need it. Named objects can always be re-retrieved with alias[name].

You can also directly modify all attributes of the object.
foo.enabled = true
foo.group = "mygroup"


To remove an object, you can use the following. As unnamed objects are unnamed, you will need some other way to access the name, such as through the original table returned on creation:
alias["name"] = nil
alias[foo.name] = nil



Most attributes have sensible defaults (enabled defaults to true, keep_evaluating true for triggers, ignore_case true for aliases, etc), but which are also overridable in creation or assignment. Some attributes (like "seconds" for timers) are also marked as required, causing an error if they're left out or set to nil.

I wrote this module to be an easy-to-use drop-in replacement for the XML trigger/alias/timer format, and also to have a natural way to modify/remove them using the same interface. As I noted previously, it uses concepts from both var.lua and addxml.lua to help achieve this. If there are any suggestions for improving the module, please let me know! It's still somewhat a work in progress.

'Soludra' on Achaea

Blog: http://jonathan.com/
GitHub: http://github.com/Twisol
[Go to top] top

Posted by Twisol   USA  (2,257 posts)  [Biography] bio
Date Reply #2 on Sat 13 Feb 2010 10:32 PM (UTC)
Message
I realized I left two debug Note/print calls in there. I just edited it to remove those.

'Soludra' on Achaea

Blog: http://jonathan.com/
GitHub: http://github.com/Twisol
[Go to top] top

Posted by Nick Gammon   Australia  (23,043 posts)  [Biography] bio   Forum Administrator
Date Reply #3 on Sat 13 Feb 2010 11:27 PM (UTC)
Message
That looks good and a reasonable candidate for the next release. :)

One little thing, stuff like:


"Trigger details must be a table")


Should that be more general (like, "Alias" or "Timer")?

The word "Trigger" appears in quite a few places but not "Alias" or "Timer".

- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

Posted by Nick Gammon   Australia  (23,043 posts)  [Biography] bio   Forum Administrator
Date Reply #4 on Sat 13 Feb 2010 11:29 PM (UTC)
Message
Also, how about starting with:


module (..., package.seeall)


That way its internal functions are not exposed at all.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

Posted by Twisol   USA  (2,257 posts)  [Biography] bio
Date Reply #5 on Sat 13 Feb 2010 11:34 PM (UTC)
Message
None of the internal functions are exposed in the first place, as they are all defined as local. Code within a file acts like it's within a do-end block, hiding the inner locals. Nothing is put in the global space, and the only item returned is the Reflex table.

Point taken about "trigger". I tried to use "reflex" or "object" where I could, but obviously I missed a few places.

'Soludra' on Achaea

Blog: http://jonathan.com/
GitHub: http://github.com/Twisol
[Go to top] top

Posted by Twisol   USA  (2,257 posts)  [Biography] bio
Date Reply #6 on Sat 13 Feb 2010 11:40 PM (UTC)
Message
Edited to call triggers/aliases/timers "reflexes" or "objects" instead of directly as triggers, or replaced with a concatenation of item_type where applicable.

'Soludra' on Achaea

Blog: http://jonathan.com/
GitHub: http://github.com/Twisol
[Go to top] top

Posted by Twisol   USA  (2,257 posts)  [Biography] bio
Date Reply #7 on Sun 14 Feb 2010 03:10 AM (UTC)

Amended on Sun 14 Feb 2010 07:53 AM (UTC) by Twisol

Message
Edited once more to fix an issue I just discovered where creating an unnamed reflex would fail if there were no others of that type in existance. (I had to change a call to GetXList() to "GetXList() or {}")

'Soludra' on Achaea

Blog: http://jonathan.com/
GitHub: http://github.com/Twisol
[Go to top] top

Posted by Twisol   USA  (2,257 posts)  [Biography] bio
Date Reply #8 on Sun 14 Feb 2010 07:53 AM (UTC)
Message
And edited again to change the 'menu' option for aliases to be translated as a boolean. These are all very small, simple fixes, I just hadn't tested it all extensively before posting.

I'm also considering extending the Reflex table itself (with __call) in order to easily support such operations as exporting a reflex (i.e. exported = Reflex("export", alias["test"]), and deletion of reflexes by table instead of by name (item = alias["test"]; Reflex("delete", item)). At that point, Reflex will be able to do everything Addxml can do except import macros, and I see no reason to support macros - the Accelerator function is much more powerful,

'Soludra' on Achaea

Blog: http://jonathan.com/
GitHub: http://github.com/Twisol
[Go to top] top

The dates and times for posts above are shown in Universal Co-ordinated Time (UTC).

To show them in your local time you can join the forum, and then set the 'time correction' field in your profile to the number of hours difference between your location and UTC time.


19,069 views.

It is now over 60 days since the last post. This thread is closed.     [Refresh] Refresh page

Go to topic:           Search the forum


[Go to top] top

Quick links: MUSHclient. MUSHclient help. Forum shortcuts. Posting templates. Lua modules. Lua documentation.

Information and images on this site are licensed under the Creative Commons Attribution 3.0 Australia License unless stated otherwise.

[Home]


Written by Nick Gammon - 5K   profile for Nick Gammon on Stack Exchange, a network of free, community-driven Q&A sites   Marriage equality

Comments to: Gammon Software support
[RH click to get RSS URL] Forum RSS feed ( https://gammon.com.au/rss/forum.xml )

[Best viewed with any browser - 2K]    [Hosted at HostDash]