hipScript for Ircle -- Writing Plugins

I've been whining for months about how I need to document the process of creating HipScript plugins. It's probably gonna be as much writing as the user documentation, but if I don't get started somehow it's never gonna happen at all. So, I'll just start typing here and eventually this'll turn into something like formal documentation.

My original goal was that a hipscript plugin would be no different from any ircle script designed to be /load'd. Limitations of the AppleScript environment made that impossible, but I managed to get pretty close. Any existing script designed to run as a loaded script can be converted to a plugin with just a little work. It's even possible to write a script that works either as a plugin or as a standalone /load script.

First, let's define some terminology..

An Ircle Event Handler is an applescript handler function for one of the pre-defined Ircle events, such as pubmsg(), or ctcp(). A list of all the ircle event handlers appears in the script named Events in the ircle examples folder.

A User Command Handler is an applescript handler function that provides a command the user can directly enter in the ircle input window. A handler named mycommand() in a loaded script would be invoked by /mycommand in the ircle input window.

User Command Parameters are the words typed by the user following a user command name. For example, in "/mycommand nickname servername" the parameters are the words nickname and servername.

A hipscript plugin can use any of the ircle event handlers it wants to, and there need be no differences between the handler as coded in a loaded script or a plugin script. There are differences, however, in how a user command handler is coded. Since ircle doesn't directly see the handlers contained in a plugin, it doesn't know what user command handlers a plugin might contain. To get around this, you code a user command dispatcher handler that makes the connection between hipscript and the user command handlers in your plugin.

The basic process of writing a hipscript plugin, then, involves these steps:

The HipScriptEventHandlerList property is a list of names of the ircle event handlers your plugin contains.. You code it at the top of the plugin. If your plugin contains handlers for pubmsg(), privmsg(), load(), and unload(), the property would be coded as:

property HipScriptEventHandlerList: {"pubmsg", "privmsg", "load", "unload"}

It doesn't matter what order the handler names appear in, but it's important that every handler you name in the list actually exists in your plugin. (Nasty errors will happen if not.)

The HipScriptUsercommandList property is a list of names of the user commands your plugin provides to the user. If your plugin provided the commands /msglogger and /autoquery to the user, the property would be coded as:

property HipScriptUsercommandList: {"/autoquery", "/msglogger"}

Again, order isn't important, but the "/" must appear on the front of each command name.

As mentioned above, ircle event handlers in a plugin can be coded exactly as they would be in a regular loaded script; just code a handler as you normally would, name it in the HipScriptEventHandlerList, and you're done.

User command handlers need a dispatcher function, and needs to access the user command parameters differently. In a normal loaded script, user parameters are obtained using something like:

tell app "Ircle" to set theParms to argstring

In a plugin environment, ircle's builtin argstring property isn't available to a user command, and instead you can get the arguments using:

set theParms to GetArgString()

Or, the parameters can be passed from the dispatcher function, which I guess I'd better get around to explaining. When the user enters a command named in a plugin's HipScriptUsercommandList, hipscript calls a handler named usercommand() in the plugin. The handler is definied as:

on usercommand(con, target, thestring, theCmd, theArgs)

-- con is the connection number the command was typed into
-- target is the channel name the command was typed into
-- thestring is all the words typed after the command name
-- theCmd is the name of the command, including the "/"
-- theArgs is all the words typed after the command name, as a list of strings

The job of the usercommand() handler is to compare theCmd against each of the command names the plugin supports, and invoke the right handler to process the command. In theory, the usercommand() handler could just do all the processing for each user command, but it makes for tidier code if the usercommand() handler is just a dispatcher that routes control into the correct handlers for each command. The "thestring" variable passed to the usercommand() handler is the same value that GetArgString() would return, or that Ircle's argstring property would contain in a loaded script. The con and target variables correspond to ircle's currentconnection and currentchannel properties. The theCmd variable is specific to dispatching hipscript user commands, and the theArgs variable is available to improve performance: since a user command handler often has to parse the input parameters into a list of words, this variable contains that list ready-to-use.

Okay, it seems like time for one of those highly-contrived-and-pointless examples that programming documents always contain. Let's suppose you have a loaded script that contains two commands, /foo, which echoes to the top window all the words entered after it on the command line, and /bar which echoes only the first and second words. The loaded script might look like this:

on foo()
    tell application "Ircle" 
        set theString to argstring
        echo theString
    end tell
end foo
   
on bar()
    tell application "Ircle" 
        set theString to argstring
        set oldDelims to AppleScript's text item delimiters
        set AppleScript's text item delimiters to space
        set word1 to text item 1 of theString
        set word2 to text item 2 of theString
        set AppleScript's text item delimiters to oldDelims
        echo "1: " & word1 & "2: " & word2
    end tell
end bar

That same script, as a plugin, might look like this:

property HipScriptUsercommandList: {"/bar", "/foo"}
   
on foo()
    tell application "Ircle" 
        set theString to GetArgString()
        echo theString
    end tell
end foo
   
on bar(theArgs)
    tell application "Ircle" 
        echo "1: " & (item 1 of theArgs) & "2: " & (item 2 of theArgs)
    end tell
end bar
   
on usercommand(con, target, thestring, theCmd, theArgs)
    if theCmd is "/foo" then
        foo()
    else if theCmd is "/bar" then
        bar(theArgs)
    end if
end usercommand

Notice how the bar() handler is actually a little simpler in the plugin case, because the parsing of the user command parameters into a list of words was already done by hipscript, and the usercommand() dispatcher just passed the ready-to-use list to the bar() handler. In the plugin case, it would have been possible (and in fact, more effecient) to pass the "thestring" variable from the usercommand() dispatcher to the foo() handler, avoiding the need for the foo() handler to call GetArgString(). The GetArgString() function exists mainly as a way to quickly convert existing user command handlers to plugin user command handlers by simply replacing the line to get argstring from ircle with GetArgString().

This seems like a good time to mention naming standards. The names of ircle event handlers are fixed by ircle; you have to name them and define their argument variables exactly as ircle does in the Events example script. The name and argument variables for the usercommand() handler are similarly fixed by hipscript and has to be just so. Individual user command handlers, on the other hand, can be named anything you want; in the case of a plugin, the name doesn't even have to be the same as the command name, and you can choose which (if any) of the argument variables you want to pass to it. The only requirement is that you define the handler and call it from usercommand() in the same way.

In the standard hipscript plugins, I have adopted the purely-arbitrary standard of naming a user command handler to be the command name with an underbar on the front. The handler for the /foo command would be _foo(), and so on. This just makes it easier for me to read the code, the leading underbar has no special meaning except in my mind.

Okay, so that covers the basics of creating a plugin that contains ircle event handlers and/or user command handlers. What's left is some explanation of how ircle event handlers in a plugin relate to ircle events in general, and the huge and messy issue of the HipScriptLib functions that can provide better performance and convenience to you in writing plugins.

The ircle event handlers thing is kind of important, I guess I'll ramble on about it for a while. HipScript contains a handler for every possible ircle event. When a plugin is loaded, HipScript examines that plugin's HipScriptEventHandlerList property, and remembers which ircle event handlers the plugin contains. When an ircle event occurs, that event handler in HipScript passes control to each plugin that has a handler for that event, until one of the plugin handlers returns TRUE (to indicate no further processing is needed) or until all plugins have gotten the event.

There is some potential for conflict here, in that two plugins could have a handler for an event, and if the first plugin to get the event returns TRUE, the second plugin will never get to see the event. This would be especially bad if the second plugin wanted to note the event or act on it in some passive way without returning TRUE to prevent other handlers from seeing it. To help minimize this problem, HipScript supports the concept of "observer" and "consumer" event handlers. An observer is a handler that promises to always return FALSE no matter what. A consumer is a handler that might under some (or all) conditions return TRUE to prevent further processing of an event. HipScript maintains its internal lists of plugin handlers in such a way to guarantee that all observers will get to see an event before any consumer has a chance to terminate further processing for the event.

To declare an event as an observer, code a "+" sign before the handler's name in the HipScriptEventHandlerList. For example:

property HipScriptEventHandlerList: {"+pubmsg", "privmsg", "load", "unload"}

This tells hipscript that the plugin's pubmsg handler promises to always return FALSE, but that its privmsg() handler might return TRUE. The consumer-versus-observer issue has no meaning for the idle, load, hipload, and unload handlers.

It is fairly important that you code those + signs on the names of handlers that always return FALSE, and it is very very important that you never code a + sign on an ircle event handler that might under any conditions return TRUE.

In addition to the standard ircle event handlers, hipscript provides several extensions. One of these is the "hipload()" handler. It is identical to load() (called when the script is loaded), except that a copy of the hipScriptLib module is passed as a parameter so that the plugin can have fast access to the functions in the library. More on this later.

A more important extension is the concept of "filtered numerics". Numerics are codes communicated from a server to a client, and they can happen at fairly high frequency. Often, a plugin needs to see a couple numerics, but not the vast majority of them. To avoid the performance problem of calling a dozen handlers for each numeric code when 99% of the time each of those handlers don't care about that particular code, HipScript can pre-filter the codes, and call a plugin's numeric() handler only for specific codes.

Suppose a plugin needs to take some action immediately after you've connected to a server. The usual way to do that is to watch for the 376 (END_OF_MOTD) code in the numeric() handler. To specify that the plugin's numeric handler wants to see only that code, use:

property HipScriptEventHandlerList: {"+#376", "privmsg", "load", "unload"}

Note that the observer-versus-consumer issue applies to filtered numerics as well, and that +#376 indicates that the plugin's numeric() handler will always return FALSE. (Returning TRUE for a 376 code would be very rude, and would break a lot of other plugins that expect to see that code. There are numerics for which being a consumer and returning TRUE might make more sense.)

In coding the numeric() handler for this example, there would be no need to compare the command number to 376 in the handler. Since only #376 was listed in HipScriptEventHandlerList, only that command would ever be passed to the plugin's numeric() handler. If several numerics were specified, such as:

property HipScriptEventHandlerList: {"+#376", "#400", "+#401", "+#402"}

Then the numeric() handler would have to check which of those command numbers was passed to it, but it could safely assume that it would only be one of those numbers, never any other.

You can, of course, just specific "numeric" in HipScriptEventHandlerList, and do your own filtering like you would in a normal loaded script. But, using the filtered numerics is vastly more effecient.

Another area in which HipScript provides some extra support is the concept of "bang commands". These (named after the leading "!" character that programmers call a "bang" character, because we're way to lazy to mumble our way through a phrase like "exclamation point") are typically commands that can be entered by a remote user to cause your script to take some action. The exampleBartender plugin demonstrates using bang commands. HipScript has some special suport for these, similar to its support for user commands. Some day, I'll go into detail about it here; for now, just look at the exampleBartender plugin, and keep in mind that bangcommand() is to HipScriptBangCommandList as usercommand() is to HipScriptUsercommandList.

Are you still reading? Man, you're either really bored, or really have a desire to write your own plugins. Okay, if you own hip waders, now's the time to put them on...we're about to wade into the muck known as...

Services HipScript provides to plugins

This gets into a pretty murky area of how AppleScript works. Glossing over some details, it works like this: when you call a handler function in a script that was dynamically loaded by another script, AppleScript first checks for a handler of that name in the current script. If it doesn't find one, it checks in the "parent" script that dynamically loaded it to see if it exists there; if it does, it runs that handler.

HipScript provides several service routines which plugins can use just as if the routine was local to the plugin. HipScriptLib contains even more such routines, but calling them is more complicated, we'll get to that later. Here are the important routines that HipScript provides to make your life easier, or to help provide a unified appearance to the end user:

ShowIn(con, target, thestring)
This function displays text locally to the user without sending it to the server. The text is displayed using HipScript's prefix (•••) and the user's choice of colors set with the /showcolor command. I highly recommend that you use this function to talk to the user from your plugin.

If the con argument is non-zero and the target argument is a valid channel or window name, the text is displayed in that channel/window of that connection. If you don't have values handy for con and/or target, use ShowIn(0, "", whatever) -- the zero for connection number tells the function to display the text in whatever window is on top.

In general, when displaying text from an ircle event handler, you should pass con and target from the event handler variables, so that the text appears in the window related to the event. This is a large part of writing plugins that are properly "connection aware".

TypeIn(con, target, thestring)
This function types the text or command into the specified connection and channel. Even more so than with ShowIn(), if an event handler needs to send some text or a command in response to an event, it is important to pass the con and target variables from the event handler to this command, so that the text or command goes to the right connection. If you get this wrong, users who are on multiple servers at once will curse you as things keep happening in the wrong connections or channels.

The text or command typed by this function goes directly to the server, bypassing hipscript internal processing. If the command you are entering might be a hipscript command or alias, use the InputIn() function instead.

InputIn(con, target, thestring)
This function is similar to TypeIn(), but it first routes the text or commmand through HipScript's internal command and alias processing. This is the only way for a plugin to input a command or text to be processed by another plugin, or to input an alias and get the variables it might contain resolved. If a command input this way is not handled by any plugins, it is sent to the server as if TypeIn() had been called.

The string passed to this function is routed not only through HipScript's command and alias processing, but through all plugins that have an input() handler. Therefore, be really careful about using this from within an input() handler -- you could end up in an infinite loop.

TypeInAll(thestring)
This function types the command or text in all open channels and chat windows. It is similar to using Ircle's /broadcast, except that it also writes to open DCC chat windows. A bug in Ircle prevents the text from echoing in a chat window, but the user on the other end sees it.
MsgToUser(con, target, thestring)
This function sends thestring as a /msg to the user target on connection con.
NoticeToUser(con, target, thestring)
This function sends thestring as a /notice to the user target on connection con.
CTCPReply(con, target, ctcpCommand, thestring)
This function sends thestring as a CTCP REPLY type /notice to user target on connection con.
GetHipScriptVersion()
This just returns the version of HipScript as a string.
StringToList(string, delim)
This parses a string into a list of strings using the delimiter character specified.

StringToList("abc def ghi", " ") Returns {"abc", "def", "ghi"}

An empty string returns an empty list {}.

ListToString(list, delim)
This assembles a string from the list of strings using the delimiter character specified.

ListToString({"1","2","3","4"}, ".") Returns "1.2.3.4".

An empty list returns an empty string "".

GetTheStringList(thestring)
This is a special performance-enhancing function for ircle event handlers. For any ircle event handler that includes an argument named "thestring" (IE, most of them), this function will get a copy of thestring already parsed into a list of space-delimited words. Since there may be a dozen handlers chained to a given ircle event, and each of those handlers might want to treat thestring as a list of words, there's a big performance boost in using this function to obtain thestring as a list of words.

In processing an individual event, thestring (as passed to this function) is parsed into a list of words delimited by spaces just once, and the results are cached and returned to the caller. If another chained handler calls this function during processing of the same event, the cached results are returned quickly without re-processing thestring.

The only value you should ever pass to this function is the thestring argument exactly as passed to the ircle event handler. To parse some other string into a list of words, use StringToList().

GetArgString()
This function is valid only in processing a user command; it cannot be used in ircle event handlers It returns the same value as ircle's argstring property would hold in a non-plugin environment. It is also the same value as is passed to the usercommand() handler in the thestring argument.
PlaySound(name)
This function plays the named sound file.

That's all for now

Oh my aching fingers. Oh my aching brain. And I haven't even started on the HipScriptLib services words yet, and those are the complicated ones.

Still, at least I've gotten a start on this beast, rambling and incoherent tho it may be. Your best bet is to read this doc, then look at some of the existing plugins, then read the doc again, then look at the plugins some more. Then come find me in DalNet #Macintosh and, if I'm not too busy or too burnt out from those who've come before you, I'll do my best to help you write your own plugins. As long as they're not lame. And I get to define what's lame. hehehe.