Termal App Studio
Introduction
Termal App Studio is the in-desktop IDE for creating third-party Termal apps. You write plain HTML / CSS / JavaScript; the Studio gives you a code editor, a component palette, a manifest editor (icon & permissions), a per-app translation manager, a one-click sandboxed test, and a packager that produces an installable .tapp bundle.
Every app runs inside an isolated sandbox (an opaque-origin iframe). It renders its own UI locally and talks to the desktop through the Termal SDK. Privileged actions (file system, network, storage, clipboard, webview) cross a message bridge that enforces the permissions declared in your manifest — so an app can only do what it asked for.
Quick start
- Open Termal App Studio from the Start menu.
- Click + to scaffold a fresh project — or click Examples to open a ready-made, fully commented app you can read and adapt (see Examples).
- Edit
app.js/app.html/app.css. Use the Components palette on the right to insert SDK snippets at the cursor. - Open the Manifest tab to set the icon, color, window size and permissions.
- Open the Translations tab to add languages and key/value strings.
- Press ▶ Run to test in the sandbox. The Console at the bottom shows your app's logs and errors.
- Press 📦 Build to produce a
.tappin Downloads, then install it. The app appears in the Start menu and under My apps in the store.
Examples
The Studio ships with ready-made, heavily-commented example apps. Click Examples in the toolbar and pick one — it is copied into a new project you can run, read and adapt (the original is never touched).
| Example | Shows |
|---|---|
| Web View | A cookie-capable webview embedded in the app window (Termal.webview). |
| SDK Tour | One button per API: toast, dialog, menu, clipboard, storage. |
| Text Editor | Read / write / list / delete files in the app's sandbox folder. |
| Notes & Todo | Persist the whole app state with key/value storage. |
| HTTP Client | Termal.net.fetch with a local or server exit. |
| Server Widget | Poll a URL from your SSH server, with auto-refresh. |
| Window Options | Size, maximize, resizable, title — plus a fixed mobile web view. |
Project structure
Projects live on the connected server, under ~/.termal/users/<you>/.apps/studio/projects/<key>/:
manifest.json app metadata (key, name, icon, permissions, …)
app.html your markup (rendered inside the app window)
app.js your logic — defines function main(Termal)
app.css your styles
i18n/<lang>.json translations, one file per language
assets/… images (e.g. an icon you imported)
At runtime the desktop loads app.html, injects your app.css, evaluates your app.js, and exposes the SDK as a global Termal object.
Manifest reference
manifest.json describes your app. You can edit it directly, or use the visual Manifest tab.
| Field | Type | Description |
|---|---|---|
key | string | Unique id (the folder name). Lowercase letters, digits, dashes. |
name | string | Display name (shown in the Start menu and store). |
version | string | e.g. 0.1.0. Part of the built filename. |
icon | object | { "type": "ti", "value": "ti-rocket" } for a Tabler icon, or { "type": "img", "value": "assets/icon.png" }. |
color | string | Accent color, e.g. #62d0a3. |
window | object | Window size & behavior:{ "w": 520, "h": 440, "resizable": true, "maximizable": true, "minimizable": true }.Set a flag to false to lock it (e.g. a fixed, mobile-sized window). See Window control. |
permissions | string[] | Capabilities your app may use. See Permissions. |
defaultLang | string | Fallback language code, e.g. en. |
langs | string[] | Languages you provide, e.g. ["en","fr"]. |
entry | string | HTML entry file. Defaults to app.html. |
Runtime & entry point
Define a global main(Termal) function — it is called once after the runtime is initialized. Alternatively, listen for the termal:ready event.
// app.js
function main(Termal) {
document.getElementById('go').onclick = () => Termal.toast('Hello!');
}
// equivalent:
window.addEventListener('termal:ready', () => { /* … */ });
Termal.on('ready', () => { /* … */ });
Your UI is the iframe's <body> — manipulate the DOM as usual. All privileged SDK calls return Promises (use await). console.log / errors are forwarded to the Studio Console.
SDK reference
Core & internationalization
| Member | Returns | Description |
|---|---|---|
Termal.t(key, fallback?) | string | Translated string for the current language (falls back to defaultLang, then fallback, then key). |
Termal.lang | string | Current language code. |
Termal.manifest | object | Your manifest. |
Termal.on(name, fn) | — | Subscribe to events: 'ready', 'lang'. |
const label = Termal.t('greeting', 'Hello'); // uses i18n/<lang>.json
Termal.on('lang', ({ lang }) => console.log('language is now', lang));
Dialogs, menu & toast no permission
| Method | Returns | Description |
|---|---|---|
Termal.dialog.prompt(title, def?) | string | null | Ask for text input. |
Termal.dialog.confirm(msg, ok?) | boolean | Yes/no confirmation. |
Termal.dialog.alert(msg) | — | Single-button message. |
Termal.menu(x, y, items) | any | Context menu at screen (x,y); resolves to the chosen item's value. |
Termal.toast(msg, kind?) | — | Transient notification (kind: 'info', 'success', 'warn', 'error'). |
const name = await Termal.dialog.prompt('Your name?');
if (await Termal.dialog.confirm('Delete this item?')) { /* … */ }
const choice = await Termal.menu(120, 80, [
{ label: 'Rename', value: 'rename', icon: 'ti-pencil' },
{ sep: true },
{ label: 'Delete', value: 'delete', danger: true },
]);
File system fs.read fs.write
The file system is sandboxed to your app's private data folder. Paths are relative; .. and absolute paths are rejected. Your app cannot read the rest of the server.
| Method | Permission | Description |
|---|---|---|
Termal.fs.list(path?) | fs.read | List entries: [{ name, type, size, mtime }]. |
Termal.fs.read(path) | fs.read | Read a text file (returns a string). |
Termal.fs.write(path, content) | fs.write | Write a text file (creates folders as needed). |
Termal.fs.mkdir(path) | fs.write | Create a folder. |
Termal.fs.remove(path, dir?) | fs.write | Delete a file (or folder when dir is true). |
await Termal.fs.write('notes.txt', 'remember the milk');
const text = await Termal.fs.read('notes.txt');
Storage storage
Simple key/value store, persisted per app on the server (survives restarts).
| Method | Returns | Description |
|---|---|---|
Termal.storage.get(key, def?) | any | Read a value (or def if missing). |
Termal.storage.set(key, value) | — | Store any JSON-serializable value. |
Termal.storage.del(key) | — | Remove a key. |
const count = (await Termal.storage.get('count', 0)) + 1;
await Termal.storage.set('count', count);
Network net
| Method | Returns | Description |
|---|---|---|
Termal.net.fetch(url, opts?) | { status, body } | HTTP request via the backend. opts: { exit, method }. |
exit: 'server' routes the request through the SSH server (it goes out via the server's IP, using curl); exit: 'local' (default) fetches from the machine running Termal. See Network exit.
const res = await Termal.net.fetch('https://api.example.com/data', { exit: 'server' });
console.log(res.status, res.body);
Webview webview
Open a URL in a real Electron webview — a proper browser context on a persistent partition, so cookies and sessions work (logins behave normally). By default it opens as its own Termal window; pass embed: true to render it inside your app's window instead.
| Option | Default | Description |
|---|---|---|
url | — | Required. http / https only. |
embed | false | true → the webview takes over the calling app's window (instead of a new one). |
exit | 'local' | 'server' (via the server's IP / SSH tunnel) or 'local' (this machine). |
title | page title | Window title. If omitted, follows the page's own title. |
bar | true | Show the info bar (network badge + current URL). |
nav | false | Show navigation controls (back / forward / reload). |
resizable, maximizable | true | Set to false for a locked-size window (e.g. a phone-shaped view). |
icon, w, h | — | Window icon and size. |
// A cookie-capable site, embedded in the app window:
await Termal.webview({ url: 'https://example.com/app', embed: true, nav: true });
// A fixed, phone-sized window:
await Termal.webview({ url: 'https://example.com', w: 400, h: 780, resizable: false, maximizable: false });
<iframe> in your app.html is inside the app's sandbox (opaque origin) and cannot use cookies, so login-based sites won't work there. Use Termal.webview for anything that needs a session.Inside the webview, normal links navigate in place (keeping the chosen network exit). Links that open a new window (target="_blank" / window.open) are opened in Termal Browser.
Clipboard clipboard
await Termal.clipboard.write('copied text');
Window control no permission
Set the initial size and locks in manifest.window. Control the window at runtime with Termal.win.*:
| Method | Description |
|---|---|
Termal.win.setSize(w, h) | Resize the window now. |
Termal.win.maximize() | Fill the desktop. |
Termal.win.restore() | Back to the normal size. |
Termal.win.minimize() | Send to the dock. |
Termal.win.center() | Center on screen. |
Termal.win.setResizable(bool) | Lock / unlock resizing. |
Termal.win.setTitle(text) | Rename the title bar. |
Termal.win.close() | Close the window. |
Termal.setTitle(text) and Termal.close() remain available as shortcuts.
Termal.win.setSize(390, 760); // phone-sized
Termal.win.setResizable(false); // lock it
Termal.win.maximize();
Permissions
Declare the capabilities your app needs in manifest.permissions (or tick the boxes in the Manifest tab). A call to a capability you did not declare is rejected with Permission denied. The user reviews granted permissions when installing your app.
| Permission | Grants access to |
|---|---|
fs.read | Reading files in the app's sandboxed data folder. |
fs.write | Writing / creating / deleting files in that folder. |
storage | The key/value store. |
net | Termal.net.fetch. |
clipboard | Termal.clipboard.write. |
webview | Termal.webview. |
Dialogs, menus, toasts and window control (Termal.win.*) are always available and need no permission.
Translations
Provide one JSON file per language under i18n/. Manage them visually in the Translations tab (add languages, edit key/value pairs).
// i18n/en.json // i18n/fr.json
{ "greeting": "Hello" } { "greeting": "Bonjour" }
At runtime, Termal.t('greeting') resolves against the current desktop language, falling back to your defaultLang. The current language is available as Termal.lang, and changes emit a 'lang' event.
Network exit
Both Termal.webview and Termal.net.fetch accept an exit option:
| Value | Behavior |
|---|---|
'local' | Traffic leaves from this machine (the device running Termal). Default. |
'server' | Traffic is tunneled through the SSH server and exits via its IP. Useful to reach the server's private network or to appear "as the server". |
AllowTcpForwarding yes on the server. net.fetch with exit: 'server' uses curl on the server.Build & install
Press 📦 Build in the Studio. The project folder is zipped on the server into <key>-<version>.tapp inside your Downloads folder, and you're offered to install it.
Installing registers the app: it asks the user to grant the declared permissions, then the app appears in the Start menu, on the desktop's app list, and under My apps in Termal App (the store), where it can also be uninstalled.
zip utility installed on the server (apt install zip).Limitations & tips
- The app runs in an opaque-origin sandbox: no access to the parent page, cookies or Node. All host interaction goes through the
TermalSDK. - A plain
<iframe>in yourapp.htmlis sandboxed and cannot use cookies. For anything that needs a login/session, useTermal.webview(real webview, persistent cookies) — withembed: trueto keep it inside your app window. - The file system is confined to your app's data folder — design around relative paths.
- Use the Console panel while developing;
console.errorand uncaught errors are reported there. - Keep your
permissionsminimal — users see them at install time.