Termal App Studio

Build, translate, test and package your own Termal apps.

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

  1. Open Termal App Studio from the Start menu.
  2. Click + to scaffold a fresh project — or click Examples to open a ready-made, fully commented app you can read and adapt (see Examples).
  3. Edit app.js / app.html / app.css. Use the Components palette on the right to insert SDK snippets at the cursor.
  4. Open the Manifest tab to set the icon, color, window size and permissions.
  5. Open the Translations tab to add languages and key/value strings.
  6. Press ▶ Run to test in the sandbox. The Console at the bottom shows your app's logs and errors.
  7. Press 📦 Build to produce a .tapp in 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).

ExampleShows
Web ViewA cookie-capable webview embedded in the app window (Termal.webview).
SDK TourOne button per API: toast, dialog, menu, clipboard, storage.
Text EditorRead / write / list / delete files in the app's sandbox folder.
Notes & TodoPersist the whole app state with key/value storage.
HTTP ClientTermal.net.fetch with a local or server exit.
Server WidgetPoll a URL from your SSH server, with auto-refresh.
Window OptionsSize, 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.

FieldTypeDescription
keystringUnique id (the folder name). Lowercase letters, digits, dashes.
namestringDisplay name (shown in the Start menu and store).
versionstringe.g. 0.1.0. Part of the built filename.
iconobject{ "type": "ti", "value": "ti-rocket" } for a Tabler icon, or { "type": "img", "value": "assets/icon.png" }.
colorstringAccent color, e.g. #62d0a3.
windowobjectWindow 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.
permissionsstring[]Capabilities your app may use. See Permissions.
defaultLangstringFallback language code, e.g. en.
langsstring[]Languages you provide, e.g. ["en","fr"].
entrystringHTML 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

MemberReturnsDescription
Termal.t(key, fallback?)stringTranslated string for the current language (falls back to defaultLang, then fallback, then key).
Termal.langstringCurrent language code.
Termal.manifestobjectYour 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

MethodReturnsDescription
Termal.dialog.prompt(title, def?)string | nullAsk for text input.
Termal.dialog.confirm(msg, ok?)booleanYes/no confirmation.
Termal.dialog.alert(msg)Single-button message.
Termal.menu(x, y, items)anyContext 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.

MethodPermissionDescription
Termal.fs.list(path?)fs.readList entries: [{ name, type, size, mtime }].
Termal.fs.read(path)fs.readRead a text file (returns a string).
Termal.fs.write(path, content)fs.writeWrite a text file (creates folders as needed).
Termal.fs.mkdir(path)fs.writeCreate a folder.
Termal.fs.remove(path, dir?)fs.writeDelete 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).

MethodReturnsDescription
Termal.storage.get(key, def?)anyRead 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

MethodReturnsDescription
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.

OptionDefaultDescription
urlRequired. http / https only.
embedfalsetrue → 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).
titlepage titleWindow title. If omitted, follows the page's own title.
bartrueShow the info bar (network badge + current URL).
navfalseShow navigation controls (back / forward / reload).
resizable, maximizabletrueSet to false for a locked-size window (e.g. a phone-shaped view).
icon, w, hWindow 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 });
Cookies — a plain <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.*:

MethodDescription
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.

PermissionGrants access to
fs.readReading files in the app's sandboxed data folder.
fs.writeWriting / creating / deleting files in that folder.
storageThe key/value store.
netTermal.net.fetch.
clipboardTermal.clipboard.write.
webviewTermal.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:

ValueBehavior
'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".
Note — the server exit reuses the same SSH SOCKS tunnel as Termal Browser and requires 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.

Requirement — building needs the 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 Termal SDK.
  • A plain <iframe> in your app.html is sandboxed and cannot use cookies. For anything that needs a login/session, use Termal.webview (real webview, persistent cookies) — with embed: true to 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.error and uncaught errors are reported there.
  • Keep your permissions minimal — users see them at install time.