Results 1 to 10 of 17

Thread: HOWTO: Getting Started with Linux Standalone Apps using XUL

Threaded View

  1. #1
    Join Date
    Jul 2005
    Location
    Remote Desert, USA
    Beans
    683

    Lightbulb HOWTO: Getting Started with Linux Standalone Apps using XUL

    Introduction

    Perhaps there's a developer out there who knows web development, but hasn't really done many standalone applications or doesn't have the time to learn the API for that, and who wants to make a standalone application on Ubuntu Linux. Perhaps this developer is you. If so, then read on.

    If you look around, most of the standalone apps running on Linux since about 2002 are being created in Python with PyGTK or PyQT. This is great and all if you know Python and are willing to learn the documented and undocumented (or at least hard-to-find facts) about PyGTK or PyQT API for drawing the GUI. The technique presented here is different, however, and will show you how to get started in writing standalone Ubuntu software in Javascript and HTML, and to use SQLite as your database.

    Here's a small example of what it looks like:



    Development Environment Installation

    After temporarily turning on your Universe option in /etc/apt/sources.list, run this:

    $ sudo apt-get install ngs-js
    $ sudo apt-get install thttpd*
    $ sudo apt-get install sqlite3*


    ...and then turn off your Universe option.

    When this is installed, you'll want to ensure (using find, whereis, or whatever) that these commands are put in the places that I expect them to be:

    /usr/bin/ngs-js
    /usr/sbin/thttpd
    /usr/bin/sqlite3


    If not, then you may want to either adjust the scripts below or make links (using 'ln' command) in these places so that things are called properly.

    Getting Started

    To build a short example to get started, we need just 4 files:
    • names.sql - the script to create our database if it doesn't exist
    • example.xul - the GUI description
    • example.js - the application's starting page
    • example - the bash script that kicks the process off


    names.sql
    Code:
    DROP TABLE names;
    CREATE TABLE names (
     firstname varchar(60),
     lastname varchar(60)
    );
    INSERT INTO names VALUES ('Marty','Mouse');
    INSERT INTO names VALUES ('Spidey','Man');
    INSERT INTO names VALUES ('Duck','McDuff');
    INSERT INTO names VALUES ('Super','Coolman');
    INSERT INTO names VALUES ('Test','Tester');
    example.xul
    Code:
    <?xml version="1.0"?>
    <?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
    <window
    id = "myapp"
    title = "SQLite Example"
    height = "420"
    minHeight = "420"
    width = "640"
    minWidth = "640"
    screenX = "10"
    screenY = "10"
    sizemode = "normal"
    wait-cursor = "false"
    xmlns = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" >
    <spacer style = "height: 4px; cursor: default;" />
    <hbox flex = "1">
    <iframe
    id = "webframe"
    flex = "1"
    src = "http://127.0.0.1:11255/example.js"
    style = "border: 0px" />
    </hbox>
    </window>
    <!-- type = "content-primary" -->
    example.js
    Code:
    #!/usr/bin/ngs-js
    
    var $LastError = '';
    var $_bAlreadyWrittenContent = 0;
    
    function echo($sMsg) {
            if (!$_bAlreadyWrittenContent) {
    	        System.stdout.writeln("Content-type: text/html\r\n");
    	        $_bAlreadyWrittenContent = 1;
            }
            System.stdout.write('' + $sMsg);
    }
    
    function DBQuery($sDBFile, $sSQL) {
            $oPipe = System.popen("/usr/bin/sqlite3 -header -separator '< >' -nullvalue '<NULL>' " + $sDBFile + " '" + $sSQL + "'", 'r');
            $i=0; $bError=0; $sBuild = '';
            $aRows = new Array();
            $aRow = new Array();
            while (!$oPipe.eof()) {
                    $sLine = $oPipe.readln();
                    if ($sLine == '') {
                            break;
                    }
                    if (($sLine.indexOf('error') != -1) || ($bError=1)) {
                            $bError = 1;
                            $sBuild += $sLine;
                    }
                    $aRows[$i] = $aRow.concat($sLine.split('< >'));
                    $i++;
            }
            $LastError = $sBuild;
            return $aRows;
    }
    
    function Main() {
    	echo("<style>\n");
    	echo("BODY {\n");
    	echo(" background: #f7f7f7");
    	echo("}\n\n");
    	echo("TABLE {\n");
    	echo(" border-collapse: collapse;\n");
    	echo(" white-space: nowrap;\n");
    	echo(" font-family: Arial,sans;\n");
    	echo(" font-size: 9pt;\n");
    	echo("}\n\n");
    	echo("TD,TH {\n");
    	echo(" border: 1px Gainsboro solid;\n");
    	echo(" padding-left: 8px;\n");
    	echo(" padding-right: 8px;\n");
    	echo(" background: #ffffff;\n");
    	echo("}\n\n");
    	echo("TH {\n");
    	echo(" background: #000000;\n");
    	echo(" color: #ffffff;\n");
    	echo("}\n\n");
    	echo("TH P {\n");
    	echo(" text-transform: capitalize;\n");
    	echo("}\n\n");
    	echo(".tdalt {\n");
    	echo(" background: #e9e9e9;\n");
    	echo("}\n\n");
    	echo("</style>\n");
    	$rsRows = DBQuery('names.db','select * from names');
    	echo("<TABLE BORDER=0 CELLSPACING=0 CELLPADDING=0>\n");
    	$i = 0;
    	for ($asRow in $rsRows) {
    			echo("<TR>\n");
    	        for ($sColVal in $asRow) {
    	        		if ($i == 0) {
    	        			echo('<TH><P>' + $sColVal + '</P></TH>');
    	        		} else {
    	        			if ($i % 2) {
    			                echo("<TD class='tdalt'>" + $sColVal + '</TD>');
    			            } else {
    			            	echo('<TD>' + $sColVal + '</TD>');
    			            }
    		            }
    	        }
    	        $i++;
    	        echo("\n</TR>\n");
    	}
    	echo("</TABLE>\n");
    }
    
    Main();
    example
    Code:
    #!/bin/bash
    
    PATH=$PATH:"$PWD"
    chmod a+x example.js
    if [ ! -f names.db ]; then
            sqlite3 -init names.sql names.db '.quit' 2>&1 > /dev/null
    fi
    thttpd -d "$PWD" -p 11255 -c '**.js' -l /tmp/thttpd.log
    firefox -chrome file:$PWD/example.xul
    Drop these files into a directory under your ordinary user account and run the application like so:

    $ sh example

    The result you get is like the attached example.png to this article.

    How The Example Works

    First, we load the example Bash script and in general it creates a names.db database file if one doesn't exist, launches a small, embeddable web server (thttpd) on port 11255, and then launches a Firefox process with an exclusive GUI file called a chrome.

    When Firefox is loaded in this manner, it does not display anything except what we specify in the XUL file. Now the XUL format is extremely rich but it can get very complex to work with it. It can support a whole list of GUI widgets that can dazzle you. If you're interested in that, see the references link at the bottom of the article. For me, I wanted to go a different route because I wanted to build applications that use what I am used to using -- Javascript and HTML instead of all this complex XUL. Therefore, as you can see in the XUL file, once we set the window properties, we load an HTML browser widget inside that immediately attaches to the web page (example.js) from the tiny web server.

    This web server operates like a normal CGI web server and so you'll want to understand a little how CGI works if you want to do things like page redirection, form handling, and so on. A reference for that is at the bottom of this article. Since CGI can be slightly difficult, once I figure something out, such as page redirection, I throw it into a web.js file as a function so that I can refer to it routinely instead of remembering how to do it on a line-by-line CGI level.

    Because you may have web apps hosted on a usual port like 80 or 443, we don't use that for our port. You have a range of 7000-65535, usually, for web apps on alternate ports, so we chose 11255. However, if you have more than one of your apps loaded, you'll need each one on an exclusive port or you'll cause a conflict with the example scripts. (The other technique is to use different directories.) If you're using a local workstation firewall, you'll want to open up port 11255. I use an iptables script, so I stuck this into it:

    # XUL apps
    iptables -A INPUT -p tcp -m tcp --dport 11255 --syn -j ACCEPT
    iptables -A INPUT -p udp -m udp --dport 11255 -j ACCEPT


    Note that the example Bash script calls thttpd and doesn't worry about the fact that it may already be in memory. This is because thttpd is smart and won't let itself be loaded more than once on the same port.

    Note also that I don't stop thttpd when the GUI is closed when you click X. This is because I don't currently know how to detect what window titles are open on the X terminal, and testing for an instance of firefox in memory won't cut it because someone may be using firefox to do web surfing in addition to loading your XUL application. So, if you know how to catch this and stop the thtpd server, please reply to this article to show us all how. It's a minor thing to have this thttpd service running on an alternate port if you're behind a subnet or a firewall, but I really would like to learn how to stop this service once the XUL application is closed.

    The web server calls our Javascript file, example.js, and because our example Bash script set it to executable, the web server will run it instead of display it as text in the browser. And inside example.js, a critical line was:

    #!/usr/bin/ngs-js

    That tells the shell what command to use to run this script. The NGS-JS project is sort of a stale project since around 2004, last I saw, but the amazing thing is that it works extremely well as a Javascript interpreter with the power to shell out to other processes, read/write files, and so on. It is written in C. The thing to understand about NGS-JS is that you first must understand Javascript fairly well, and then learn the extra stuff in NGS-JS that it does beyond standard Javascript.

    From our example.js, it loads a Main() function which builds a style sheet, queries the database, and iterates the result in an HTML table.

    To make things a little easier for myself with the Javascript, if there's a function or set of functions I tend to call frequently, and if I know the equivalent of that in my language of choice (PHP), then I encapsulate that into a function that tries to look as much as like what I'm used to in my favorite language. For instance, I created an echo() function as you can see. Unlike the example above, usually I'll be putting these functions into separate files I like to call "modules", stick them in a directory called "modules", and then load these with NGS-JS's load() statement:

    load('modules/web.js');
    load('modules/db.js');


    You'll want to watch how I do that echo() function because it uses a CGI technique "Content-type: text/html\r\n" before printing text. This is necessary for your browser to display the web page properly. Otherwise, you end up with a white block on your page and nothing exciting going on.

    The database query interacts with SQLite. If you're not used to this database, you should be. It's a sensational piece of work, has a great license, is free, supports a small set of ANSI-92 SQL statements, and makes for a great embeddable database in your smaller projects. For me, I use only two databases on Linux: SQLite for smaller projects or standalone applications like this, or PostgreSQL for anything else. I only use MySQL when I'm on a team project that requires it.

    The way I handle the database query, since NGS-JS doesn't have a handler for it, is to use NGS-JS's System.popen() function to shell out and access our SQLite3 command. I then parse the result and stick it in an array of rows with each row containing an array of columns.

    And voila!, that's how it's done.

    Debugging

    You may want to know how to debug your Javascript pages. (By the way, don't confuse Javascript pages with Java Server Pages -- these are two separate things entirely.) The way I do it is at command line like so:

    $ ngs-js example.js

    However, typing 'ngs-js' can get tiresome. An alternate way is:

    $ ./example.js

    ...or, you can do:

    $ sudo ln -s /usr/bin/ngs-js /usr/bin/js

    ...and then do:

    $ js example.js

    If the debugging gets too hard, you can always break out what you're trying to do into a function and then use a test script to call just that to see the result.

    Extending - Where To Go from Here

    By now, the gears are probably moving in your head and you're wondering where to go from here, such a server file read/write, drawing gadgets, AJAX, page redirection, form processing, file uploads, and a whole host of other options. Unfortunately I won't be able to cover all of this in this example, so you can ask me if you're stuck after looking at the references below.

    In general, here's what you'll need to know:
    • file I/O - you have two avenues -- either use System.popen() from NGS-JS and use normal Bash statements for file I/O, or use the built-in NGS-JS commands for file I/O
    • drawing gadgets - HTML and DHTML works great, as does CSS and anything else you can do in Firefox, so you already have a rich API in the web page itself. But if it's not rich enough for you, consider reading up on XUL and adding that in. You can also use AJAX.
    • AJAX - you can create a subfolder in your project directory called "ajax" and place your AJAX scripts in there. You can use the normal AJAX API in Firefox to interact with those pages for a result.
    • page redirection - this is a function of CGI, but in general it looks like this:

      Code:
      function Redirect($sPageNamePath) {
              System.stdout.writeln('Content-type: text/plain');
              System.stdout.writeln('Location: ' + $sPageNamePath);
      }
    • form processing - this is a function of CGI and comes to you through environment variables established by the web server, which can be read like so:

      Code:
      System.getenv('QUERY_STRING');
      ...but it only works with GET type form posts. If a POST type of form post is used, you'll need to read from standard input (STDIN) by the amount of what's posted from the CONTENT-LENGTH environment variable. This is explained multiple times on the web but most of the examples are given in Perl and you have to re-interpret that in Javascript. There are also C-based GNU tools that can be used to do this for you and you can use System.popen() to shell out, get that input, and return a result back.
    • file uploads - this gets fairly complex to describe, but is a function of CGI and requires doing a series of steps that you can find in the CGI reference below.


    Why Not Perl Instead of Javascript?

    You may want to know why I don't use Perl for this example and use NGS-JS. Good point. Perl works great instead of NGS-JS, and has typically been used for CGI processing for many years, especially when web servers were just getting started on the web. However, it's all about using what you know, and many people already know a good bit of Javascript because you pretty much have to know this in order to do web development.

    Another advantage of NGS-JS is that it's very small -- smaller than Perl. It makes a great, embeddable platform for standalone desktop development.

    But if Perl, PHP, Python, Ruby, Pike, Scheme, or whatever language you want is your preference, then by all means do what you feel is best.

    Conclusion

    Using very minimal XUL that you write once and alter very little, and then do most of your work in Javascript, HTML, and SQL, you can produce standalone desktop applications for Linux. And if that's not good enough for you, you can add Perl, AJAX, more complex XUL, and a whole host of other options to richen the experience.

    If you're bold enough, you could revise this example to make an entire Quickbooks for Small Business knockoff (badly needed, I'm afraid) standalone desktop application and many other types of standalone applications. You can also make control panels, a simulated standalone GUI for a remote website, a chat program, and all kinds of things.

    For Further Reference


    Note on many of these references I recommend you use a command like wget (with the required options) to download all the pages to your local hard drive in case the site ever goes down or are no longer maintained.
    Attached Images Attached Images
    Last edited by SuperMike; January 8th, 2007 at 08:53 AM.
    SuperMike
    When in doubt, follow the penguins.
    Evil Kitty is watching you

Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •