Untied application from root path
authorDarren <darren@darrenwhitlen.com>
Tue, 16 Oct 2012 23:33:26 +0000 (00:33 +0100)
committerDarren <darren@darrenwhitlen.com>
Tue, 16 Oct 2012 23:33:26 +0000 (00:33 +0100)
31 files changed:
client/assets/backbone-git.js [moved from client/backbone-git.js with 100% similarity]
client/assets/css/style.css [moved from client/css/style.css with 100% similarity]
client/assets/dev/app.js [moved from client/dev/app.js with 100% similarity]
client/assets/dev/applet_chanlist.js [moved from client/dev/applet_chanlist.js with 100% similarity]
client/assets/dev/applet_nickserv.js [moved from client/dev/applet_nickserv.js with 100% similarity]
client/assets/dev/applet_settings.js [moved from client/dev/applet_settings.js with 100% similarity]
client/assets/dev/build.js [moved from client/dev/build.js with 100% similarity]
client/assets/dev/model_applet.js [moved from client/dev/model_applet.js with 100% similarity]
client/assets/dev/model_application.js [moved from client/dev/model_application.js with 96% similarity]
client/assets/dev/model_channel.js [moved from client/dev/model_channel.js with 100% similarity]
client/assets/dev/model_gateway.js [moved from client/dev/model_gateway.js with 100% similarity]
client/assets/dev/model_member.js [moved from client/dev/model_member.js with 100% similarity]
client/assets/dev/model_memberlist.js [moved from client/dev/model_memberlist.js with 100% similarity]
client/assets/dev/model_panel.js [moved from client/dev/model_panel.js with 100% similarity]
client/assets/dev/model_panellist.js [moved from client/dev/model_panellist.js with 100% similarity]
client/assets/dev/model_server.js [moved from client/dev/model_server.js with 100% similarity]
client/assets/dev/utils.js [moved from client/dev/utils.js with 100% similarity]
client/assets/dev/view.js [moved from client/dev/view.js with 100% similarity]
client/assets/img/background-light.png [moved from client/img/background-light.png with 100% similarity]
client/assets/img/ico.png [moved from client/img/ico.png with 100% similarity]
client/assets/img/more.png [moved from client/img/more.png with 100% similarity]
client/assets/img/redcross.png [moved from client/img/redcross.png with 100% similarity]
client/assets/img/resize_handle.png [moved from client/img/resize_handle.png with 100% similarity]
client/assets/img/server_tab.png [moved from client/img/server_tab.png with 100% similarity]
client/assets/jquery-1.7.1.min.js [moved from client/jquery-1.7.1.min.js with 100% similarity]
client/assets/kiwi.js [new file with mode: 0644]
client/assets/kiwi.min.js [new file with mode: 0644]
client/assets/underscore-min.js [moved from client/underscore-min.js with 100% similarity]
client/index.html
server/app.js
server/config.json

similarity index 100%
rename from client/dev/app.js
rename to client/assets/dev/app.js
similarity index 96%
rename from client/dev/model_application.js
rename to client/assets/dev/model_application.js
index 3e1051c326d0d44eb1c629f945ce13eb7401bef3..2f0610c6af3fed166979be47ebb7548d18327f2e 100644 (file)
@@ -26,6 +26,9 @@ kiwi.model.Application = function () {
                 this.set('container', options[0].container);\r
             }\r
 \r
+            // The base url to the kiwi server\r
+            this.set('base_path', options[0].base_path ? options[0].base_path : '/client');\r
+\r
             // Best guess at where the kiwi server is\r
             this.detectKiwiServer();\r
         };\r
@@ -131,10 +134,9 @@ kiwi.model.Application = function () {
             };\r
 \r
             // Process the URL part by part, extracting as we go\r
-            parts = window.location.pathname.toString().split('/');\r
-            parts.shift();\r
+            parts = window.location.pathname.toString().replace(this.get('base_path'), '').split('/');\r
 \r
-            if (parts.length > 0 && parts[0].toLowerCase() === 'client') {\r
+            if (parts.length > 0) {\r
                 parts.shift();\r
 \r
                 if (parts.length > 0 && parts[0]) {\r
similarity index 100%
rename from client/dev/view.js
rename to client/assets/dev/view.js
similarity index 100%
rename from client/img/ico.png
rename to client/assets/img/ico.png
diff --git a/client/assets/kiwi.js b/client/assets/kiwi.js
new file mode 100644 (file)
index 0000000..f8546ea
--- /dev/null
@@ -0,0 +1,3773 @@
+(function (global) {
+
+// Holds anything kiwi client specific (ie. front, gateway, kiwi.plugs..)\r
+/**\r
+*   @namespace\r
+*/\r
+var kiwi = {};\r
+\r
+kiwi.model = {};\r
+kiwi.view = {};\r
+kiwi.applets = {};\r
+\r
+\r
+/**\r
+ * A global container for third party access\r
+ * Will be used to access a limited subset of kiwi functionality\r
+ * and data (think: plugins)\r
+ */\r
+kiwi.global = {\r
+       utils: undefined, // Re-usable methods\r
+       gateway: undefined,\r
+       user: undefined,\r
+       server: undefined,\r
+       command: undefined,  // The control box\r
+\r
+       // TODO: think of a better term for this as it will also refer to queries\r
+       channels: undefined,\r
+\r
+       // Entry point to start the kiwi application\r
+       start: function (opts) {\r
+               opts = opts || {};\r
+\r
+               kiwi.app = new kiwi.model.Application(opts);\r
+\r
+               if (opts.kiwi_server) {\r
+                       kiwi.app.kiwi_server = opts.kiwi_server;\r
+               }\r
+\r
+               kiwi.app.start();\r
+\r
+               return true;\r
+       }\r
+};\r
+\r
+\r
+\r
+// If within a closure, expose the kiwi globals\r
+if (typeof global !== 'undefined') {\r
+       global.kiwi = kiwi.global;\r
+}
+
+
+kiwi.model.Application = function () {\r
+    // Set to a reference to this object within initialize()\r
+    var that = null;\r
+\r
+    // The auto connect details entered into the server select box\r
+    var auto_connect_details = {};\r
+\r
+\r
+    var model = function () {\r
+        /** Instance of kiwi.model.PanelList */\r
+        this.panels = null;\r
+\r
+        /** kiwi.view.Application */\r
+        this.view = null;\r
+\r
+        /** kiwi.view.StatusMessage */\r
+        this.message = null;\r
+\r
+        /* Address for the kiwi server */\r
+        this.kiwi_server = null;\r
+\r
+        this.initialize = function (options) {\r
+            that = this;\r
+\r
+            if (options[0].container) {\r
+                this.set('container', options[0].container);\r
+            }\r
+\r
+            // Best guess at where the kiwi server is\r
+            this.detectKiwiServer();\r
+        };\r
+\r
+        this.start = function () {\r
+            // Only debug if set in the querystring\r
+            if (!getQueryVariable('debug')) {\r
+                manageDebug(false);\r
+            } else {\r
+                //manageDebug(true);\r
+            }\r
+            \r
+            // Set the gateway up\r
+            kiwi.gateway = new kiwi.model.Gateway();\r
+            this.bindGatewayCommands(kiwi.gateway);\r
+\r
+            this.initializeClient();\r
+            this.initializeGlobals();\r
+\r
+            this.view.barsHide(true);\r
+\r
+            this.panels.server.server_login.bind('server_connect', function (event) {\r
+                var server_login = this;\r
+                auto_connect_details = event;\r
+\r
+                server_login.networkConnecting();\r
+                \r
+                $script(that.kiwi_server + '/socket.io/socket.io.js?ts='+(new Date().getTime()), function () {\r
+                    if (!window.io) {\r
+                        kiwiServerNotFound();\r
+                        return;\r
+                    }\r
+                    kiwi.gateway.set('kiwi_server', that.kiwi_server + '/kiwi');\r
+                    kiwi.gateway.set('nick', event.nick);\r
+                    \r
+                    kiwi.gateway.connect(event.server, event.port, event.ssl, event.password, function () {});\r
+                });\r
+            });\r
+\r
+            // TODO: Shouldn't really be here but it's not working in the view.. :/\r
+            // Hack for firefox browers: Focus is not given on this event loop iteration\r
+            setTimeout(function(){\r
+                kiwi.app.panels.server.server_login.$el.find('.nick').select();\r
+            }, 0);\r
+        };\r
+\r
+\r
+        function kiwiServerNotFound (e) {\r
+            that.panels.server.server_login.showError();\r
+        }\r
+\r
+\r
+        this.detectKiwiServer = function () {\r
+            // If running from file, default to localhost:7777 by default\r
+            if (window.location.protocol === 'file:') {\r
+                this.kiwi_server = 'http://localhost:7778';\r
+            } else {\r
+                // Assume the kiwi server is on the same server\r
+                this.kiwi_server = window.location.protocol + '//' + window.location.host;\r
+            }\r
+        };\r
+\r
+\r
+        this.initializeClient = function () {\r
+            this.view = new kiwi.view.Application({model: this, el: this.get('container')});\r
+            \r
+            /**\r
+             * Set the UI components up\r
+             */\r
+            this.panels = new kiwi.model.PanelList();\r
+\r
+            this.controlbox = new kiwi.view.ControlBox({el: $('#controlbox')[0]});\r
+            this.bindControllboxCommands(this.controlbox);\r
+\r
+            this.topicbar = new kiwi.view.TopicBar({el: $('#topic')[0]});\r
+\r
+            this.message = new kiwi.view.StatusMessage({el: $('#status_message')[0]});\r
+\r
+            this.resize_handle = new kiwi.view.ResizeHandler({el: $('#memberlists_resize_handle')[0]});\r
+            \r
+            this.panels.server.view.show();\r
+\r
+            // Rejigg the UI sizes\r
+            this.view.doLayout();\r
+\r
+            this.populateDefaultServerSettings();\r
+        };\r
+\r
+\r
+        this.initializeGlobals = function () {\r
+            kiwi.global.control = this.controlbox;\r
+        };\r
+\r
+\r
+        this.populateDefaultServerSettings = function () {\r
+            var parts;\r
+            var defaults = {\r
+                nick: getQueryVariable('nick') || 'kiwi_' + Math.ceil(Math.random() * 10000).toString(),\r
+                server: 'irc.kiwiirc.com',\r
+                port: 6667,\r
+                ssl: false,\r
+                channel: window.location.hash || '#kiwiirc'\r
+            };\r
+\r
+            // Process the URL part by part, extracting as we go\r
+            parts = window.location.pathname.toString().split('/');\r
+            parts.shift();\r
+\r
+            if (parts.length > 0 && parts[0].toLowerCase() === 'client') {\r
+                parts.shift();\r
+\r
+                if (parts.length > 0 && parts[0]) {\r
+                    // TODO: Extract the port from this hostname\r
+                    defaults.server = parts[0];\r
+                    parts.shift();\r
+                }\r
+\r
+                if (parts.length > 0 && parts[0]) {\r
+                    defaults.channel = '#' + parts[0];\r
+                    parts.shift();\r
+                }\r
+            }\r
+\r
+            // Set any random numbers if needed\r
+            defaults.nick = defaults.nick.replace('?', Math.floor(Math.random() * 100000).toString());\r
+\r
+            // Populate the server select box with defaults\r
+            this.panels.server.server_login.populateFields(defaults);\r
+        };\r
+\r
+\r
+        this.bindGatewayCommands = function (gw) {\r
+            gw.on('onmotd', function (event) {\r
+                that.panels.server.addMsg(kiwi.gateway.get('name'), event.msg, 'motd');\r
+            });\r
+\r
+\r
+            gw.on('onconnect', function (event) {\r
+                that.view.barsShow();\r
+                \r
+                if (auto_connect_details.channel) {\r
+                    that.controlbox.processInput('/JOIN ' + auto_connect_details.channel);\r
+                }\r
+            });\r
+\r
+\r
+            (function () {\r
+                var gw_stat = 0;\r
+\r
+                gw.on('disconnect', function (event) {\r
+                    var msg = 'You have been disconnected. Attempting to reconnect for you..';\r
+                    that.message.text(msg, {timeout: 10000});\r
+\r
+                    // Mention the disconnection on every channel\r
+                    $.each(kiwi.app.panels.models, function (idx, panel) {\r
+                        if (!panel || !panel.isChannel()) return;\r
+                        panel.addMsg('', msg, 'action quit');\r
+                    });\r
+                    kiwi.app.panels.server.addMsg('', msg, 'action quit');\r
+\r
+                    gw_stat = 1;\r
+                });\r
+                gw.on('reconnecting', function (event) {\r
+                    msg = 'You have been disconnected. Attempting to reconnect again in ' + (event.delay/1000) + ' seconds..';\r
+                    kiwi.app.panels.server.addMsg('', msg, 'action quit');\r
+                });\r
+                gw.on('connect', function (event) {\r
+                    if (gw_stat !== 1) return;\r
+\r
+                    var msg = 'It\'s OK, you\'re connected again :)';\r
+                    that.message.text(msg, {timeout: 5000});\r
+\r
+                    // Mention the disconnection on every channel\r
+                    $.each(kiwi.app.panels.models, function (idx, panel) {\r
+                        if (!panel || !panel.isChannel()) return;\r
+                        panel.addMsg('', msg, 'action join');\r
+                    });\r
+                    kiwi.app.panels.server.addMsg('', msg, 'action join');\r
+\r
+                    gw_stat = 0;\r
+                });\r
+            })();\r
+\r
+\r
+            gw.on('onjoin', function (event) {\r
+                var c, members, user;\r
+                c = that.panels.getByName(event.channel);\r
+                if (!c) {\r
+                    c = new kiwi.model.Channel({name: event.channel});\r
+                    that.panels.add(c);\r
+                }\r
+\r
+                members = c.get('members');\r
+                if (!members) return;\r
+\r
+                user = new kiwi.model.Member({nick: event.nick, ident: event.ident, hostname: event.hostname});\r
+                members.add(user);\r
+                // TODO: highlight the new channel in some way\r
+            });\r
+\r
+\r
+            gw.on('onpart', function (event) {\r
+                var channel, members, user,\r
+                    part_options = {};\r
+\r
+                part_options.type = 'part';\r
+                part_options.message = event.message || '';\r
+\r
+                channel = that.panels.getByName(event.channel);\r
+                if (!channel) return;\r
+\r
+                // If this is us, close the panel\r
+                if (event.nick === kiwi.gateway.get('nick')) {\r
+                    channel.close();\r
+                    return;\r
+                }\r
+\r
+                members = channel.get('members');\r
+                if (!members) return;\r
+\r
+                user = members.getByNick(event.nick);\r
+                if (!user) return;\r
+\r
+                members.remove(user, part_options);\r
+            });\r
+\r
+\r
+            gw.on('onquit', function (event) {\r
+                var member, members,\r
+                    quit_options = {};\r
+\r
+                quit_options.type = 'quit';\r
+                quit_options.message = event.message || '';\r
+\r
+                $.each(that.panels.models, function (index, panel) {\r
+                    if (!panel.isChannel()) return;\r
+\r
+                    member = panel.get('members').getByNick(event.nick);\r
+                    if (member) {\r
+                        panel.get('members').remove(member, quit_options);\r
+                    }\r
+                });\r
+            });\r
+\r
+\r
+            gw.on('onkick', function (event) {\r
+                var channel, members, user,\r
+                    part_options = {};\r
+\r
+                part_options.type = 'kick';\r
+                part_options.by = event.nick;\r
+                part_options.message = event.message || '';\r
+\r
+                channel = that.panels.getByName(event.channel);\r
+                if (!channel) return;\r
+\r
+                members = channel.get('members');\r
+                if (!members) return;\r
+\r
+                user = members.getByNick(event.kicked);\r
+                if (!user) return;\r
+\r
+                members.remove(user, part_options);\r
+\r
+                if (event.kicked === kiwi.gateway.get('nick')) {\r
+                    members.reset([]);\r
+                }\r
+                \r
+            });\r
+\r
+\r
+            gw.on('onmsg', function (event) {\r
+                var panel,\r
+                    is_pm = (event.channel == kiwi.gateway.get('nick'));\r
+\r
+                if (is_pm) {\r
+                    // If a panel isn't found for this PM, create one\r
+                    panel = that.panels.getByName(event.nick);\r
+                    if (!panel) {\r
+                        panel = new kiwi.model.Channel({name: event.nick});\r
+                        that.panels.add(panel);\r
+                    }\r
+\r
+                } else {\r
+                    // If a panel isn't found for this channel, reroute to the\r
+                    // server panel\r
+                    panel = that.panels.getByName(event.channel);\r
+                    if (!panel) {\r
+                        panel = that.panels.server;\r
+                    }\r
+                }\r
+                \r
+                panel.addMsg(event.nick, event.msg);\r
+            });\r
+\r
+\r
+            gw.on('onnotice', function (event) {\r
+                var panel;\r
+\r
+                // Find a panel for the destination(channel) or who its from\r
+                panel = that.panels.getByName(event.target) || that.panels.getByName(event.nick);\r
+                if (!panel) {\r
+                    panel = that.panels.server;\r
+                }\r
+\r
+                panel.addMsg('[' + (event.nick||'') + ']', event.msg);\r
+            });\r
+\r
+\r
+            gw.on('onaction', function (event) {\r
+                var panel,\r
+                    is_pm = (event.channel == kiwi.gateway.get('nick'));\r
+\r
+                if (is_pm) {\r
+                    // If a panel isn't found for this PM, create one\r
+                    panel = that.panels.getByName(event.nick);\r
+                    if (!panel) {\r
+                        panel = new kiwi.model.Channel({name: event.nick});\r
+                        that.panels.add(panel);\r
+                    }\r
+\r
+                } else {\r
+                    // If a panel isn't found for this channel, reroute to the\r
+                    // server panel\r
+                    panel = that.panels.getByName(event.channel);\r
+                    if (!panel) {\r
+                        panel = that.panels.server;\r
+                    }\r
+                }\r
+\r
+                panel.addMsg('', '* ' + event.nick + ' ' + event.msg, 'action');\r
+            });\r
+\r
+\r
+            gw.on('ontopic', function (event) {\r
+                var c;\r
+                c = that.panels.getByName(event.channel);\r
+                if (!c) return;\r
+\r
+                // Set the channels topic\r
+                c.set('topic', event.topic);\r
+\r
+                // If this is the active channel, update the topic bar too\r
+                if (c.get('name') === kiwi.app.panels.active.get('name')) {\r
+                    that.topicbar.setCurrentTopic(event.topic);\r
+                }\r
+            });\r
+\r
+\r
+            gw.on('ontopicsetby', function (event) {\r
+                var c, when;\r
+                c = that.panels.getByName(event.channel);\r
+                if (!c) return;\r
+\r
+                when = formatDate(new Date(event.when * 1000));\r
+                c.addMsg('', 'Topic set by ' + event.nick + ' at ' + when, 'topic');\r
+            });\r
+\r
+\r
+            gw.on('onuserlist', function (event) {\r
+                var channel;\r
+                channel = that.panels.getByName(event.channel);\r
+\r
+                // If we didn't find a channel for this, may aswell leave\r
+                if (!channel) return;\r
+\r
+                channel.temp_userlist = channel.temp_userlist || [];\r
+                _.each(event.users, function (item) {\r
+                    var user = new kiwi.model.Member({nick: item.nick, modes: item.modes});\r
+                    channel.temp_userlist.push(user);\r
+                });\r
+            });\r
+\r
+\r
+            gw.on('onuserlist_end', function (event) {\r
+                var channel;\r
+                channel = that.panels.getByName(event.channel);\r
+\r
+                // If we didn't find a channel for this, may aswell leave\r
+                if (!channel) return;\r
+\r
+                // Update the members list with the new list\r
+                channel.get('members').reset(channel.temp_userlist || []);\r
+\r
+                // Clear the temporary userlist\r
+                delete channel.temp_userlist;\r
+            });\r
+\r
+\r
+            gw.on('onmode', function (event) {\r
+                var channel, i, prefixes, members, member, find_prefix;\r
+                \r
+                // Build a nicely formatted string to be displayed to a regular human\r
+                function friendlyModeString (event_modes, alt_target) {\r
+                    var modes = {}, return_string;\r
+\r
+                    // If no default given, use the main event info\r
+                    if (!event_modes) {\r
+                        event_modes = event.modes;\r
+                        alt_target = event.target;\r
+                    }\r
+\r
+                    // Reformat the mode object to make it easier to work with\r
+                    _.each(event_modes, function (mode){\r
+                        var param = mode.param || alt_target || '';\r
+\r
+                        // Make sure we have some modes for this param\r
+                        if (!modes[param]) {\r
+                            modes[param] = {'+':'', '-':''};\r
+                        }\r
+\r
+                        modes[param][mode.mode[0]] += mode.mode.substr(1);\r
+                    });\r
+\r
+                    // Put the string together from each mode\r
+                    return_string = [];\r
+                    _.each(modes, function (modeset, param) {\r
+                        var str = '';\r
+                        if (modeset['+']) str += '+' + modeset['+'];\r
+                        if (modeset['-']) str += '-' + modeset['-'];\r
+                        return_string.push(str + ' ' + param);\r
+                    });\r
+                    return_string = return_string.join(', ');\r
+\r
+                    return return_string;\r
+                }\r
+\r
+\r
+                channel = that.panels.getByName(event.target);\r
+                if (channel) {\r
+                    prefixes = kiwi.gateway.get('user_prefixes');\r
+                    find_prefix = function (p) {\r
+                        return event.modes[i].mode[1] === p.mode;\r
+                    };\r
+                    for (i = 0; i < event.modes.length; i++) {\r
+                        if (_.any(prefixes, find_prefix)) {\r
+                            if (!members) {\r
+                                members = channel.get('members');\r
+                            }\r
+                            member = members.getByNick(event.modes[i].param);\r
+                            if (!member) {\r
+                                console.log('MODE command recieved for unknown member %s on channel %s', event.modes[i].param, event.target);\r
+                                return;\r
+                            } else {\r
+                                if (event.modes[i].mode[0] === '+') {\r
+                                    member.addMode(event.modes[i].mode[1]);\r
+                                } else if (event.modes[i].mode[0] === '-') {\r
+                                    member.removeMode(event.modes[i].mode[1]);\r
+                                }\r
+                                members.sort();\r
+                                //channel.addMsg('', '== ' + event.nick + ' set mode ' + event.modes[i].mode + ' ' + event.modes[i].param, 'action mode');\r
+                            }\r
+                        } else {\r
+                            // Channel mode being set\r
+                            // TODO: Store this somewhere?\r
+                            //channel.addMsg('', 'CHANNEL === ' + event.nick + ' set mode ' + event.modes[i].mode + ' on ' + event.target, 'action mode');\r
+                        }\r
+                    }\r
+\r
+                    channel.addMsg('', '== ' + event.nick + ' sets mode ' + friendlyModeString(), 'action mode');\r
+                } else {\r
+                    // This is probably a mode being set on us.\r
+                    if (event.target.toLowerCase() === kiwi.gateway.get("nick").toLowerCase()) {\r
+                        that.panels.server.addMsg('', '== ' + event.nick + ' set mode ' + friendlyModeString(), 'action mode');\r
+                    } else {\r
+                       console.log('MODE command recieved for unknown target %s: ', event.target, event);\r
+                    }\r
+                }\r
+            });\r
+\r
+\r
+            gw.on('onnick', function (event) {\r
+                var member;\r
+\r
+                $.each(that.panels.models, function (index, panel) {\r
+                    if (!panel.isChannel()) return;\r
+\r
+                    member = panel.get('members').getByNick(event.nick);\r
+                    if (member) {\r
+                        member.set('nick', event.newnick);\r
+                        panel.addMsg('', '== ' + event.nick + ' is now known as ' + event.newnick, 'action nick');\r
+                    }\r
+                });\r
+            });\r
+\r
+\r
+            gw.on('onwhois', function (event) {\r
+                /*globals secondsToTime */\r
+                var logon_date, idle_time = '', panel;\r
+\r
+                if (event.end) {\r
+                    return;\r
+                }\r
+\r
+                if (typeof event.idle !== 'undefined') {\r
+                    idle_time = secondsToTime(parseInt(event.idle, 10));\r
+                    idle_time = idle_time.h.toString().lpad(2, "0") + ':' + idle_time.m.toString().lpad(2, "0") + ':' + idle_time.s.toString().lpad(2, "0");\r
+                }\r
+\r
+                panel = kiwi.app.panels.active;\r
+                if (event.ident) {\r
+                    panel.addMsg(event.nick, 'is ' + event.nick + '!' + event.ident + '@' + event.host + ' * ' + event.msg, 'whois');\r
+                } else if (event.chans) {\r
+                    panel.addMsg(event.nick, 'on ' + event.chans, 'whois');\r
+                } else if (event.server) {\r
+                    panel.addMsg(event.nick, 'using ' + event.server, 'whois');\r
+                } else if (event.msg) {\r
+                    panel.addMsg(event.nick, event.msg, 'whois');\r
+                } else if (event.logon) {\r
+                    logon_date = new Date();\r
+                    logon_date.setTime(event.logon * 1000);\r
+                    logon_date = formatDate(logon_date);\r
+\r
+                    panel.addMsg(event.nick, 'idle for ' + idle_time + ', signed on ' + logon_date, 'whois');\r
+                } else {\r
+                    panel.addMsg(event.nick, 'idle for ' + idle_time, 'whois');\r
+                }\r
+            });\r
+\r
+\r
+            gw.on('onlist_start', function (data) {\r
+                if (kiwi.app.channel_list) {\r
+                    kiwi.app.channel_list.close();\r
+                    delete kiwi.app.channel_list;\r
+                }\r
+\r
+                var panel = new kiwi.model.Applet(),\r
+                    applet = new kiwi.applets.Chanlist();\r
+\r
+                panel.load(applet);\r
+                \r
+                kiwi.app.panels.add(panel);\r
+                panel.view.show();\r
+                \r
+                kiwi.app.channel_list = applet;\r
+            });\r
+\r
+\r
+            gw.on('onlist_channel', function (data) {\r
+                // TODO: Put this listener within the applet itself\r
+                kiwi.app.channel_list.addChannel(data.chans);\r
+            });\r
+\r
+\r
+            gw.on('onlist_end', function (data) {\r
+                // TODO: Put this listener within the applet itself\r
+                delete kiwi.app.channel_list;\r
+            });\r
+\r
+\r
+            gw.on('onirc_error', function (data) {\r
+                var panel, tmp;\r
+\r
+                if (data.channel !== undefined && !(panel = kiwi.app.panels.getByName(data.channel))) {\r
+                    panel = kiwi.app.panels.server;\r
+                }\r
+\r
+                switch (data.error) {\r
+                case 'banned_from_channel':\r
+                    panel.addMsg(' ', '== You are banned from ' + data.channel + '. ' + data.reason, 'status');\r
+                    kiwi.app.message.text('You are banned from ' + data.channel + '. ' + data.reason);\r
+                    break;\r
+                case 'bad_channel_key':\r
+                    panel.addMsg(' ', '== Bad channel key for ' + data.channel, 'status');\r
+                    kiwi.app.message.text('Bad channel key or password for ' + data.channel);\r
+                    break;\r
+                case 'invite_only_channel':\r
+                    panel.addMsg(' ', '== ' + data.channel + ' is invite only.', 'status');\r
+                    kiwi.app.message.text(data.channel + ' is invite only');\r
+                    break;\r
+                case 'channel_is_full':\r
+                    panel.addMsg(' ', '== ' + data.channel + ' is full.', 'status');\r
+                    kiwi.app.message.text(data.channel + ' is full');\r
+                    break;\r
+                case 'chanop_privs_needed':\r
+                    panel.addMsg(' ', '== ' + data.reason, 'status');\r
+                    kiwi.app.message.text(data.reason + ' (' + data.channel + ')');\r
+                    break;\r
+                case 'no_such_nick':\r
+                    tmp = kiwi.app.panels.getByName(data.nick);\r
+                    if (tmp) {\r
+                        tmp.addMsg(' ', '== ' + data.nick + ': ' + data.reason, 'status');\r
+                    } else {\r
+                        kiwi.app.panels.server.addMsg(' ', '== ' + data.nick + ': ' + data.reason, 'status');\r
+                    }\r
+                    break;\r
+                case 'nickname_in_use':\r
+                    kiwi.app.panels.server.addMsg(' ', '== The nickname ' + data.nick + ' is already in use. Please select a new nickname', 'status');\r
+                    if (kiwi.app.panels.server !== kiwi.app.panels.active) {\r
+                        kiwi.app.message.text('The nickname "' + data.nick + '" is already in use. Please select a new nickname');\r
+                    }\r
+\r
+                    // Only show the nickchange component if the controlbox is open\r
+                    if (that.controlbox.$el.css('display') !== 'none') {\r
+                        (new kiwi.view.NickChangeBox()).render();\r
+                    }\r
+\r
+                    break;\r
+                default:\r
+                    // We don't know what data contains, so don't do anything with it.\r
+                    //kiwi.front.tabviews.server.addMsg(null, ' ', '== ' + data, 'status');\r
+                }\r
+            });\r
+        };\r
+\r
+\r
+\r
+        /**\r
+         * Bind to certain commands that may be typed into the control box\r
+         */\r
+        this.bindControllboxCommands = function (controlbox) {\r
+            // Default aliases\r
+            $.extend(controlbox.preprocessor.aliases, {\r
+                '/p': '/part $1+',\r
+                '/me': '/action $1+',\r
+                '/j': '/join $1+',\r
+                '/q': '/query $1+',\r
+                '/k': '/kick $1+',\r
+\r
+                '/slap': '/me throws the juciest, sweetest kiwi at $1. Hits right in the kisser!',\r
+                '/throw': '/slap $1+'\r
+            });\r
+\r
+            controlbox.on('unknown_command', unknownCommand);\r
+\r
+            controlbox.on('command', allCommands);\r
+            controlbox.on('command_msg', msgCommand);\r
+\r
+            controlbox.on('command_action', actionCommand);\r
+\r
+            controlbox.on('command_join', joinCommand);\r
+\r
+            controlbox.on('command_part', partCommand);\r
+\r
+            controlbox.on('command_nick', function (ev) {\r
+                kiwi.gateway.changeNick(ev.params[0]);\r
+            });\r
+\r
+            controlbox.on('command_query', queryCommand);\r
+\r
+            controlbox.on('command_topic', topicCommand);\r
+\r
+            controlbox.on('command_notice', noticeCommand);\r
+\r
+            controlbox.on('command_quote', quoteCommand);\r
+\r
+            controlbox.on('command_kick', kickCommand);\r
+\r
+\r
+            controlbox.on('command_css', function (ev) {\r
+                var queryString = '?reload=' + new Date().getTime();\r
+                $('link[rel="stylesheet"]').each(function () {\r
+                    this.href = this.href.replace(/\?.*|$/, queryString);\r
+                });\r
+            });\r
+\r
+            controlbox.on('command_js', function (ev) {\r
+                if (!ev.params[0]) return;\r
+                $script(ev.params[0] + '?' + (new Date().getTime()));\r
+            });\r
+\r
+            controlbox.on('command_alias', function (ev) {\r
+                var name, rule;\r
+\r
+                // No parameters passed so list them\r
+                if (!ev.params[1]) {\r
+                    $.each(controlbox.preprocessor.aliases, function (name, rule) {\r
+                        kiwi.app.panels.server.addMsg(' ', name + '   =>   ' + rule);\r
+                    });\r
+                    return;\r
+                }\r
+\r
+                // Deleting an alias?\r
+                if (ev.params[0] === 'del' || ev.params[0] === 'delete') {\r
+                    name = ev.params[1];\r
+                    if (name[0] !== '/') name = '/' + name;\r
+                    delete controlbox.preprocessor.aliases[name];\r
+                    return;\r
+                }\r
+\r
+                // Add the alias\r
+                name = ev.params[0];\r
+                ev.params.shift();\r
+                rule = ev.params.join(' ');\r
+\r
+                // Make sure the name starts with a slash\r
+                if (name[0] !== '/') name = '/' + name;\r
+\r
+                // Now actually add the alias\r
+                controlbox.preprocessor.aliases[name] = rule;\r
+            });\r
+\r
+            controlbox.on('command_applet', appletCommand);\r
+            controlbox.on('command_settings', settingsCommand);\r
+        };\r
+\r
+        // A fallback action. Send a raw command to the server\r
+        function unknownCommand (ev) {\r
+            var raw_cmd = ev.command + ' ' + ev.params.join(' ');\r
+            console.log('RAW: ' + raw_cmd);\r
+            kiwi.gateway.raw(raw_cmd);\r
+        }\r
+\r
+        function allCommands (ev) {}\r
+\r
+        function joinCommand (ev) {\r
+            var channel, channel_names;\r
+\r
+            channel_names = ev.params.join(' ').split(',');\r
+\r
+            $.each(channel_names, function (index, channel_name) {\r
+                // Trim any whitespace off the name\r
+                channel_name = channel_name.trim();\r
+\r
+                // Check if we have the panel already. If not, create it\r
+                channel = that.panels.getByName(channel_name);\r
+                if (!channel) {\r
+                    channel = new kiwi.model.Channel({name: channel_name});\r
+                    kiwi.app.panels.add(channel);\r
+                }\r
+\r
+                kiwi.gateway.join(channel_name);\r
+            });\r
+\r
+            if (channel) channel.view.show();\r
+            \r
+        }\r
+\r
+        function queryCommand (ev) {\r
+            var destination, panel;\r
+\r
+            destination = ev.params[0];\r
+\r
+            // Check if we have the panel already. If not, create it\r
+            panel = that.panels.getByName(destination);\r
+            if (!panel) {\r
+                panel = new kiwi.model.Channel({name: destination});\r
+                kiwi.app.panels.add(panel);\r
+            }\r
+\r
+            if (panel) panel.view.show();\r
+            \r
+        }\r
+\r
+        function msgCommand (ev) {\r
+            var destination = ev.params[0],\r
+                panel = that.panels.getByName(destination) || that.panels.server;\r
+\r
+            ev.params.shift();\r
+\r
+            panel.addMsg(kiwi.gateway.get('nick'), ev.params.join(' '));\r
+            kiwi.gateway.privmsg(destination, ev.params.join(' '));\r
+        }\r
+\r
+        function actionCommand (ev) {\r
+            if (kiwi.app.panels.active === kiwi.app.panels.server) {\r
+                return;\r
+            }\r
+\r
+            var panel = kiwi.app.panels.active;\r
+            panel.addMsg('', '* ' + kiwi.gateway.get('nick') + ' ' + ev.params.join(' '), 'action');\r
+            kiwi.gateway.action(panel.get('name'), ev.params.join(' '));\r
+        }\r
+\r
+        function partCommand (ev) {\r
+            if (ev.params.length === 0) {\r
+                kiwi.gateway.part(kiwi.app.panels.active.get('name'));\r
+            } else {\r
+                _.each(ev.params, function (channel) {\r
+                    kiwi.gateway.part(channel);\r
+                });\r
+            }\r
+            // TODO: More responsive = close tab now, more accurate = leave until part event\r
+            //kiwi.app.panels.remove(kiwi.app.panels.active);\r
+        }\r
+\r
+        function topicCommand (ev) {\r
+            var channel_name;\r
+\r
+            if (ev.params.length === 0) return;\r
+\r
+            if (that.isChannelName(ev.params[0])) {\r
+                channel_name = ev.params[0];\r
+                ev.params.shift();\r
+            } else {\r
+                channel_name = kiwi.app.panels.active.get('name');\r
+            }\r
+\r
+            kiwi.gateway.topic(channel_name, ev.params.join(' '));\r
+        }\r
+\r
+        function noticeCommand (ev) {\r
+            var destination;\r
+\r
+            // Make sure we have a destination and some sort of message\r
+            if (ev.params.length <= 1) return;\r
+\r
+            destination = ev.params[0];\r
+            ev.params.shift();\r
+\r
+            kiwi.gateway.notice(destination, ev.params.join(' '));\r
+        }\r
+\r
+        function quoteCommand (ev) {\r
+            var raw = ev.params.join(' ');\r
+            kiwi.gateway.raw(raw);\r
+        }\r
+\r
+        function kickCommand (ev) {\r
+            var nick, panel = kiwi.app.panels.active;\r
+\r
+            if (!panel.isChannel()) return;\r
+\r
+            // Make sure we have a nick\r
+            if (ev.params.length === 0) return;\r
+\r
+            nick = ev.params[0];\r
+            ev.params.shift();\r
+\r
+            kiwi.gateway.kick(panel.get('name'), nick, ev.params.join(' '));\r
+        }\r
+\r
+        function settingsCommand (ev) {\r
+            var panel = new kiwi.model.Applet();\r
+            panel.load(new kiwi.applets.Settings());\r
+            \r
+            kiwi.app.panels.add(panel);\r
+            panel.view.show();\r
+        }\r
+\r
+        function appletCommand (ev) {\r
+            if (!ev.params[0]) return;\r
+\r
+            var panel = new kiwi.model.Applet();\r
+\r
+            if (ev.params[1]) {\r
+                // Url and name given\r
+                panel.load(ev.params[0], ev.params[1]);\r
+            } else {\r
+                // Load a pre-loaded applet\r
+                if (kiwi.applets[ev.params[0]]) {\r
+                    panel.load(new kiwi.applets[ev.params[0]]());\r
+                } else {\r
+                    kiwi.app.panels.server.addMsg('', 'Applet "' + ev.params[0] + '" does not exist');\r
+                    return;\r
+                }\r
+            }\r
+            \r
+            kiwi.app.panels.add(panel);\r
+            panel.view.show();\r
+        }\r
+\r
+\r
+\r
+\r
+\r
+        this.isChannelName = function (channel_name) {\r
+            var channel_prefix = kiwi.gateway.get('channel_prefix');\r
+\r
+            if (!channel_name || !channel_name.length) return false;\r
+            return (channel_prefix.indexOf(channel_name[0]) > -1);\r
+        };\r
+\r
+    };\r
+\r
+\r
+    model = Backbone.Model.extend(new model());\r
+\r
+    return new model(arguments);\r
+};\r
+
+
+
+kiwi.model.Gateway = function () {\r
+\r
+    // Set to a reference to this object within initialize()\r
+    var that = null;\r
+\r
+    this.defaults = {\r
+        /**\r
+        *   The name of the network\r
+        *   @type    String\r
+        */\r
+        name: 'Server',\r
+\r
+        /**\r
+        *   The address (URL) of the network\r
+        *   @type    String\r
+        */\r
+        address: '',\r
+\r
+        /**\r
+        *   The current nickname\r
+        *   @type   String\r
+        */\r
+        nick: '',\r
+\r
+        /**\r
+        *   The channel prefix for this network\r
+        *   @type    String\r
+        */\r
+        channel_prefix: '#',\r
+\r
+        /**\r
+        *   The user prefixes for channel owner/admin/op/voice etc. on this network\r
+        *   @type   Array\r
+        */\r
+        user_prefixes: ['~', '&', '@', '+'],\r
+\r
+        /**\r
+        *   The URL to the Kiwi server\r
+        *   @type   String\r
+        */\r
+        kiwi_server: '//kiwi'\r
+    };\r
+\r
+\r
+    this.initialize = function () {\r
+        that = this;\r
+        \r
+        // For ease of access. The socket.io object\r
+        this.socket = this.get('socket');\r
+    };\r
+\r
+\r
+    /**\r
+    *   Connects to the server\r
+    *   @param  {String}    host        The hostname or IP address of the IRC server to connect to\r
+    *   @param  {Number}    port        The port of the IRC server to connect to\r
+    *   @param  {Boolean}   ssl         Whether or not to connect to the IRC server using SSL\r
+    *   @param  {String}    password    The password to supply to the IRC server during registration\r
+    *   @param  {Function}  callback    A callback function to be invoked once Kiwi's server has connected to the IRC server\r
+    */\r
+    this.connect = function (host, port, ssl, password, callback) {\r
+        this.socket = io.connect(this.get('kiwi_server'), {\r
+            'try multiple transports': true,\r
+            'connect timeout': 3000,\r
+            'max reconnection attempts': 7,\r
+            'reconnection delay': 2000,\r
+            'sync disconnect on unload': false\r
+        });\r
+        this.socket.on('connect_failed', function (reason) {\r
+            // TODO: When does this even actually get fired? I can't find a case! ~Darren\r
+            console.debug('Unable to connect Socket.IO', reason);\r
+            console.log("kiwi.gateway.socket.on('connect_failed')");\r
+            //kiwi.front.tabviews.server.addMsg(null, ' ', 'Unable to connect to Kiwi IRC.\n' + reason, 'error');\r
+            this.socket.disconnect();\r
+            this.trigger("connect_fail", {reason: reason});\r
+        });\r
+\r
+        this.socket.on('error', function (e) {\r
+            this.trigger("connect_fail", {reason: e});\r
+            console.log("kiwi.gateway.socket.on('error')", {reason: e});\r
+        });\r
+\r
+        this.socket.on('connecting', function (transport_type) {\r
+            console.log("kiwi.gateway.socket.on('connecting')");\r
+            that.trigger("connecting");\r
+        });\r
+\r
+        /**\r
+         * Once connected to the kiwi server send the IRC connect command along\r
+         * with the IRC server details.\r
+         * A `connect` event is sent from the kiwi server once connected to the\r
+         * IRCD and the nick has been accepted.\r
+         */\r
+        this.socket.on('connect', function () {\r
+            this.emit('kiwi', {command: 'connect', nick: that.get('nick'), hostname: host, port: port, ssl: ssl, password:password}, function (err, server_num) {\r
+                if (!err) {\r
+                    that.server_num = server_num;\r
+                    console.log("kiwi.gateway.socket.on('connect')");\r
+                } else {\r
+                    console.log("kiwi.gateway.socket.on('error')", {reason: err});\r
+                }\r
+            });\r
+        });\r
+\r
+        this.socket.on('too_many_connections', function () {\r
+            this.trigger("connect_fail", {reason: 'too_many_connections'});\r
+        });\r
+\r
+        this.socket.on('irc', function (data, callback) {\r
+            that.parse(data.command, data.data);\r
+        });\r
+\r
+        this.socket.on('disconnect', function () {\r
+            that.trigger("disconnect", {});\r
+            console.log("kiwi.gateway.socket.on('disconnect')");\r
+        });\r
+\r
+        this.socket.on('close', function () {\r
+            console.log("kiwi.gateway.socket.on('close')");\r
+        });\r
+\r
+        this.socket.on('reconnecting', function (reconnectionDelay, reconnectionAttempts) {\r
+            console.log("kiwi.gateway.socket.on('reconnecting')");\r
+            that.trigger("reconnecting", {delay: reconnectionDelay, attempts: reconnectionAttempts});\r
+        });\r
+\r
+        this.socket.on('reconnect_failed', function () {\r
+            console.log("kiwi.gateway.socket.on('reconnect_failed')");\r
+        });\r
+    };\r
+\r
+\r
+\r
+    this.isConnected = function () {\r
+        return this.socket.socket.connected;\r
+    };\r
+\r
+\r
+    /*\r
+        Events:\r
+            msg\r
+            action\r
+            server_connect\r
+            options\r
+            motd\r
+            notice\r
+            userlist\r
+            nick\r
+            join\r
+            topic\r
+            part\r
+            kick\r
+            quit\r
+            whois\r
+            syncchannel_redirect\r
+            debug\r
+    */\r
+    /**\r
+    *   Parses the response from the server\r
+    */\r
+    this.parse = function (command, data) {\r
+        console.log('gateway event', command, data);\r
+        if (command !== undefined) {\r
+            that.trigger('on' + command, data);\r
+\r
+            switch (command) {\r
+            case 'options':\r
+                $.each(data.options, function (name, value) {\r
+                    switch (name) {\r
+                    case 'CHANTYPES':\r
+                        // TODO: Check this. Why is it only getting the first char?\r
+                        that.set('channel_prefix', value.join('').charAt(0));\r
+                        break;\r
+                    case 'NETWORK':\r
+                        that.set('name', value);\r
+                        break;\r
+                    case 'PREFIX':\r
+                        that.set('user_prefixes', value);\r
+                        break;\r
+                    }\r
+                });\r
+                break;\r
+\r
+            case 'connect':\r
+                that.set('nick', data.nick);\r
+                break;\r
+\r
+            case 'nick':\r
+                if (data.nick === that.get('nick')) {\r
+                    that.set('nick', data.newnick);\r
+                }\r
+                break;\r
+            /*\r
+            case 'sync':\r
+                if (kiwi.gateway.onSync && kiwi.gateway.syncing) {\r
+                    kiwi.gateway.syncing = false;\r
+                    kiwi.gateway.onSync(item);\r
+                }\r
+                break;\r
+            */\r
+\r
+            case 'kiwi':\r
+                this.emit('kiwi.' + data.namespace, data.data);\r
+                break;\r
+            }\r
+        }\r
+    };\r
+\r
+    /**\r
+    *   Sends data to the server\r
+    *   @private\r
+    *   @param  {Object}    data        The data to send\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    this.sendData = function (data, callback) {\r
+        this.socket.emit('irc', {server: 0, data: JSON.stringify(data)}, callback);\r
+    };\r
+\r
+    /**\r
+    *   Sends a PRIVMSG message\r
+    *   @param  {String}    target      The target of the message (e.g. a channel or nick)\r
+    *   @param  {String}    msg         The message to send\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    this.privmsg = function (target, msg, callback) {\r
+        var data = {\r
+            method: 'privmsg',\r
+            args: {\r
+                target: target,\r
+                msg: msg\r
+            }\r
+        };\r
+\r
+        this.sendData(data, callback);\r
+    };\r
+\r
+    /**\r
+    *   Sends a NOTICE message\r
+    *   @param  {String}    target      The target of the message (e.g. a channel or nick)\r
+    *   @param  {String}    msg         The message to send\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    this.notice = function (target, msg, callback) {\r
+        var data = {\r
+            method: 'notice',\r
+            args: {\r
+                target: target,\r
+                msg: msg\r
+            }\r
+        };\r
+\r
+        this.sendData(data, callback);\r
+    };\r
+\r
+    /**\r
+    *   Sends a CTCP message\r
+    *   @param  {Boolean}   request     Indicates whether this is a CTCP request (true) or reply (false)\r
+    *   @param  {String}    type        The type of CTCP message, e.g. 'VERSION', 'TIME', 'PING' etc.\r
+    *   @param  {String}    target      The target of the message, e.g a channel or nick\r
+    *   @param  {String}    params      Additional paramaters\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    this.ctcp = function (request, type, target, params, callback) {\r
+        var data = {\r
+            method: 'ctcp',\r
+            args: {\r
+                request: request,\r
+                type: type,\r
+                target: target,\r
+                params: params\r
+            }\r
+        };\r
+\r
+        this.sendData(data, callback);\r
+    };\r
+\r
+    /**\r
+    *   @param  {String}    target      The target of the message (e.g. a channel or nick)\r
+    *   @param  {String}    msg         The message to send\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    this.action = function (target, msg, callback) {\r
+        this.ctcp(true, 'ACTION', target, msg, callback);\r
+    };\r
+\r
+    /**\r
+    *   Joins a channel\r
+    *   @param  {String}    channel     The channel to join\r
+    *   @param  {String}    key         The key to the channel\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    this.join = function (channel, key, callback) {\r
+        var data = {\r
+            method: 'join',\r
+            args: {\r
+                channel: channel,\r
+                key: key\r
+            }\r
+        };\r
+\r
+        this.sendData(data, callback);\r
+    };\r
+\r
+    /**\r
+    *   Leaves a channel\r
+    *   @param  {String}    channel     The channel to part\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    this.part = function (channel, callback) {\r
+        var data = {\r
+            method: 'part',\r
+            args: {\r
+                channel: channel\r
+            }\r
+        };\r
+\r
+        this.sendData(data, callback);\r
+    };\r
+\r
+    /**\r
+    *   Queries or modifies a channell topic\r
+    *   @param  {String}    channel     The channel to query or modify\r
+    *   @param  {String}    new_topic   The new topic to set\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    this.topic = function (channel, new_topic, callback) {\r
+        var data = {\r
+            method: 'topic',\r
+            args: {\r
+                channel: channel,\r
+                topic: new_topic\r
+            }\r
+        };\r
+\r
+        this.sendData(data, callback);\r
+    };\r
+\r
+    /**\r
+    *   Kicks a user from a channel\r
+    *   @param  {String}    channel     The channel to kick the user from\r
+    *   @param  {String}    nick        The nick of the user to kick\r
+    *   @param  {String}    reason      The reason for kicking the user\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    this.kick = function (channel, nick, reason, callback) {\r
+        var data = {\r
+            method: 'kick',\r
+            args: {\r
+                channel: channel,\r
+                nick: nick,\r
+                reason: reason\r
+            }\r
+        };\r
+\r
+        this.sendData(data, callback);\r
+    };\r
+\r
+    /**\r
+    *   Disconnects us from the server\r
+    *   @param  {String}    msg         The quit message to send to the IRC server\r
+    *   @param  {Function}   callback    A callback function\r
+    */\r
+    this.quit = function (msg, callback) {\r
+        msg = msg || "";\r
+        var data = {\r
+            method: 'quit',\r
+            args: {\r
+                message: msg\r
+            }\r
+        };\r
+\r
+        this.sendData(data, callback);\r
+    };\r
+\r
+    /**\r
+    *   Sends a string unmodified to the IRC server\r
+    *   @param  {String}    data        The data to send to the IRC server\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    this.raw = function (data, callback) {\r
+        data = {\r
+            method: 'raw',\r
+            args: {\r
+                data: data\r
+            }\r
+        };\r
+\r
+        this.sendData(data, callback);\r
+    };\r
+\r
+    /**\r
+    *   Changes our nickname\r
+    *   @param  {String}    new_nick    Our new nickname\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    this.changeNick = function (new_nick, callback) {\r
+        var data = {\r
+            method: 'nick',\r
+            args: {\r
+                nick: new_nick\r
+            }\r
+        };\r
+\r
+        this.sendData(data, callback);\r
+    };\r
+\r
+    /**\r
+    *   Sends data to a fellow Kiwi IRC user\r
+    *   @param  {String}    target      The nick of the Kiwi IRC user to send to\r
+    *   @param  {String}    data        The data to send\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    this.kiwi = function (target, data, callback) {\r
+        data = {\r
+            method: 'kiwi',\r
+            args: {\r
+                target: target,\r
+                data: data\r
+            }\r
+        };\r
+\r
+        this.sendData(data, callback);\r
+    };\r
+\r
+\r
+    return new (Backbone.Model.extend(this))(arguments);\r
+};
+
+
+kiwi.model.Member = Backbone.Model.extend({\r
+    sortModes: function (modes) {\r
+        return modes.sort(function (a, b) {\r
+            var a_idx, b_idx, i;\r
+            var user_prefixes = kiwi.gateway.get('user_prefixes');\r
+\r
+            for (i = 0; i < user_prefixes.length; i++) {\r
+                if (user_prefixes[i].mode === a) {\r
+                    a_idx = i;\r
+                }\r
+            }\r
+            for (i = 0; i < user_prefixes.length; i++) {\r
+                if (user_prefixes[i].mode === b) {\r
+                    b_idx = i;\r
+                }\r
+            }\r
+            if (a_idx < b_idx) {\r
+                return -1;\r
+            } else if (a_idx > b_idx) {\r
+                return 1;\r
+            } else {\r
+                return 0;\r
+            }\r
+        });\r
+    },\r
+    initialize: function (attributes) {\r
+        var nick, modes, prefix;\r
+        nick = this.stripPrefix(this.get("nick"));\r
+\r
+        modes = this.get("modes");\r
+        modes = modes || [];\r
+        this.sortModes(modes);\r
+        this.set({"nick": nick, "modes": modes, "prefix": this.getPrefix(modes)}, {silent: true});\r
+    },\r
+    addMode: function (mode) {\r
+        var modes_to_add = mode.split(''),\r
+            modes, prefix;\r
+\r
+        modes = this.get("modes");\r
+        $.each(modes_to_add, function (index, item) {\r
+            modes.push(item);\r
+        });\r
+        \r
+        modes = this.sortModes(modes);\r
+        this.set({"prefix": this.getPrefix(modes), "modes": modes});\r
+    },\r
+    removeMode: function (mode) {\r
+        var modes_to_remove = mode.split(''),\r
+            modes, prefix;\r
+\r
+        modes = this.get("modes");\r
+        modes = _.reject(modes, function (m) {\r
+            return (_.indexOf(modes_to_remove, m) !== -1);\r
+        });\r
+\r
+        this.set({"prefix": this.getPrefix(modes), "modes": modes});\r
+    },\r
+    getPrefix: function (modes) {\r
+        var prefix = '';\r
+        var user_prefixes = kiwi.gateway.get('user_prefixes');\r
+\r
+        if (typeof modes[0] !== 'undefined') {\r
+            prefix = _.detect(user_prefixes, function (prefix) {\r
+                return prefix.mode === modes[0];\r
+            });\r
+            prefix = (prefix) ? prefix.symbol : '';\r
+        }\r
+        return prefix;\r
+    },\r
+    stripPrefix: function (nick) {\r
+        var tmp = nick, i, j, k;\r
+        var user_prefixes = kiwi.gateway.get('user_prefixes');\r
+        i = 0;\r
+\r
+        for (j = 0; j < nick.length; j++) {\r
+            for (k = 0; k < user_prefixes.length; k++) {\r
+                if (nick.charAt(j) === user_prefixes[k].symbol) {\r
+                    i++;\r
+                    break;\r
+                }\r
+            }\r
+        }\r
+\r
+        return tmp.substr(i);\r
+    },\r
+    displayNick: function (full) {\r
+        var display = this.get('nick');\r
+\r
+        if (full) {\r
+            if (this.get("ident")) {\r
+                display += ' [' + this.get("ident") + '@' + this.get("hostname") + ']';\r
+            }\r
+        }\r
+\r
+        return display;\r
+    }\r
+});
+
+
+kiwi.model.MemberList = Backbone.Collection.extend({\r
+    model: kiwi.model.Member,\r
+    comparator: function (a, b) {\r
+        var i, a_modes, b_modes, a_idx, b_idx, a_nick, b_nick;\r
+        var user_prefixes = kiwi.gateway.get('user_prefixes');\r
+        a_modes = a.get("modes");\r
+        b_modes = b.get("modes");\r
+        // Try to sort by modes first\r
+        if (a_modes.length > 0) {\r
+            // a has modes, but b doesn't so a should appear first\r
+            if (b_modes.length === 0) {\r
+                return -1;\r
+            }\r
+            a_idx = b_idx = -1;\r
+            // Compare the first (highest) mode\r
+            for (i = 0; i < user_prefixes.length; i++) {\r
+                if (user_prefixes[i].mode === a_modes[0]) {\r
+                    a_idx = i;\r
+                }\r
+            }\r
+            for (i = 0; i < user_prefixes.length; i++) {\r
+                if (user_prefixes[i].mode === b_modes[0]) {\r
+                    b_idx = i;\r
+                }\r
+            }\r
+            if (a_idx < b_idx) {\r
+                return -1;\r
+            } else if (a_idx > b_idx) {\r
+                return 1;\r
+            }\r
+            // If we get to here both a and b have the same highest mode so have to resort to lexicographical sorting\r
+\r
+        } else if (b_modes.length > 0) {\r
+            // b has modes but a doesn't so b should appear first\r
+            return 1;\r
+        }\r
+        a_nick = a.get("nick").toLocaleUpperCase();\r
+        b_nick = b.get("nick").toLocaleUpperCase();\r
+        // Lexicographical sorting\r
+        if (a_nick < b_nick) {\r
+            return -1;\r
+        } else if (a_nick > b_nick) {\r
+            return 1;\r
+        } else {\r
+            return 0;\r
+        }\r
+    },\r
+    initialize: function (options) {\r
+        this.view = new kiwi.view.MemberList({"model": this});\r
+    },\r
+    getByNick: function (nick) {\r
+        if (typeof nick !== 'string') return;\r
+        return this.find(function (m) {\r
+            return nick.toLowerCase() === m.get('nick').toLowerCase();\r
+        });\r
+    }\r
+});
+
+
+kiwi.model.Panel = Backbone.Model.extend({\r
+    initialize: function (attributes) {\r
+        var name = this.get("name") || "";\r
+        this.view = new kiwi.view.Panel({"model": this, "name": name});\r
+        this.set({\r
+            "scrollback": [],\r
+            "name": name\r
+        }, {"silent": true});\r
+    },\r
+\r
+    addMsg: function (nick, msg, type, opts) {\r
+        var message_obj, bs, d;\r
+\r
+        opts = opts || {};\r
+\r
+        // Time defaults to now\r
+        if (!opts || typeof opts.time === 'undefined') {\r
+            d = new Date();\r
+            opts.time = d.getHours().toString().lpad(2, "0") + ":" + d.getMinutes().toString().lpad(2, "0") + ":" + d.getSeconds().toString().lpad(2, "0");\r
+        }\r
+\r
+        // CSS style defaults to empty string\r
+        if (!opts || typeof opts.style === 'undefined') {\r
+            opts.style = '';\r
+        }\r
+\r
+        // Run through the plugins\r
+        message_obj = {"msg": msg, "time": opts.time, "nick": nick, "chan": this.get("name"), "type": type, "style": opts.style};\r
+        //tmp = kiwi.plugs.run('addmsg', message_obj);\r
+        if (!message_obj) {\r
+            return;\r
+        }\r
+\r
+        // The CSS class (action, topic, notice, etc)\r
+        if (typeof message_obj.type !== "string") {\r
+            message_obj.type = '';\r
+        }\r
+\r
+        // Make sure we don't have NaN or something\r
+        if (typeof message_obj.msg !== "string") {\r
+            message_obj.msg = '';\r
+        }\r
+\r
+        // Update the scrollback\r
+        bs = this.get("scrollback");\r
+        bs.push(message_obj);\r
+\r
+        // Keep the scrolback limited\r
+        if (bs.length > 250) {\r
+            bs.splice(250);\r
+        }\r
+        this.set({"scrollback": bs}, {silent: true});\r
+\r
+        this.trigger("msg", message_obj);\r
+    },\r
+\r
+    closePanel: function () {\r
+        if (this.view) {\r
+            this.view.unbind();\r
+            this.view.remove();\r
+            this.view = undefined;\r
+            delete this.view;\r
+        }\r
+\r
+        var members = this.get('members');\r
+        if (members) {\r
+            members.reset([]);\r
+            this.unset('members');\r
+        }\r
+\r
+        kiwi.app.panels.remove(this);\r
+\r
+        this.unbind();\r
+        this.destroy();\r
+\r
+        // If closing the active panel, switch to the server panel\r
+        if (this.cid === kiwi.app.panels.active.cid) {\r
+            kiwi.app.panels.server.view.show();\r
+        }\r
+    },\r
+\r
+    // Alias to closePanel() for child objects to override\r
+    close: function () {\r
+        return this.closePanel();\r
+    },\r
+\r
+    isChannel: function () {\r
+        var channel_prefix = kiwi.gateway.get('channel_prefix'),\r
+            this_name = this.get('name');\r
+\r
+        if (this.isApplet() || !this_name) return false;\r
+        return (channel_prefix.indexOf(this_name[0]) > -1);\r
+    },\r
+\r
+    isApplet: function () {\r
+        return this.applet ? true : false;\r
+    },\r
+\r
+    isServer: function () {\r
+        return this.server ? true : false;\r
+    },\r
+\r
+    isActive: function () {\r
+        return (kiwi.app.panels.active === this);\r
+    }\r
+});
+
+
+kiwi.model.PanelList = Backbone.Collection.extend({\r
+    model: kiwi.model.Panel,\r
+\r
+    comparator: function (chan) {\r
+        return chan.get("name");\r
+    },\r
+    initialize: function () {\r
+        this.view = new kiwi.view.Tabs({"el": $('#tabs')[0], "model": this});\r
+\r
+        // Automatically create a server tab\r
+        this.add(new kiwi.model.Server({'name': kiwi.gateway.get('name')}));\r
+        this.server = this.getByName(kiwi.gateway.get('name'));\r
+\r
+        // Holds the active panel\r
+        this.active = null;\r
+\r
+        // Keep a tab on the active panel\r
+        this.bind('active', function (active_panel) {\r
+            this.active = active_panel;\r
+        }, this);\r
+\r
+    },\r
+    getByName: function (name) {\r
+        if (typeof name !== 'string') return;\r
+        return this.find(function (c) {\r
+            return name.toLowerCase() === c.get('name').toLowerCase();\r
+        });\r
+    }\r
+});
+
+
+// TODO: Channel modes\r
+// TODO: Listen to gateway events for anythign related to this channel\r
+kiwi.model.Channel = kiwi.model.Panel.extend({\r
+    initialize: function (attributes) {\r
+        var name = this.get("name") || "",\r
+            members;\r
+\r
+        this.view = new kiwi.view.Channel({"model": this, "name": name});\r
+        this.set({\r
+            "members": new kiwi.model.MemberList(),\r
+            "name": name,\r
+            "scrollback": [],\r
+            "topic": ""\r
+        }, {"silent": true});\r
+\r
+        members = this.get("members");\r
+        members.bind("add", function (member) {\r
+            this.addMsg(' ', '== ' + member.displayNick(true) + ' has joined', 'action join');\r
+        }, this);\r
+\r
+        members.bind("remove", function (member, members, options) {\r
+            var msg = (options.message) ? '(' + options.message + ')' : '';\r
+\r
+            if (options.type === 'quit') {\r
+                this.addMsg(' ', '== ' + member.displayNick(true) + ' has quit ' + msg, 'action quit');\r
+            } else if(options.type === 'kick') {\r
+                this.addMsg(' ', '== ' + member.displayNick(true) + ' was kicked by ' + options.by + ' ' + msg, 'action kick');\r
+            } else {\r
+                this.addMsg(' ', '== ' + member.displayNick(true) + ' has left ' + msg, 'action part');\r
+            }\r
+        }, this);\r
+    }\r
+});
+
+
+kiwi.model.Server = kiwi.model.Panel.extend({\r
+    // Used to determine if this is a server panel\r
+    server: true,\r
+\r
+    initialize: function (attributes) {\r
+        var name = "Server";\r
+        this.view = new kiwi.view.Panel({"model": this, "name": name});\r
+        this.set({\r
+            "scrollback": [],\r
+            "name": name\r
+        }, {"silent": true});\r
+\r
+        //this.addMsg(' ', '--> Kiwi IRC: Such an awesome IRC client', '', {style: 'color:#009900;'});\r
+\r
+        this.server_login = new kiwi.view.ServerSelect();\r
+        \r
+        this.view.$el.append(this.server_login.$el);\r
+        this.server_login.show();\r
+    }\r
+});
+
+
+kiwi.model.Applet = kiwi.model.Panel.extend({\r
+    // Used to determine if this is an applet panel. Applet panel tabs are treated\r
+    // differently than others\r
+    applet: true,\r
+\r
+\r
+    initialize: function (attributes) {\r
+        // Temporary name\r
+        var name = "applet_"+(new Date().getTime().toString()) + Math.ceil(Math.random()*100).toString();\r
+        this.view = new kiwi.view.Applet({model: this, name: name});\r
+\r
+        this.set({\r
+            "name": name\r
+        }, {"silent": true});\r
+\r
+        // Holds the loaded applet\r
+        this.loaded_applet = null;\r
+    },\r
+\r
+    // Load an applet within this panel\r
+    load: function (applet_object, applet_name) {\r
+        if (typeof applet_object === 'object') {\r
+            // Make sure this is a valid Applet\r
+            if (applet_object.get || applet_object.extend) {\r
+\r
+                // Try find a title for the applet\r
+                this.set('title', applet_object.get('title') || 'Unknown Applet');\r
+\r
+                // Update the tabs title if the applet changes it\r
+                applet_object.bind('change:title', function (obj, new_value) {\r
+                    this.set('title', new_value);\r
+                }, this);\r
+\r
+                // If this applet has a UI, add it now\r
+                this.view.$el.html('');\r
+                if (applet_object.view) {\r
+                    this.view.$el.append(applet_object.view.$el);\r
+                }\r
+\r
+                // Keep a reference to this applet\r
+                this.loaded_applet = applet_object;\r
+            }\r
+\r
+        } else if (typeof applet_object === 'string') {\r
+            // Treat this as a URL to an applet script and load it\r
+            this.loadFromUrl(applet_object, applet_name);\r
+        }\r
+\r
+        return this;\r
+    },\r
+\r
+    loadFromUrl: function(applet_url, applet_name) {\r
+        var that = this;\r
+\r
+        this.view.$el.html('Loading..');\r
+        $script(applet_url, function () {\r
+            // Check if the applet loaded OK\r
+            if (!kiwi.applets[applet_name]) {\r
+                that.view.$el.html('Not found');\r
+                return;\r
+            }\r
+\r
+            // Load a new instance of this applet\r
+            that.load(new kiwi.applets[applet_name]());\r
+        });\r
+    },\r
+\r
+    close: function () {\r
+        this.view.$el.remove();\r
+        this.destroy();\r
+        \r
+        this.view = undefined;\r
+\r
+        // Call the applets dispose method if it has one\r
+        if (this.loaded_applet && this.loaded_applet.dispose) {\r
+            this.loaded_applet.dispose();\r
+        }\r
+\r
+        this.closePanel();\r
+    }\r
+});
+
+
+(function () {\r
+    var View = Backbone.View.extend({\r
+        events: {\r
+            'click .save': 'saveSettings'\r
+        },\r
+\r
+        initialize: function (options) {\r
+            this.$el = $($('#tmpl_applet_settings').html());\r
+        },\r
+        \r
+        saveSettings: function () {\r
+            var theme = $('.theme', this.$el).val();\r
+\r
+            // Clear any current theme\r
+            kiwi.app.view.$el.removeClass(function (i, css) {\r
+                return (css.match (/\btheme_\S+/g) || []).join(' ');\r
+            });\r
+\r
+            if (theme) kiwi.app.view.$el.addClass('theme_' + theme);\r
+        }\r
+    });\r
+\r
+\r
+\r
+    kiwi.applets.Settings = Backbone.Model.extend({\r
+        initialize: function () {\r
+            this.set('title', 'Settings');\r
+            this.view = new View();\r
+        }\r
+    });\r
+})();
+
+
+(function () {\r
+    var View = Backbone.View.extend({\r
+        events: {\r
+            'click .save': 'saveSettings'\r
+        },\r
+\r
+        initialize: function (options) {\r
+            this.$el = $($('#tmpl_applet_settings').html());\r
+        },\r
+        \r
+        saveSettings: function () {\r
+            var theme = $('.theme', this.$el).val(),\r
+                containers = $('#panels > .panel_container');\r
+\r
+            // Clear any current theme\r
+            containers.removeClass(function (i, css) {\r
+                return (css.match (/\btheme_\S+/g) || []).join(' ');\r
+            });\r
+\r
+            if (theme) containers.addClass('theme_' + theme);\r
+        }\r
+    });\r
+\r
+\r
+\r
+    kiwi.applets.nickserv = Backbone.Model.extend({\r
+        initialize: function () {\r
+            this.set('title', 'Nickserv Login');\r
+            //this.view = new View();\r
+\r
+            kiwi.global.control.on('command_login', this.loginCommand, this);\r
+        },\r
+\r
+        loginCommand: function (event) {\r
+            console.log('waheeyy');\r
+        }\r
+    });\r
+})();
+
+
+(function () {\r
+\r
+    var View = Backbone.View.extend({\r
+        events: {\r
+        },\r
+\r
+\r
+\r
+        initialize: function (options) {\r
+            this.$el = $($('#tmpl_channel_list').html());\r
+\r
+            this.channels = [];\r
+\r
+            // Sort the table by num. users?\r
+            this.ordered = false;\r
+\r
+            // Waiting to add the table back into the DOM?\r
+            this.waiting = false;\r
+        },\r
+\r
+\r
+        render: function () {\r
+            var table = $('table', this.$el),\r
+                tbody = table.children('tbody:first').detach();\r
+            /*tbody.children().each(function (child) {\r
+                var i, chan;\r
+                child = $(child);\r
+                chan = child.children('td:first').text();\r
+                for (i = 0; i < chanList.length; i++) {\r
+                    if (chanList[i].channel === chan) {\r
+                        chanList[i].html = child.detach();\r
+                        break;\r
+                    }\r
+                }\r
+            });*/\r
+\r
+            if (this.ordered) {\r
+                this.channels.sort(function (a, b) {\r
+                    return b.num_users - a.num_users;\r
+                });\r
+            }\r
+\r
+            _.each(this.channels, function (chan) {\r
+                tbody.append(chan.html);\r
+            });\r
+            table.append(tbody);\r
+        }\r
+    });\r
+\r
+\r
+\r
+\r
+    kiwi.applets.Chanlist = Backbone.Model.extend({\r
+        initialize: function () {\r
+            this.set('title', 'Channel List');\r
+            this.view = new View();\r
+        },\r
+\r
+\r
+        addChannel: function (channels) {\r
+            var that = this;\r
+\r
+            if (!_.isArray(channels)) {\r
+                channels = [channels];\r
+            }\r
+            _.each(channels, function (chan) {\r
+                var html, channel;\r
+                html = '<tr><td><a class="chan">' + chan.channel + '</a></td><td class="num_users" style="text-align: center;">' + chan.num_users + '</td><td style="padding-left: 2em;">' + formatIRCMsg(chan.topic) + '</td></tr>';\r
+                chan.html = html;\r
+                that.view.channels.push(chan);\r
+            });\r
+\r
+            if (!that.view.waiting) {\r
+                that.view.waiting = true;\r
+                _.defer(function () {\r
+                    that.view.render();\r
+                    that.view.waiting = false;\r
+                });\r
+            }\r
+        },\r
+\r
+\r
+        dispose: function () {\r
+            this.view.channels = null;\r
+            this.view.unbind();\r
+            this.view.$el.html('');\r
+            this.view.remove();\r
+            this.view = null;\r
+        }\r
+    });\r
+\r
+\r
+})();
+
+
+/*jslint devel: true, browser: true, continue: true, sloppy: true, forin: true, plusplus: true, maxerr: 50, indent: 4, nomen: true, regexp: true*/
+/*globals $, front, gateway, Utilityview */
+
+
+
+/**
+*   Suppresses console.log
+*   @param  {Boolean}   debug   Whether to re-enable console.log or not
+*/
+function manageDebug(debug) {
+    var log, consoleBackUp;
+    if (window.console) {
+        consoleBackUp = window.console.log;
+        window.console.log = function () {
+            if (debug) {
+                consoleBackUp.apply(console, arguments);
+            }
+        };
+    } else {
+        log = window.opera ? window.opera.postError : alert;
+        window.console = {};
+        window.console.log = function (str) {
+            if (debug) {
+                log(str);
+            }
+        };
+    }
+}
+
+/**
+*   Generate a random string of given length
+*   @param      {Number}    string_length   The length of the random string
+*   @returns    {String}                    The random string
+*/
+function randomString(string_length) {
+    var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz",
+        randomstring = '',
+        i,
+        rnum;
+    for (i = 0; i < string_length; i++) {
+        rnum = Math.floor(Math.random() * chars.length);
+        randomstring += chars.substring(rnum, rnum + 1);
+    }
+    return randomstring;
+}
+
+/**
+*   String.trim shim
+*/
+if (typeof String.prototype.trim === 'undefined') {
+    String.prototype.trim = function () {
+        return this.replace(/^\s+|\s+$/g, "");
+    };
+}
+
+/**
+*   String.lpad shim
+*   @param      {Number}    length      The length of padding
+*   @param      {String}    characher   The character to pad with
+*   @returns    {String}                The padded string
+*/
+if (typeof String.prototype.lpad === 'undefined') {
+    String.prototype.lpad = function (length, character) {
+        var padding = "",
+            i;
+        for (i = 0; i < length; i++) {
+            padding += character;
+        }
+        return (padding + this).slice(-length);
+    };
+}
+
+
+/**
+*   Convert seconds into hours:minutes:seconds
+*   @param      {Number}    secs    The number of seconds to converts
+*   @returns    {Object}            An object representing the hours/minutes/second conversion of secs
+*/
+function secondsToTime(secs) {
+    var hours, minutes, seconds, divisor_for_minutes, divisor_for_seconds, obj;
+    hours = Math.floor(secs / (60 * 60));
+
+    divisor_for_minutes = secs % (60 * 60);
+    minutes = Math.floor(divisor_for_minutes / 60);
+
+    divisor_for_seconds = divisor_for_minutes % 60;
+    seconds = Math.ceil(divisor_for_seconds);
+
+    obj = {
+        "h": hours,
+        "m": minutes,
+        "s": seconds
+    };
+    return obj;
+}
+
+
+
+
+
+
+/* Command input Alias + re-writing */
+function InputPreProcessor () {
+    this.recursive_depth = 3;
+
+    this.aliases = {};
+    this.vars = {version: 1};
+
+    // Current recursive depth
+    var depth = 0;
+
+
+    // Takes an array of words to process!
+    this.processInput = function (input) {
+        var words = input || [],
+            alias = this.aliases[words[0]],
+            alias_len,
+            current_alias_word = '',
+            compiled = [];
+
+        // If an alias wasn't found, return the original input
+        if (!alias) return input;
+
+        // Split the alias up into useable words
+        alias = alias.split(' ');
+        alias_len = alias.length;
+
+        // Iterate over each word and pop them into the final compiled array.
+        // Any $ words are processed with the result ending into the compiled array.
+        for (var i=0; i<alias_len; i++) {
+            current_alias_word = alias[i];
+
+            // Non $ word
+            if (current_alias_word[0] !== '$') {
+                compiled.push(current_alias_word);
+                continue;
+            }
+
+            // Refering to an input word ($N)
+            if (!isNaN(current_alias_word[1])) {
+                var num = current_alias_word.match(/\$(\d+)(\+)?(\d+)?/);
+
+                // Did we find anything or does the word it refers to non-existant?
+                if (!num || !words[num[1]]) continue;
+                
+                if (num[2] === '+' && num[3]) {
+                    // Add X number of words
+                    compiled = compiled.concat(words.slice(parseInt(num[1], 10), parseInt(num[1], 10) + parseInt(num[3], 10)));
+                } else if (num[2] === '+') {
+                    // Add the remaining of the words
+                    compiled = compiled.concat(words.slice(parseInt(num[1], 10)));
+                } else {
+                    // Add a single word
+                    compiled.push(words[parseInt(num[1], 10)]);
+                }
+
+                continue;
+            }
+
+
+            // Refering to a variable
+            if (typeof this.vars[current_alias_word.substr(1)] !== 'undefined') {
+
+                // Get the variable
+                compiled.push(this.vars[current_alias_word.substr(1)]);
+
+                continue;
+            }
+
+        }
+
+        return compiled;
+    };
+
+
+    this.process = function (input) {
+        input = input || '';
+
+        var words = input.split(' ');
+
+        depth++;
+        if (depth >= this.recursive_depth) {
+            depth--;
+            return input;
+        }
+
+        if (this.aliases[words[0]]) {
+            words = this.processInput(words);
+            
+            if (this.aliases[words[0]]) {
+                words = this.process(words.join(' ')).split(' ');
+            }
+
+        }
+
+        depth--;
+        return words.join(' ');
+    };
+}
+
+
+
+
+
+
+
+
+
+
+
+/**
+ * Convert HSL to RGB formatted colour
+ */
+function hsl2rgb(h, s, l) {
+    var m1, m2, hue;
+    var r, g, b
+    s /=100;
+    l /= 100;
+    if (s == 0)
+        r = g = b = (l * 255);
+    else {
+        function HueToRgb(m1, m2, hue) {
+            var v;
+            if (hue < 0)
+                hue += 1;
+            else if (hue > 1)
+                hue -= 1;
+
+            if (6 * hue < 1)
+                v = m1 + (m2 - m1) * hue * 6;
+            else if (2 * hue < 1)
+                v = m2;
+            else if (3 * hue < 2)
+                v = m1 + (m2 - m1) * (2/3 - hue) * 6;
+            else
+                v = m1;
+
+            return 255 * v;
+        }
+        if (l <= 0.5)
+            m2 = l * (s + 1);
+        else
+            m2 = l + s - l * s;
+        m1 = l * 2 - m2;
+        hue = h / 360;
+        r = HueToRgb(m1, m2, hue + 1/3);
+        g = HueToRgb(m1, m2, hue);
+        b = HueToRgb(m1, m2, hue - 1/3);
+    }
+    return [r,g,b];
+}
+
+
+
+
+
+/**
+*   Formats a message. Adds bold, underline and colouring
+*   @param      {String}    msg The message to format
+*   @returns    {String}        The HTML formatted message
+*/
+function formatIRCMsg (msg) {
+    var re, next;
+
+    if ((!msg) || (typeof msg !== 'string')) {
+        return '';
+    }
+
+    // bold
+    if (msg.indexOf(String.fromCharCode(2)) !== -1) {
+        next = '<b>';
+        while (msg.indexOf(String.fromCharCode(2)) !== -1) {
+            msg = msg.replace(String.fromCharCode(2), next);
+            next = (next === '<b>') ? '</b>' : '<b>';
+        }
+        if (next === '</b>') {
+            msg = msg + '</b>';
+        }
+    }
+
+    // underline
+    if (msg.indexOf(String.fromCharCode(31)) !== -1) {
+        next = '<u>';
+        while (msg.indexOf(String.fromCharCode(31)) !== -1) {
+            msg = msg.replace(String.fromCharCode(31), next);
+            next = (next === '<u>') ? '</u>' : '<u>';
+        }
+        if (next === '</u>') {
+            msg = msg + '</u>';
+        }
+    }
+
+    // colour
+    /**
+    *   @inner
+    */
+    msg = (function (msg) {
+        var replace, colourMatch, col, i, match, to, endCol, fg, bg, str;
+        replace = '';
+        /**
+        *   @inner
+        */
+        colourMatch = function (str) {
+            var re = /^\x03([0-9][0-5]?)(,([0-9][0-5]?))?/;
+            return re.exec(str);
+        };
+        /**
+        *   @inner
+        */
+        col = function (num) {
+            switch (parseInt(num, 10)) {
+            case 0:
+                return '#FFFFFF';
+            case 1:
+                return '#000000';
+            case 2:
+                return '#000080';
+            case 3:
+                return '#008000';
+            case 4:
+                return '#FF0000';
+            case 5:
+                return '#800040';
+            case 6:
+                return '#800080';
+            case 7:
+                return '#FF8040';
+            case 8:
+                return '#FFFF00';
+            case 9:
+                return '#80FF00';
+            case 10:
+                return '#008080';
+            case 11:
+                return '#00FFFF';
+            case 12:
+                return '#0000FF';
+            case 13:
+                return '#FF55FF';
+            case 14:
+                return '#808080';
+            case 15:
+                return '#C0C0C0';
+            default:
+                return null;
+            }
+        };
+        if (msg.indexOf('\x03') !== -1) {
+            i = msg.indexOf('\x03');
+            replace = msg.substr(0, i);
+            while (i < msg.length) {
+                /**
+                *   @inner
+                */
+                match = colourMatch(msg.substr(i, 6));
+                if (match) {
+                    //console.log(match);
+                    // Next colour code
+                    to = msg.indexOf('\x03', i + 1);
+                    endCol = msg.indexOf(String.fromCharCode(15), i + 1);
+                    if (endCol !== -1) {
+                        if (to === -1) {
+                            to = endCol;
+                        } else {
+                            to = ((to < endCol) ? to : endCol);
+                        }
+                    }
+                    if (to === -1) {
+                        to = msg.length;
+                    }
+                    //console.log(i, to);
+                    fg = col(match[1]);
+                    bg = col(match[3]);
+                    str = msg.substring(i + 1 + match[1].length + ((bg !== null) ? match[2].length + 1 : 0), to);
+                    //console.log(str);
+                    replace += '<span style="' + ((fg !== null) ? 'color: ' + fg + '; ' : '') + ((bg !== null) ? 'background-color: ' + bg + ';' : '') + '">' + str + '</span>';
+                    i = to;
+                } else {
+                    if ((msg[i] !== '\x03') && (msg[i] !== String.fromCharCode(15))) {
+                        replace += msg[i];
+                    }
+                    i++;
+                }
+            }
+            return replace;
+        }
+        return msg;
+    }(msg));
+    
+    return msg;
+}
+
+
+
+
+function formatDate (d) {
+    d = d || new Date();
+    return d.toLocaleDateString() + ', ' + d.getHours().toString() + ':' + d.getMinutes().toString() + ':' + d.getSeconds().toString();
+}
+
+
+
+
+
+
+
+
+/*
+    PLUGINS
+    Each function in each object is looped through and ran. The resulting text
+    is expected to be returned.
+*/
+var plugins = [
+    {
+        name: "images",
+        onaddmsg: function (event, opts) {
+            if (!event.msg) {
+                return event;
+            }
+
+            event.msg = event.msg.replace(/^((https?\:\/\/|ftp\:\/\/)|(www\.))(\S+)(\w{2,4})(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?(\.jpg|\.jpeg|\.gif|\.bmp|\.png)$/gi, function (url) {
+                // Don't let any future plugins change it (ie. html_safe plugins)
+                event.event_bubbles = false;
+
+                var img = '<img class="link_img_a" src="' + url + '" height="100%" width="100%" />';
+                return '<a class="link_ext link_img" target="_blank" rel="nofollow" href="' + url + '" style="height:50px;width:50px;display:block">' + img + '<div class="tt box"></div></a>';
+            });
+
+            return event;
+        }
+    },
+
+    {
+        name: "html_safe",
+        onaddmsg: function (event, opts) {
+            event.msg = $('<div/>').text(event.msg).html();
+            event.nick = $('<div/>').text(event.nick).html();
+
+            return event;
+        }
+    },
+
+    {
+        name: "activity",
+        onaddmsg: function (event, opts) {
+            //if (kiwi.front.cur_channel.name.toLowerCase() !== kiwi.front.tabviews[event.tabview.toLowerCase()].name) {
+            //    kiwi.front.tabviews[event.tabview].activity();
+            //}
+
+            return event;
+        }
+    },
+
+    {
+        name: "highlight",
+        onaddmsg: function (event, opts) {
+            //var tab = Tabviews.getTab(event.tabview.toLowerCase());
+
+            // If we have a highlight...
+            //if (event.msg.toLowerCase().indexOf(kiwi.gateway.nick.toLowerCase()) > -1) {
+            //    if (Tabview.getCurrentTab() !== tab) {
+            //        tab.highlight();
+            //    }
+            //    if (kiwi.front.isChannel(tab.name)) {
+            //        event.msg = '<span style="color:red;">' + event.msg + '</span>';
+            //    }
+            //}
+
+            // If it's a PM, highlight
+            //if (!kiwi.front.isChannel(tab.name) && tab.name !== "server"
+            //    && Tabview.getCurrentTab().name.toLowerCase() !== tab.name
+            //) {
+            //    tab.highlight();
+            //}
+
+            return event;
+        }
+    },
+
+
+
+    {
+        //Following method taken from: http://snipplr.com/view/13533/convert-text-urls-into-links/
+        name: "linkify_plain",
+        onaddmsg: function (event, opts) {
+            if (!event.msg) {
+                return event;
+            }
+
+            event.msg = event.msg.replace(/((https?\:\/\/|ftp\:\/\/)|(www\.))(\S+)(\w{2,4})(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/gi, function (url) {
+                var nice;
+                // If it's any of the supported images in the images plugin, skip it
+                if (url.match(/(\.jpg|\.jpeg|\.gif|\.bmp|\.png)$/)) {
+                    return url;
+                }
+
+                nice = url;
+                if (url.match('^https?:\/\/')) {
+                    //nice = nice.replace(/^https?:\/\//i,'')
+                    nice = url; // Shutting up JSLint...
+                } else {
+                    url = 'http://' + url;
+                }
+
+                //return '<a class="link_ext" target="_blank" rel="nofollow" href="' + url + '">' + nice + '<div class="tt box"></div></a>';
+                return '<a class="link_ext" target="_blank" rel="nofollow" href="' + url + '">' + nice + '</a>';
+            });
+
+            return event;
+        }
+    },
+
+    {
+        name: "lftobr",
+        onaddmsg: function (event, opts) {
+            if (!event.msg) {
+                return event;
+            }
+
+            event.msg = event.msg.replace(/\n/gi, function (txt) {
+                return '<br/>';
+            });
+
+            return event;
+        }
+    },
+
+
+    /*
+     * Disabled due to many websites closing kiwi with iframe busting
+    {
+        name: "inBrowser",
+        oninit: function (event, opts) {
+            $('#windows a.link_ext').live('mouseover', this.mouseover);
+            $('#windows a.link_ext').live('mouseout', this.mouseout);
+            $('#windows a.link_ext').live('click', this.mouseclick);
+        },
+
+        onunload: function (event, opts) {
+            // TODO: make this work (remove all .link_ext_browser as created in mouseover())
+            $('#windows a.link_ext').die('mouseover', this.mouseover);
+            $('#windows a.link_ext').die('mouseout', this.mouseout);
+            $('#windows a.link_ext').die('click', this.mouseclick);
+        },
+
+
+
+        mouseover: function (e) {
+            var a = $(this),
+                tt = $('.tt', a),
+                tooltip;
+
+            if (tt.text() === '') {
+                tooltip = $('<a class="link_ext_browser">Open in Kiwi..</a>');
+                tt.append(tooltip);
+            }
+
+            tt.css('top', -tt.outerHeight() + 'px');
+            tt.css('left', (a.outerWidth() / 2) - (tt.outerWidth() / 2));
+        },
+
+        mouseout: function (e) {
+            var a = $(this),
+                tt = $('.tt', a);
+        },
+
+        mouseclick: function (e) {
+            var a = $(this),
+                t;
+
+            switch (e.target.className) {
+            case 'link_ext':
+            case 'link_img_a':
+                return true;
+                //break;
+            case 'link_ext_browser':
+                t = new Utilityview('Browser');
+                t.topic = a.attr('href');
+
+                t.iframe = $('<iframe border="0" class="utility_view" src="" style="width:100%;height:100%;border:none;"></iframe>');
+                t.iframe.attr('src', a.attr('href'));
+                t.div.append(t.iframe);
+                t.show();
+                break;
+            }
+            return false;
+
+        }
+    },
+    */
+
+    {
+        name: "nick_colour",
+        onaddmsg: function (event, opts) {
+            if (!event.msg) {
+                return event;
+            }
+
+            //if (typeof kiwi.front.tabviews[event.tabview].nick_colours === 'undefined') {
+            //    kiwi.front.tabviews[event.tabview].nick_colours = {};
+            //}
+
+            //if (typeof kiwi.front.tabviews[event.tabview].nick_colours[event.nick] === 'undefined') {
+            //    kiwi.front.tabviews[event.tabview].nick_colours[event.nick] = this.randColour();
+            //}
+
+            //var c = kiwi.front.tabviews[event.tabview].nick_colours[event.nick];
+            var c = this.randColour();
+            event.nick = '<span style="color:' + c + ';">' + event.nick + '</span>';
+
+            return event;
+        },
+
+
+
+        randColour: function () {
+            var h = this.rand(-250, 0),
+                s = this.rand(30, 100),
+                l = this.rand(20, 70);
+            return 'hsl(' + h + ',' + s + '%,' + l + '%)';
+        },
+
+
+        rand: function (min, max) {
+            return parseInt(Math.random() * (max - min + 1), 10) + min;
+        }
+    },
+
+    {
+        name: "kiwitest",
+        oninit: function (event, opts) {
+            console.log('registering namespace');
+            $(gateway).bind("kiwi.lol.browser", function (e, data) {
+                console.log('YAY kiwitest');
+                console.log(data);
+            });
+        }
+    }
+];
+
+
+
+
+
+
+
+/**
+*   @namespace
+*/
+kiwi.plugs = {};
+/**
+*   Loaded plugins
+*/
+kiwi.plugs.loaded = {};
+/**
+*   Load a plugin
+*   @param      {Object}    plugin  The plugin to be loaded
+*   @returns    {Boolean}           True on success, false on failure
+*/
+kiwi.plugs.loadPlugin = function (plugin) {
+    var plugin_ret;
+    if (typeof plugin.name !== 'string') {
+        return false;
+    }
+
+    plugin_ret = kiwi.plugs.run('plugin_load', {plugin: plugin});
+    if (typeof plugin_ret === 'object') {
+        kiwi.plugs.loaded[plugin_ret.plugin.name] = plugin_ret.plugin;
+        kiwi.plugs.loaded[plugin_ret.plugin.name].local_data = new kiwi.dataStore('kiwi_plugin_' + plugin_ret.plugin.name);
+    }
+    kiwi.plugs.run('init', {}, {run_only: plugin_ret.plugin.name});
+
+    return true;
+};
+
+/**
+*   Unload a plugin
+*   @param  {String}    plugin_name The name of the plugin to unload
+*/
+kiwi.plugs.unloadPlugin = function (plugin_name) {
+    if (typeof kiwi.plugs.loaded[plugin_name] !== 'object') {
+        return;
+    }
+
+    kiwi.plugs.run('unload', {}, {run_only: plugin_name});
+    delete kiwi.plugs.loaded[plugin_name];
+};
+
+
+
+/**
+*   Run an event against all loaded plugins
+*   @param      {String}    event_name  The name of the event
+*   @param      {Object}    event_data  The data to pass to the plugin
+*   @param      {Object}    opts        Options
+*   @returns    {Object}                Event data, possibly modified by the plugins
+*/
+kiwi.plugs.run = function (event_name, event_data, opts) {
+    var ret = event_data,
+        ret_tmp,
+        plugin_name;
+
+    // Set some defaults if not provided
+    event_data = (typeof event_data === 'undefined') ? {} : event_data;
+    opts = (typeof opts === 'undefined') ? {} : opts;
+
+    for (plugin_name in kiwi.plugs.loaded) {
+        // If we're only calling 1 plugin, make sure it's that one
+        if (typeof opts.run_only === 'string' && opts.run_only !== plugin_name) {
+            continue;
+        }
+
+        if (typeof kiwi.plugs.loaded[plugin_name]['on' + event_name] === 'function') {
+            try {
+                ret_tmp = kiwi.plugs.loaded[plugin_name]['on' + event_name](ret, opts);
+                if (ret_tmp === null) {
+                    return null;
+                }
+                ret = ret_tmp;
+
+                if (typeof ret.event_bubbles === 'boolean' && ret.event_bubbles === false) {
+                    delete ret.event_bubbles;
+                    return ret;
+                }
+            } catch (e) {
+            }
+        }
+    }
+
+    return ret;
+};
+
+
+
+/**
+*   @constructor
+*   @param  {String}    data_namespace  The namespace for the data store
+*/
+kiwi.dataStore = function (data_namespace) {
+    var namespace = data_namespace;
+
+    this.get = function (key) {
+        return $.jStorage.get(data_namespace + '_' + key);
+    };
+
+    this.set = function (key, value) {
+        return $.jStorage.set(data_namespace + '_' + key, value);
+    };
+};
+
+kiwi.data = new kiwi.dataStore('kiwi');
+
+
+
+
+/*
+ * jQuery jStorage plugin 
+ * https://github.com/andris9/jStorage/
+ */
+(function(f){if(!f||!(f.toJSON||Object.toJSON||window.JSON)){throw new Error("jQuery, MooTools or Prototype needs to be loaded before jStorage!")}var g={},d={jStorage:"{}"},h=null,j=0,l=f.toJSON||Object.toJSON||(window.JSON&&(JSON.encode||JSON.stringify)),e=f.evalJSON||(window.JSON&&(JSON.decode||JSON.parse))||function(m){return String(m).evalJSON()},i=false;_XMLService={isXML:function(n){var m=(n?n.ownerDocument||n:0).documentElement;return m?m.nodeName!=="HTML":false},encode:function(n){if(!this.isXML(n)){return false}try{return new XMLSerializer().serializeToString(n)}catch(m){try{return n.xml}catch(o){}}return false},decode:function(n){var m=("DOMParser" in window&&(new DOMParser()).parseFromString)||(window.ActiveXObject&&function(p){var q=new ActiveXObject("Microsoft.XMLDOM");q.async="false";q.loadXML(p);return q}),o;if(!m){return false}o=m.call("DOMParser" in window&&(new DOMParser())||window,n,"text/xml");return this.isXML(o)?o:false}};function k(){if("localStorage" in window){try{if(window.localStorage){d=window.localStorage;i="localStorage"}}catch(p){}}else{if("globalStorage" in window){try{if(window.globalStorage){d=window.globalStorage[window.location.hostname];i="globalStorage"}}catch(o){}}else{h=document.createElement("link");if(h.addBehavior){h.style.behavior="url(#default#userData)";document.getElementsByTagName("head")[0].appendChild(h);h.load("jStorage");var n="{}";try{n=h.getAttribute("jStorage")}catch(m){}d.jStorage=n;i="userDataBehavior"}else{h=null;return}}}b()}function b(){if(d.jStorage){try{g=e(String(d.jStorage))}catch(m){d.jStorage="{}"}}else{d.jStorage="{}"}j=d.jStorage?String(d.jStorage).length:0}function c(){try{d.jStorage=l(g);if(h){h.setAttribute("jStorage",d.jStorage);h.save("jStorage")}j=d.jStorage?String(d.jStorage).length:0}catch(m){}}function a(m){if(!m||(typeof m!="string"&&typeof m!="number")){throw new TypeError("Key name must be string or numeric")}return true}f.jStorage={version:"0.1.5.1",set:function(m,n){a(m);if(_XMLService.isXML(n)){n={_is_xml:true,xml:_XMLService.encode(n)}}g[m]=n;c();return n},get:function(m,n){a(m);if(m in g){if(g[m]&&typeof g[m]=="object"&&g[m]._is_xml&&g[m]._is_xml){return _XMLService.decode(g[m].xml)}else{return g[m]}}return typeof(n)=="undefined"?null:n},deleteKey:function(m){a(m);if(m in g){delete g[m];c();return true}return false},flush:function(){g={};c();return true},storageObj:function(){function m(){}m.prototype=g;return new m()},index:function(){var m=[],n;for(n in g){if(g.hasOwnProperty(n)){m.push(n)}}return m},storageSize:function(){return j},currentBackend:function(){return i},storageAvailable:function(){return !!i},reInit:function(){var m,o;if(h&&h.addBehavior){m=document.createElement("link");h.parentNode.replaceChild(m,h);h=m;h.style.behavior="url(#default#userData)";document.getElementsByTagName("head")[0].appendChild(h);h.load("jStorage");o="{}";try{o=h.getAttribute("jStorage")}catch(n){}d.jStorage=o;i="userDataBehavior"}b()}};k()})(window.jQuery||window.$);
+
+
+/*jslint white:true, regexp: true, nomen: true, devel: true, undef: true, browser: true, continue: true, sloppy: true, forin: true, newcap: true, plusplus: true, maxerr: 50, indent: 4 */\r
+/*global kiwi */\r
+\r
+kiwi.view.MemberList = Backbone.View.extend({\r
+    tagName: "ul",\r
+    events: {\r
+        "click .nick": "nickClick"\r
+    },\r
+    initialize: function (options) {\r
+        this.model.bind('all', this.render, this);\r
+        $(this.el).appendTo('#memberlists');\r
+    },\r
+    render: function () {\r
+        var $this = $(this.el);\r
+        $this.empty();\r
+        this.model.forEach(function (member) {\r
+            $('<li><a class="nick"><span class="prefix">' + member.get("prefix") + '</span>' + member.get("nick") + '</a></li>')\r
+                .appendTo($this)\r
+                .data('member', member);\r
+        });\r
+    },\r
+    nickClick: function (x) {\r
+        var target = $(x.currentTarget).parent('li'),\r
+            member = target.data('member'),\r
+            userbox = new kiwi.view.UserBox();\r
+        \r
+        userbox.member = member;\r
+        $('.userbox', this.$el).remove();\r
+        target.append(userbox.$el);\r
+    },\r
+    show: function () {\r
+        $('#memberlists').children().removeClass('active');\r
+        $(this.el).addClass('active');\r
+    }\r
+});\r
+\r
+\r
+\r
+kiwi.view.UserBox = Backbone.View.extend({\r
+    events: {\r
+        'click .query': 'queryClick',\r
+        'click .info': 'infoClick'\r
+    },\r
+\r
+    initialize: function () {\r
+        this.$el = $($('#tmpl_userbox').html());\r
+    },\r
+\r
+    queryClick: function (event) {\r
+        var panel = new kiwi.model.Channel({name: this.member.get('nick')});\r
+        kiwi.app.panels.add(panel);\r
+        panel.view.show();\r
+    },\r
+\r
+    infoClick: function (event) {\r
+        kiwi.gateway.raw('WHOIS ' + this.member.get('nick'));\r
+    }\r
+});\r
+\r
+kiwi.view.NickChangeBox = Backbone.View.extend({\r
+    events: {\r
+        'submit': 'changeNick',\r
+        'click .cancel': 'close'\r
+    },\r
+    \r
+    initialize: function () {\r
+        this.$el = $($('#tmpl_nickchange').html());\r
+    },\r
+    \r
+    render: function () {\r
+        // Add the UI component and give it focus\r
+        kiwi.app.controlbox.$el.prepend(this.$el);\r
+        this.$el.find('input').focus();\r
+\r
+        this.$el.css('bottom', kiwi.app.controlbox.$el.outerHeight(true));\r
+    },\r
+    \r
+    close: function () {\r
+        this.$el.remove();\r
+\r
+    },\r
+\r
+    changeNick: function (event) {\r
+        var that = this;\r
+        kiwi.gateway.changeNick(this.$el.find('input').val(), function (err, val) {\r
+            that.close();\r
+        });\r
+        return false;\r
+    }\r
+});\r
+\r
+kiwi.view.ServerSelect = function () {\r
+    // Are currently showing all the controlls or just a nick_change box?\r
+    var state = 'all';\r
+\r
+    var model = Backbone.View.extend({\r
+        events: {\r
+            'submit form': 'submitForm',\r
+            'click .show_more': 'showMore'\r
+        },\r
+\r
+        initialize: function () {\r
+            this.$el = $($('#tmpl_server_select').html());\r
+\r
+            kiwi.gateway.bind('onconnect', this.networkConnected, this);\r
+            kiwi.gateway.bind('connecting', this.networkConnecting, this);\r
+\r
+            kiwi.gateway.bind('onirc_error', function (data) {\r
+                $('button', this.$el).attr('disabled', null);\r
+\r
+                if (data.error == 'nickname_in_use') {\r
+                    this.setStatus('Nickname already taken');\r
+                    this.show('nick_change');\r
+                }\r
+            }, this);\r
+        },\r
+\r
+        submitForm: function (event) {\r
+            if (state === 'nick_change') {\r
+                this.submitNickChange(event);\r
+            } else {\r
+                this.submitLogin(event);\r
+            }\r
+\r
+            $('button', this.$el).attr('disabled', 1);\r
+            return false;\r
+        },\r
+\r
+        submitLogin: function (event) {\r
+            // If submitting is disabled, don't do anything\r
+            if ($('button', this.$el).attr('disabled')) return;\r
+            \r
+            var values = {\r
+                nick: $('.nick', this.$el).val(),\r
+                server: $('.server', this.$el).val(),\r
+                port: $('.port', this.$el).val(),\r
+                ssl: $('.ssl', this.$el).prop('checked'),\r
+                password: $('.password', this.$el).val(),\r
+                channel: $('.channel', this.$el).val()\r
+            };\r
+\r
+            this.trigger('server_connect', values);\r
+        },\r
+\r
+        submitNickChange: function (event) {\r
+            kiwi.gateway.changeNick($('.nick', this.$el).val());\r
+            this.networkConnecting();\r
+        },\r
+\r
+        showMore: function (event) {\r
+            $('.more', this.$el).slideDown('fast');\r
+            $('.server', this.$el).select();\r
+        },\r
+\r
+        populateFields: function (defaults) {\r
+            var nick, server, channel;\r
+\r
+            defaults = defaults || {};\r
+\r
+            nick = defaults.nick || '';\r
+            server = defaults.server || '';\r
+            port = defaults.port || 6667;\r
+            ssl = defaults.ssl || 0;\r
+            password = defaults.password || '';\r
+            channel = defaults.channel || '';\r
+\r
+            $('.nick', this.$el).val(nick);\r
+            $('.server', this.$el).val(server);\r
+            $('.port', this.$el).val(port);\r
+            $('.ssl', this.$el).prop('checked', ssl);\r
+            $('.password', this.$el).val(password);\r
+            $('.channel', this.$el).val(channel);\r
+        },\r
+\r
+        hide: function () {\r
+            this.$el.slideUp();\r
+        },\r
+\r
+        show: function (new_state) {\r
+            new_state = new_state || 'all';\r
+\r
+            this.$el.show();\r
+\r
+            if (new_state === 'all') {\r
+                $('.show_more', this.$el).show();\r
+\r
+            } else if (new_state === 'more') {\r
+                $('.more', this.$el).slideDown('fast');\r
+\r
+            } else if (new_state === 'nick_change') {\r
+                $('.more', this.$el).hide();\r
+                $('.show_more', this.$el).hide();\r
+            }\r
+\r
+            state = new_state;\r
+        },\r
+\r
+        setStatus: function (text, class_name) {\r
+            $('.status', this.$el)\r
+                .text(text)\r
+                .attr('class', 'status')\r
+                .addClass(class_name)\r
+                .show();\r
+        },\r
+        clearStatus: function () {\r
+            $('.status', this.$el).hide();\r
+        },\r
+\r
+        networkConnected: function (event) {\r
+            this.setStatus('Connected :)', 'ok');\r
+            $('form', this.$el).hide();\r
+        },\r
+\r
+        networkConnecting: function (event) {\r
+            this.setStatus('Connecting..', 'ok');\r
+        },\r
+\r
+        showError: function (event) {\r
+            this.setStatus('Error connecting', 'error');\r
+            $('button', this.$el).attr('disabled', null);\r
+            this.show();\r
+        }\r
+    });\r
+\r
+\r
+    return new model(arguments);\r
+};\r
+\r
+\r
+kiwi.view.Panel = Backbone.View.extend({\r
+    tagName: "div",\r
+    className: "messages",\r
+    events: {\r
+        "click .chan": "chanClick"\r
+    },\r
+\r
+    initialize: function (options) {\r
+        this.initializePanel(options);\r
+    },\r
+\r
+    initializePanel: function (options) {\r
+        this.$el.css('display', 'none');\r
+        options = options || {};\r
+\r
+        // Containing element for this panel\r
+        if (options.container) {\r
+            this.$container = $(options.container);\r
+        } else {\r
+            this.$container = $('#panels .container1');\r
+        }\r
+\r
+        this.$el.appendTo(this.$container);\r
+\r
+        this.alert_level = 0;\r
+\r
+        this.model.bind('msg', this.newMsg, this);\r
+        this.msg_count = 0;\r
+\r
+        this.model.set({"view": this}, {"silent": true});\r
+    },\r
+\r
+    render: function () {\r
+        this.$el.empty();\r
+        this.model.get("backscroll").forEach(this.newMsg);\r
+    },\r
+    newMsg: function (msg) {\r
+        // TODO: make sure that the message pane is scrolled to the bottom (Or do we? ~Darren)\r
+        var re, line_msg, $this = this.$el,\r
+            nick_colour_hex;\r
+\r
+        // Escape any HTML that may be in here\r
+        msg.msg =  $('<div />').text(msg.msg).html();\r
+\r
+        // Make the channels clickable\r
+        re = new RegExp('\\B([' + kiwi.gateway.get('channel_prefix') + '][^ ,.\\007]+)', 'g');\r
+        msg.msg = msg.msg.replace(re, function (match) {\r
+            return '<a class="chan">' + match + '</a>';\r
+        });\r
+\r
+\r
+        // Make links clickable\r
+        msg.msg = msg.msg.replace(/((https?\:\/\/|ftp\:\/\/)|(www\.))(\S+)(\w{2,4})(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]*))?/gi, function (url) {\r
+            var nice;\r
+\r
+            // Add the http is no protoocol was found\r
+            if (url.match(/^www\./)) {\r
+                url = 'http://' + url;\r
+            }\r
+\r
+            nice = url;\r
+            if (nice.length > 100) {\r
+                nice = nice.substr(0, 100) + '...';\r
+            }\r
+\r
+            return '<a class="link_ext" target="_blank" rel="nofollow" href="' + url + '">' + nice + '</a>';\r
+        });\r
+\r
+\r
+        // Convert IRC formatting into HTML formatting\r
+        msg.msg = formatIRCMsg(msg.msg);\r
+\r
+\r
+        // Add some colours to the nick (Method based on IRSSIs nickcolor.pl)\r
+        nick_colour_hex = (function (nick) {\r
+            var nick_int = 0, rgb;\r
+\r
+            _.map(nick.split(''), function (i) { nick_int += i.charCodeAt(0); });\r
+            rgb = hsl2rgb(nick_int % 255, 70, 35);\r
+            rgb = rgb[2] | (rgb[1] << 8) | (rgb[0] << 16);\r
+\r
+            return '#' + rgb.toString(16);\r
+        })(msg.nick);\r
+\r
+        msg.nick_style = 'color:' + nick_colour_hex + ';';\r
+\r
+        // Build up and add the line\r
+        line_msg = '<div class="msg <%= type %>"><div class="time"><%- time %></div><div class="nick" style="<%= nick_style %>"><%- nick %></div><div class="text" style="<%= style %>"><%= msg %> </div></div>';\r
+        $this.append(_.template(line_msg, msg));\r
+\r
+        // Activity/alerts based on the type of new message\r
+        if (msg.type.match(/^action /)) {\r
+            this.alert('action');\r
+        } else if (msg.msg.indexOf(kiwi.gateway.get('nick')) > -1) {\r
+            kiwi.app.view.alertWindow('* People are talking!');\r
+            this.alert('highlight');\r
+        } else {\r
+            // If this is the active panel, send an alert out\r
+            if (this.model.isActive()) {\r
+                kiwi.app.view.alertWindow('* People are talking!');\r
+            }\r
+            this.alert('activity');\r
+        }\r
+\r
+        this.scrollToBottom();\r
+\r
+        // Make sure our DOM isn't getting too large (Acts as scrollback)\r
+        this.msg_count++;\r
+        if (this.msg_count > 250) {\r
+            $('.msg:first', this.$el).remove();\r
+            this.msg_count--;\r
+        }\r
+    },\r
+    chanClick: function (x) {\r
+        kiwi.gateway.join($(x.srcElement).text());\r
+    },\r
+    show: function () {\r
+        var $this = this.$el;\r
+\r
+        // Hide all other panels and show this one\r
+        this.$container.children().css('display', 'none');\r
+        $this.css('display', 'block');\r
+\r
+        // Show this panels memberlist\r
+        var members = this.model.get("members");\r
+        if (members) {\r
+            $('#memberlists').show();\r
+            members.view.show();\r
+        } else {\r
+            // Memberlist not found for this panel, hide any active ones\r
+            $('#memberlists').hide().children().removeClass('active');\r
+        }\r
+\r
+        kiwi.app.view.doLayout();\r
+\r
+        this.scrollToBottom();\r
+        this.alert('none');\r
+\r
+        this.trigger('active', this.model);\r
+        kiwi.app.panels.trigger('active', this.model);\r
+    },\r
+\r
+\r
+    alert: function (level) {\r
+        // No need to highlight if this si the active panel\r
+        if (this.model == kiwi.app.panels.active) return;\r
+\r
+        var types, type_idx;\r
+        types = ['none', 'action', 'activity', 'highlight'];\r
+\r
+        // Default alert level\r
+        level = level || 'none';\r
+\r
+        // If this alert level does not exist, assume clearing current level\r
+        type_idx = _.indexOf(types, level);\r
+        if (!type_idx) {\r
+            level = 'none';\r
+            type_idx = 0;\r
+        }\r
+\r
+        // Only 'upgrade' the alert. Never down (unless clearing)\r
+        if (type_idx !== 0 && type_idx <= this.alert_level) {\r
+            return;\r
+        }\r
+\r
+        // Clear any existing levels\r
+        this.model.tab.removeClass(function (i, css) {\r
+            return (css.match(/\balert_\S+/g) || []).join(' ');\r
+        });\r
+\r
+        // Add the new level if there is one\r
+        if (level !== 'none') {\r
+            this.model.tab.addClass('alert_' + level);\r
+        }\r
+\r
+        this.alert_level = type_idx;\r
+    },\r
+\r
+\r
+    // Scroll to the bottom of the panel\r
+    scrollToBottom: function () {\r
+        // TODO: Don't scroll down if we're scrolled up the panel a little\r
+        this.$container[0].scrollTop = this.$container[0].scrollHeight;\r
+    }\r
+});\r
+\r
+kiwi.view.Applet = kiwi.view.Panel.extend({\r
+    className: 'applet',\r
+    initialize: function (options) {\r
+        this.initializePanel(options);\r
+    }\r
+});\r
+\r
+kiwi.view.Channel = kiwi.view.Panel.extend({\r
+    initialize: function (options) {\r
+        this.initializePanel(options);\r
+        this.model.bind('change:topic', this.topic, this);\r
+    },\r
+\r
+    topic: function (topic) {\r
+        if (typeof topic !== 'string' || !topic) {\r
+            topic = this.model.get("topic");\r
+        }\r
+        \r
+        this.model.addMsg('', '== Topic for ' + this.model.get('name') + ' is: ' + topic, 'topic');\r
+\r
+        // If this is the active channel then update the topic bar\r
+        if (kiwi.app.panels.active === this) {\r
+            kiwi.app.topicbar.setCurrentTopic(this.model.get("topic"));\r
+        }\r
+    }\r
+});\r
+\r
+// Model for this = kiwi.model.PanelList\r
+kiwi.view.Tabs = Backbone.View.extend({\r
+    events: {\r
+        'click li': 'tabClick',\r
+        'click li img': 'partClick'\r
+    },\r
+\r
+    initialize: function () {\r
+        this.model.on("add", this.panelAdded, this);\r
+        this.model.on("remove", this.panelRemoved, this);\r
+        this.model.on("reset", this.render, this);\r
+\r
+        this.model.on('active', this.panelActive, this);\r
+\r
+        this.tabs_applets = $('ul.applets', this.$el);\r
+        this.tabs_msg = $('ul.channels', this.$el);\r
+\r
+        kiwi.gateway.on('change:name', function (gateway, new_val) {\r
+            $('span', this.model.server.tab).text(new_val);\r
+        }, this);\r
+    },\r
+    render: function () {\r
+        var that = this;\r
+\r
+        this.tabs_msg.empty();\r
+        \r
+        // Add the server tab first\r
+        this.model.server.tab\r
+            .data('panel_id', this.model.server.cid)\r
+            .appendTo(this.tabs_msg);\r
+\r
+        // Go through each panel adding its tab\r
+        this.model.forEach(function (panel) {\r
+            // If this is the server panel, ignore as it's already added\r
+            if (panel == that.model.server) return;\r
+\r
+            panel.tab\r
+                .data('panel_id', panel.cid)\r
+                .appendTo(panel.isApplet() ? this.tabs_applets : this.tabs_msg);\r
+        });\r
+\r
+        kiwi.app.view.doLayout();\r
+    },\r
+\r
+    updateTabTitle: function (panel, new_title) {\r
+        $('span', panel.tab).text(new_title);\r
+    },\r
+\r
+    panelAdded: function (panel) {\r
+        // Add a tab to the panel\r
+        panel.tab = $('<li><span>' + (panel.get('title') || panel.get('name')) + '</span></li>');\r
+\r
+        if (panel.isServer()) {\r
+            panel.tab.addClass('server');\r
+        }\r
+\r
+        panel.tab.data('panel_id', panel.cid)\r
+            .appendTo(panel.isApplet() ? this.tabs_applets : this.tabs_msg);\r
+\r
+        panel.bind('change:title', this.updateTabTitle);\r
+        kiwi.app.view.doLayout();\r
+    },\r
+    panelRemoved: function (panel) {\r
+        panel.tab.remove();\r
+        delete panel.tab;\r
+\r
+        kiwi.app.view.doLayout();\r
+    },\r
+\r
+    panelActive: function (panel) {\r
+        // Remove any existing tabs or part images\r
+        $('img', this.$el).remove();\r
+        this.tabs_applets.children().removeClass('active');\r
+        this.tabs_msg.children().removeClass('active');\r
+\r
+        panel.tab.addClass('active');\r
+\r
+        // Only show the part image on non-server tabs\r
+        if (!panel.isServer()) {\r
+            panel.tab.append('<img src="img/redcross.png" />');\r
+        }\r
+    },\r
+\r
+    tabClick: function (e) {\r
+        var tab = $(e.currentTarget);\r
+\r
+        var panel = this.model.getByCid(tab.data('panel_id'));\r
+        if (!panel) {\r
+            // A panel wasn't found for this tab... wadda fuck\r
+            return;\r
+        }\r
+\r
+        panel.view.show();\r
+    },\r
+\r
+    partClick: function (e) {\r
+        var tab = $(e.currentTarget).parent();\r
+        var panel = this.model.getByCid(tab.data('panel_id'));\r
+\r
+        // Only need to part if it's a channel\r
+        // If the nicklist is empty, we haven't joined the channel as yet\r
+        if (panel.isChannel() && panel.get('members').models.length > 0) {\r
+            kiwi.gateway.part(panel.get('name'));\r
+        } else {\r
+            panel.close();\r
+        }\r
+    },\r
+\r
+    next: function () {\r
+        var next = kiwi.app.panels.active.tab.next();\r
+        if (!next.length) next = $('li:first', this.tabs_msgs);\r
+\r
+        next.click();\r
+    },\r
+    prev: function () {\r
+        var prev = kiwi.app.panels.active.tab.prev();\r
+        if (!prev.length) prev = $('li:last', this.tabs_msgs);\r
+\r
+        prev.click();\r
+    }\r
+});\r
+\r
+\r
+\r
+kiwi.view.TopicBar = Backbone.View.extend({\r
+    events: {\r
+        'keydown input': 'process'\r
+    },\r
+\r
+    initialize: function () {\r
+        kiwi.app.panels.bind('active', function (active_panel) {\r
+            this.setCurrentTopic(active_panel.get('topic'));\r
+        }, this);\r
+    },\r
+\r
+    process: function (ev) {\r
+        var inp = $(ev.currentTarget),\r
+            inp_val = inp.val();\r
+\r
+        if (ev.keyCode !== 13) return;\r
+\r
+        if (kiwi.app.panels.active.isChannel()) {\r
+            kiwi.gateway.topic(kiwi.app.panels.active.get('name'), inp_val);\r
+        }\r
+    },\r
+\r
+    setCurrentTopic: function (new_topic) {\r
+        new_topic = new_topic || '';\r
+\r
+        // We only want a plain text version\r
+        new_topic = $('<div>').html(formatIRCMsg(new_topic));\r
+        $('input', this.$el).val(new_topic.text());\r
+    }\r
+});\r
+\r
+\r
+\r
+kiwi.view.ControlBox = Backbone.View.extend({\r
+    events: {\r
+        'keydown input.inp': 'process',\r
+        'click .nick': 'showNickChange'\r
+    },\r
+\r
+    initialize: function () {\r
+        var that = this;\r
+\r
+        this.buffer = [];  // Stores previously run commands\r
+        this.buffer_pos = 0;  // The current position in the buffer\r
+\r
+        this.preprocessor = new InputPreProcessor();\r
+        this.preprocessor.recursive_depth = 5;\r
+\r
+        // Hold tab autocomplete data\r
+        this.tabcomplete = {active: false, data: [], prefix: ''};\r
+\r
+        kiwi.gateway.bind('change:nick', function () {\r
+            $('.nick', that.$el).text(this.get('nick'));\r
+        });\r
+    },\r
+\r
+    showNickChange: function (ev) {\r
+        (new kiwi.view.NickChangeBox()).render();\r
+    },\r
+\r
+    process: function (ev) {\r
+        var that = this,\r
+            inp = $(ev.currentTarget),\r
+            inp_val = inp.val(),\r
+            meta;\r
+\r
+        if (navigator.appVersion.indexOf("Mac") !== -1) {\r
+            meta = ev.ctrlKey;\r
+        } else {\r
+            meta = ev.altKey;\r
+        }\r
+\r
+        // If not a tab key, reset the tabcomplete data\r
+        if (this.tabcomplete.active && ev.keyCode !== 9) {\r
+            this.tabcomplete.active = false;\r
+            this.tabcomplete.data = [];\r
+            this.tabcomplete.prefix = '';\r
+        }\r
+        \r
+        switch (true) {\r
+        case (ev.keyCode === 13):              // return\r
+            inp_val = inp_val.trim();\r
+\r
+            if (inp_val) {\r
+                this.processInput(inp.val());\r
+\r
+                this.buffer.push(inp.val());\r
+                this.buffer_pos = this.buffer.length;\r
+            }\r
+\r
+            inp.val('');\r
+\r
+            break;\r
+\r
+        case (ev.keyCode === 38):              // up\r
+            if (this.buffer_pos > 0) {\r
+                this.buffer_pos--;\r
+                inp.val(this.buffer[this.buffer_pos]);\r
+            }\r
+            break;\r
+\r
+        case (ev.keyCode === 40):              // down\r
+            if (this.buffer_pos < this.buffer.length) {\r
+                this.buffer_pos++;\r
+                inp.val(this.buffer[this.buffer_pos]);\r
+            }\r
+            break;\r
+\r
+        case (ev.keyCode === 37 && meta):            // left\r
+            kiwi.app.panels.view.prev();\r
+            return false;\r
+\r
+        case (ev.keyCode === 39 && meta):            // right\r
+            kiwi.app.panels.view.next();\r
+            return false;\r
+\r
+        case (ev.keyCode === 9):                     // tab\r
+            this.tabcomplete.active = true;\r
+            if (_.isEqual(this.tabcomplete.data, [])) {\r
+                // Get possible autocompletions\r
+                var ac_data = [];\r
+                $.each(kiwi.app.panels.active.get('members').models, function (i, member) {\r
+                    if (!member) return;\r
+                    ac_data.push(member.get('nick'));\r
+                });\r
+                ac_data = _.sortBy(ac_data, function (nick) {\r
+                    return nick;\r
+                });\r
+                this.tabcomplete.data = ac_data;\r
+            }\r
+\r
+            if (inp_val[inp[0].selectionStart - 1] === ' ') {\r
+                return false;\r
+            }\r
+            \r
+            (function () {\r
+                var tokens = inp_val.substring(0, inp[0].selectionStart).split(' '),\r
+                    val,\r
+                    p1,\r
+                    newnick,\r
+                    range,\r
+                    nick = tokens[tokens.length - 1];\r
+                if (this.tabcomplete.prefix === '') {\r
+                    this.tabcomplete.prefix = nick;\r
+                }\r
+\r
+                this.tabcomplete.data = _.select(this.tabcomplete.data, function (n) {\r
+                    return (n.toLowerCase().indexOf(that.tabcomplete.prefix.toLowerCase()) === 0);\r
+                });\r
+\r
+                if (this.tabcomplete.data.length > 0) {\r
+                    p1 = inp[0].selectionStart - (nick.length);\r
+                    val = inp_val.substr(0, p1);\r
+                    newnick = this.tabcomplete.data.shift();\r
+                    this.tabcomplete.data.push(newnick);\r
+                    val += newnick;\r
+                    val += inp_val.substr(inp[0].selectionStart);\r
+                    inp.val(val);\r
+\r
+                    if (inp[0].setSelectionRange) {\r
+                        inp[0].setSelectionRange(p1 + newnick.length, p1 + newnick.length);\r
+                    } else if (inp[0].createTextRange) { // not sure if this bit is actually needed....\r
+                        range = inp[0].createTextRange();\r
+                        range.collapse(true);\r
+                        range.moveEnd('character', p1 + newnick.length);\r
+                        range.moveStart('character', p1 + newnick.length);\r
+                        range.select();\r
+                    }\r
+                }\r
+            }).apply(this);\r
+            return false;\r
+        }\r
+    },\r
+\r
+\r
+    processInput: function (command_raw) {\r
+        var command, params,\r
+            pre_processed;\r
+        \r
+        // The default command\r
+        if (command_raw[0] !== '/') {\r
+            command_raw = '/msg ' + kiwi.app.panels.active.get('name') + ' ' + command_raw;\r
+        }\r
+\r
+        // Process the raw command for any aliases\r
+        this.preprocessor.vars.server = kiwi.gateway.get('name');\r
+        this.preprocessor.vars.channel = kiwi.app.panels.active.get('name');\r
+        this.preprocessor.vars.destination = this.preprocessor.vars.channel;\r
+        command_raw = this.preprocessor.process(command_raw);\r
+\r
+        // Extract the command and parameters\r
+        params = command_raw.split(' ');\r
+        if (params[0][0] === '/') {\r
+            command = params[0].substr(1).toLowerCase();\r
+            params = params.splice(1);\r
+        } else {\r
+            // Default command\r
+            command = 'msg';\r
+            params.unshift(kiwi.app.panels.active.get('name'));\r
+        }\r
+\r
+        // Trigger the command events\r
+        this.trigger('command', {command: command, params: params});\r
+        this.trigger('command_' + command, {command: command, params: params});\r
+\r
+        // If we didn't have any listeners for this event, fire a special case\r
+        // TODO: This feels dirty. Should this really be done..?\r
+        if (!this._callbacks['command_' + command]) {\r
+            this.trigger('unknown_command', {command: command, params: params});\r
+        }\r
+    }\r
+});\r
+\r
+\r
+\r
+\r
+kiwi.view.StatusMessage = Backbone.View.extend({\r
+    initialize: function () {\r
+        this.$el.hide();\r
+\r
+        // Timer for hiding the message after X seconds\r
+        this.tmr = null;\r
+    },\r
+\r
+    text: function (text, opt) {\r
+        // Defaults\r
+        opt = opt || {};\r
+        opt.type = opt.type || '';\r
+        opt.timeout = opt.timeout || 5000;\r
+\r
+        this.$el.text(text).attr('class', opt.type);\r
+        this.$el.slideDown(kiwi.app.view.doLayout);\r
+\r
+        if (opt.timeout) this.doTimeout(opt.timeout);\r
+    },\r
+\r
+    html: function (html, opt) {\r
+        // Defaults\r
+        opt = opt || {};\r
+        opt.type = opt.type || '';\r
+        opt.timeout = opt.timeout || 5000;\r
+\r
+        this.$el.html(text).attr('class', opt.type);\r
+        this.$el.slideDown(kiwi.app.view.doLayout);\r
+\r
+        if (opt.timeout) this.doTimeout(opt.timeout);\r
+    },\r
+\r
+    hide: function () {\r
+        this.$el.slideUp(kiwi.app.view.doLayout);\r
+    },\r
+\r
+    doTimeout: function (length) {\r
+        if (this.tmr) clearTimeout(this.tmr);\r
+        var that = this;\r
+        this.tmr = setTimeout(function () { that.hide(); }, length);\r
+    }\r
+});\r
+\r
+\r
+\r
+\r
+kiwi.view.ResizeHandler = Backbone.View.extend({\r
+    events: {\r
+        'mousedown': 'startDrag',\r
+        'mouseup': 'stopDrag'\r
+    },\r
+\r
+    initialize: function () {\r
+        this.dragging = false;\r
+        this.starting_width = {};\r
+\r
+        $(window).on('mousemove', $.proxy(this.onDrag, this));\r
+    },\r
+\r
+    startDrag: function (event) {\r
+        this.dragging = true;\r
+    },\r
+\r
+    stopDrag: function (event) {\r
+        this.dragging = false;\r
+    },\r
+\r
+    onDrag: function (event) {\r
+        if (!this.dragging) return;\r
+\r
+        this.$el.css('left', event.clientX - (this.$el.outerWidth(true) / 2));\r
+        $('#memberlists').css('width', this.$el.parent().width() - (this.$el.position().left + this.$el.outerWidth()));\r
+        kiwi.app.view.doLayout();\r
+    }\r
+});\r
+\r
+\r
+\r
+kiwi.view.Application = Backbone.View.extend({\r
+    initialize: function () {\r
+        $(window).resize(this.doLayout);\r
+        $('#toolbar').resize(this.doLayout);\r
+        $('#controlbox').resize(this.doLayout);\r
+\r
+        this.doLayout();\r
+\r
+        $(document).keydown(this.setKeyFocus);\r
+\r
+        // Confirmation require to leave the page\r
+        window.onbeforeunload = function () {\r
+            if (kiwi.gateway.isConnected()) {\r
+                return 'This will close all KiwiIRC conversations. Are you sure you want to close this window?';\r
+            }\r
+        };\r
+    },\r
+\r
+\r
+    // Globally shift focus to the command input box on a keypress\r
+    setKeyFocus: function (ev) {\r
+        // If we're copying text, don't shift focus\r
+        if (ev.ctrlKey || ev.altKey) {\r
+            return;\r
+        }\r
+\r
+        // If we're typing into an input box somewhere, ignore\r
+        if (ev.target.tagName.toLowerCase() === 'input') {\r
+            return;\r
+        }\r
+\r
+        $('#controlbox .inp').focus();\r
+    },\r
+\r
+\r
+    doLayout: function () {\r
+        var el_panels = $('#panels');\r
+        var el_memberlists = $('#memberlists');\r
+        var el_toolbar = $('#toolbar');\r
+        var el_controlbox = $('#controlbox');\r
+        var el_resize_handle = $('#memberlists_resize_handle');\r
+\r
+        var css_heights = {\r
+            top: el_toolbar.outerHeight(true),\r
+            bottom: el_controlbox.outerHeight(true)\r
+        };\r
+\r
+        el_panels.css(css_heights);\r
+        el_memberlists.css(css_heights);\r
+        el_resize_handle.css(css_heights);\r
+\r
+        if (el_memberlists.css('display') != 'none') {\r
+            // Handle + panels to the side of the memberlist\r
+            el_panels.css('right', el_memberlists.outerWidth(true) + el_resize_handle.outerWidth(true));\r
+            el_resize_handle.css('left', el_memberlists.position().left - el_resize_handle.outerWidth(true));\r
+        } else {\r
+            // Memberlist is hidden so handle + panels to the right edge\r
+            el_panels.css('right', el_resize_handle.outerWidth(true));\r
+            el_resize_handle.css('left', el_panels.outerWidth(true));\r
+        }\r
+    },\r
+\r
+\r
+    alertWindow: function (title) {\r
+        if (!this.alertWindowTimer) {\r
+            this.alertWindowTimer = new (function () {\r
+                var that = this;\r
+                var tmr;\r
+                var has_focus = true;\r
+                var state = 0;\r
+                var default_title = 'Kiwi IRC';\r
+                var title = 'Kiwi IRC';\r
+\r
+                this.setTitle = function (new_title) {\r
+                    new_title = new_title || default_title;\r
+                    window.document.title = new_title;\r
+                    return new_title;\r
+                };\r
+\r
+                this.start = function (new_title) {\r
+                    // Don't alert if we already have focus\r
+                    if (has_focus) return;\r
+\r
+                    title = new_title;\r
+                    if (tmr) return;\r
+                    tmr = setInterval(this.update, 1000);\r
+                };\r
+\r
+                this.stop = function () {\r
+                    // Stop the timer and clear the title\r
+                    if (tmr) clearInterval(tmr);\r
+                    tmr = null;\r
+                    this.setTitle();\r
+\r
+                    // Some browsers don't always update the last title correctly\r
+                    // Wait a few seconds and then reset\r
+                    setTimeout(this.reset, 2000);\r
+                };\r
+\r
+                this.reset = function () {\r
+                    if (tmr) return;\r
+                    that.setTitle();\r
+                };\r
+\r
+\r
+                this.update = function () {\r
+                    if (state === 0) {\r
+                        that.setTitle(title);\r
+                        state = 1;\r
+                    } else {\r
+                        that.setTitle();\r
+                        state = 0;\r
+                    }\r
+                };\r
+\r
+                $(window).focus(function (event) {\r
+                    has_focus = true;\r
+                    that.stop();\r
+\r
+                    // Some browsers don't always update the last title correctly\r
+                    // Wait a few seconds and then reset\r
+                    setTimeout(this.reset, 2000);\r
+                });\r
+\r
+                $(window).blur(function (event) {\r
+                    has_focus = false;\r
+                });\r
+            })();\r
+        }\r
+\r
+        this.alertWindowTimer.start(title);\r
+    },\r
+\r
+\r
+    barsHide: function (instant) {\r
+        var that = this;\r
+\r
+        if (!instant) {\r
+            $('#toolbar').slideUp({queue: false, duration: 400, step: this.doLayout});\r
+            $('#controlbox').slideUp({queue: false, duration: 400, step: this.doLayout});\r
+        } else {\r
+            $('#toolbar').slideUp(0);\r
+            $('#controlbox').slideUp(0);\r
+            this.doLayout();\r
+        }\r
+    },\r
+\r
+    barsShow: function (instant) {\r
+        var that = this;\r
+\r
+        if (!instant) {\r
+            $('#toolbar').slideDown({queue: false, duration: 400, step: this.doLayout});\r
+            $('#controlbox').slideDown({queue: false, duration: 400, step: this.doLayout});\r
+        } else {\r
+            $('#toolbar').slideDown(0);\r
+            $('#controlbox').slideDown(0);\r
+            this.doLayout();\r
+        }\r
+    }\r
+});
+
+
+
+})(window);
\ No newline at end of file
diff --git a/client/assets/kiwi.min.js b/client/assets/kiwi.min.js
new file mode 100644 (file)
index 0000000..764f3b2
--- /dev/null
@@ -0,0 +1 @@
+(function(a){function c(a){var b,c;window.console?(c=window.console.log,window.console.log=function(){a&&c.apply(console,arguments)}):(b=window.opera?window.opera.postError:alert,window.console={},window.console.log=function(c){a&&b(c)})}function d(a){var b="0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz",c="",d,e;for(d=0;d<a;d++)e=Math.floor(Math.random()*b.length),c+=b.substring(e,e+1);return c}function e(a){var b,c,d,e,f,g;return b=Math.floor(a/3600),e=a%3600,c=Math.floor(e/60),f=e%60,d=Math.ceil(f),g={h:b,m:c,s:d},g}function f(){this.recursive_depth=3,this.aliases={},this.vars={version:1};var a=0;this.processInput=function(a){var b=a||[],c=this.aliases[b[0]],d,e="",f=[];if(!c)return a;c=c.split(" "),d=c.length;for(var g=0;g<d;g++){e=c[g];if(e[0]!=="$"){f.push(e);continue}if(!isNaN(e[1])){var h=e.match(/\$(\d+)(\+)?(\d+)?/);if(!h||!b[h[1]])continue;h[2]==="+"&&h[3]?f=f.concat(b.slice(parseInt(h[1],10),parseInt(h[1],10)+parseInt(h[3],10))):h[2]==="+"?f=f.concat(b.slice(parseInt(h[1],10))):f.push(b[parseInt(h[1],10)]);continue}if(typeof this.vars[e.substr(1)]!="undefined"){f.push(this.vars[e.substr(1)]);continue}}return f},this.process=function(b){b=b||"";var c=b.split(" ");return a++,a>=this.recursive_depth?(a--,b):(this.aliases[c[0]]&&(c=this.processInput(c),this.aliases[c[0]]&&(c=this.process(c.join(" ")).split(" "))),a--,c.join(" "))}}function g(a,b,c){var d,e,f,g,h,i;b/=100,c/=100;if(b==0)g=h=i=c*255;else{function j(a,b,c){var d;return c<0?c+=1:c>1&&(c-=1),6*c<1?d=a+(b-a)*c*6:2*c<1?d=b:3*c<2?d=a+(b-a)*(2/3-c)*6:d=a,255*d}c<=.5?e=c*(b+1):e=c+b-c*b,d=c*2-e,f=a/360,g=j(d,e,f+1/3),h=j(d,e,f),i=j(d,e,f-1/3)}return[g,h,i]}function h(a){var b,c;if(!a||typeof a!="string")return"";if(a.indexOf(String.fromCharCode(2))!==-1){c="<b>";while(a.indexOf(String.fromCharCode(2))!==-1)a=a.replace(String.fromCharCode(2),c),c=c==="<b>"?"</b>":"<b>";c==="</b>"&&(a+="</b>")}if(a.indexOf(String.fromCharCode(31))!==-1){c="<u>";while(a.indexOf(String.fromCharCode(31))!==-1)a=a.replace(String.fromCharCode(31),c),c=c==="<u>"?"</u>":"<u>";c==="</u>"&&(a+="</u>")}return a=function(a){var b,c,d,e,f,g,h,i,j,k;b="",c=function(a){var b=/^\x03([0-9][0-5]?)(,([0-9][0-5]?))?/;return b.exec(a)},d=function(a){switch(parseInt(a,10)){case 0:return"#FFFFFF";case 1:return"#000000";case 2:return"#000080";case 3:return"#008000";case 4:return"#FF0000";case 5:return"#800040";case 6:return"#800080";case 7:return"#FF8040";case 8:return"#FFFF00";case 9:return"#80FF00";case 10:return"#008080";case 11:return"#00FFFF";case 12:return"#0000FF";case 13:return"#FF55FF";case 14:return"#808080";case 15:return"#C0C0C0";default:return null}};if(a.indexOf("\ 3")!==-1){e=a.indexOf("\ 3"),b=a.substr(0,e);while(e<a.length)f=c(a.substr(e,6)),f?(g=a.indexOf("\ 3",e+1),h=a.indexOf(String.fromCharCode(15),e+1),h!==-1&&(g===-1?g=h:g=g<h?g:h),g===-1&&(g=a.length),i=d(f[1]),j=d(f[3]),k=a.substring(e+1+f[1].length+(j!==null?f[2].length+1:0),g),b+='<span style="'+(i!==null?"color: "+i+"; ":"")+(j!==null?"background-color: "+j+";":"")+'">'+k+"</span>",e=g):(a[e]!=="\ 3"&&a[e]!==String.fromCharCode(15)&&(b+=a[e]),e++);return b}return a}(a),a}function i(a){return a=a||new Date,a.toLocaleDateString()+", "+a.getHours().toString()+":"+a.getMinutes().toString()+":"+a.getSeconds().toString()}var b={};b.model={},b.view={},b.applets={},b.global={utils:undefined,gateway:undefined,user:undefined,server:undefined,command:undefined,channels:undefined,start:function(a){return a=a||{},b.app=new b.model.Application(a),a.kiwi_server&&(b.app.kiwi_server=a.kiwi_server),b.app.start(),!0}},typeof a!="undefined"&&(a.kiwi=b.global),b.model.Application=function(){var a=null,d={},f=function(){function f(b){a.panels.server.server_login.showError()}function g(a){var c=a.command+" "+a.params.join(" ");console.log("RAW: "+c),b.gateway.raw(c)}function h(a){}function j(c){var d,e;e=c.params.join(" ").split(","),$.each(e,function(c,e){e=e.trim(),d=a.panels.getByName(e),d||(d=new b.model.Channel({name:e}),b.app.panels.add(d)),b.gateway.join(e)}),d&&d.view.show()}function k(c){var d,e;d=c.params[0],e=a.panels.getByName(d),e||(e=new b.model.Channel({name:d}),b.app.panels.add(e)),e&&e.view.show()}function l(c){var d=c.params[0],e=a.panels.getByName(d)||a.panels.server;c.params.shift(),e.addMsg(b.gateway.get("nick"),c.params.join(" ")),b.gateway.privmsg(d,c.params.join(" "))}function m(a){if(b.app.panels.active===b.app.panels.server)return;var c=b.app.panels.active;c.addMsg("","* "+b.gateway.get("nick")+" "+a.params.join(" "),"action"),b.gateway.action(c.get("name"),a.params.join(" "))}function n(a){a.params.length===0?b.gateway.part(b.app.panels.active.get("name")):_.each(a.params,function(a){b.gateway.part(a)})}function o(c){var d;if(c.params.length===0)return;a.isChannelName(c.params[0])?(d=c.params[0],c.params.shift()):d=b.app.panels.active.get("name"),b.gateway.topic(d,c.params.join(" "))}function p(a){var c;if(a.params.length<=1)return;c=a.params[0],a.params.shift(),b.gateway.notice(c,a.params.join(" "))}function q(a){var c=a.params.join(" ");b.gateway.raw(c)}function r(a){var c,d=b.app.panels.active;if(!d.isChannel())return;if(a.params.length===0)return;c=a.params[0],a.params.shift(),b.gateway.kick(d.get("name"),c,a.params.join(" "))}function s(a){var c=new b.model.Applet;c.load(new b.applets.Settings),b.app.panels.add(c),c.view.show()}function t(a){if(!a.params[0])return;var c=new b.model.Applet;if(a.params[1])c.load(a.params[0],a.params[1]);else if(b.applets[a.params[0]])c.load(new b.applets[a.params[0]]);else{b.app.panels.server.addMsg("",'Applet "'+a.params[0]+'" does not exist');return}b.app.panels.add(c),c.view.show()}this.panels=null,this.view=null,this.message=null,this.kiwi_server=null,this.initialize=function(b){a=this,b[0].container&&this.set("container",b[0].container),this.detectKiwiServer()},this.start=function(){getQueryVariable("debug")||c(!1),b.gateway=new b.model.Gateway,this.bindGatewayCommands(b.gateway),this.initializeClient(),this.initializeGlobals(),this.view.barsHide(!0),this.panels.server.server_login.bind("server_connect",function(c){var e=this;d=c,e.networkConnecting(),$script(a.kiwi_server+"/socket.io/socket.io.js?ts="+(new Date).getTime(),function(){if(!window.io){f();return}b.gateway.set("kiwi_server",a.kiwi_server+"/kiwi"),b.gateway.set("nick",c.nick),b.gateway.connect(c.server,c.port,c.ssl,c.password,function(){})})}),setTimeout(function(){b.app.panels.server.server_login.$el.find(".nick").select()},0)},this.detectKiwiServer=function(){window.location.protocol==="file:"?this.kiwi_server="http://localhost:7778":this.kiwi_server=window.location.protocol+"//"+window.location.host},this.initializeClient=function(){this.view=new b.view.Application({model:this,el:this.get("container")}),this.panels=new b.model.PanelList,this.controlbox=new b.view.ControlBox({el:$("#controlbox")[0]}),this.bindControllboxCommands(this.controlbox),this.topicbar=new b.view.TopicBar({el:$("#topic")[0]}),this.message=new b.view.StatusMessage({el:$("#status_message")[0]}),this.resize_handle=new b.view.ResizeHandler({el:$("#memberlists_resize_handle")[0]}),this.panels.server.view.show(),this.view.doLayout(),this.populateDefaultServerSettings()},this.initializeGlobals=function(){b.global.control=this.controlbox},this.populateDefaultServerSettings=function(){var a,b={nick:getQueryVariable("nick")||"kiwi_"+Math.ceil(Math.random()*1e4).toString(),server:"irc.kiwiirc.com",port:6667,ssl:!1,channel:window.location.hash||"#kiwiirc"};a=window.location.pathname.toString().split("/"),a.shift(),a.length>0&&a[0].toLowerCase()==="client"&&(a.shift(),a.length>0&&a[0]&&(b.server=a[0],a.shift()),a.length>0&&a[0]&&(b.channel="#"+a[0],a.shift())),b.nick=b.nick.replace("?",Math.floor(Math.random()*1e5).toString()),this.panels.server.server_login.populateFields(b)},this.bindGatewayCommands=function(c){c.on("onmotd",function(c){a.panels.server.addMsg(b.gateway.get("name"),c.msg,"motd")}),c.on("onconnect",function(b){a.view.barsShow(),d.channel&&a.controlbox.processInput("/JOIN "+d.channel)}),function(){var d=0;c.on("disconnect",function(c){var e="You have been disconnected. Attempting to reconnect for you..";a.message.text(e,{timeout:1e4}),$.each(b.app.panels.models,function(a,b){if(!b||!b.isChannel())return;b.addMsg("",e,"action quit")}),b.app.panels.server.addMsg("",e,"action quit"),d=1}),c.on("reconnecting",function(a){msg="You have been disconnected. Attempting to reconnect again in "+a.delay/1e3+" seconds..",b.app.panels.server.addMsg("",msg,"action quit")}),c.on("connect",function(c){if(d!==1)return;var e="It's OK, you're connected again :)";a.message.text(e,{timeout:5e3}),$.each(b.app.panels.models,function(a,b){if(!b||!b.isChannel())return;b.addMsg("",e,"action join")}),b.app.panels.server.addMsg("",e,"action join"),d=0})}(),c.on("onjoin",function(c){var d,e,f;d=a.panels.getByName(c.channel),d||(d=new b.model.Channel({name:c.channel}),a.panels.add(d)),e=d.get("members");if(!e)return;f=new b.model.Member({nick:c.nick,ident:c.ident,hostname:c.hostname}),e.add(f)}),c.on("onpart",function(c){var d,e,f,g={};g.type="part",g.message=c.message||"",d=a.panels.getByName(c.channel);if(!d)return;if(c.nick===b.gateway.get("nick")){d.close();return}e=d.get("members");if(!e)return;f=e.getByNick(c.nick);if(!f)return;e.remove(f,g)}),c.on("onquit",function(b){var c,d,e={};e.type="quit",e.message=b.message||"",$.each(a.panels.models,function(a,d){if(!d.isChannel())return;c=d.get("members").getByNick(b.nick),c&&d.get("members").remove(c,e)})}),c.on("onkick",function(c){var d,e,f,g={};g.type="kick",g.by=c.nick,g.message=c.message||"",d=a.panels.getByName(c.channel);if(!d)return;e=d.get("members");if(!e)return;f=e.getByNick(c.kicked);if(!f)return;e.remove(f,g),c.kicked===b.gateway.get("nick")&&e.reset([])}),c.on("onmsg",function(c){var d,e=c.channel==b.gateway.get("nick");e?(d=a.panels.getByName(c.nick),d||(d=new b.model.Channel({name:c.nick}),a.panels.add(d))):(d=a.panels.getByName(c.channel),d||(d=a.panels.server)),d.addMsg(c.nick,c.msg)}),c.on("onnotice",function(b){var c;c=a.panels.getByName(b.target)||a.panels.getByName(b.nick),c||(c=a.panels.server),c.addMsg("["+(b.nick||"")+"]",b.msg)}),c.on("onaction",function(c){var d,e=c.channel==b.gateway.get("nick");e?(d=a.panels.getByName(c.nick),d||(d=new b.model.Channel({name:c.nick}),a.panels.add(d))):(d=a.panels.getByName(c.channel),d||(d=a.panels.server)),d.addMsg("","* "+c.nick+" "+c.msg,"action")}),c.on("ontopic",function(c){var d;d=a.panels.getByName(c.channel);if(!d)return;d.set("topic",c.topic),d.get("name")===b.app.panels.active.get("name")&&a.topicbar.setCurrentTopic(c.topic)}),c.on("ontopicsetby",function(b){var c,d;c=a.panels.getByName(b.channel);if(!c)return;d=i(new Date(b.when*1e3)),c.addMsg("","Topic set by "+b.nick+" at "+d,"topic")}),c.on("onuserlist",function(c){var d;d=a.panels.getByName(c.channel);if(!d)return;d.temp_userlist=d.temp_userlist||[],_.each(c.users,function(a){var c=new b.model.Member({nick:a.nick,modes:a.modes});d.temp_userlist.push(c)})}),c.on("onuserlist_end",function(b){var c;c=a.panels.getByName(b.channel);if(!c)return;c.get("members").reset(c.temp_userlist||[]),delete c.temp_userlist}),c.on("onmode",function(c){function j(a,b){var d={},e;return a||(a=c.modes,b=c.target),_.each(a,function(a){var c=a.param||b||"";d[c]||(d[c]={"+":"","-":""}),d[c][a.mode[0]]+=a.mode.substr(1)}),e=[],_.each(d,function(a,b){var c="";a["+"]&&(c+="+"+a["+"]),a["-"]&&(c+="-"+a["-"]),e.push(c+" "+b)}),e=e.join(", "),e}var d,e,f,g,h,i;d=a.panels.getByName(c.target);if(d){f=b.gateway.get("user_prefixes"),i=function(a){return c.modes[e].mode[1]===a.mode};for(e=0;e<c.modes.length;e++)if(_.any(f,i)){g||(g=d.get("members")),h=g.getByNick(c.modes[e].param);if(!h){console.log("MODE command recieved for unknown member %s on channel %s",c.modes[e].param,c.target);return}c.modes[e].mode[0]==="+"?h.addMode(c.modes[e].mode[1]):c.modes[e].mode[0]==="-"&&h.removeMode(c.modes[e].mode[1]),g.sort()}d.addMsg("","== "+c.nick+" sets mode "+j(),"action mode")}else c.target.toLowerCase()===b.gateway.get("nick").toLowerCase()?a.panels.server.addMsg("","== "+c.nick+" set mode "+j(),"action mode"):console.log("MODE command recieved for unknown target %s: ",c.target,c)}),c.on("onnick",function(b){var c;$.each(a.panels.models,function(a,d){if(!d.isChannel())return;c=d.get("members").getByNick(b.nick),c&&(c.set("nick",b.newnick),d.addMsg("","== "+b.nick+" is now known as "+b.newnick,"action nick"))})}),c.on("onwhois",function(a){var c,d="",f;if(a.end)return;typeof a.idle!="undefined"&&(d=e(parseInt(a.idle,10)),d=d.h.toString().lpad(2,"0")+":"+d.m.toString().lpad(2,"0")+":"+d.s.toString().lpad(2,"0")),f=b.app.panels.active,a.ident?f.addMsg(a.nick,"is "+a.nick+"!"+a.ident+"@"+a.host+" * "+a.msg,"whois"):a.chans?f.addMsg(a.nick,"on "+a.chans,"whois"):a.server?f.addMsg(a.nick,"using "+a.server,"whois"):a.msg?f.addMsg(a.nick,a.msg,"whois"):a.logon?(c=new Date,c.setTime(a.logon*1e3),c=i(c),f.addMsg(a.nick,"idle for "+d+", signed on "+c,"whois")):f.addMsg(a.nick,"idle for "+d,"whois")}),c.on("onlist_start",function(a){b.app.channel_list&&(b.app.channel_list.close(),delete b.app.channel_list);var c=new b.model.Applet,d=new b.applets.Chanlist;c.load(d),b.app.panels.add(c),c.view.show(),b.app.channel_list=d}),c.on("onlist_channel",function(a){b.app.channel_list.addChannel(a.chans)}),c.on("onlist_end",function(a){delete b.app.channel_list}),c.on("onirc_error",function(c){var d,e;c.channel!==undefined&&!(d=b.app.panels.getByName(c.channel))&&(d=b.app.panels.server);switch(c.error){case"banned_from_channel":d.addMsg(" ","== You are banned from "+c.channel+". "+c.reason,"status"),b.app.message.text("You are banned from "+c.channel+". "+c.reason);break;case"bad_channel_key":d.addMsg(" ","== Bad channel key for "+c.channel,"status"),b.app.message.text("Bad channel key or password for "+c.channel);break;case"invite_only_channel":d.addMsg(" ","== "+c.channel+" is invite only.","status"),b.app.message.text(c.channel+" is invite only");break;case"channel_is_full":d.addMsg(" ","== "+c.channel+" is full.","status"),b.app.message.text(c.channel+" is full");break;case"chanop_privs_needed":d.addMsg(" ","== "+c.reason,"status"),b.app.message.text(c.reason+" ("+c.channel+")");break;case"no_such_nick":e=b.app.panels.getByName(c.nick),e?e.addMsg(" ","== "+c.nick+": "+c.reason,"status"):b.app.panels.server.addMsg(" ","== "+c.nick+": "+c.reason,"status");break;case"nickname_in_use":b.app.panels.server.addMsg(" ","== The nickname "+c.nick+" is already in use. Please select a new nickname","status"),b.app.panels.server!==b.app.panels.active&&b.app.message.text('The nickname "'+c.nick+'" is already in use. Please select a new nickname'),a.controlbox.$el.css("display")!=="none"&&(new b.view.NickChangeBox).render();break;default:}})},this.bindControllboxCommands=function(a){$.extend(a.preprocessor.aliases,{"/p":"/part $1+","/me":"/action $1+","/j":"/join $1+","/q":"/query $1+","/k":"/kick $1+","/slap":"/me throws the juciest, sweetest kiwi at $1. Hits right in the kisser!","/throw":"/slap $1+"}),a.on("unknown_command",g),a.on("command",h),a.on("command_msg",l),a.on("command_action",m),a.on("command_join",j),a.on("command_part",n),a.on("command_nick",function(a){b.gateway.changeNick(a.params[0])}),a.on("command_query",k),a.on("command_topic",o),a.on("command_notice",p),a.on("command_quote",q),a.on("command_kick",r),a.on("command_css",function(a){var b="?reload="+(new Date).getTime();$('link[rel="stylesheet"]').each(function(){this.href=this.href.replace(/\?.*|$/,b)})}),a.on("command_js",function(a){if(!a.params[0])return;$script(a.params[0]+"?"+(new Date).getTime())}),a.on("command_alias",function(c){var d,e;if(!c.params[1]){$.each(a.preprocessor.aliases,function(a,c){b.app.panels.server.addMsg(" ",a+"   =>   "+c)});return}if(c.params[0]==="del"||c.params[0]==="delete"){d=c.params[1],d[0]!=="/"&&(d="/"+d),delete a.preprocessor.aliases[d];return}d=c.params[0],c.params.shift(),e=c.params.join(" "),d[0]!=="/"&&(d="/"+d),a.preprocessor.aliases[d]=e}),a.on("command_applet",t),a.on("command_settings",s)},this.isChannelName=function(a){var c=b.gateway.get("channel_prefix");return!a||!a.length?!1:c.indexOf(a[0])>-1}};return f=Backbone.Model.extend(new f),new f(arguments)},b.model.Gateway=function(){var a=null;return this.defaults={name:"Server",address:"",nick:"",channel_prefix:"#",user_prefixes:["~","&","@","+"],kiwi_server:"//kiwi"},this.initialize=function(){a=this,this.socket=this.get("socket")},this.connect=function(b,c,d,e,f){this.socket=io.connect(this.get("kiwi_server"),{"try multiple transports":!0,"connect timeout":3e3,"max reconnection attempts":7,"reconnection delay":2e3,"sync disconnect on unload":!1}),this.socket.on("connect_failed",function(a){console.debug("Unable to connect Socket.IO",a),console.log("kiwi.gateway.socket.on('connect_failed')"),this.socket.disconnect(),this.trigger("connect_fail",{reason:a})}),this.socket.on("error",function(a){this.trigger("connect_fail",{reason:a}),console.log("kiwi.gateway.socket.on('error')",{reason:a})}),this.socket.on("connecting",function(b){console.log("kiwi.gateway.socket.on('connecting')"),a.trigger("connecting")}),this.socket.on("connect",function(){this.emit("kiwi",{command:"connect",nick:a.get("nick"),hostname:b,port:c,ssl:d,password:e},function(b,c){b?console.log("kiwi.gateway.socket.on('error')",{reason:b}):(a.server_num=c,console.log("kiwi.gateway.socket.on('connect')"))})}),this.socket.on("too_many_connections",function(){this.trigger("connect_fail",{reason:"too_many_connections"})}),this.socket.on("irc",function(b,c){a.parse(b.command,b.data)}),this.socket.on("disconnect",function(){a.trigger("disconnect",{}),console.log("kiwi.gateway.socket.on('disconnect')")}),this.socket.on("close",function(){console.log("kiwi.gateway.socket.on('close')")}),this.socket.on("reconnecting",function(b,c){console.log("kiwi.gateway.socket.on('reconnecting')"),a.trigger("reconnecting",{delay:b,attempts:c})}),this.socket.on("reconnect_failed",function(){console.log("kiwi.gateway.socket.on('reconnect_failed')")})},this.isConnected=function(){return this.socket.socket.connected},this.parse=function(b,c){console.log("gateway event",b,c);if(b!==undefined){a.trigger("on"+b,c);switch(b){case"options":$.each(c.options,function(b,c){switch(b){case"CHANTYPES":a.set("channel_prefix",c.join("").charAt(0));break;case"NETWORK":a.set("name",c);break;case"PREFIX":a.set("user_prefixes",c)}});break;case"connect":a.set("nick",c.nick);break;case"nick":c.nick===a.get("nick")&&a.set("nick",c.newnick);break;case"kiwi":this.emit("kiwi."+c.namespace,c.data)}}},this.sendData=function(a,b){this.socket.emit("irc",{server:0,data:JSON.stringify(a)},b)},this.privmsg=function(a,b,c){var d={method:"privmsg",args:{target:a,msg:b}};this.sendData(d,c)},this.notice=function(a,b,c){var d={method:"notice",args:{target:a,msg:b}};this.sendData(d,c)},this.ctcp=function(a,b,c,d,e){var f={method:"ctcp",args:{request:a,type:b,target:c,params:d}};this.sendData(f,e)},this.action=function(a,b,c){this.ctcp(!0,"ACTION",a,b,c)},this.join=function(a,b,c){var d={method:"join",args:{channel:a,key:b}};this.sendData(d,c)},this.part=function(a,b){var c={method:"part",args:{channel:a}};this.sendData(c,b)},this.topic=function(a,b,c){var d={method:"topic",args:{channel:a,topic:b}};this.sendData(d,c)},this.kick=function(a,b,c,d){var e={method:"kick",args:{channel:a,nick:b,reason:c}};this.sendData(e,d)},this.quit=function(a,b){a=a||"";var c={method:"quit",args:{message:a}};this.sendData(c,b)},this.raw=function(a,b){a={method:"raw",args:{data:a}},this.sendData(a,b)},this.changeNick=function(a,b){var c={method:"nick",args:{nick:a}};this.sendData(c,b)},this.kiwi=function(a,b,c){b={method:"kiwi",args:{target:a,data:b}},this.sendData(b,c)},new(Backbone.Model.extend(this))(arguments)},b.model.Member=Backbone.Model.extend({sortModes:function(a){return a.sort(function(a,c){var d,e,f,g=b.gateway.get("user_prefixes");for(f=0;f<g.length;f++)g[f].mode===a&&(d=f);for(f=0;f<g.length;f++)g[f].mode===c&&(e=f);return d<e?-1:d>e?1:0})},initialize:function(a){var b,c,d;b=this.stripPrefix(this.get("nick")),c=this.get("modes"),c=c||[],this.sortModes(c),this.set({nick:b,modes:c,prefix:this.getPrefix(c)},{silent:!0})},addMode:function(a){var b=a.split(""),c,d;c=this.get("modes"),$.each(b,function(a,b){c.push(b)}),c=this.sortModes(c),this.set({prefix:this.getPrefix(c),modes:c})},removeMode:function(a){var b=a.split(""),c,d;c=this.get("modes"),c=_.reject(c,function(a){return _.indexOf(b,a)!==-1}),this.set({prefix:this.getPrefix(c),modes:c})},getPrefix:function(a){var c="",d=b.gateway.get("user_prefixes");return typeof a[0]!="undefined"&&(c=_.detect(d,function(b){return b.mode===a[0]}),c=c?c.symbol:""),c},stripPrefix:function(a){var c=a,d,e,f,g=b.gateway.get("user_prefixes");d=0;for(e=0;e<a.length;e++)for(f=0;f<g.length;f++)if(a.charAt(e)===g[f].symbol){d++;break}return c.substr(d)},displayNick:function(a){var b=this.get("nick");return a&&this.get("ident")&&(b+=" ["+this.get("ident")+"@"+this.get("hostname")+"]"),b}}),b.model.MemberList=Backbone.Collection.extend({model:b.model.Member,comparator:function(a,c){var d,e,f,g,h,i,j,k=b.gateway.get("user_prefixes");e=a.get("modes"),f=c.get("modes");if(e.length>0){if(f.length===0)return-1;g=h=-1;for(d=0;d<k.length;d++)k[d].mode===e[0]&&(g=d);for(d=0;d<k.length;d++)k[d].mode===f[0]&&(h=d);if(g<h)return-1;if(g>h)return 1}else if(f.length>0)return 1;return i=a.get("nick").toLocaleUpperCase(),j=c.get("nick").toLocaleUpperCase(),i<j?-1:i>j?1:0},initialize:function(a){this.view=new b.view.MemberList({model:this})},getByNick:function(a){if(typeof a!="string")return;return this.find(function(b){return a.toLowerCase()===b.get("nick").toLowerCase()})}}),b.model.Panel=Backbone.Model.extend({initialize:function(a){var c=this.get("name")||"";this.view=new b.view.Panel({model:this,name:c}),this.set({scrollback:[],name:c},{silent:!0})},addMsg:function(a,b,c,d){var e,f,g;d=d||{};if(!d||typeof d.time=="undefined")g=new Date,d.time=g.getHours().toString().lpad(2,"0")+":"+g.getMinutes().toString().lpad(2,"0")+":"+g.getSeconds().toString().lpad(2,"0");if(!d||typeof d.style=="undefined")d.style="";e={msg:b,time:d.time,nick:a,chan:this.get("name"),type:c,style:d.style};if(!e)return;typeof e.type!="string"&&(e.type=""),typeof e.msg!="string"&&(e.msg=""),f=this.get("scrollback"),f.push(e),f.length>250&&f.splice(250),this.set({scrollback:f},{silent:!0}),this.trigger("msg",e)},closePanel:function(){this.view&&(this.view.unbind(),this.view.remove(),this.view=undefined,delete this.view);var a=this.get("members");a&&(a.reset([]),this.unset("members")),b.app.panels.remove(this),this.unbind(),this.destroy(),this.cid===b.app.panels.active.cid&&b.app.panels.server.view.show()},close:function(){return this.closePanel()},isChannel:function(){var a=b.gateway.get("channel_prefix"),c=this.get("name");return this.isApplet()||!c?!1:a.indexOf(c[0])>-1},isApplet:function(){return this.applet?!0:!1},isServer:function(){return this.server?!0:!1},isActive:function(){return b.app.panels.active===this}}),b.model.PanelList=Backbone.Collection.extend({model:b.model.Panel,comparator:function(a){return a.get("name")},initialize:function(){this.view=new b.view.Tabs({el:$("#tabs")[0],model:this}),this.add(new b.model.Server({name:b.gateway.get("name")})),this.server=this.getByName(b.gateway.get("name")),this.active=null,this.bind("active",function(a){this.active=a},this)},getByName:function(a){if(typeof a!="string")return;return this.find(function(b){return a.toLowerCase()===b.get("name").toLowerCase()})}}),b.model.Channel=b.model.Panel.extend({initialize:function(a){var c=this.get("name")||"",d;this.view=new b.view.Channel({model:this,name:c}),this.set({members:new b.model.MemberList,name:c,scrollback:[],topic:""},{silent:!0}),d=this.get("members"),d.bind("add",function(a){this.addMsg(" ","== "+a.displayNick(!0)+" has joined","action join")},this),d.bind("remove",function(a,b,c){var d=c.message?"("+c.message+")":"";c.type==="quit"?this.addMsg(" ","== "+a.displayNick(!0)+" has quit "+d,"action quit"):c.type==="kick"?this.addMsg(" ","== "+a.displayNick(!0)+" was kicked by "+c.by+" "+d,"action kick"):this.addMsg(" ","== "+a.displayNick(!0)+" has left "+d,"action part")},this)}}),b.model.Server=b.model.Panel.extend({server:!0,initialize:function(a){var c="Server";this.view=new b.view.Panel({model:this,name:c}),this.set({scrollback:[],name:c},{silent:!0}),this.server_login=new b.view.ServerSelect,this.view.$el.append(this.server_login.$el),this.server_login.show()}}),b.model.Applet=b.model.Panel.extend({applet:!0,initialize:function(a){var c="applet_"+(new Date).getTime().toString()+Math.ceil(Math.random()*100).toString();this.view=new b.view.Applet({model:this,name:c}),this.set({name:c},{silent:!0}),this.loaded_applet=null},load:function(a,b){if(typeof a=="object"){if(a.get||a.extend)this.set("title",a.get("title")||"Unknown Applet"),a.bind("change:title",function(a,b){this.set("title",b)},this),this.view.$el.html(""),a.view&&this.view.$el.append(a.view.$el),this.loaded_applet=a}else typeof a=="string"&&this.loadFromUrl(a,b);return this},loadFromUrl:function(a,c){var d=this;this.view.$el.html("Loading.."),$script(a,function(){if(!b.applets[c]){d.view.$el.html("Not found");return}d.load(new b.applets[c])})},close:function(){this.view.$el.remove(),this.destroy(),this.view=undefined,this.loaded_applet&&this.loaded_applet.dispose&&this.loaded_applet.dispose(),this.closePanel()}}),function(){var a=Backbone.View.extend({events:{"click .save":"saveSettings"},initialize:function(a){this.$el=$($("#tmpl_applet_settings").html())},saveSettings:function(){var a=$(".theme",this.$el).val();b.app.view.$el.removeClass(function(a,b){return(b.match(/\btheme_\S+/g)||[]).join(" ")}),a&&b.app.view.$el.addClass("theme_"+a)}});b.applets.Settings=Backbone.Model.extend({initialize:function(){this.set("title","Settings"),this.view=new a}})}(),function(){var a=Backbone.View.extend({events:{"click .save":"saveSettings"},initialize:function(a){this.$el=$($("#tmpl_applet_settings").html())},saveSettings:function(){var a=$(".theme",this.$el).val(),b=$("#panels > .panel_container");b.removeClass(function(a,b){return(b.match(/\btheme_\S+/g)||[]).join(" ")}),a&&b.addClass("theme_"+a)}});b.applets.nickserv=Backbone.Model.extend({initialize:function(){this.set("title","Nickserv Login"),b.global.control.on("command_login",this.loginCommand,this)},loginCommand:function(a){console.log("waheeyy")}})}(),function(){var a=Backbone.View.extend({events:{},initialize:function(a){this.$el=$($("#tmpl_channel_list").html()),this.channels=[],this.ordered=!1,this.waiting=!1},render:function(){var a=$("table",this.$el),b=a.children("tbody:first").detach();this.ordered&&this.channels.sort(function(a,b){return b.num_users-a.num_users}),_.each(this.channels,function(a){b.append(a.html)}),a.append(b)}});b.applets.Chanlist=Backbone.Model.extend({initialize:function(){this.set("title","Channel List"),this.view=new a},addChannel:function(a){var b=this;_.isArray(a)||(a=[a]),_.each(a,function(a){var c,d;c='<tr><td><a class="chan">'+a.channel+'</a></td><td class="num_users" style="text-align: center;">'+a.num_users+'</td><td style="padding-left: 2em;">'+h(a.topic)+"</td></tr>",a.html=c,b.view.channels.push(a)}),b.view.waiting||(b.view.waiting=!0,_.defer(function(){b.view.render(),b.view.waiting=!1}))},dispose:function(){this.view.channels=null,this.view.unbind(),this.view.$el.html(""),this.view.remove(),this.view=null}})}(),typeof String.prototype.trim=="undefined"&&(String.prototype.trim=function(){return this.replace(/^\s+|\s+$/g,"")}),typeof String.prototype.lpad=="undefined"&&(String.prototype.lpad=function(a,b){var c="",d;for(d=0;d<a;d++)c+=b;return(c+this).slice(-a)});var j=[{name:"images",onaddmsg:function(a,b){return a.msg?(a.msg=a.msg.replace(/^((https?\:\/\/|ftp\:\/\/)|(www\.))(\S+)(\w{2,4})(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?(\.jpg|\.jpeg|\.gif|\.bmp|\.png)$/gi,function(b){a.event_bubbles=!1;var c='<img class="link_img_a" src="'+b+'" height="100%" width="100%" />';return'<a class="link_ext link_img" target="_blank" rel="nofollow" href="'+b+'" style="height:50px;width:50px;display:block">'+c+'<div class="tt box"></div></a>'}),a):a}},{name:"html_safe",onaddmsg:function(a,b){return a.msg=$("<div/>").text(a.msg).html(),a.nick=$("<div/>").text(a.nick).html(),a}},{name:"activity",onaddmsg:function(a,b){return a}},{name:"highlight",onaddmsg:function(a,b){return a}},{name:"linkify_plain",onaddmsg:function(a,b){return a.msg?(a.msg=a.msg.replace(/((https?\:\/\/|ftp\:\/\/)|(www\.))(\S+)(\w{2,4})(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/gi,function(a){var b;return a.match(/(\.jpg|\.jpeg|\.gif|\.bmp|\.png)$/)?a:(b=a,a.match("^https?://")?b=a:a="http://"+a,'<a class="link_ext" target="_blank" rel="nofollow" href="'+a+'">'+b+"</a>")}),a):a}},{name:"lftobr",onaddmsg:function(a,b){return a.msg?(a.msg=a.msg.replace(/\n/gi,function(a){return"<br/>"}),a):a}},{name:"nick_colour",onaddmsg:function(a,b){if(!a.msg)return a;var c=this.randColour();return a.nick='<span style="color:'+c+';">'+a.nick+"</span>",a},randColour:function(){var a=this.rand(-250,0),b=this.rand(30,100),c=this.rand(20,70);return"hsl("+a+","+b+"%,"+c+"%)"},rand:function(a,b){return parseInt(Math.random()*(b-a+1),10)+a}},{name:"kiwitest",oninit:function(a,b){console.log("registering namespace"),$(gateway).bind("kiwi.lol.browser",function(a,b){console.log("YAY kiwitest"),console.log(b)})}}];b.plugs={},b.plugs.loaded={},b.plugs.loadPlugin=function(a){var c;return typeof a.name!="string"?!1:(c=b.plugs.run("plugin_load",{plugin:a}),typeof c=="object"&&(b.plugs.loaded[c.plugin.name]=c.plugin,b.plugs.loaded[c.plugin.name].local_data=new b.dataStore("kiwi_plugin_"+c.plugin.name)),b.plugs.run("init",{},{run_only:c.plugin.name}),!0)},b.plugs.unloadPlugin=function(a){if(typeof b.plugs.loaded[a]!="object")return;b.plugs.run("unload",{},{run_only:a}),delete b.plugs.loaded[a]},b.plugs.run=function(a,c,d){var e=c,f,g;c=typeof c=="undefined"?{}:c,d=typeof d=="undefined"?{}:d;for(g in b.plugs.loaded){if(typeof d.run_only=="string"&&d.run_only!==g)continue;if(typeof b.plugs.loaded[g]["on"+a]=="function")try{f=b.plugs.loaded[g]["on"+a](e,d);if(f===null)return null;e=f;if(typeof e.event_bubbles=="boolean"&&e.event_bubbles===!1)return delete e.event_bubbles,e}catch(h){}}return e},b.dataStore=function(a){var b=a;this.get=function(b){return $.jStorage.get(a+"_"+b)},this.set=function(b,c){return $.jStorage.set(a+"_"+b,c)}},b.data=new b.dataStore("kiwi"),function(a){function i(){if("localStorage"in window)try{window.localStorage&&(c=window.localStorage,h="localStorage")}catch(a){}else if("globalStorage"in window)try{window.globalStorage&&(c=window.globalStorage[window.location.hostname],h="globalStorage")}catch(b){}else{d=document.createElement("link");if(!d.addBehavior){d=null;return}d.style.behavior="url(#default#userData)",document.getElementsByTagName("head")[0].appendChild(d),d.load("jStorage");var e="{}";try{e=d.getAttribute("jStorage")}catch(f){}c.jStorage=e,h="userDataBehavior"}j()}function j(){if(c.jStorage)try{b=g(String(c.jStorage))}catch(a){c.jStorage="{}"}else c.jStorage="{}";e=c.jStorage?String(c.jStorage).length:0}function k(){try{c.jStorage=f(b),d&&(d.setAttribute("jStorage",c.jStorage),d.save("jStorage")),e=c.jStorage?String(c.jStorage).length:0}catch(a){}}function l(a){if(!a||typeof a!="string"&&typeof a!="number")throw new TypeError("Key name must be string or numeric");return!0}if(!a||!(a.toJSON||Object.toJSON||window.JSON))throw new Error("jQuery, MooTools or Prototype needs to be loaded before jStorage!");var b={},c={jStorage:"{}"},d=null,e=0,f=a.toJSON||Object.toJSON||window.JSON&&(JSON.encode||JSON.stringify),g=a.evalJSON||window.JSON&&(JSON.decode||JSON.parse)||function(a){return String(a).evalJSON()},h=!1;_XMLService={isXML:function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1},encode:function(a){if(!this.isXML(a))return!1;try{return(new XMLSerializer).serializeToString(a)}catch(b){try{return a.xml}catch(c){}}return!1},decode:function(a){var b="DOMParser"in window&&(new DOMParser).parseFromString||window.ActiveXObject&&function(a){var b=new ActiveXObject("Microsoft.XMLDOM");return b.async="false",b.loadXML(a),b},c;return b?(c=b.call("DOMParser"in window&&new DOMParser||window,a,"text/xml"),this.isXML(c)?c:!1):!1}},a.jStorage={version:"0.1.5.1",set:function(a,c){return l(a),_XMLService.isXML(c)&&(c={_is_xml:!0,xml:_XMLService.encode(c)}),b[a]=c,k(),c},get:function(a,c){return l(a),a in b?b[a]&&typeof b[a]=="object"&&b[a]._is_xml&&b[a]._is_xml?_XMLService.decode(b[a].xml):b[a]:typeof c=="undefined"?null:c},deleteKey:function(a){return l(a),a in b?(delete b[a],k(),!0):!1},flush:function(){return b={},k(),!0},storageObj:function(){function a(){}return a.prototype=b,new a},index:function(){var a=[],c;for(c in b)b.hasOwnProperty(c)&&a.push(c);return a},storageSize:function(){return e},currentBackend:function(){return h},storageAvailable:function(){return!!h},reInit:function(){var a,b;if(d&&d.addBehavior){a=document.createElement("link"),d.parentNode.replaceChild(a,d),d=a,d.style.behavior="url(#default#userData)",document.getElementsByTagName("head")[0].appendChild(d),d.load("jStorage"),b="{}";try{b=d.getAttribute("jStorage")}catch(e){}c.jStorage=b,h="userDataBehavior"}j()}},i()}(window.jQuery||window.$),b.view.MemberList=Backbone.View.extend({tagName:"ul",events:{"click .nick":"nickClick"},initialize:function(a){this.model.bind("all",this.render,this),$(this.el).appendTo("#memberlists")},render:function(){var a=$(this.el);a.empty(),this.model.forEach(function(b){$('<li><a class="nick"><span class="prefix">'+b.get("prefix")+"</span>"+b.get("nick")+"</a></li>").appendTo(a).data("member",b)})},nickClick:function(a){var c=$(a.currentTarget).parent("li"),d=c.data("member"),e=new b.view.UserBox;e.member=d,$(".userbox",this.$el).remove(),c.append(e.$el)},show:function(){$("#memberlists").children().removeClass("active"),$(this.el).addClass("active")}}),b.view.UserBox=Backbone.View.extend({events:{"click .query":"queryClick","click .info":"infoClick"},initialize:function(){this.$el=$($("#tmpl_userbox").html())},queryClick:function(a){var c=new b.model.Channel({name:this.member.get("nick")});b.app.panels.add(c),c.view.show()},infoClick:function(a){b.gateway.raw("WHOIS "+this.member.get("nick"))}}),b.view.NickChangeBox=Backbone.View.extend({events:{submit:"changeNick","click .cancel":"close"},initialize:function(){this.$el=$($("#tmpl_nickchange").html())},render:function(){b.app.controlbox.$el.prepend(this.$el),this.$el.find("input").focus(),this.$el.css("bottom",b.app.controlbox.$el.outerHeight(!0))},close:function(){this.$el.remove()},changeNick:function(a){var c=this;return b.gateway.changeNick(this.$el.find("input").val(),function(a,b){c.close()}),!1}}),b.view.ServerSelect=function(){var a="all",c=Backbone.View.extend({events:{"submit form":"submitForm","click .show_more":"showMore"},initialize:function(){this.$el=$($("#tmpl_server_select").html()),b.gateway.bind("onconnect",this.networkConnected,this),b.gateway.bind("connecting",this.networkConnecting,this),b.gateway.bind("onirc_error",function(a){$("button",this.$el).attr("disabled",null),a.error=="nickname_in_use"&&(this.setStatus("Nickname already taken"),this.show("nick_change"))},this)},submitForm:function(b){return a==="nick_change"?this.submitNickChange(b):this.submitLogin(b),$("button",this.$el).attr("disabled",1),!1},submitLogin:function(a){if($("button",this.$el).attr("disabled"))return;var b={nick:$(".nick",this.$el).val(),server:$(".server",this.$el).val(),port:$(".port",this.$el).val(),ssl:$(".ssl",this.$el).prop("checked"),password:$(".password",this.$el).val(),channel:$(".channel",this.$el).val()};this.trigger("server_connect",b)},submitNickChange:function(a){b.gateway.changeNick($(".nick",this.$el).val()),this.networkConnecting()},showMore:function(a){$(".more",this.$el).slideDown("fast"),$(".server",this.$el).select()},populateFields:function(a){var b,c,d;a=a||{},b=a.nick||"",c=a.server||"",port=a.port||6667,ssl=a.ssl||0,password=a.password||"",d=a.channel||"",$(".nick",this.$el).val(b),$(".server",this.$el).val(c),$(".port",this.$el).val(port),$(".ssl",this.$el).prop("checked",ssl),$(".password",this.$el).val(password),$(".channel",this.$el).val(d)},hide:function(){this.$el.slideUp()},show:function(b){b=b||"all",this.$el.show(),b==="all"?$(".show_more",this.$el).show():b==="more"?$(".more",this.$el).slideDown("fast"):b==="nick_change"&&($(".more",this.$el).hide(),$(".show_more",this.$el).hide()),a=b},setStatus:function(a,b){$(".status",this.$el).text(a).attr("class","status").addClass(b).show()},clearStatus:function(){$(".status",this.$el).hide()},networkConnected:function(a){this.setStatus("Connected :)","ok"),$("form",this.$el).hide()},networkConnecting:function(a){this.setStatus("Connecting..","ok")},showError:function(a){this.setStatus("Error connecting","error"),$("button",this.$el).attr("disabled",null),this.show()}});return new c(arguments)},b.view.Panel=Backbone.View.extend({tagName:"div",className:"messages",events:{"click .chan":"chanClick"},initialize:function(a){this.initializePanel(a)},initializePanel:function(a){this.$el.css("display","none"),a=a||{},a.container?this.$container=$(a.container):this.$container=$("#panels .container1"),this.$el.appendTo(this.$container),this.alert_level=0,this.model.bind("msg",this.newMsg,this),this.msg_count=0,this.model.set({view:this},{silent:!0})},render:function(){this.$el.empty(),this.model.get("backscroll").forEach(this.newMsg)},newMsg:function(a){var c,d,e=this.$el,f;a.msg=$("<div />").text(a.msg).html(),c=new RegExp("\\B(["+b.gateway.get("channel_prefix")+"][^ ,.\\007]+)","g"),a.msg=a.msg.replace(c,function(a){return'<a class="chan">'+a+"</a>"}),a.msg=a.msg.replace(/((https?\:\/\/|ftp\:\/\/)|(www\.))(\S+)(\w{2,4})(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]*))?/gi,function(a){var b;return a.match(/^www\./)&&(a="http://"+a),b=a,b.length>100&&(b=b.substr(0,100)+"..."),'<a class="link_ext" target="_blank" rel="nofollow" href="'+a+'">'+b+"</a>"}),a.msg=h(a.msg),f=function(a){var b=0,c;return _.map(a.split(""),function(a){b+=a.charCodeAt(0)}),c=g(b%255,70,35),c=c[2]|c[1]<<8|c[0]<<16,"#"+c.toString(16)}(a.nick),a.nick_style="color:"+f+";",d='<div class="msg <%= type %>"><div class="time"><%- time %></div><div class="nick" style="<%= nick_style %>"><%- nick %></div><div class="text" style="<%= style %>"><%= msg %> </div></div>',e.append(_.template(d,a)),a.type.match(/^action /)?this.alert("action"):a.msg.indexOf(b.gateway.get("nick"))>-1?(b.app.view.alertWindow("* People are talking!"),this.alert("highlight")):(this.model.isActive()&&b.app.view.alertWindow("* People are talking!"),this.alert("activity")),this.scrollToBottom(),this.msg_count++,this.msg_count>250&&($(".msg:first",this.$el).remove(),this.msg_count--)},chanClick:function(a){b.gateway.join($(a.srcElement).text())},show:function(){var a=this.$el;this.$container.children().css("display","none"),a.css("display","block");var c=this.model.get("members");c?($("#memberlists").show(),c.view.show()):$("#memberlists").hide().children().removeClass("active"),b.app.view.doLayout(),this.scrollToBottom(),this.alert("none"),this.trigger("active",this.model),b.app.panels.trigger("active",this.model)},alert:function(a){if(this.model==b.app.panels.active)return;var c,d;c=["none","action","activity","highlight"],a=a||"none",d=_.indexOf(c,a),d||(a="none",d=0);if(d!==0&&d<=this.alert_level)return;this.model.tab.removeClass(function(a,b){return(b.match(/\balert_\S+/g)||[]).join(" ")}),a!=="none"&&this.model.tab.addClass("alert_"+a),this.alert_level=d},scrollToBottom:function(){this.$container[0].scrollTop=this.$container[0].scrollHeight}}),b.view.Applet=b.view.Panel.extend({className:"applet",initialize:function(a){this.initializePanel(a)}}),b.view.Channel=b.view.Panel.extend({initialize:function(a){this.initializePanel(a),this.model.bind("change:topic",this.topic,this)},topic:function(a){if(typeof a!="string"||!a)a=this.model.get("topic");this.model.addMsg("","== Topic for "+this.model.get("name")+" is: "+a,"topic"),b.app.panels.active===this&&b.app.topicbar.setCurrentTopic(this.model.get("topic"))}}),b.view.Tabs=Backbone.View.extend({events:{"click li":"tabClick","click li img":"partClick"},initialize:function(){this.model.on("add",this.panelAdded,this),this.model.on("remove",this.panelRemoved,this),this.model.on("reset",this.render,this),this.model.on("active",this.panelActive,this),this.tabs_applets=$("ul.applets",this.$el),this.tabs_msg=$("ul.channels",this.$el),b.gateway.on("change:name",function(a,b){$("span",this.model.server.tab).text(b)},this)},render:function(){var a=this;this.tabs_msg.empty(),this.model.server.tab.data("panel_id",this.model.server.cid).appendTo(this.tabs_msg),this.model.forEach(function(b){if(b==a.model.server)return;b.tab.data("panel_id",b.cid).appendTo(b.isApplet()?this.tabs_applets:this.tabs_msg)}),b.app.view.doLayout()},updateTabTitle:function(a,b){$("span",a.tab).text(b)},panelAdded:function(a){a.tab=$("<li><span>"+(a.get("title")||a.get("name"))+"</span></li>"),a.isServer()&&a.tab.addClass("server"),a.tab.data("panel_id",a.cid).appendTo(a.isApplet()?this.tabs_applets:this.tabs_msg),a.bind("change:title",this.updateTabTitle),b.app.view.doLayout()},panelRemoved:function(a){a.tab.remove(),delete a.tab,b.app.view.doLayout()},panelActive:function(a){$("img",this.$el).remove(),this.tabs_applets.children().removeClass("active"),this.tabs_msg.children().removeClass("active"),a.tab.addClass("active"),a.isServer()||a.tab.append('<img src="img/redcross.png" />')},tabClick:function(a){var b=$(a.currentTarget),c=this.model.getByCid(b.data("panel_id"));if(!c)return;c.view.show()},partClick:function(a){var c=$(a.currentTarget).parent(),d=this.model.getByCid(c.data("panel_id"));d.isChannel()&&d.get("members").models.length>0?b.gateway.part(d.get("name")):d.close()},next:function(){var a=b.app.panels.active.tab.next();a.length||(a=$("li:first",this.tabs_msgs)),a.click()},prev:function(){var a=b.app.panels.active.tab.prev();a.length||(a=$("li:last",this.tabs_msgs)),a.click()}}),b.view.TopicBar=Backbone.View.extend({events:{"keydown input":"process"},initialize:function(){b.app.panels.bind("active",function(a){this.setCurrentTopic(a.get("topic"))},this)},process:function(a){var c=$(a.currentTarget),d=c.val();if(a.keyCode!==13)return;b.app.panels.active.isChannel()&&b.gateway.topic(b.app.panels.active.get("name"),d)},setCurrentTopic:function(a){a=a||"",a=$("<div>").html(h(a)),$("input",this.$el).val(a.text())}}),b.view.ControlBox=Backbone.View.extend({events:{"keydown input.inp":"process","click .nick":"showNickChange"},initialize:function(){var a=this;this.buffer=[],this.buffer_pos=0,this.preprocessor=new f,this.preprocessor.recursive_depth=5,this.tabcomplete={active:!1,data:[],prefix:""},b.gateway.bind("change:nick",function(){$(".nick",a.$el).text(this.get("nick"))})},showNickChange:function(a){(new b.view.NickChangeBox).render()},process:function(a){var c=this,d=$(a.currentTarget),e=d.val(),f;navigator.appVersion.indexOf("Mac")!==-1?f=a.ctrlKey:f=a.altKey,this.tabcomplete.active&&a.keyCode!==9&&(this.tabcomplete.active=!1,this.tabcomplete.data=[],this.tabcomplete.prefix="");switch(!0){case a.keyCode===13:e=e.trim(),e&&(this.processInput(d.val()),this.buffer.push(d.val()),this.buffer_pos=this.buffer.length),d.val("");break;case a.keyCode===38:this.buffer_pos>0&&(this.buffer_pos--,d.val(this.buffer[this.buffer_pos]));break;case a.keyCode===40:this.buffer_pos<this.buffer.length&&(this.buffer_pos++,d.val(this.buffer[this.buffer_pos]));break;case a.keyCode===37&&f:return b.app.panels.view.prev(),!1;case a.keyCode===39&&f:return b.app.panels.view.next(),!1;case a.keyCode===9:this.tabcomplete.active=!0;if(_.isEqual(this.tabcomplete.data,[])){var g=[];$.each(b.app.panels.active.get("members").models,function(a,b){if(!b)return;g.push(b.get("nick"))}),g=_.sortBy(g,function(a){return a}),this.tabcomplete.data=g}if(e[d[0].selectionStart-1]===" ")return!1;return function(){var a=e.substring(0,d[0].selectionStart).split(" "),b,f,g,h,i=a[a.length-1];this.tabcomplete.prefix===""&&(this.tabcomplete.prefix=i),this.tabcomplete.data=_.select(this.tabcomplete.data,function(a){return a.toLowerCase().indexOf(c.tabcomplete.prefix.toLowerCase())===0}),this.tabcomplete.data.length>0&&(f=d[0].selectionStart-i.length,b=e.substr(0,f),g=this.tabcomplete.data.shift(),this.tabcomplete.data.push(g),b+=g,b+=e.substr(d[0].selectionStart),d.val(b),d[0].setSelectionRange?d[0].setSelectionRange(f+g.length,f+g.length):d[0].createTextRange&&(h=d[0].createTextRange(),h.collapse(!0),h.moveEnd("character",f+g.length),h.moveStart("character",f+g.length),h.select()))}.apply(this),!1}},processInput:function(a){var c,d,e;a[0]!=="/"&&(a="/msg "+b.app.panels.active.get("name")+" "+a),this.preprocessor.vars.server=b.gateway.get("name"),this.preprocessor.vars.channel=b.app.panels.active.get("name"),this.preprocessor.vars.destination=this.preprocessor.vars.channel,a=this.preprocessor.process(a),d=a.split(" "),d[0][0]==="/"?(c=d[0].substr(1).toLowerCase(),d=d.splice(1)):(c="msg",d.unshift(b.app.panels.active.get("name"))),this.trigger("command",{command:c,params:d}),this.trigger("command_"+c,{command:c,params:d}),this._callbacks["command_"+c]||this.trigger("unknown_command",{command:c,params:d})}}),b.view.StatusMessage=Backbone.View.extend({initialize:function(){this.$el.hide(),this.tmr=null},text:function(a,c){c=c||{},c.type=c.type||"",c.timeout=c.timeout||5e3,this.$el.text(a).attr("class",c.type),this.$el.slideDown(b.app.view.doLayout),c.timeout&&this.doTimeout(c.timeout)},html:function(a,c){c=c||{},c.type=c.type||"",c.timeout=c.timeout||5e3,this.$el.html(text).attr("class",c.type),this.$el.slideDown(b.app.view.doLayout),c.timeout&&this.doTimeout(c.timeout)},hide:function(){this.$el.slideUp(b.app.view.doLayout)},doTimeout:function(a){this.tmr&&clearTimeout(this.tmr);var b=this;this.tmr=setTimeout(function(){b.hide()},a)}}),b.view.ResizeHandler=Backbone.View.extend({events:{mousedown:"startDrag",mouseup:"stopDrag"},initialize:function(){this.dragging=!1,this.starting_width={},$(window).on("mousemove",$.proxy(this.onDrag,this))},startDrag:function(a){this.dragging=!0},stopDrag:function(a){this.dragging=!1},onDrag:function(a){if(!this.dragging)return;this.$el.css("left",a.clientX-this.$el.outerWidth(!0)/2),$("#memberlists").css("width",this.$el.parent().width()-(this.$el.position().left+this.$el.outerWidth())),b.app.view.doLayout()}}),b.view.Application=Backbone.View.extend({initialize:function(){$(window).resize(this.doLayout),$("#toolbar").resize(this.doLayout),$("#controlbox").resize(this.doLayout),this.doLayout(),$(document).keydown(this.setKeyFocus),window.onbeforeunload=function(){if(b.gateway.isConnected())return"This will close all KiwiIRC conversations. Are you sure you want to close this window?"}},setKeyFocus:function(a){if(a.ctrlKey||a.altKey)return;if(a.target.tagName.toLowerCase()==="input")return;$("#controlbox .inp").focus()},doLayout:function(){var a=$("#panels"),b=$("#memberlists"),c=$("#toolbar"),d=$("#controlbox"),e=$("#memberlists_resize_handle"),f={top:c.outerHeight(!0),bottom:d.outerHeight(!0)};a.css(f),b.css(f),e.css(f),b.css("display")!="none"?(a.css("right",b.outerWidth(!0)+e.outerWidth(!0)),e.css("left",b.position().left-e.outerWidth(!0))):(a.css("right",e.outerWidth(!0)),e.css("left",a.outerWidth(!0)))},alertWindow:function(a){this.alertWindowTimer||(this.alertWindowTimer=new function(){var a=this,b,c=!0,d=0,e="Kiwi IRC",f="Kiwi IRC";this.setTitle=function(a){return a=a||e,window.document.title=a,a},this.start=function(a){if(c)return;f=a;if(b)return;b=setInterval(this.update,1e3)},this.stop=function(){b&&clearInterval(b),b=null,this.setTitle(),setTimeout(this.reset,2e3)},this.reset=function(){if(b)return;a.setTitle()},this.update=function(){d===0?(a.setTitle(f),d=1):(a.setTitle(),d=0)},$(window).focus(function(b){c=!0,a.stop(),setTimeout(this.reset,2e3)}),$(window).blur(function(a){c=!1})}),this.alertWindowTimer.start(a)},barsHide:function(a){var b=this;a?($("#toolbar").slideUp(0),$("#controlbox").slideUp(0),this.doLayout()):($("#toolbar").slideUp({queue:!1,duration:400,step:this.doLayout}),$("#controlbox").slideUp({queue:!1,duration:400,step:this.doLayout}))},barsShow:function(a){var b=this;a?($("#toolbar").slideDown(0),$("#controlbox").slideDown(0),this.doLayout()):($("#toolbar").slideDown({queue:!1,duration:400,step:this.doLayout}),$("#controlbox").slideDown({queue:!1,duration:400,step:this.doLayout}))}})})(window)
\ No newline at end of file
index a4884c2c7ec71e11dab92d2dbf905d21373f1b0c..6036794a070aba0b713dfa3561e90e25afae0e3d 100644 (file)
@@ -6,14 +6,14 @@
 
 <title> KiwiIRC </title>
 
-<link rel="stylesheet" type="text/css" href="/css/style.css" />
+<link rel="stylesheet" type="text/css" href="/client/assets/css/style.css" />
 
 </head>
 <body>
     <div id="kiwi">
         <div id="toolbar">
             <a class="kiwi_logo" href="http://www.kiwiirc.com/" target="_blank">
-                <h2><img src="/img/ico.png" alt="KiwiIRC Logo" title="Kiwi IRC" /> Powered by Kiwi IRC</h2>
+                <h2><img src="/client/assets/img/ico.png" alt="KiwiIRC Logo" title="Kiwi IRC" /> Powered by Kiwi IRC</h2>
             </a>
 
             <div id="tabs">
@@ -92,7 +92,7 @@
 
             <div style="position:relative;float:left;width:320px;margin-left:3em;color:#555555;">
                 <a class="kiwi_logo" href="http://www.kiwiirc.com/" target="_blank">
-                    <img src="/img/ico.png" alt="KiwiIRC Logo" title="Kiwi IRC" /> <br />
+                    <img src="/client/assets/img/ico.png" alt="KiwiIRC Logo" title="Kiwi IRC" /> <br />
                     <h1>Powered by Kiwi IRC</h1>
                 </a>
 
 
         // Common dependancies that are required at all times
         var scripts = [
-            ['/jquery-1.7.1.min.js', '/underscore-min.js'],
-            '/backbone-git.js'
+            ['jquery-1.7.1.min.js', 'underscore-min.js'],
+            'backbone-git.js'
         ];
 
         // If in debug mode, load each development script
         if (getQueryVariable('debug')) {
             console.log('Loading debugging scripts');
             scripts = scripts.concat([
-                '/dev/app.js',
+                'dev/app.js',
                 [
-                    '/dev/model_application.js',
-                    '/dev/model_gateway.js'
+                    'dev/model_application.js',
+                    'dev/model_gateway.js'
                 ],
                 [
-                    '/dev/model_panellist.js',
-                    '/dev/model_panel.js',
-                    '/dev/model_member.js',
-                    '/dev/model_memberlist.js'
+                    'dev/model_panellist.js',
+                    'dev/model_panel.js',
+                    'dev/model_member.js',
+                    'dev/model_memberlist.js'
                 ],
                 
                 [
-                    '/dev/model_channel.js',
-                    '/dev/model_server.js',
-                    '/dev/model_applet.js'
+                    'dev/model_channel.js',
+                    'dev/model_server.js',
+                    'dev/model_applet.js'
                 ],
 
                 [
-                    '/dev/applet_settings.js',
-                    '/dev/applet_nickserv.js',
-                    '/dev/applet_chanlist.js'
+                    'dev/applet_settings.js',
+                    'dev/applet_nickserv.js',
+                    'dev/applet_chanlist.js'
                 ],
 
                 [
-                    '/dev/utils.js',
-                    '/dev/view.js'
+                    'dev/utils.js',
+                    'dev/view.js'
                 ]
             ]);
         } else {
-            scripts.push('/kiwi.js');
+            scripts.push('kiwi.js');
         }
 
 
         function startApp () {
             var opts = {
                 container: $('#kiwi'),
+                base_path: base_path
 
                 // Override the kiwi_server to use. (Think: running on standalone client..)
                 //kiwi_server: 'http://kiwiirc.com:80'
         // Load each script
         var cur_script = 0;
         function loadNextScript () {
+            var to_load,
+                base = base_path + '/assets/';
+
             // Start the kiwi app if all scripts have been loaded
             if (cur_script === scripts.length) {
                 startApp();
                 return;
             }
 
-            $script(scripts[cur_script], loadNextScript);
+            if (typeof scripts[cur_script] === 'string') {
+                to_load = base + scripts[cur_script];
+            } else {
+                to_load = [];
+                for(var idx in scripts[cur_script]) {
+                    to_load.push(base + scripts[cur_script][idx]);
+                }
+            }
+            
+            $script(to_load, loadNextScript);
 
             cur_script++;
         }
 
+        // Entry path for the kiwi application
+        var base_path = '/client';
+
         // Start loading scripts
         loadNextScript();
     };
index c1974018db3a0ad5a3a1e10134314f2e296fd96e..2d42e1f98444ecc595713c616c2fb9c0a737d7c6 100644 (file)
@@ -84,8 +84,15 @@ function StaticFileServer(public_html) {
 }
 
 StaticFileServer.prototype.serve = function (request, response) {
+    // The incoming requests root directory (ie. /kiwiclient/)
+    var root_path = kiwi.config.http_base_path || '/client',
+        root_path_regex = root_path.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
+
+    // Any asset request to head into the asset dir
+    request.url = request.url.replace(root_path + '/assets/', '/assets/');
+
     // Any requests for /client to load the index file
-    if (request.url.match(/^\/client/)) {
+    if (request.url.match(new RegExp('^' + root_path_regex, 'i'))) {
         request.url = '/';
     }
 
index 75a38becb2f273dd1af6a28fe8c82be69bd9be55..b59fd1b415cb68af5216f6f1bdd4966519577a36 100755 (executable)
@@ -24,6 +24,7 @@
 
     "handle_http":      true,
     "public_http":      "./../client/",
+    "http_base_path":   "/client",
 
     "max_client_conns": 2,