Candidate #1: Chrome
Once I knew I was going for some type of raw tcp socket support an extensive google search was the next step. The first thing I stumbled upon was Chrome's packaged app Network Communications section. Naturally I dropped the research stage and went straight to hacking.Packaged apps deliver an experience as capable as a native app, but as safe as a web page. Just like web apps, packaged apps are written in HTML5, JavaScript, and CSS. But packaged apps look and behave like native apps, and they have native-like capabilities that are much more powerful than those available to web apps.
Getting started
I already knew from some work on my chrome plugin that developing extensions for Chrome is as easy as it gets. Throw together a manifest.json that has the correct permissions, and you're allowed to use the API.{ "manifest_version": 2, "version": "0.1", "name": "SchizIRC Chrome", "app": { "background": { "scripts": ["main.js"] } }, "permissions": [{ "socket": [ "tcp-connect:*:6667", "tcp-listen:*:6667" ] }] }Main.js contains nothing fancy, It just listens for the onLaunched event and creates the main window with some predefined bounds.
chrome.app.runtime.onLaunched.addListener(function() { chrome.app.window.create('index-chrome.html', { bounds: { width: 980, height: 650 } }, function(createdWindow) { appWindow = createdWindow.dom; }); });
Loading a chrome app in developer mode is as easy as it gets as well:
- Visit chrome://extensions in your browser
- Ensure that the Developer mode checkbox in the top right-hand corner is checked.
- Click Load unpacked extension… to pop up a file-selection dialog.
- Find the directory where you cloned SchizIRC into and select it.
- Press 'Launch'
Note that regular CTRL+R, F5 or even window.location.reload() won't work, when developing a packaged app, but the main app window has a 'reload app' menu item in the contextmenu that does this. You will also be able to use the full Webkit Inspector from this menu.I threw together a basic layout using Divshot; Some tabs, a title, an input field and a button. This was enough to get me started on where I wanted to go. My app also got a working title: SchizIRC (because why not)
Divshot |
After taking all the assets offline (required for a chrome app, and needed to be done anyway for future packaging) I ended up with this directory structure:
manifest.json
index-chrome.html
main.js
index.html
css\bootstrap-combined.min.css
css\google-bootstrap.css
css\schizirc.css
js\Irc.js
js\controller.js
js\gui.js
js\vendor\mootools-more.js
js\vendor\mootools-core.js
js\vendor\mustache.js
js\vendor\bootstrap.min.js
Time to get classy
As shown above, I threw down some classes: Irc.js, Controller.js (which will server as the main kickstarter for the app, already eyeballing we're going to have several app wrappers / frameworks here that need a common entry point) and Gui.js, which will bind DOM events to my app's events. Code says more than words:/** * Controller.js : Generic entry point for the app. */ Controller = new Class({ gui: false, server: false, config: { // some default config options i've prepared, these will be overwritten by the config in the future server: 'irc.freenode.net', port: '6667', defaultChannels: '#schizirc', nick: 'SchizoIRC', userName: 'SchizoIRC', realName: 'SchizoIRC v0.01', appVersion: 'SchizoIRC v0.01', finger: 'omnomnom', password: null, logging: true }, initialize: function() { console.log("IRC Client initted." ); this.irc = new IRC(this.config); this.gui = new Gui(); this.irc.connect(); } }); window.onload = function() { window.app = new Controller(); };User interface bindings are handled by the User Interface Class (which needs some refactoring for the future but will do for now)
/** * Gui.js implements DOM bindings to change the user interface * Will delegate user interaction to the app components that need it and implement the base tab view */ Gui = new Class({ Implements: [Events], // allow this class to fire and receive events. initialize: function() { window.addEvent('mousedown:relay(.nav-tabs li)', this.switchTabs); // handle all clicks on tab labels window.addEvent('/tab/changed', this.showActiveTab); // a custom event channel fired when a tab has changed window.addEvent('/tab/change', this.switchTab); // a custom event channel that changes a tab to $name this.showActiveTab('home'); // always show the home tab. }, switchTabs: function() { $$(".nav-tabs li").removeClass('active'); // deactivate all tab labels. $(this).addClass('active'); // activate the tab label that was clicked window.fireEvent('/tab/changed', this.getAttribute('data-tab')); // fire our tab changed event. }, switchTab: function(newTab) { console.log("Change tab to: ", newTab, $$("li[data-tab="+newTab+"]")); $$(".nav-tabs li").removeClass('active'); // hide all tab labels. $$("li[data-tab="+newTab+"]").addClass('active'); // show our new tab window.fireEvent('/tab/changed', newTab); // notify that the active tab has changed. }, showActiveTab: function(name) { $$('section[data-tab]').hide(); // hide all tab content panels $$('section[data-tab='+name+']').show(); // show the one passed as name $$('section[data-tab='+name+'] input[type=text]')[0].focus(); // focus the text field in the tab } });
Now on to the real deal, the IRC class. This will hold the connection object, the info about the server, channels, etc.
IRC = new Class({ Implements: [Options, Events], options: { // set some default IRC options that will be overwritten with options passed from class Controller server: 'irc.freenode.net', port: '6667', defaultChannels: '#schizirc', nick: 'SchizoIRC', userName: 'SchizoIRC', realName: 'SchizoIRC v0.01', appVersion: 'SchizoIRC v0.01', finger: 'omnomnom', autoJoinOnInvite: true }, connection: false, server: false, channels: false, initialize: function(options) { this.setOptions(options, this.options); // merge the passed options with this.options. if(!this.connection) { // create a new IRC Connection object that gets the current options. this.connection = new IRC.Connection(this.options); } }, connect: function() { this.connection.connect(); }, disconnect: function() { this.connection.disconnect(); } });