Desktop Harness (Cookbook)
Getting Started
Development
Main and Renderer
- Adding Logic To The Main Process
- Adding Logic To The Renderer Process
- Communicating Between Main And Renderer
- Logging Activity In The Main Process
- Logging Activity In The Renderer Process
Framing
- Automatic Application Versioning and Updates
- Automatic Profile Load and Save
- Adding A Native Menu To Your App
Dialogs & Notifications
- Showing a Native Dialog
- Showing a Native Error Dialog
- Showing a Native Notification
- Showing the User a Dialog Before Quitting
File Operations
Getting Started
Creating A TIBET Desktop Project
The tibet clone
command is used to create all new TIBET projects.
For an Electron project add --dna electron
to your clone command so your new project is based on our pre-built Electron 'dna':
tibet clone {{appname}} --dna electron
cd {{appname}}
tibet init
Running Your App
The tibet start
command can be used to start any TIBET project, regardless of
the project dna.
Navigate to your project directory and enter tibet start
:
$ cd {{appname}}
$ tibet start
...
NOTE: tibet start
actually delegates to tibet electron
however we recommend you use tibet start
for consistency across your TIBET-based projects.
NOTE: TIBET Desktop projects include package.json
entries to ensure tibet start
is set as the start script. This ensures your packaged applications operate correctly out-of-the-box.
Development
Basic TIBET Desktop Development
TIBET development for Electron-based projects is consistent with TIBET development on other supported platforms. This guide covers a few things unique to Electron (such as main vs. renderer processes) however the rest of your development process is the same.
To get a general feel for TIBET we recommend you spend a few minutes with the TIBET Quickstart and TIBET Essentials guides.
Each of our guides work with TIBET Desktop projects. Simply add --dna electron
to the tibet clone
command you use to create your hello
project.
All of TIBET's command line tools (init
, start
, lint
, test
, build
, release
, deploy
, version
, etc.) work with TIBET Desktop.
Linting Your Application
TIBET's built-in JavaScript, CSS, and XML/XHTML linting tools work as expected with Desktop projects, ensuring all your "source" is lint-free.
Within your project directory tree enter tibet lint
:
$ tibet lint
checked 0 of 22 total files
(8 filtered, 14 unchanged)
0 errors, 0 warnings. Clean!
Lint checks keep track of file changes so that only recently changed files are tested. You can also focus on just your JavaScript, CSS, or markup if you like.
See the tibet lint manpage for more details.
Testing Your Application
Your project is testable using the built-in tibet test
command:
$ tibet test
# Loading TIBET platform at 2019-08-02T01:53:29.910Z
# TIBET reflection suite loaded and active in 6469ms
# TIBET starting test run
# 2 suite(s) found.
1..3
#
# tibet test APP --suite='APP'
ok - Has a namespace.
ok - Has an application type.
# pass: 2 total, 2 pass, 0 fail, 0 error, 0 skip, 0 todo, 0 only.
#
# tibet test APP.hello.app.Type --suite='APP.hello:app'
ok - Is a templated tag.
# pass: 1 total, 1 pass, 0 fail, 0 error, 0 skip, 0 todo, 0 only.
#
# PASS: 3 total, 3 pass, 0 fail, 0 error, 0 skip, 0 todo, 0 only.
TIBET's test harness launches your application in a headless fashion, loads any tests it finds, and executes them. This approach can be used effectively to test your rendering process. It can also be used to integration test your main process logic.
NOTE: Unit testing the main process is also possible, however it requires a little more effort. Stay tuned for more on this topic.
See the tibet test manpage and our TIBET Testing documentation for details on how to fully test your TIBET applications.
Main & Renderer
Adding Logic To The Main Process
TIBET Deskto projects use an electron.js
file which loads your TIBET application along with optional plugins.
You can add logic to the Electron main process via this plugin mechanism - a dynamically configurable set of modules that TIBET loads into Electron when the application starts.
An excellent example can be found in the preload.js
module supplied with every
TIBET Desktop project in the plugins
directory.
Let's modify a copy of that module to see how the process works.
cd
into theplugins
directory and copypreload.js
totester.js
.Edit
tester.js
and empty the body of the main function from theRequires
block to the end of the function.Insert the following code:
logger.warn('This is only a warning');
Edit the
tibet.json
file and addtester
to the Array of entries underelectron.plugins.core
.Start your application:
tibet start
Describing the details of functionality available to you in the 'main process' (aka the "server") is best left to the Electron Documentation.
Adding Logic To The Renderer Process
To add logic to the renderer process follow the same process you'd use to add functionality to any TIBET client process. In other words, follow the same approach outlined in the TIBET Quickstart and TIBET Essentials guides.
Communicating Between Main and Renderer
TIBET makes it easy to communicate between Electron's two primary processes by leveraging the same communication model it uses for everything else - signaling.
See TIBET Signaling for details on TIBET signaling.
Signaling From Renderer To Main
To communicate with the main process from a TIBET application running in an Electron renderer process use the following outline.
In the renderer process you'll want to use signalMain
on the
TP.electron.ElectronMain
type:
TP.electron.ElectronMain.signalMain('TP.sig.SampleSignal', {"foo":"bar"});
In the Main process you set up a handler for that signal:
ipcMain.handle('TP.sig.SampleSignal',
function(event, payload) {
// event contains native Electron Event object.
// payload contains a POJO: {"foo":"bar"}
});
You can return a Promise from the main process using the following pattern.
In the renderer process:
TP.electron.ElectronMain.signalMain('TP.sig.SampleSignal2', {"foo":"bar"}).then(
function() {...});
In the main process:
ipcMain.handle('TP.sig.SampleSignal',
function(event, payload) {
return await dialog.showMessageBox(...);
});
Signaling From Main To Renderer
Communicating from the Electron main process to a TIBET application running in a renderer process also uses TIBET's signaling mechanism.
In the renderer process subscribe to the signal with TP.electron.ElectronMain
.
This is typically done in the init
instance method TIBET uses to initialize a
new instance:
APP.MyApp.MyType.Inst.defineMethod('init',
function() {
// The TP.sig is optional here.
this.observe(TP.electron.ElectronMain, 'TestSignal');
});
Also in the renderer process, define a handler for the target signal:
APP.MyApp.MyType.Inst.defineHandler('TestSignal',
function(aSignal) {
// The payload contains {"foo":"bar"} that we sent from the main process.
APP.info('Got to the TestSignal handler: ' + aSignal.getPayload());
return this;
});
Last, add code to the main process to send the signal:
mainWindow.webContents.send('TP.sig.TestSignal', {"foo":"bar"});
That's all there is to it.
Logging Activity In The Main Process
When you define a module to be loaded into the main process TIBET will provide
tha function with an options
parameter. A 'logger' object is provided in the
options
which allows you access to a common logging module from TIBET.
module.exports = function(options) {
var logger,
meta;
logger = options.logger;
meta = {
type: 'plugin',
name: 'preload'
};
logger.system('preloading utilities', meta);
...
};
TIBET's default logger object has the following methods for level-based logging:
logger.trace();
logger.debug();
logger.info();
logger.warn();
logger.error();
logger.fatal();
logger.system();
Logging Activity In The Renderer Process
To log data in the renderer process leverage TIBET's lgging functionality available as documented in TIBET Logging.
Framing
Automatic Application Versioning and Updates
For deploying automatically versioned TIBET applications, see Deploying via electron-builder
Automatic Profile Load and Save
TIBET Desktop will automatically save any data that is registered under the
top-level profile
key in TIBET's configuration system. Putting the following
code in the Application object's AppDidStart
signal handler method would cause
this value to be saved:
APP.MyApp.Application.Inst.defineHandler('AppDidStart',
function(aSignal) {
...
TP.sys.setcfg('profile.favoritecolor', 'red');
});
This data is written to the tibet.json
file under the profile
key when a
'proper shutdown' (i.e. via the quit
menu item) is initiated in the TIBET
Desktop app.
For now, the only data that is tracked by TIBET itself in the profile system are the user's windows size and position.
Adding A Native Menu To Your App
NOTE: This feature is currently being reworked to be more powerful and is missing from the current TIBET release. This section is representative of the functionality that will be available in a future release.
To function the tag must be rendered in a TIBET application. Usually you'll use
your application's app
tag to render top-level shared components such as
dialogs. You'll find a project's app tag in
{{project}}/src/tags/APP.{{appname}}.app/
. For this example we'll add the
dialog to the APP.{{appname}}.app.xhtml
file.
Rendering The Tag
<electron:menu label="MyMenu">
<electron:menuitem label="DoThing1" on:click="DoFirstThing"/>
<electron:menu label="MySubmenu">
<electron:menuitem label="DoSubmenu1" on:click="DoSubFirstThingFirst"/>
<electron:menuitem label="DoSubmenu2" on:click="DoSubFirstThingSecond"/>
</electron:menu>
<electron:menuitem label="DoThing2" on:click="DoSecondThing"/>
</electron:menu>
Handling the menu triggers
To handle the menu triggers for the menu items, simply implement standard TIBET
handlers on the Application object. You'll find a project's Application subtype
in {{project}}/src/APP.{{appname}}.Application.js
.
APP.MyApp.Application.Inst.defineHandler('DoFirstThing',
function(aSignal) {
// Handle the 'DoFirstThing' menu item here.
return this;
});
Dialogs And Notifications
Showing a Native Dialog
Use an electron:dialog
tag with no type
.
<electron:dialog ... />
The electron:dialog
tag gives you markup-driven access to Electron's native
dialogs without the need for coding. For regular dialog boxes, we do not specify
a type
.
Rendering The Tag
To function the tag must be rendered in a TIBET application. Usually you'll use
your application's app
tag to render top-level shared components such as
dialogs. You'll find a project's app tag in
{{project}}/src/tags/APP.{{appname}}.app/
. For this example we'll add the
dialog to the APP.{{appname}}.app.xhtml
file.
<electron:dialog title="Warn User">
<label>Please don't do that</label>
<button>OK, I won't</button>
<button>No, I will anyway!</button>
</electron:dialog>
Activating The Dialog
To trigger the dialog to open you'll need to "activate" it. You can do this in a
number of ways. The simplest is to target it with a signal. In the example below
we'll add an id
to the dialog so we can target it and we'll include a button
to fire the activation signal we want:
<electron:dialog id="MyWarnDialog" title="Warn User">
<label>Please don't do that</label>
<button>OK, I won't</button>
<button>No, I will anyway!</button>
</electron:dialog>
<button on:click="{signal: UIActivate, origin: 'MyWarnDialog'}">Warn User</button>
Getting the results
To get the results of the dialog, simply use TIBET Data Binding to bind the results to a URN data container:
<electron:dialog id="MyWarnDialog" title="Warn User" bind:out="urn:tibet:warnresponse">
<label>Please don't do that</label>
<button>OK, I won't</button>
<button>No, I will anyway!</button>
</electron:dialog>
<button on:click="{signal: UIActivate, origin: 'MyWarnDialog'}">Warn User</button>
Viewing the results
Then, to see the result that was clicked (the index of the button that was
clicked), bind the URN to a <textarea/>
:
<textarea bind:in="urn:tibet:warnresponse"/>
To view and manipulate the results programmatically, peek inside the URN container:
response = TP.uc('urn:tibet:warnresponse').getContent();
Taking action based on response
To take an action based on the button that the user clicked, simply grab the result and switch on its value:
// Create a Number here via the TP.nc() call
response = TP.nc(TP.uc('urn:tibet:warnresponse').getContent());
switch(response) {
case 0:
// The user clicked 'OK, I won't'
break;
case 1:
// The user clicked 'No, I will anyway!'
break;
}
Showing a Native Error Dialog
Use an electron:dialog
tag with a type
of error
.
<electron:dialog type="error" ... />
The electron:dialog
tag gives you markup-driven access to Electron's native
error dialogs without the need for coding.
Rendering The Tag
To function the tag must be rendered in a TIBET application. Usually you'll use
your application's app
tag to render top-level shared components such as
dialogs. You'll find a project's app tag in
{{project}}/src/tags/APP.{{appname}}.app/
. For this example we'll add the
dialog to the APP.{{appname}}.app.xhtml
file.
<electron:dialog type="error" title="Error Condition">
<label>There was an error condition!</label>
</electron:dialog>
Activating The Dialog
To trigger the dialog to open you'll need to "activate" it. You can do this in a
number of ways. The simplest is to target it with a signal. In the example below
we'll add an id
to the dialog so we can target it and we'll include a button
to fire the activation signal we want:
<electron:dialog type="error" id="MyErrorDialog" title="Error Condition">
<label>There was an error condition!</label>
</electron:dialog>
<button on:click="{signal: UIActivate, origin: 'MyErrorDialog'}">Pop an error
dialog</button>
An error dialog box has no return value, so there are no further actions we can take with it.
Showing a Native Notification
Use an electron:notification
tag.
<electron:notification ... />
The electron:notification
tag gives you markup-driven access to Electron's
native notification system without the need for coding.
Rendering The Tag
To function the tag must be rendered in a TIBET application. Usually you'll use
your application's app
tag to render top-level shared components such as
notifications. You'll find a project's app tag in
{{project}}/src/tags/APP.{{appname}}.app/
. For this example we'll add the
dialog to the APP.{{appname}}.app.xhtml
file.
<electron:notification title="Hi there">
<body>Just wanted to say "Hi there"!</body>
</electron:notification>
Activating The Notification
To trigger the notification to open you'll need to "activate" it. You can do this
in a number of ways. The simplest is to target it with a signal. In the example
below we'll add an id
to the notification so we can target it and we'll include
a button to fire the activation signal we want:
<electron:notification id="MyNotification" title="Hi there">
<body>Just wanted to say "Hi there"!</body>
</electron:notification>
<button on:click="{signal: UIActivate, origin: 'MyNotification'}">Send the user
a notification.</button>
Notifications have no return value, so there are no further actions we can take with them.
Showing the User a Dialog Before Quitting
TODO
File Operations
Opening A File By Using A Native Open Dialog
Use an electron:dialog
tag with a type
of open
.
<electron:dialog type="open" ... />
The electron:dialog
tag gives you markup-driven access to Electron's native
open dialogs without the need for coding.
Rendering The Tag
To function the tag must be rendered in a TIBET application. Usually you'll use
your application's app
tag to render top-level shared components such as
dialogs. You'll find a project's app tag in
{{project}}/src/tags/APP.{{appname}}.app/
. For this example we'll add the
dialog to the APP.{{appname}}.app.xhtml
file.
<electron:dialog type="open"/>
Activating The Dialog
To trigger the dialog to open you'll need to "activate" it. You can do this in a
number of ways. The simplest is to target it with a signal. In the example below
we'll add an id
to the dialog so we can target it and we'll include a button
to fire the activation signal we want:
<electron:dialog type="open" id="MyOpenDialog"/>
<button on:click="{signal: UIActivate, origin: 'MyOpenDialog'}">Open File</button>
Getting the results
To get the results of the open dialog, simply use TIBET Data Binding to bind the results to a URN data container:
<electron:dialog type="open" id="MyOpenDialog" bind:out="urn:tibet:openfilenames"/>
<button on:click="{signal: UIActivate, origin: 'MyOpenDialog'}">Open File</button>
Viewing the results
Then, to see the filename(s) that were selected, bind the URN to a <textarea/>
:
<textarea bind:in="urn:tibet:openfilenames"/>
To view and manipulate the results programmatically, peek inside the URN container:
filenames = TP.uc('urn:tibet:openfilenames').getContent();
filenames.forEach(
function(filename) {
APP.info(filename);
});
Viewing the file content
To view the file content, simply create a URL from the filenames and get their content:
filenames = TP.uc('urn:tibet:openfilenames').getContent();
filenames.forEach(
function(fn) {
// Print the contents to the application log.
APP.info(TP.uc(fn).getContent());
});
Saving A File By Using A Native Save Dialog
Use an electron:dialog
tag with a type
of save
.
<electron:dialog type="save" ... />
The electron:dialog
tag gives you markup-driven access to Electron's native
save dialogs without the need for coding.
Rendering The Tag
To function the tag must be rendered in a TIBET application. Usually you'll use
your application's app
tag to render top-level shared components such as
dialogs. You'll find a project's app tag in
{{project}}/src/tags/APP.{{appname}}.app/
. For this example we'll add the
dialog to the APP.{{appname}}.app.xhtml
file.
<electron:dialog type="save"/>
Activating The Dialog
To trigger the dialog to open you'll need to "activate" it. You can do this in a
number of ways. The simplest is to target it with a signal. In the example below
we'll add an id
to the dialog so we can target it and we'll include a button
to fire the activation signal we want:
<electron:dialog type="save" id="MySaveDialog"/>
<button on:click="{signal: UIActivate, origin: 'MySaveDialog'}">Save File</button>
Getting the results
To get the results of the save dialog, simply use TIBET Data Binding to bind the results to a URN data container:
<electron:dialog type="save" id="MySaveDialog" bind:out="urn:tibet:savefilename"/>
<button on:click="{signal: UIActivate, origin: 'MySaveDialog'}">Save File</button>
Viewing the results
Then, to see the filename that was entered, bind the URN to a <textarea/>
:
<textarea bind:in="urn:tibet:savefilename"/>
To view and manipulate the results programmatically, peek inside the URN container:
filename = TP.uc('urn:tibet:savefilename').getContent();
Saving the file content
To save the file content, simply create a URL from the filename, set its content
and call save
:
filename = TP.uc('urn:tibet:savefilename').getContent();
request = TP.request(TP.hc('refresh', true)));
TP.uc(filename).setContent('This is some content to save').save(request);
Moreā¦
For more Cookbook development recipes and TIBET development concepts see the documentation for the Client Stack and its individual layers.