HTML App Launcher

App Launcher
App Launcher

    
I love web technology for one simple reason; making GUIs that are useful and have great logic flow is easy to do and virtually universal for platform access. I’m a lazy guy and want to launch apps quickly and through my browser than having to go to a Start Menu or search for the proper cli command. Today, I’ll show you how to make it so you can launch your favorite apps through a web page. First, we need to get all the necessary programs and setup our project folder structure.


To Top


To Bottom

Programs & Project Structure

Installs

 sudo apt-get install sqlite3 php-sqlite3 sqlitebrowser php firefox 

The first few programs (sqlite3, php-sqlite3, and sqlitebrowser) are pretty straightforward for why we need them. These deal with our database and sqlitebrowser is a quick way of viewing, editing, and adding data entries to our database. PHP will be the server side language which we are using to interact with the database and run the programs we have. Firefox is the browser I’m using to view the app launcher and has a plugin you can install to set your default New Tab page. Now that we have these installed, let’s create our project folder and the skeletal structure.

Folder & File Setup

mkdir appRunner

cd appRunner

mkdir -p resources/{css,js,php,db}

touch index.html resources/css/base.css resources/css/main.css \
                 resources/js/events.js resources/js/launcher.js \
				 resources/php/launcher.php resources/php/dbController.php

    
Since I’m using Linux, I like running easy commands that’ll get the layout built quickly. We are simply making a folder called appRunner and then changing directory to it. We then make a parent folder called resources with the respective sub folders: css, js, php, and db. Once that is completed, we create a new index file plus a slew of files for the application itself. With the base layout created, we still have some preliminary work to do before writing code. Let’s get the database up and running with some app entries.

Database Setup & Enteries

cd resources/db/

sqlite3 apps.db

CREATE TABLE Apps(id INTEGER, app MAX, icon BLOB, command MAX);

    
INTEGER is just that, a number. MAX means that the field can take the largest string possible and BLOB means the field is able to store binary data such as the image itself. Now, let’s insert initial app data so we have reference info to work off of. Here I have added 4 entries using sqlitebrowser‘s interface. Note: You can add your own apps and their commands than use mine since you may or may not have what I have.

App data
App data

OK, we have the base application setup and we are ready to code. But, first, we are gunna review what our logic will be and why we setup everything the way we did.

Logic & Code Flow

HTML

<body onload="getApps()">

<div class="container">
    <div id="appsMenu">

    </div>

    <div id="message">

    </div>
</div>

<script src="resources/js/events.js" charset="utf-8"></script>
<script src="resources/js/launcher.js" charset="utf-8"></script>

     For this quick setup, the main focus is about the application list and a message field. We also want an onload event on the body to make a request to the server to return all stored apps in the database. In addition, we are splitting up the JavaScript scripts into two files to keep things cleaner. The events.js will handle user interactions while launcher.js is the meat of the client side logic. Code and the logic breakdown for these js files are further down.

CSS

.container {
    display: table;
    margin: 0 auto;
}

.app {
    float: left;
    padding: 0em 2em;
}

.app:hover {
    cursor: pointer;
    background-color: rgba(43, 126, 144, 1);
    color: rgba(255, 255, 255, 1);
}

.icon {
    width: 72px;
    height: 72px;
}

     Here is the most important part of our CSS files. We have two files but main.css has the core logic while base.css has some house cleaning logic. Here, we setup a container with a display of table and margins set to center align the div. Our .app sets our div elements to float left so that apps are listed in a row than column. We do padding to make things look nice and a hover part to indicate what’s being selected. The .icon CSS sets our app icon images to be 72×72 pixles.

JS

// What we are working from with our JavaScript logic
<div name="34::Leafpad" class="app">
<img class="icon" src="" />
<p class="appName">Leafpad</p>
</div>

document.ondblclick = (event) => {
    let elm = event.target;

    // left-click detect
    if (event.which == 1) {
        if (elm.className.includes("app")) {
            launchApp(elm.getAttribute("name"));
        } else if (elm.className.includes("icon") ||
                   elm.className.includes("appName")) {
                       launchApp(elm.parentElement.getAttribute("name"));
        }
    }
}

     Here is the double click logic when clicking on our listed application. Basically, we check to see if there are any class names and from there if we are clicking on the root or sub element of our HTML structure for our listed application. We could make this cleaner by having an internal div that’s absolute positioned and is z-indexed above the image and paragraph tag. Then, we would look for just that; but, this method is a little more robust if you want controls for the app such as deletion, hiding, renaming, or something else. We then call a command in our launcher.js file to run the application by sending info to the server. The info we send is the id and name which we get by pulling the name attribute info from the app’s root div.

const launchApp = (app) => {
    let args = app.split("::");
    doAjax("launchApp=true&id=" + args[0] + "&app=" + args[1]);
}

const doAjax = (data) => {
    let xhttp = new XMLHttpRequest();

    xhttp.onreadystatechange = function() {
        if (this.readyState === 4 && this.status === 200) {
            // Send the returned data to further process
            if (this.responseXML != null) {
                handleXMLReturnData(this.responseXML);
            } else {
                alertMessage("error", "No content returned. No apps in database.");
            }
        }
    };

    xhttp.open("POST", "resources/php/appManager.php", true);
    xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    xhttp.overrideMimeType('application/xml'); // Force return to be XML
    xhttp.send(data);
}

     You will note that here we are splitting the passed string from the events.js into its respective id and app name to pass to our doAjax command. I’ve setup the data this way to try and insure there will be no duplicates in the database query return info. So, even if there is a duplicate id value the app name makes sure we get the correct one. The rest here is simple AJAX logic where we pass the arguments and an identifying argument to the server script to determine the proper action to take. We then process the XML return data from the server as a message or the application data we queried for.

const handleXMLReturnData = (xml) => {
    let rootTag = xml.children[0];
    if (rootTag.tagName.includes("LIST")) {
        //<APP>
        //  <ID><ID></ID>
        //  <NAME><APP_NAME></NAME>
        //  <ICON><BASE64_ENCODED_ICON></ICON>
        //</APP>

        let apps = rootTag.getElementsByTagName("APP");
        let size = apps.length;
        for (var i = 0; i < size; i++) {
            let app  = apps[i];
            let id   = app.children[0].innerHTML;
            let name = app.children[1].innerHTML;
            let icon = app.children[2].innerHTML;
            createAppRef(id, name, icon);
        }
    } else if (rootTag.tagName.includes("SERV_MSG")) {
        alertMessage(rootTag.className, rootTag.innerHTML);
    }
}

const createAppRef = (id, appName, icon) => {
    let divTag  = document.createElement("DIV");
    let imgTag  = document.createElement("img");
    let pTag    = document.createElement("P");
    let text    = document.createTextNode(appName);

    divTag.setAttribute("name", id + "::" + appName);
    divTag.className = "app";
    imgTag.className = "icon";
    pTag.className   = "appName";
    imgTag.src       = "data:image/png;base64," + icon;

    pTag.append(text);
    divTag.append(imgTag);
    divTag.append(pTag);
    document.getElementById('appsMenu').append(divTag);
}

Now, handleXMLReturnData is parsing the return data and either displaying a server message or returning the list of apps in the database. The listed apps are parsed from a loop and we create the proper HTML for each using createAppRef. Note that we simply create a div, img, and p tag and then setup proper class names and the root’s name attribute. For the image, b/c it’s stored as a BLOB we return a BASE64 encoded string which we assign to the src attribute of img. We are identifying them as png type images since that’s what I’m using for each. This keeps things quick ‘n dirty but isn’t the best setup long term. OK, let’s finish this up by looking at our main PHP file.

PHP

function launchApp($ID, $APP) {
    $db = new SQLite3('apps.db');

    if($db === false) {
        $message = "Server: [Error] --> Database connection failed!";
        serverMessage("error", $message);
        die("ERROR: Could not connect to db.");
    }

    $stmt = $db->prepare('Select command FROM Apps WHERE id = :id AND app = :app');
    $stmt->bindValue(":id", $ID, SQLITE3_INTEGER);
    $stmt->bindValue(":app", $APP, SQLITE3_TEXT);
    $res = $stmt->execute();

    $row = $res->fetchArray(SQLITE3_ASSOC);
    $command = 'nohup ' . $row['command'] . ' > /dev/null &';
    shell_exec($command);

    $message = "Server: [Success] --> Launched " . $APP . "!";
    serverMessage("success", $message);
}


// Determin action
chdir("../db/");
if (isset($_POST['getData'])) {
    getData();
} elseif (isset($_POST['launchApp'],
                $_POST['id'],
                $_POST['app'])) {
    launchApp($_POST['id'], $_POST['app']);
} else {
    $message = "Server: [Error] --> Illegal Access Method!";
    serverMessage("error", $message);
}

?>

     When we launch an app, all that we are really doing is passing the id and app name to the PHP scriopt. The script queries the database and returns the command that is stored. Then, the script will run a shell_exec command with the returned string. This is what launchApp is doing here. It then return a server message of success or failure which our JavaScript handles and puts onto our page. Look at the Full PHP section to see how the apps list is returned and how messages are handled.

Conclusion

Discussion

     And that’s it for this tut. It is a simple way of making a personalized launcher for your favorite apps so that you don’t need to go to the Start Menu each time. You can use this by running a quick and dirty server through this guide:


Servers
Quick Test Server

If you are using Firefox and want it as your default tab when you open new tabs then please check out my plugin: Alt Tab Page

Full Code

Full HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>App Runner</title>
    <link rel="stylesheet" href="resources/css/base.css">
    <link rel="stylesheet" href="resources/css/main.css">
</head>
<body onload="getApps()">

<div class="container">
    <div id="appsMenu">

    </div>

    <div id="message">

    </div>
</div>

<script src="resources/js/events.js" charset="utf-8"></script>
<script src="resources/js/launcher.js" charset="utf-8"></script>
</body>
</html>

Full CSS

base.css
html, body {
    margin: 0em;
	padding: 0em;
}

ul, ol, li {
    list-style: none;
}
main.css
.container {
    display: table;
    margin: 0 auto;
}

.app {
    float: left;
    padding: 0em 2em;
}

.app:hover {
    cursor: pointer;
    background-color: rgba(43, 126, 144, 1);
    color: rgba(255, 255, 255, 1);
}

.icon {
    width: 72px;
    height: 72px;
}


/* Messages coloring */
.error, .warnning, .success {
    float: left;
    clear: both;
    width: 100%;
    text-align: center;
}


.error { color: rgb(255, 0, 0); }
.warnning { color: rgb(255, 168, 0); }
.success { color: rgb(136, 204, 39); }

Full JS

events.js
document.ondblclick = (event) => {
    let elm = event.target;

    // left-click detect
    if (event.which == 1) {
        if (elm.className.includes("app")) {
            launchApp(elm.getAttribute("name"));
        } else if (elm.className.includes("icon") ||
                   elm.className.includes("appName")) {
                       launchApp(elm.parentElement.getAttribute("name"));
        }
    }
}
launcher.js
const getApps = () => {
    doAjax("getData=true");
}

const launchApp = (app) => {
    let args = app.split("::");
    doAjax("launchApp=true&id=" + args[0] + "&app=" + args[1]);
}

const doAjax = (data) => {
    let xhttp = new XMLHttpRequest();

    xhttp.onreadystatechange = function() {
        if (this.readyState === 4 && this.status === 200) {
            // Send the returned data to further process
            if (this.responseXML != null) {
                handleXMLReturnData(this.responseXML);
            } else {
                alertMessage("error", "No content returned. No apps in database.");
            }
        }
    };

    xhttp.open("POST", "resources/php/appManager.php", true);
    xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    xhttp.overrideMimeType('application/xml'); // Force return to be XML
    xhttp.send(data);
}

const handleXMLReturnData = (xml) => {
    let rootTag = xml.children[0];
    if (rootTag.tagName.includes("LIST")) {
        let apps = rootTag.getElementsByTagName("APP");
        let size = apps.length;
        for (var i = 0; i < size; i++) {
            let app  = apps[i];
            let id   = app.children[0].innerHTML;
            let name = app.children[1].innerHTML;
            let icon = app.children[2].innerHTML;
            createAppRef(id, name, icon);
        }
    } else if (rootTag.tagName.includes("SERV_MSG")) {
        alertMessage(rootTag.className, rootTag.innerHTML);
    }
}

const createAppRef = (id, appName, icon) => {
    let divTag  = document.createElement("DIV");
    let imgTag  = document.createElement("img");
    let pTag    = document.createElement("P");
    let text    = document.createTextNode(appName);

    divTag.setAttribute("name", id + "::" + appName);
    divTag.className = "app";
    imgTag.className = "icon";
    pTag.className   = "appName";
    imgTag.src       = "data:image/png;base64," + icon;

    pTag.append(text);
    divTag.append(imgTag);
    divTag.append(pTag);
    document.getElementById('appsMenu').append(divTag);
}

const alertMessage = (type, message) => {
    document.getElementById('message').innerHTML =
        "<p class=\"" + type +"\">" + message + "</p>";
}

Full PHP

appManager.php
<?php
include_once 'serverMessanger.php';

function getData() {
    $db = new SQLite3('apps.db');

    if($db === false) {
        $message = "Server: [Error] --> Database connection failed!";
        serverMessage("error", $message);
        die("ERROR: Could not connect to db.");
    }

    $res = $db->query('Select id, app, icon FROM Apps');
    $GeneratedXML = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><LIST>";
    while ($row = $res->fetchArray(SQLITE3_ASSOC)) {
        $GeneratedXML .= "<APP>" .
               "<ID>"   . $row['id'] . "</ID>" .
               "<NAME>" . $row['app'] . "</NAME>" .
               "<ICON>" . base64_encode($row['icon']) . "</ICON>" .
           "</APP>";
    }
    $GeneratedXML .= "</LIST>";
    echo $GeneratedXML;
}

function launchApp($ID, $APP) {
    $db = new SQLite3('apps.db');

    if($db === false) {
        $message = "Server: [Error] --> Database connection failed!";
        serverMessage("error", $message);
        die("ERROR: Could not connect to db.");
    }

    $stmt = $db->prepare('Select command FROM Apps WHERE id = :id AND app = :app');
    $stmt->bindValue(":id", $ID, SQLITE3_INTEGER);
    $stmt->bindValue(":app", $APP, SQLITE3_TEXT);
    $res = $stmt->execute();

    $row = $res->fetchArray(SQLITE3_ASSOC);
    $command = 'nohup ' . $row['command'] . ' > /dev/null &';
    shell_exec($command);

    $message = "Server: [Success] --> Launched " . $APP . "!";
    serverMessage("success", $message);
}


// Determin action
chdir("../db/");
if (isset($_POST['getData'])) {
    getData();
} elseif (isset($_POST['launchApp'],
                $_POST['id'],
                $_POST['app'])) {
    launchApp($_POST['id'], $_POST['app']);
} else {
    $message = "Server: [Error] --> Illegal Access Method!";
    serverMessage("error", $message);
}

?>
serverMessanger.php
<?php
function serverMessage($TYPE, $MESSAGE) {
    $GeneratedXML = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
    $GeneratedXML .= "<SERV_MSG class='" . $TYPE . "'>" . $MESSAGE ."</SERV_MSG>";
    echo $GeneratedXML;
}
?>