Skip to content

Desktop Kit

Desktop application support with Tauri. It exposes a DesktopDriver that registers with the Core and acts as a bridge between your Iskra App’s logic and Tauri’s native APIs (windows, IPC, menus).

import { App } from '@iskra-bun/core';
import { DesktopDriver } from '@iskra-bun/desktop-kit';
const app = new App({ name: 'MiAppDeEscritorio' });
app.register(new DesktopDriver());
await app.start();
// Al arrancar solo registra un warning: todavia no hay integracion con Tauri.

The DesktopDriver implements the Core’s Driver interface, so it takes part in the standard lifecycle: it is initialized in app.start() and released in app.stop().

The driver stores a reference to the App during init() and is then ready to relay messages between the domain and Tauri’s native layer.

import { DesktopDriver } from '@iskra-bun/desktop-kit';
const driver = new DesktopDriver();
// driver.name → 'DesktopDriver'
// driver.init(app) → guarda la App y prepara el puente
// driver.start() → activa el puente y loguea el arranque
app.register(driver);
Member Type Description
name string Driver identifier ('DesktopDriver').
init(app) Promise<void> Receives the App and stores the internal reference.
start() Promise<void> Activates the Tauri bridge and emits the startup log.

Window management is done with the Tauri API (@tauri-apps/api). The DesktopDriver gives you the hook point; from your handlers you can control the current window.

import { getCurrentWindow } from '@tauri-apps/api/window';
const appWindow = getCurrentWindow();
await appWindow.setTitle('Iskra Desktop');
await appWindow.maximize();
await appWindow.setResizable(false);

React to window events from the App’s bus:

app.on('window:focus', async (ctx) => {
ctx.logger.info('La ventana recupero el foco');
});
const appWindow = getCurrentWindow();
await appWindow.onFocusChanged(({ payload: focused }) => {
app.emit('window:focus', { focused });
});

Tauri exposes Rust commands to the frontend via invoke. Wrap those calls in your App’s services to keep the domain decoupled from Tauri.

import { invoke } from '@tauri-apps/api/core';
// Llamar a un comando Rust registrado en el backend de Tauri
const usuarios = await invoke<Usuario[]>('listar_usuarios', { activos: true });

Register the service in the Core’s DI container to reuse it:

class DesktopBridge {
// The Rust command asks where to save with the native dialog.
async guardarArchivo(contenido: string) {
return invoke('guardar_archivo', { contenido });
}
}
app.context.set('desktopBridge', new DesktopBridge());
// En cualquier handler
const bridge = app.context.get('desktopBridge');
await bridge.guardarArchivo('Hola Iskra');

Don’t give a command a path from the webview: a script that gets into the page (an injected string, a compromised dependency) could then read or write any file the user can, such as ~/.ssh/id_ed25519. Let the Rust command pick the file with the native dialog (tauri-plugin-dialog) and read or write it there, as the desktop-app template’s abrir_archivo does. Declare each command in build.rs (tauri_build::Attributes::new().app_manifest(AppManifest::new().commands(&[...]))) and grant it in a capability (allow-<command>): otherwise any webview can invoke it.

Listen to events emitted from Rust and relay them to the Iskra bus:

import { listen } from '@tauri-apps/api/event';
await listen<{ progreso: number }>('descarga:progreso', (evento) => {
app.emit('descarga:progreso', evento.payload);
});

Native menus are built with Tauri’s menu API. Connect each item to an App event to process it with your handlers.

import { Menu, MenuItem } from '@tauri-apps/api/menu';
const itemAcercaDe = await MenuItem.new({
text: 'Acerca de',
action: () => app.emit('menu:acerca-de', {}),
});
const menu = await Menu.new({ items: [itemAcercaDe] });
await menu.setAsAppMenu();
app.on('menu:acerca-de', async (ctx) => {
ctx.logger.info('Abrir dialogo Acerca de');
});

The DesktopDriver takes no options of its own: all native configuration (window size, permissions, icons) lives in tauri.conf.json, Tauri’s configuration file.

tauri.conf.json
{
"productName": "Iskra Desktop",
"app": {
"windows": [
{ "title": "Iskra Desktop", "width": 1024, "height": 768, "resizable": true }
]
}
}

The logical name of your Iskra App is still managed from the Core:

const app = new App({ name: 'Iskra Desktop', logger: { level: 'info' } });
src/main.ts
import { App } from '@iskra-bun/core';
import { DesktopDriver } from '@iskra-bun/desktop-kit';
import { getCurrentWindow } from '@tauri-apps/api/window';
import { invoke } from '@tauri-apps/api/core';
const app = new App({ name: 'Iskra Desktop' });
app.register(new DesktopDriver());
app.on('archivo:abrir', async (ctx) => {
// abrir_archivo abre el dialogo nativo y lee en Rust el archivo elegido.
const archivo = await invoke<{ ruta: string; contenido: string } | null>('abrir_archivo');
if (archivo) ctx.logger.info({ bytes: archivo.contenido.length }, 'Archivo cargado');
});
async function main() {
await app.start();
const ventana = getCurrentWindow();
await ventana.setTitle('Iskra Desktop');
app.emit('archivo:abrir', {});
}
main().catch((err) => app.logger.error({ err }, 'Fallo al arrancar'));

For a ready-to-use template, see templates/desktop-app. If your app also targets mobile, check out @iskra-bun/mobile-kit and the universal-app template.