Hack 83. Add a New Command-Line Option


Customize the startup process and the process
of starting windows by adding support for new command-line
arguments.
The set of Firefox command-line arguments isn't
fixed. You can add more options if you want. In the complex case, new
command-line options can be compiled up
into dynamic link libraries using C/C++. It's easier
to use a simple JavaScript script, though. This hack shows how to
implement the simple case. We'll make a
--my or /my option that
displays a page stored in the install area on startup.
7.10.1. Preparation
The straightforward thing to do is to add an option that starts up a
window of your own design. In order to do that quickly, you have to
sidestep the Extensions system by hacking on the install area
directly. You have to create a specific set of objects to make
everything hang together properly.
As in [Hack #82], start by
shutting down all Mozilla programs in preparation for new component
registration. Find the file named
compatibility.ini in your Firefox profile. Edit
it and decrement the Build ID date by one day.
Save the file. The compreg.dat component
registry is in the same directory if you want to inspect it while
you're there.
|
Next, move from the profile directory to the Firefox install
directory. In there, you'll find a
components directory. Go to that directory.
You'll see there's a number of DLL
(or .so) dynamic link libraries, with their
accompanying .xpt type libraries. Ignore those.
Instead, note the existing .js files. You need
to make one of those.
The jsconsole-clhander.js file is a good example
of a command-line option implemented in JavaScript. Some of the other
JavaScript files in that directory do things unrelated to command
lines, so beware.
7.10.2. Making the Script Outline
Here's an outline of the script you need to create:
// Tell mozilla there's a new module of components
function NSGetModule( ) {}
// Make a module to hold the sole component
var myCommandModule = {
registerSelf : function ( ) {},
getClassObject : function ( ) {},
unregisterSelf : function ( ) {},
canUnload : function ( ) {}
};
// The module creates the component using a factory object
var myCommandComponentFactory = {
createInstance : function ( ) {},
lockFactory : function ( ) {}
};
// The component itself is just a simple record
var myCommandComponent = {
commandLineArgument : ",
prefNameForStartup : ",
chromeUrlForTask : ",
helpText : ",
handlesArgs : true,
defaultArgs : ",
openWindowWithArgs : true
}
As in [Hack #82], the
NSGetModule() function is included to be detected
by Firefox at startup time. It's the hook that will
install all the rest of the claptrap. After that, you have three
objects to create:
The
myCommandModule object has an
nsIModule interface. It is the topmost container
that holds the components we're creating.
The
myCommandComponentFactory object has an
nsIFactory interface. It will be returned by the
module when Firefox detects that a new command-line object is needed.
The
myCommandComponent object has an
nsICmdLineHandler interface. It represents the
command-line option we're creating. Because the
required interface is quite simple, the object looks like a simple
collection of data, which it is.
In this skeleton code, we've been a bit naughty and
specified the component as a singleton object rather than as a full
JavaScript object constructor. The factory object would really like
to construct a whole new handler object each time. It works as is,
though, and we're unlikely to use the same
command-line option twice anyway.
We won't ever call or use any of these objects from
other scripts. An object embedded in the Firefox browser called the
command-line manager will call them for us
whenever that's necessary. NSGetModule() is called by the module registration
system, as before. This hack is a good example of the extensive
delegation programming that's
common across Firefox's design. One object defers to
another in a handover style of processing.
7.10.3. Filling in the Script
Filling in the details of this script outline can be done quickly. We
haven't bothered here with all the fancy error
handling; you can add that if you want. Just copy it from one of the
other command handlers. Put the following completed script in
myoption.js in the
components directory:
// give the new component an identity
const HandlerPrefix = "@mozilla.org/commandlinehandler/general-startup;1?type=";
const MyContractID = HandlerPrefix + "my";
const MyClassID = Components.ID('{75034172-c6bd-4f3d-bbb0-0c572428e3c1}');
// Tell mozilla there's a new module of components
function NSGetModule( ) { return myCommandModule; }
// Make a module to hold the sole component
var myCommandModule = {
registerSelf : function (compMgr, file, location, type) {
var reg = compMgr.QueryInterface(
Components.interfaces.nsIComponentRegistrar
);
reg.registerFactoryLocation(
MyClassID, "My Service", MyContractID, file, location, type
);
},
getClassObject : function (compMgr, cid, iid) {
return myCommandComponentFactory;
},
unregisterSelf : function ( ) { },
canUnload : function ( ) { return false; }
};
// The module creates the component using a factory object
var myCommandComponentFactory = {
createInstance : function (outer,iid) { return myCommandComponent;},
lockFactory : function ( ) { }
}
// The component itself is just a simple record
var myCommandComponent = {
commandLineArgument : "-my",
prefNameForStartup : "general.startup.my",
chromeUrlForTask : "resource:/res/mypagel",
helpText : "My Help",
handlesArgs : true,
defaultArgs : "default",
openWindowWithArgs : true
}
Let's run through the code briefly.
We're creating a -my command-line
option. --my will also work, and we get
/my on Windows as an extra bonus. The only part of
the contract ID that can vary is the very last bit, as shown in the
bolded line. The rest must be stated to match what the command-line
manager expects. To get a class ID, just run
uuidgen or guidgen from the
command line, and cut and paste the output.
NSGetModule() passes back our module object, a
trivial task. The module object is able to register the sole
component type we've created. It's
too dumb to be able to unregister or do other fancy management. It
can also pass back our factory object if it's asked
to. Finally, the factory object can extrude our singleton handler
instance. You can see how a request for a command-line handler is
delegated first from the module level down to the factory level, and
then finally down to a component instance. Who uses the final
component instance? The command-line manager embedded inside Firefox
does.
Here are the interesting bitsfirst, the handler itself. The
only things worth changing in the handler record are
commandLineArgument and
chromeUrlForTask. The former is the option
we're creating. The latter, despite the name, can be
any URL. We've chosen an HTML page that
we'll put inside the Firefox install area. The
resource: URI scheme points to the top of that
area, so we'll be able to put
mypagel into the res
subdirectory under there. That's a safe place for a
startup file to live.
The second interesting bit is debugging. The first time you run
Firefox, your module is registered. Unless you have fundamental
syntax problems, that much should work. You can confirm it by finding
the filename in compreg.dat in your profile.
After that, you get no feedback at all unless you've
got a debug build of Firefox. In that case, you can use
dump(). For the normal case,
you'll have to check your JavaScript very carefully.
Use a syntax-highlighting editor and examine constant strings and
keywords carefully for typos. To see your new option work
successfully, just run:
firefox -my
If nothing appears to work, try -jsconsole to
prove to yourself that everything is still sane.
Don't forget to fully shut down Firefox if you
suspect it's become confused.
This command-line stuff is really designed for XUL-based windows, not
HTML ones. It will work for HTML windows if you're
careful. Some weird results are possible if your HTML page
isn't coded tightly, though. Start with a trivial
page before you try anything fancy. Either way, the displayed
document is free to do its own scripting. That code can pick up where
the command-line code left off. Hack the createInstance() method in the script if you really want to do extra
processing before the window opens.