Version 85 of XiRCON-II

Updated 2003-01-29 04:15:57

DG is authoring an IRC Client. It is a work in progress. No downloadable beta is ready, yet... watch this space.


This IRC client is a faithful XiRCON [L1 ] clone regarding how the user scripting operates. Here are some screenshots:

  • Doing Japanese:

http://tomasoft.sf.net/in_a_japanese_channel.gif

  • Doing Chinese:

http://tomasoft.sf.net/263.gif

  • Doing Klingon (in utf-8 over IRC and back using the CTCP/2 method of escaping):

http://tomasoft.sf.net/klingon.gif

  • being normal (boring):

http://tomasoft.sf.net/in_an_english_channel.gif


The client has been painfully designed to be 2 complimentary halves:

1.�The�back-end
The irc_engine Tcl extension DLL (100% Tcl API only, with a little STL) for handling everything that is not seen (ie. sockets, scripts, default behavior, IAL, etc..).
2.�The�UI
Whatever UI the user prefers according to the command control API. It could done in Tk, an implimentation in curses, Win32 GUI, or as odd as streaming html output with form input for posting.

The IRC_Engine back-end --

The irc_engine extension provides a single Incr Tcl class. The UI half (partialy undefined) also provides an Incr Tcl class. The two are brought together using inheritence like so:

 source irc_engine.itcl

 # ===============================================================
 # By selecting which IRC::ui class is sourced, we can switch
 # to what UI we want to use.
 # ===============================================================
 source irc_ui.itcl

 itcl::class IRC::connection {

    # ===============================================
    # Bring the 2 halves together using inheritence.
    # ===============================================
    inherit engine ui

    constructor {args} {
        eval engine::constructor $args
    } {}
    public method destroy {} {itcl::delete object $this}
 }

 ### Create a connection instance with a couple user scripts.
 set a [IRC::connection #auto someUserScript1.tcl someUserScript2.tcl]

 ### Connect to IRC.
 $a connect irc.qeast.net davygrvy DG {yo moma!}

In the above, $a is now the connection object. All behavior of the engine is located in a script file called default.tcl [L2 ]. The behavior of irc_engine can be scripted by the user by loading scripts which are queried for event hooks prior to doing the default behavior. I do have a generic framework to the system, but only Tcl is supported. I have code started for perl, python, and java, but I'll have to get back to it later [L3 ].

Here's where it gets interesting for user scripts.. Each tcl user script is run in its own interpreter. It maintains some of its own commands such as [IRC::on], but aliases most up to the global interp (I also call it the controlling or UI interp). So from the script, a call to IRC::echo ... will alias up to the global as connection0 echo ... where connection0 is the Incr Tcl object. This allows us to have our scripts track with their connection object. Where this gets more interesting is that from this aliasing, I can assume commands that will be in the top-most object that are provided by the UI (thanks to inheritence). The echo method is located in the UI half, yet the user script is in a separate interp of the irc_engine extension half and always linked to its parent object.

irc_engine uses Tcl's event loop. This diagram shows some of the code paths:

http://tomasoft.sf.net/incoming_trail.gif

Tcl::Socket is just a wrapper for Tcl's socket API stuff. When a line from the server is received, it is parsed into its protocol parts [L4 ] [L5 ] and is the container for the parts that will get passed around to the subsiquent modules. IRCSplitAndQ [L6 ] [L7 ] is responsible for any text mappings (or multiple mappings) that need to be decoded, any CTCP/2 encoding escapes, any CTCP unquoting, any mode splitting, and removing/queueing of all embedded CTCP commands prior to the edited original getting posted as a job to the event loop. Phew!

http://tomasoft.sf.net/ircevent_trail.gif

When a job we had posted from IRCSplitAndQ is ready to be serviced by Tcl, our Tcl_EventProc named EvalCallback is called which just sets an interp lock then calls IRCEngine::EvalOneIRCEvent in the correct connection object [L8 ] [L9 ]. Within the Tcl_Event structure is our IRCParse C++ object all prepared to be serviced. First we run PreEvent(line), so certain IRC events such as 'join' can be set into the channels list prior to scripts being run. The same is true in the inverse, as a 'part' event will be removed from the channels list from PostEvent(). Space is left here for the inclusion of an Internal Address List when I get around to it.

Now after PreEvent() is run, all script providers in LIFO order are handed the IRCParse object. If any of the providers return IRCUserScriptProvider::COMPLETED (an enum), then the event is considered complete and all processing stops. If no providers claim the event as completed, then the default script will process it. If the default script doesn't handle it, and the mode for the connection is 'debugging' rather than normal, it will be displayed in red in the status window with event name or numeric. Same as:

 echo "\006CC\006***\006C\006 [event] [join [lrange [args] 1 end]]" status

Without the debugging mode, no red color or event code is displayed. Same as:

 echo "*** [join [lrange [args] 1 end]]" status

Here's an example of the user scripting and how identical it is to the idea of XiRCON. Old-time XiRCON'ers will notice, of course, the new use of namespaces and the msgcat package for handling multilingual strings. We must improve on XiRCON, too.

 package require IRC_Engine_User

 namespace eval ::IRC {
    on PRIVMSG {
        set dest [lindex [args] 0]
        set msg [lindex [args] 1]

        ### ignore an empty one.
        if {![string length $msg]} {complete; return}

        if {![icomp $dest [my_nick]]} {
            ### private msg
            echo "[color highlight]*[color nick][nick][color highlight]*[color private] $msg" query [nick]

        } elseif {[string index $dest 0] == "\$"} {
            ### server (global) message
            echo "[color highlight]<[color nick][nick][color highlight]>[color default] $msg" status

        } else {
            echo "[color highlight]<[color nick][nick][color highlight]>[color default] $msg" channel $dest

        }
        complete
    }
    on $RPL_TOPICWHOTIME     {
        echo "[color change]*** [mc {Topic for}] [color channel][lindex [args] 1][color change] [mc {was set by}] [color nick][lindex [args] 2][color change] [mc {on}] [clock format [lindex [args] 3]]"
        complete
    }
    on $RPL_UMODEIS         {
        echo "[color change]*** [mc {Your modes are}] [color mode]\"[join [lrange [args] 1 end]]\"" status
        complete
    }
 }

As in XiRCON, these are the commands available to an event hook for asking about the line that fired the event:

http://tomasoft.sf.net/ircparser.gif

Not shown is raw_line and it returns the whole thing as a string prior to any processing. raw_args is also prior to any processing. All return strings except for args which is a list (and is very processed). raw_line will be empty for any embedded event such as ctcp or mode+o. nick will equal host for a server message.

Unlike XiRCON, a PRIVMSG event that contains a CTCP in its entire trailing param, will fire a PRIVMSG anyway, even though it will be empty. It may seem silly at first, but the original raw_line would then have no way to get through. Think about it for a sec... It was a PRIVMSG that came in, and just so happened to contain within it a CTCP. It doesn't make sense to me to drop the original PRIVMSG. That was the event. Same is true for a NOTICE with a ctcp_reply.


The UI Half --

The first major feature is the support of ALL embedded text attributes ever known to mankind and robots alike. It supports ircII, mIrc, ANSI, besirc/hydra, and CTCP/2. As CTCP/2 is the most in features, this is the native usage of the display. The other attribute types are pre-translated up to CTCP/2. The CTCP/2 parser is also in the IRC_Engine as an Itcl class for use by Tk applications.

CTCP/2 embedded text attributes are described @ http://www.lag.net/~robey/ctcp . All attibutes in section 2 are supported -> http://www.lag.net/~robey/ctcp/ctcp2.2.txt . One extension was added using the CTCP/2 method for extending, for doing the tag color types. This will be especially useful for a UI in Tk and allows the display to define how "normal", "highlight", etc.. will be rendered rather than the parser filling in literal values. These tag colors are returned by the color command and are not intended for transmision -- internal use only. Similar to XiRCON's \aXX color codes where X is a hexidecimal number.

It should be noted that the parser reads it all, and whether an implimentation of a UI does actually do the attributes is a bit outside of my claims. For example, if I make a Win32 console text-mode UI for this client, I will have a choice of only 12 colors. As a 24-bit depth is available to the CTCP/2 color attribute, the 24-bit value will need to be truncated for that display type (ie. 256 levels of red get mashed to 3 levels, off/on/bright).

I should slow down for a moment and say that the UI is more of an API than an actual complete and realized implimention.

To better describe the concept of implementation seperate from the UI, here is an example of a UI that generates HTML as its output (a little DHTML with CSS). This is the link to the output [L10 ] of this UI script on a few channels on EfNet [L11 ]. With DKF's help pointing-out the usefulness of CSS, here it is in it's final form:

 #------------------------------------------------------------------------
 #  irc_ui_html.itcl --
 #
 #  UI half for irc_engine.dll for DHTML output.  Proof-of-concept.
 #
 #------------------------------------------------------------------------

 package require IRC_Engine

 itcl::class IRC::ui {
    constructor {args} {
        ### create a CTCP2 parser in tag mode.
        set displayAction [CTCP2::parse #auto [itcl::code $this dodisplay] literal \
                -fg 0x$tagColors(default,f) -bg 0x$tagColors(default,b) \
                -sp $CharSpacing]

        set f [open "irc_output.html" w]
        fconfigure $f -encoding utf-8
        puts $f [subst {
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
 <html>
 <head>
    <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=[fconfigure $f -encoding]">
    <META NAME="Generator" CONTENT="IRC_Engine">
    <TITLE>Live on Internet Relay Chat</TITLE>
    <style type="text/css">
        /* standard line class */
        .ircline   { text-indent: -2em; padding-left: 2em; text-align: left; white-space: pre; background-image: none; font-family: [expr {"$CharSpacing" == "fixed" ? "monospace" : "san-serif"}] }
        /* standard attributes classes (overlayed) */
        .blink      { text-decoration: blink }
        .underline  { text-decoration: underline }
        .overstrike { text-decoration: line-through }
        .bold       { font-weight: bold }
        .italic     { font-style: italic }
        .reverse    { color: expression(this.parentNode.currentStyle.backgroundColor); background-color: expression(this.parentNode.currentStyle.color) }
        /* tag color classes (absolute) */
        .default    { color: #$tagColors(default,f);        background-color: #$tagColors(default,b) }
        .public     { color: #$tagColors(public,f);        background-color: #$tagColors(public,b) }
        .private    { color: #$tagColors(private,f);        background-color: #$tagColors(private,b) }
        .action     { color: #$tagColors(action,f);        background-color: #$tagColors(action,b) }
        .notice     { color: #$tagColors(notice,f);        background-color: #$tagColors(notice,b) }
        .ctcp       { color: #$tagColors(ctcp,f);        background-color: #$tagColors(ctcp,b) }
        .change     { color: #$tagColors(change,f);        background-color: #$tagColors(change,b) }
        .join       { color: #$tagColors(join,f);        background-color: #$tagColors(join,b) }
        .part       { color: #$tagColors(part,f);        background-color: #$tagColors(part,b) }
        .kick       { color: #$tagColors(kick,f);        background-color: #$tagColors(kick,b) }
        .quit       { color: #$tagColors(quit,f);        background-color: #$tagColors(quit,b) }
        .highlight  { color: #$tagColors(highlight,f);        background-color: #$tagColors(highlight,b) }
        .error      { color: #$tagColors(error,f);        background-color: #$tagColors(error,b) }
        .nick       { color: #$tagColors(nick,f);        background-color: #$tagColors(nick,b) }
        .channel    { color: #$tagColors(channel,f);        background-color: #$tagColors(channel,b) }
        .mode       { color: #$tagColors(mode,f);        background-color: #$tagColors(mode,b) }
        .socket     { color: #$tagColors(socket,f);        background-color: #$tagColors(socket,b) }
    </style>
 </head>
 <body text="#$tagColors(default,f)" bgcolor="#$tagColors(default,b)">
 <a href="http://validator.w3.org/check/referer"><img border="0"
          src="http://www.w3.org/Icons/valid-html401"
          alt="Valid HTML 4.01!" height="31" width="88"></a>}]
    }
    destructor {
        if {[info exist displayAction]} {$displayAction destroy}
        if {[info exist f]} {
            puts $f {</body>}
            puts $f {</html>}
            close $f
        }
    }
    public {
        method destroy {} {itcl::delete object $this}
        method echo {what {where {}} {which {}}}
 #        method window {args}
        method menu {args}
        method hotkey {args}
        method alias {args}
        method channel {args}
        method query {args}
        method chat {args}
        method queries {args}
        method chats {args}
        method say {args}
        method input {args}
    }
    private {
        method getTimeStamp {}
        method haveReadyAttributes {var}
        method clearReadyAttributes {}
        method dodisplay {args}

        variable displayAction
        variable f

        variable overlayclasses [list]
        variable segIsUrl 0
        variable scale {}
        variable spacing {}
        variable tagName {}
        variable fg {}
        variable bg {}

        ### Master list of colors.
        common tagColors
        array set tagColors [list \
            default,f        C0C0C0                default,b        000000 \
            public,f        C0C0C0                public,b        000000 \
            private,f        C0C0C0                private,b        000000 \
            action,f        FF7F00                action,b        000000 \
            notice,f        7F3F00                notice,b        000000 \
            ctcp,f        FF0000                ctcp,b                000000 \
            change,f        A000A0                change,b        000000 \
            join,f        007F7F                join,b                000000 \
            part,f        007F7F                part,b                000000 \
            kick,f        007F00                kick,b                000000 \
            quit,f        007F00                quit,b                000000 \
            highlight,f        FF00FF                highlight,b        000000 \
            error,f        FFFF00                error,b                FF0000 \
            nick,f        00FFFF                nick,b                000000 \
            channel,f        00FF00                channel,b        000000 \
            mode,f        FFFF00                mode,b                000000 \
            socket,f        FFFFFF                socket,b        7F7F7F]

        common CharSpacing fixed ;# or proportional
    }
 }

 itcl::body ::IRC::ui::getTimeStamp {} {
    return [clock format [clock seconds] -format "%a, %e %b %Y %H:%M:%S"]
 }

 itcl::body ::IRC::ui::haveReadyAttributes {var} {
    upvar $var local
    set local [list]
    set classes [list]
    set styles [list]

    if {[llength $overlayclasses]} {
        lappend classes $overlayclasses
    }
    if {[string length $tagName]} {
        lappend classes $tagName
    }
    if {[string length $scale]} {
        lappend styles "font-size: $scale"
    }
    if {[string length $spacing]} {
        lappend styles "font-family: $spacing"
    }
    if {[string length $fg]} {
        lappend styles "color: $fg"
    }
    if {[string length $bg]} {
        lappend styles "background-color: $bg"
    }

    set a [llength $classes]
    set b [llength $styles]

    if {$a || $b} {
        if {$a} {lappend local "class=\"[join $classes]\""}
        if {$b} {lappend local "style=\"[join $styles]\""}
        set local [join $local]
        return 1
    } else {
        return 0
    }
 }

 itcl::body ::IRC::ui::clearReadyAttributes {} {
    set overlayclasses [list]
    set segIsUrl 0
    set scale {}
    set spacing {}
    set tagName {}
    set fg {}
    set bg {}
 }

 itcl::body ::IRC::ui::dodisplay {args} {
    puts $args
    set cmd [lindex $args 0]
    switch -- $cmd {
        "tag" {
            set tagName [lindex $args 1]
            set fg {}
            set bg {}

            ### We must return the color values to the parser.  Doing so allows it
            ### to handle context errors that might follow.  ie. can't set
            ### fore to black if back is black.
            ###
            return [list 0x$tagColors($tagName,f) 0x$tagColors($tagName,b)]
        }
        "bold" -
        "reverse" -
        "underline" -
        "overstrike" -
        "italic" -
        "blink"        {
            ### toggle it.
            if {[lindex $args 1]} {
                lappend overlayclasses $cmd
            } else {
                if {[set i [lsearch -exact $overlayclasses $cmd]] != -1} {
                    set overlayclasses [lreplace $overlayclasses $i $i]
                }
            }
        }
        "url" {
            ### the segment to follow will be a URL.
            set segIsUrl [lindex $args 1]
        }
        "spacing" {
            set spacing [expr {"[lindex $args 1]" == "fixed" ? "monospace" : "san-serif"}]
        }
        "fontsize" {
            ### has the range -5 to 5
            switch -- [lindex $args 1] {
                -5  { set scale 50%  }
                -4  { set scale 60%  }
                -3  { set scale 70%  }
                -2  { set scale 80%  }
                -1  { set scale 90%  }
                0   { set scale {}   }
                1   { set scale 110% }
                2   { set scale 120% }
                3   { set scale 130% }
                4   { set scale 140% }
                5   { set scale 150% }
            }
        }
        "forecolor" {
            set tagName {}
            set fg "rgb([lindex $args 1],[lindex $args 2],[lindex $args 3])"
        }
        "backcolor" {
            set tagName {}
            set bg "rgb([lindex $args 1],[lindex $args 2],[lindex $args 3])"
        }
        "segment" {
            if {[set inSpan [haveReadyAttributes attributes]]} {
                puts -nonewline $f "<span $attributes>"
            }

            if {$segIsUrl} {
                puts -nonewline $f "<a href=\"[lindex $args 1]\" target=\"_workspot\">[lindex $args 1]</a>"
            } else {
                ### Push the text out.  quote some specials.
                puts -nonewline $f [string map {& &amp; < &lt; > &gt;} [lindex $args 1]]
            }

            if {$inSpan} {
                ### close the span tag.
                puts -nonewline $f {</span>}
            }
        }
    }
 }

 itcl::body ::IRC::ui::echo {what {where {}} {which {}}} {

    ### Start the line in our paragraph rule.
    puts -nonewline $f "<div class=\"ircline\" title=\"[getTimeStamp]\">"

    ### Grind it through our CTCP/2 parser to get out to the display.
    $displayAction parse $what

    ### wipe-out any left-over attributes.
    clearReadyAttributes

    ### Close our paragraph rule
    puts $f {</div>}

    ### Force it out.
    flush $f
 }

The HTML looks great. I like the timestamp in the balloon help (kinda kitch). I'm amazed how identical I got it to look to my test GUI done with a windows' richedit control. The only problem I see is the handling of whitespace. In the main <div> rule, I do set white-space: pre so multiple spaces aren't truncated, but alas, IE6 is not doing this for me. Using an open-ended <pre> works, but wouldn't generate valid HTML in this manner. This must be a bug in IE6..

Yes, IE6 does have bugs. [L12 ] lists them. text-decoration: blink is also not supported.

[More to come]


[ Category Internet | Category Application | Category Games | Category Object Orientation ]