Skip to content

IPC and Error Handling

Pablo Martínez López edited this page Nov 9, 2026 · 25 revisions

IPC and Error Handling

English · Español

Everything the screen asks of Rust goes through one typed function, call() in src/lib/ipc.ts; everything Rust pushes back goes through typed events listened to with on(). Failures always arrive as an AppError { code, params } that the screen turns into a sentence in the current language. This page lists all 177 commands, the error model, the generated TypeScript bindings, the long-job protocol, and the file dialogs.

The bridge: call() and on()

src/lib/ipc.ts exports:

Export Role
interface Commands One entry per Rust command: name: { args: {...}; result: T }. The compiler checks every call() against it.
call<C extends keyof Commands>(cmd, args?) Thin wrapper over invoke from @tauri-apps/api/core. The only place invoke is imported (verified by grep: no other file in src/ calls invoke).
interface Events Event name to payload type for the events the screen listens to with on() (19 entries). The online-play notices (red://aviso, salas://…) and mesa://originales are not in it: they are listened to with listen directly (see Events).
on(evento, f) Dynamic-imports listen from @tauri-apps/api/event; returns the unlisten function (as a promise).

Conventions that bite:

  • Argument names are camelCase on the JS side. Rust parameters are snake_case; Tauri maps them: job_id is jobId, scryfall_id is scryfallId, type_filter is typeFilter, highres_only is highresOnly. Struct fields inside payloads follow each struct's #[serde(rename_all = "camelCase")].
  • Unit results become null (result: null in Commands), and Option<T> becomes T | null.
  • Commands is hand-maintained. The argument and result types it references are generated (see ts-rs bindings), but the list itself is not. Adding a Rust command without a Commands entry compiles fine and simply cannot be called from typed code. Today the 177 names in generate_handler! and the 177 keys of Commands match exactly (checked by diffing both lists).
  • Direct Tauri imports outside ipc.ts exist only for non-command APIs: @tauri-apps/plugin-dialog (src/lib/dialogos.ts), getCurrentWindow().show() (src/main.tsx), getCurrentWebview() for file drops (src/features/preview/arrastrar-ficheros.ts), getVersion() (src/features/settings/settings-dialog.tsx), convertFileSrc (src/lib/image-url.ts), getCurrentWindow() for the close guard (src/features/shell/confirmar-cierre.tsx) and full screen (src/features/mesa/barra-mesa.tsx), the Channel class of @tauri-apps/api/core (src/features/mesa/conexion.ts, see Channels), and listen from @tauri-apps/api/event for the events that are not in Events (src/features/mesa/red/conexion-red.ts, src/features/salas/conexion-salas.ts, src/features/mesa/originales.ts).

Registration on the Rust side

// src-tauri/src/lib.rs
.invoke_handler(tauri::generate_handler![
    commands::sistema::ping,
    commands::carpetas::carpeta_recordar,
    // ... 216 entries
])

Each area is a module in src-tauri/src/commands/ (mod.rs lists them): actualizar, cards, carpetas, decklist, diseno, export, fichero, fuentes, iconos, identify, imagen, layout, library, mesa, pasar, red, salas, sistema, stats, temas. The two link commands (enlace_pendiente, enlace_invitacion) live outside that folder, in src-tauri/src/enlace.rs, and so does pbdeck_pendiente (src-tauri/src/abrir.rs). Commands take State<'_, AppState> (see Rust App Layer), sometimes an AppHandle (to emit events or use plugins), and return AppResult<T> (Result<T, AppError>). Some return T directly because they cannot fail: cheap lookups (ping, parse_decklist, deck_list_text, arrange_cards, cache_stats, themes_installed, ocr_ready, names_index_info, design_structure, design_remove, update_status, red_estado, salas_estado, enlace_pendiente, enlace_invitacion), the cancel commands (job_cancel, export_cancel), the live-movement datagrams (mesa_fantasma, mesa_senal, unit result), and the jobs that only hand back an id and report failures in their done event (resolve_decklist, export_pdf, identify_batch, mesa_empezar, mesa_retomar). The jobs red_unirse, red_empezar and salas_deck return AppResult<u32> because they can refuse before starting (Busy, a code that does not parse, no room); design_complete_lang also returns AppResult<u32> but never fails before the id.

Command reference

Files are under src-tauri/src/commands/. "Job" means the command returns a job id at once and reports progress and completion through events (see Long jobs).

System (sistema.rs)

Command Purpose
ping Returns "pong"; proves the bridge works.
ping_error Always fails with InvalidInput; proves errors cross the bridge and translate.
app_plataforma Plataforma (escritorio, android or ios, from cfg!(target_os)): the screen asks it once (src/lib/plataforma.ts) to decide touch behaviour and what to hide on a phone ("Show in folder", for instance). It is the same enum the room server's Hola carries so that a phone is not chosen as host.
app_salir app.exit(0): the Android Back button on the library has no "close", so the screen asks twice ("Press again to exit") and then calls this, after saving its own state.
app_pantalla_completa(si) Hides or shows Android's system bars. The screen no longer calls it: the activity keeps the app full screen all the time (Mobile). On desktop it does nothing (Unsupported from android.rs is taken as "nothing to do" by sin_barras_es_ok; any other error is returned).
app_compartir_texto(texto) Shares a text (a game code or invitation link) with another phone app. The plugin only shares files, so the text goes into invitacion.txt in the cache and is shared as text/plain. Unsupported outside Android: the screen only offers it there.

Folders for dialogs (carpetas.rs)

Command Purpose
carpeta_recordar Remembers the folder of a chosen path under app_settings key carpeta_importar or carpeta_exportar (TipoCarpeta).
carpeta_inicial Returns the path to open a dialog at: last folder of that kind (if it still exists), joined with a suggested file name.

Files on a phone (fichero.rs, and pbdeck_pendiente in src-tauri/src/abrir.rs; see Paths and content URIs)

Command Purpose
fichero_temporal(nombre) A fresh path inside the cache (app_cache_dir/fichero/<uuid>/<name>, name sanitised) where an export is written before sharing it. The copy is kept: Copia::conservar.
compartir_fichero(ruta) Shares a file with another app (ACTION_SEND). Only a content:// or a file inside app_cache_dir (canonicalised, so .. or a link cannot leave it); any other path is InvalidInput, motivo: compartir_fuera, so the webview cannot hand the database or the node key to another app (fichero::para_compartir). Runs in spawn_blocking. Unsupported outside Android.
pbdeck_pendiente The content:// of a .pbdeck that another app opened with this one (or that started the app), once (then null). Later ones arrive as the pbdeck://abierto event. Nothing is imported by Rust at that moment: the screen offers it, with the importer's options, and the person confirms.

Updater (actualizar.rs; logic in src-tauri/src/actualizar.rs, see Releases and Auto Updates)

Command Purpose
update_status Current version, whether this install can update (soportada) and instalaElSistema (Android: the system installer asks to confirm and relaunches the app; there is no "restart").
update_check Looks up latest.json; returns ActualizacionInfo or null.
update_download Downloads the found version; emits update://progress.
update_install Installs the downloaded version and restarts. Refuses with Busy while AppState::ocupado().

Decklist text (decklist.rs; see Decklist Parsing, Export)

Command Purpose
parse_decklist Parses a pasted list into ParsedDecklist (proxy_core::decklist::parse). Sync and cheap; called while typing.
deck_list_text Writes the deck as a list in a FormatoLista (Moxfield, Arena, MTGO, plain) for copying.
deck_list_save Saves that list to an absolute .txt path (rejects anything else with InvalidInput).

Layout (layout.rs; see Layout Engine)

Command Purpose
compute_page_plan layout::plan(&deck) to PagePlan. Errors: InvalidInput with motivo card_does_not_fit or invalid_settings.
arrange_cards "Auto arrange": returns the new card id order.

Library: decks, tags, blobs, settings, presets, packages (library.rs; see Deck Library, Storage and Blobs, Deck Package Format)

Command Purpose
deck_list Summaries of all decks.
deck_create New empty deck with the app's generic back.
deck_open Loads a deck (and records it as opened).
deck_peek Loads a deck without recording it as opened (cover picker).
deck_details Colors, class, price, languages, formats of decks (all if ids is empty), from the Scryfall cache, no network.
deck_save Persists the deck; returns SaveResult { updatedAt }. Called by autosave.
deck_rename / deck_duplicate / deck_delete Rename, copy with a new name, delete a deck.
tag_list / tag_create / tag_update / tag_delete User tags (create returns the existing tag if the name exists).
tag_icon_set Sets or clears (null) a tag's icon, an Iconify id (InvalidInput, motivo: icono, if it is not one).
deck_tags_set Adds and removes tags on several decks at once.
deck_pin Favorite flag for several decks.
deck_notes_set Notes text of a deck.
deck_cover_set Chosen cover card (or null).
deck_lang_set Manual deck language override (or null to compute it).
blob_import_files Copies user-chosen image files into the blob store; returns FaceImage[]. The names may be content:// URIs.
blob_generic_back The app's generic card back as a FaceImage.
blob_gc Deletes unreferenced blobs older than 24 h; Busy while a job runs.
optimizar_decks, optimizar_estimar(ids), optimizar_lanzar(ids) Decks with images to optimize, what it would free, and the job (optimizar://progress, optimizar://done; cancelled with job_cancel); Busy while a job runs or a game is open.
app_setting_get / app_setting_set Key/value settings in SQLite (app_settings).
cache_stats / cache_clear Size of / deletes app_cache_dir/renders. Never touches blobs.
deck_export_package Writes a deck with all images to a .pbdeck; returns bytes.
deck_import_package Imports a .pbdeck (new ids, images verified); returns the Deck.
preset_list / preset_save / preset_delete Named presets by PresetKind (print settings etc.).

Cards: Scryfall and lists (cards.rs; see Scryfall Integration, Add Card and Variants, Set Browser)

Command Purpose
resolve_decklist Job. Resolves a ParsedDecklist to cards with stored images. Events resolve://progress, resolve://done.
job_cancel Cancels any job started from AppState::jobs (same implementation as export_cancel). Returns whether the job still existed.
card_autocomplete Scryfall autocomplete names.
card_search Scryfall syntax search, at most 50 results (CardSummary[]); optional language and highres-only.
card_prints All printings of a card name (variant picker).
cartas_buscar One page of the Search screen (FiltrosCartas, page from 1) → PaginaCartas (cartas, total, hayMas). No query or no results is an empty page, not an error. See Card-Search.
carta_ficha / carta_aclaraciones The details of a printing (FichaCarta); its rulings (AclaracionCarta[]).
card_fetch Downloads one printing and returns it as a DeckCard.
card_fetch_externa Same for a card from a non-Scryfall source (FuenteCarta, today Cockatrice tokens).
deck_tokens_detect Proposes missing tokens for a deck (FichaPropuesta[]); adds nothing.
sets_list / set_cards Set catalogue; cards of a set, optionally by type.
import_archidekt Imports a public Archidekt deck by URL.

Export (export.rs; see Export, Imaging and PDF)

Command Purpose
export_pdf Job. Exports the given deck snapshot to a PDF path or content:// (several files if the deck splits; on Android, one ZIP). Events export://progress, export://done.
export_estimate Approximate PDF size (bytes, images, pages), how many PDFs (files) and the memory warning (memory, lowMemory); see Export.
export_calibration Writes the duplex calibration sheet; texts come from the screen.
export_cancel Cancels the export job.
export_open / export_reveal Open the file with the system viewer / show it in its folder. Only existing .pdf or .pbdeck files are accepted (exportado()), else InvalidInput. On Android export_open shares the file instead and export_reveal is Unsupported.

Single card image (imagen.rs)

Command Purpose
card_export_image Saves one face of a card (front or back; deckBack is the deck's back) as an image, the same one the PDF would print (edits, darkening, designed card), at the PDF quality's DPI and without bleed. The format comes from the file extension (PNG if there is none; an unknown one is InvalidInput with motivo: formato). Returns the written path. Runs in spawn_blocking.

Statistics (stats.rs; see Statistics)

Command Purpose
deck_stats DeckStats for cards and an optional DeckKind; asks Scryfall /cards/collection for missing or stale (over 24 h) data.

Identify and link own cards (identify.rs; see Card Identification and OCR)

Command Purpose
ocr_ready Are the OCR models downloaded?
card_identify Proposes Scryfall cards for one stored image (hint text and optional OCR).
identify_batch Job. Identifies many images; per-item result in identify://progress, end in identify://done. Cancel with job_cancel.
card_link Card info for a chosen Scryfall id (cache first, then network, then stored for offline stats).
card_link_externa Same for an external-source card.

Name index and themes (fuentes.rs, temas.rs; see External Sources, Themes and Skins)

Command Purpose
names_index_info / names_index_download / names_index_remove Status, download (emits names://progress) and removal of the MTGJSON multi-language name index.
themes_catalog The tweakcn registry.
theme_fetch One theme by id or link, to preview.
theme_install / themes_installed / theme_remove Install (CSS + fonts), list, remove tweakcn themes.
themes_open_community Opens the tweakcn community gallery in the browser.

Icons (iconos.rs; see Icons)

Command Purpose
iconos_colecciones Every visible Iconify collection (ColeccionIconos { prefijo, nombre, total, categoria }), cached a week.
iconos_nombres(prefijo) The names of one collection, cached a week.
iconos_buscar(texto) Search by name across every collection (up to 999 ids).
iconos_datos(ids) { id: DatosIcono { body, vb } } for the icons asked for (memory, chosen icons on disk, or the API); unsafe SVG bodies are dropped.
iconos_elegir(id) Stores a chosen icon in app_data_dir/iconos/elegidos.json so it shows offline; NotFound if it does not exist.

Card designer (diseno.rs; see Designer App Layer, Designer Engine, Frame Packs)

Command Purpose
design_draft Draft design for a deck card (mechanical snapshot EN/ES, original names, next free footer number).
collection_list / collection_save / collection_delete Design collections.
fonts_list / font_import / font_remove Own fonts.
packs_list / pack_import / pack_remove Frame packs.
fonts_free_list / font_free_download Free (OFL) font catalogue and download.
original_info / original_download State and download of the "Original" frames (emits original://progress).
packs_downloadable_list / pack_downloadable_download Downloadable packs and download (emits packs://progress).
pack_extra_use Copies a pack extra (back, token, finished card) into the blob store.
symbols_status Mana-symbol status; downloads if missing or stale (refresh).
design_register Registers the design being edited for pbimg://…/d/<hash>/<width>; returns the hash.
design_overlay The card without artwork plus the artwork window (pbimg://…/c/…), for live artwork moves; null for composed or large cards.
design_fit_report Warnings: text that does not fit, missing symbols or fonts.
design_structure How a face is laid out (layout and split text) for the editor.
design_suggest_translation Experimental translation suggestion for a face.
design_drafts_for_deck Job. Drafts for all undesigned cards. Events design://progress, design://done.
design_complete_lang Job. Completes a design language on the deck's designed cards: downloads the CJK fonts for Japanese, Korean or Chinese and the printing each card is missing. Each finished card arrives in designlang://progress, the end in designlang://done; cancel with job_cancel.
design_export_png Job. Exports designed cards as PNG into a folder. Events designpng://progress, designpng://done.
design_save / design_remove Apply a design to a card (thumbnail into blob store) / revert the card.
design_rerender / design_renumber Refresh stale thumbnails / renumber designs by deck order; return the changed cards.
art_from_clipboard / art_from_blob / art_from_scryfall / art_from_card Obtain artwork: system clipboard, stored file, Scryfall art_crop, crop of an existing card (fractions 0–1).

Game table (mesa.rs; logic in src-tauri/src/mesa/, see Game Table)

Command Purpose
mesa_empezar Job. Prepares the decks of a ConfigMesa and opens the game. Events mesa://progress, mesa://done.
mesa_retomar Job. Resumes the saved game, preparing again any image missing from the cache. Same events.
mesa_estado EstadoMesa { abierta, deckIds, guardada }: whether a game is open and, if not, the summary of the saved one.
mesa_suscribir Takes a Channel<MensajeMesa>; sends the whole view and, from then on, one patch per event.
mesa_vista The whole VistaMesa (when a patch arrives out of order), without the log.
mesa_accion Sends an Accion (optionally como another local player); the change arrives through the channel, a refusal is Rejected (espectador for a spectator).
mesa_mirar_como In a local game of several, shows (and plays as) another player; Rejected red online.
mesa_encuentro, mesa_banquillo, mesa_banquillo_cambiar, mesa_siguiente_partida The best-of scoreboard, a player's deck and sideboard, swapping cards between games and opening the next game. mesa_banquillo and mesa_banquillo_cambiar are async: in an online game each player changes only their own deck, and a guest asks the host and waits up to 10 s (Network sin_respuesta otherwise). Rejections: ajeno, invitado, sin_terminar, sin_mazos, cambio.
mesa_resumen The ResumenPartida of the open game (places, rounds, duration), counted by mesa::resumir from the log.
partidas_de_deck HistorialDeck of a deck: its finished games (FilaPartida), wins and the games' ids.
partidas_todas HistorialGeneral: a page of every finished game of every deck (limite, desde), with the total and the wins.
partida_borrar Deletes a game from the history together with its replay.
repeticion_abrir Opens a game of the history as a replay in its own session (VistaRepeticion); Rejected partida_abierta with a game open, sin_registro without a record, Unsupported with version if it comes from a newer catalogue.
repeticion_actual, repeticion_ir, repeticion_salir Replay the game that has just ended, in place (Rejected sin_terminar if it has not ended); go to a point (pos, como); leave the replay (back to the finished game or, if it was opened from the history, close the table). While replaying, mesa_accion is Rejected repeticion except for chat and quick notices; the actions of other players online keep being applied to the live game.
mesa_guardar Saves the game now (leaving the tab, closing the window).
mesa_salir Leaves the table: with guardar the game stays saved, without it the file is deleted.
mesa_info_carta Rules text of a card the viewer may see («Inspeccionar»).
mesa_origen_scryfall An OrigenCarta for a Scryfall card or token not in the deck (downloads its images).
mesa_sparring_carta The OrigenCarta of one of the sparring phantom's cards, by name (only names in its pool; InvalidInput otherwise).
mesa_originales Starts «Originals in my language»: for the given Scryfall-linked cards (≤ 1,000) looks up the printing in the interface language and answers each through mesa://originales. Screen only.
mesa_listas ListasDeDeck { listas, cuentan } of a deck: its game lists and how many cards count without one.
mesa_lista_cartas The deck's eligible copies with their data (CartaDeLista, from Scryfall or the design) for the editor.
mesa_lista_comprobar AvisoLista[] of a list for a format (only warnings or blocking ones, see Game Table).
mesa_lista_guardar / mesa_lista_borrar Save (create or replace; returns it with its id) or delete a game list. mesa_empezar refuses a deck that needs a list and has none (InvalidInput, motivo: falta_lista) or a list that is not valid (motivo: lista).
mesa_tapete_get / mesa_tapete_set / mesa_tapete_ajustes The mat of a deck (deck: null = the general one): read, set or remove, change only veil and blur.
mesa_dados_get / mesa_dados_set / mesa_dados_imagen The look of the player's dice and coin (VistaAspectoDados): read, save (colours checked), set or remove the image of their own.

Online play with a direct code (red.rs; logic in src-tauri/src/red/, see Online Play)

Command Purpose
red_identidad / red_identidad_set The Identidad used online (public key, nickname, avatar); change the nickname and the avatar (a blob hash or none).
red_identidad_exportar / red_identidad_importar Save the installation's identity to a file / replace it with one exported before. Importing is Busy (motivo: partida) with a game, a direct-code room or a server room open; a file that is not an identity is InvalidInput identidad.
red_estado EstadoRed { papel, codigo, sala, empezada, puedeCeder }: whether this app hosts, plays or watches, the code to invite (host only), who is in the room (host only; a guest gets it through red://aviso; each row has an id for choosing seats and teams) and, for the host, whether leaving now hands the game over to another player (puedeCeder) instead of closing it.
red_crear Opens an online game as host: starts the node, serves whoever comes with the code and returns it (PB1-…). Busy partida if a game or room is already open.
red_unirse Job. Joins with a PeticionUnion (code, nickname, role, deck, list, format): prepares the deck (if playing) and connects. Events mesa://progress, mesa://done; a refusal from the host is Rejected with its motivo.
red_apodo Changes this app's name in the waiting room: the host tells everyone at once, a guest asks the host (AlHost::Apodo). NotFound sin_sala without a room.
red_anfitrion_deck The host changes the deck name it is seen with in the waiting room (None: none). NotFound sin_sala if there is no room or it has started.
red_deck Job. A guest changes the deck (and list) it plays with in the waiting room: prepares it and sends it to the host. Events mesa://progress, mesa://done.
red_vuelta The online game this guest dropped out of and can go back to (Option<VueltaRed>: nickname and time), from red/vuelta.rs.
red_vuelta_olvidar Forgets that game.
red_volver Job. Joins that game again with its code, as a player without a deck (the host recognises the node and gives the seat back); also with the table open after losing the connection. NotFound sin_vuelta without one; a Rejected from the host forgets it.
red_empezar Job. The host starts the game with those in the room who brought a deck; config carries the host first (and their deck) and, after it, what the host chose for each guest (colour and team) in the order of invitados, the room ids (EnSala.id) by seat; if they are not exactly the players with a deck in the room, Rejected sala_cambio. There is no sparring online. Same events. NotFound sin_sala without an unstarted room.
red_salir Leaves the online game (in the room or started): the host hands it to another player if it can (Anfitrion::irse) and otherwise closes it for everyone; a guest says goodbye. The table itself is closed by mesa_salir.
mesa_fantasma, mesa_senal Live movement: send a ghost (card or cursor, coordinates 0–10,000) or a 2 s ring as datagrams; the host sets the sender and limits the rate. Without an online game, or as a spectator, they do nothing.

Room server and playgroups (salas.rs; client in src-tauri/src/red/servidor/, see Online Play)

Every command that needs the server fails with Network sin_servidor when there is no client; the server's own refusals come back as Server (see Error model).

Command Purpose
salas_conectar Starts the server client if it was not running (with the nickname) and returns EstadoSalas.
salas_estado EstadoSalas: whether a server is configured, whether it is connected, this installation's key (yo), the room and the role in it, and the deck prepared for the room.
salas_playgroups / salas_pg_crear / salas_pg_salir / salas_pg_expulsar / salas_pg_rol List, create, leave a playgroup, expel a member (by key), change a member's RolPlaygroup; each returns the new Playgroup[].
salas_invitar / salas_revocar An Invitacion { codigo, caduca } to a playgroup (or, with null, to the room one is in) / revoke one.
salas_usar Uses an invitation code; returns Usada (playgroups or the room that was entered).
salas_sala_crear / salas_sala_unirse Create a room (SalaNueva, 2–10 players) / join one as player or spectator; returns EstadoSala. Both, and salas_usar, are Busy partida with a game or a direct-code room open.
salas_sala_salir Leaves the room (and its game, if any).
salas_sala_expulsar, salas_sala_expulsar_jugador Expel a member from the room (by key) or a player from the table (by seat): the creator through the room server, or the host with a direct code (Rejected no_anfitrion otherwise).
salas_deck Job. Prepares the deck to play in the room and tells the server. Events mesa://progress, mesa://done; NotFound sin_sala outside a room.
salas_empezar / salas_terminar Tell the server the room's game starts / has ended.

Moving decks between devices (pasar.rs; the logic in src-tauri/src/pasar/, see Deck Sharing)

Command Purpose
pasar_recibir_abrir / pasar_recibir_estado / pasar_recibir_cerrar / pasar_recibir_cancelar "Receive": open it (starts the shared iroh node if needed; returns the EstadoReceptor with the PBD1- code, its QR and the six-digit code), read its state, close it, or cancel what is being received. Changes arrive as the pasar://receptor event.
pasar_peticion(aceptar) / pasar_responder(decisiones) Accept or refuse a device on the local network that wants to send; answer, deck by deck, what to do with the offer (Decision: how much to take and, if the same deck already exists, Resolucion).
pasar_cerca_buscar / pasar_cerca_parar Look for devices nearby that have "Receive" open (mDNS; takes the Wi-Fi multicast lock on Android); the list arrives as pasar://cerca.
pasar_compartir_codigo(texto) Offers the «Recibir» code to another app through the system's share sheet. The text must be a valid PBD1- code. Android only; elsewhere Unsupported.
pasar_codigo_comprobar(texto) Checks a pasted or scanned PBD1- code before sending. A game code (PB1-) pasted here is InvalidInput with motivo: codigo_de_partida; and a deck code pasted where a game code goes (red_unirse) is codigo_de_deck, which has its own sentence. See Error model.
pasar_nombre / pasar_nombre_set(nombre) This device's name when passing decks; empty returns to the system's. Async: on Android it asks the Kotlin plugin.
pasar_enviar(destino, decks, modo) / pasar_enviar_cancelar / pasar_emisor_estado / pasar_emisor_olvidar Send decks to a Destino (nearby device or code); returns at once, progress arrives as pasar://emisor.
pasar_completar(deckId) Job (an id, with pasar://imagenes-progress and pasar://imagenes-done): download the images a deck received as "originals only" is missing.
deck_paquete_info, deck_paquete_guardar, deck_paquete_compartir Size of a deck's .pbdeck in each ModoPaquete (original, optimizado, sinProxies); save it (a path or content://) or share it with another app in one of them.
paquete_leer, paquete_importar Read a .pbdeck without importing it (what it holds and whether the same deck is already here) / import it with what the person chose; InvalidInput eleccion if nothing was chosen and falta_resolucion if the deck already exists and no resolution was given.

Invitation links (src-tauri/src/enlace.rs)

Command Purpose
enlace_pendiente The code of the proxybuilder://unirse/<code> (or tapete://unirse/<code>) link that started the app, once (then null). Links that arrive later come as the salas://enlace event.
enlace_invitacion The invitation link of a code (the one copied in the panel), or null.

Sync versus async

Plain fn commands are synchronous and must be cheap (SQLite lookups, layout::plan). async fn commands await network or offload work. Heavy CPU work is explicitly offloaded: export_pdf uses tauri::async_runtime::spawn_blocking; the designer wraps renders in a helper that runs on a separate thread (commands/diseno.rs, "Corre f en un hilo aparte"). Inferred: because Tauri runs plain sync commands on the main thread, keeping them cheap matters for UI responsiveness; check the Tauri docs when adding a heavy sync command.

Error model

AppError

Defined in src-tauri/src/error.rs:

pub struct AppError { pub code: ErrorCode, pub params: BTreeMap<String, String> }
impl AppError { pub fn new(code) -> Self; pub fn with(self, clave, valor: impl ToString) -> Self }
pub type AppResult<T> = Result<T, AppError>;

It derives Serialize and TS, so a failed invoke rejects with the JSON object { code: "InvalidInput", params: { motivo: "…" } }. Rust never writes user-facing prose: params carries data for the sentence, not a message.

ErrorCode has 14 variants:

Code Meaning Typical sources
Network No connection or server not answering; with motivo from error::red / error::estado_http (HTTP) or error::red_sin_http (iroh, mDNS), a sentence per cause error.rs (red, estado_http, red_sin_http; the last one used by pasar/), used by scryfall/client.rs, fuentes/mod.rs, commands/cards.rs (Archidekt), ocr.rs, diseno/{letras_libres,letras_cjk,original}.rs; updater (Reqwest, Network)
RateLimited HTTP 429 after retries scryfall/client.rs
NotFound Card, deck, blob, theme, update version missing render.rs leer_blob (blob param), card_link, actualizar.rs
Io Disk read or write From<std::io::Error>, export_open failures, a content:// that cannot be opened (fichero.rs io_uri)
Db SQLite error From<rusqlite::Error>, lock poisoning is Internal
InvalidInput Nonsense from the screen From<LayoutError>, deck_list_save, exportado(), card_export_image (motivo: formato), game lists (falta_lista, lista, lista_sin_nombre), online (codigo, deck, identidad), a code of the wrong kind (codigo_de_deck, codigo_de_partida, red/codigo.rs), moving decks (eleccion, falta_resolucion, oferta_grande, sin_espacio, sin_oferta, huella, importar), fichero.rs leer_bytes over its limit (fichero_grande) and compartir_fichero with a path outside the cache (compartir_fuera)
Cancelled The user cancelled jobs flag checks in resolve, export, identify, PNG, design batch; PdfError::Cancelled; updater AuthenticationFailed
Pdf PDF could not be built From<PdfError>, krilla image errors in exportar
Image Image unreadable or too large From<ImagingError>, From<ProbeError>
Unsupported File format not accepted, or something that exists only on another platform ImagingError::Unsupported, ProbeError::Unsupported; mesa/guardada.rs with motivo: version for a saved game or replay from a newer catalogue; android.rs outside Android (detalle: android), export_reveal on a content:// (detalle: content), elegirCarpetaParaExportar on a phone (detalle: carpeta, thrown on the screen) and, on Android, motivo: instalar_permiso when the system does not yet allow this app to install packages (see Releases and Auto Updates)
Busy A job, or a game open on the table, prevents this now (motivo: partida when it is the game or an online room) blob_gc, optimizar_lanzar, update_install, red_crear, red_unirse, red_volver, red_identidad_importar, salas_usar, salas_sala_crear, salas_sala_unirse, and JS-side guards
Internal Unexpected (programming error, failed thread pool, poisoned lock) various map_err
Rejected The game table refuses that action. params.motivo is either the engine's Rechazo (NoExiste, SinPermiso, ZonaVacia, FueraDeTurno, Limite, NoSePuede, written with {:?}) or a reason of the app layer in snake_case: the session (repeticion, como, red, no_existe, no_anfitrion), the best-of match (invitado, sin_encuentro, sin_terminar, encuentro_terminado, cambio), the replay (partida_abierta, sin_registro, sin_terminar, no_es_repeticion), a spectator acting (espectador), a direct-code host without the majority (sin_mayoria), and the host's refusal to a guest (codigo, version, llena, empezada, sin_deck, ritmo, sin_partida, expulsado, no_activo…) mesa/sesion.rs (impl From<Rechazo> for AppError), mesa/encuentro.rs, mesa/repeticion.rs, commands/mesa.rs (mesa_accion, repeticion_abrir, repeticion_actual, mesa_info_carta), red/invitado.rs, salas_sala_expulsar_jugador
Server The room server said no; params.code is the protocol code (version, firma, limite, no_existe, caducada, llena, sin_permiso, ya_empezada, expulsado, mensaje), with the server's params red/servidor/cliente.rs (pedir, and the handshake, which carries only code)

Parameter keys used across the app (counted by grep): detalle (free-form technical detail, most common), motivo (machine reason such as card_does_not_fit, invalid_settings, too_large, corrupt), estado (HTTP status), blob, url, fichero, deck, carta, ficha, plus pack-loading keys (estilo, hueco, falta, color, version). Gotcha: most sentences in src/lib/errores.ts contain no {placeholders}, so these params are usually not shown to the user; they are for logs, tests and future texts. The exception is estado in the Network rechazo and estado sentences.

proxy-core errors and conversion

proxy-core does not know AppError. Each module has a thiserror enum and the app converts with From:

Core error Defined in Converted in Becomes
LayoutError (CardDoesNotFit, InvalidSettings) proxy-core/src/layout/mod.rs src-tauri/src/error.rs InvalidInput + motivo
ImagingError (Unsupported, Corrupt, TooLarge, Encode) proxy-core/src/imaging/mod.rs src-tauri/src/render.rs Unsupported; Image + motivo=too_large; Image + detalle
ProbeError (Unsupported, Corrupt) proxy-core/src/imaging/probe.rs src-tauri/src/store/blobs.rs Unsupported; Image + motivo=corrupt
PdfError (Geometry, Image, Write, Cancelled) proxy-core/src/pdf/mod.rs src-tauri/src/commands/export.rs Cancelled; otherwise Pdf + detalle
PackError proxy-core/src/diseno/pack.rs src-tauri/src/diseno/recursos.rs → de_pack pack keys (falta, estilo, hueco, color, version)
DisenoError proxy-core/src/diseno/mod.rs designer app layer Inferred: mapped at call sites with map_err (not read in full)
LineError proxy-core/src/decklist/mod.rs not an error type at the bridge Serialized inside ParsedLine; the screen shows a text per variant

Also in the app crate: From<std::io::Error> (Io) and From<rusqlite::Error> (Db) in error.rs; the updater maps tauri_plugin_updater::Error in actualizar.rs → error().

Both crates deny unwrap()/expect() outside tests (#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]), so failures must flow into these types.

Translation in the UI

src/lib/errores.ts:

  • TEXTOS: Record<ErrorCode, string> has one Spanish source sentence per code, wrapped in paraTraducir(...) so the i18n tooling (pnpm idiomas) sees it; English comes from src/i18n/en/. The Record<ErrorCode, …> type makes the compiler fail if Rust adds a code the screen does not cover (after pnpm bindings).
  • esAppError(e): object whose code is a key of TEXTOS. Rejections from invoke are unknown, and a plain JS object such as { code: "Busy", params: {} } qualifies, which is how the front end raises Busy itself.
  • MOTIVOS: Partial<Record<ErrorCode, Record<string, string>>>: a sentence of its own for known reasons of a code, looked up by params.motivo or, failing that, params.code. Today Rejected has one per Rechazo variant plus the host's refusals (see below) and partida_abierta, sin_registro, sin_terminar and repeticion (game history and replay); InvalidInput has the game list reasons (falta_lista, lista, lista_sin_nombre) and codigo, deck, identidad; Unsupported has version and instalar_permiso (Android: «Android needs your permission to install the update: its setting has been opened…»); InvalidInput also has codigo_de_deck (the code of "Receive a deck", PBD1-, pasted where a game code goes); Busy has partida («Hay una partida abierta: termínala o sal de la mesa para actualizar.», written for the update but also shown when an online command is refused for the same reason); and Server has one per protocol code. Reasons with no entry (como, red, no_existe, no_es_repeticion, invitado, sin_encuentro, encuentro_terminado, cambio) show the code's general sentence. An unknown reason falls back to the code's general sentence, so a newer engine never shows a raw reason.
  • mensajeDeError(e, t): AppError → t(MOTIVOS[code][motivo] ?? TEXTOS[code], params); Error → its message; anything else → String(e).
  • useLang().mensaje(e) (src/components/language-provider.tsx) is mensajeDeError bound to the active t. Screens call mensaje(e) in catch blocks and show it in a toast or inline.

Guarded by src/lib/__tests__/errores.test.ts and the Rust test el_error_viaja_como_codigo_y_datos in error.rs.

Errors inside jobs

A job command never rejects for work failures; its done event carries error: AppError | null next to result. The front end checks d.error and treats Cancelled as a quiet outcome (src/features/decklist/acciones.ts → resolver returns null; src/features/export/acciones.ts → exportarA announces "Exportación cancelada").

Online play reasons

The texts of src/lib/errores.ts::MOTIVOS also cover online play: Network with motivo sin_servidor, sin_respuesta or desconectado; NotFound sin_sala; InvalidInput codigo (a direct code that does not parse), deck and identidad (an imported file that is not an identity); Rejected from the host (codigo, version, llena, empezada, sin_deck, ritmo, espectador, sin_partida, expulsado, no_anfitrion, besides the engine's reasons; a refusal of an action sent later reaches a guest as MensajeMesa::Rechazo, not as the error of mesa_accion); and every Server code, expulsado included. red and como (looking or acting as another player online) have no sentence of their own. Network notices go as events (red://aviso, salas://…), not errors. See Online Play.

Plain HTTP failures (Scryfall, Archidekt, external sources, downloads) build their Network error with crate::error::red(reqwest::Error) or crate::error::estado_http(StatusCode), never by hand, so the screen can tell them apart: conexion (could not connect: check the connection, firewall or antivirus), tiempo (timed out), respuesta (an answer the app does not understand), rechazo (401/403, shown with {estado}: usually a VPN, a proxy or a blocked network) and estado (any other error status, with {estado}). Both functions also write a warn line to proxy-builder.log with the host and the full chain of causes, which is what to ask for when someone reports a network problem. Network failures that are not HTTP (the iroh connections and mDNS of Deck Sharing) use crate::error::red_sin_http(motivo, Option<&dyn Error>) instead: the same Network with motivo, and with an underlying error the same chain of causes in the log and in detalle; without one (a timeout, a rejection from the other side) only the motivo is logged. The code stays Network, so retries and matches!(e.code, ErrorCode::Network | …) keep working. See Scryfall Integration.

ts-rs bindings

Shared types derive TS and carry #[ts(export)]; some also #[serde(rename_all = "camelCase")]. There are 295 generated files in src/bindings/ (do not edit; the header says so).

  • pnpm bindings runs cargo test --manifest-path src-tauri/Cargo.toml --workspace export_bindings. ts-rs generates one export_bindings_<Type> test per exported type, and running the tests writes the files.
  • The output directory is set by .cargo/config.toml at the repo root: TS_RS_EXPORT_DIR = { value = "src/bindings", relative = true }. It must live at the root, not in src-tauri/.cargo/, because Cargo reads config from the directory it is launched in, and the pnpm scripts launch from the root.
  • u64/i64 fields use #[ts(type = "number")] (e.g. ExportResult.bytes, SaveResult.updatedAt) to avoid bigint.
  • Doc comments on Rust types are copied into the .ts files (note the Spanish text in AppError.ts).
  • ErrorCode is a string union; AppError.params is { [key in string]: string }.
  • CI runs pnpm check (which includes tsc --noEmit), so a stale binding that no longer matches Commands fails the build. See Testing and CI.

Long jobs and progress events

Rust side

src-tauri/src/jobs.rs → Jobs (field AppState.jobs):

Method Behaviour
alta() Allocates the next u32 id (atomic counter) and an Arc<AtomicBool> cancel flag; returns both.
cancelar(id) Sets the flag; false if the job is already gone.
baja(id) Removes the job; the job calls it when done.

Separately, AppState::trabajo() returns a TrabajoEnMarcha guard (RAII, counter trabajos); AppState::ocupado() is true while any guard lives. It is used so blob GC and updater installs never run under a long job. Guards are held by resolve_decklist, export_pdf, identify_batch, design_drafts_for_deck, design_complete_lang, design_export_png, names_index_download, card_fetch, card_fetch_externa, deck_tokens_detect, mesa_empezar, mesa_retomar, red_unirse, red_volver, red_empezar, salas_deck (grep for .trabajo()). ocupado() is also true while a game is open on the table (AppState::hay_partida()), because an update would restart the app in the middle of it.

Canonical job command shape (from resolve_decklist and export_pdf):

  1. let (job_id, cancelado) = state.jobs.alta();
  2. Spawn (tauri::async_runtime::spawn or spawn_blocking), hold let _marca = estado.trabajo();
  3. Run, calling app.emit("<name>://progress", XProgressEvent { job_id, … }), checking cancelado.load(...) between steps and returning Cancelled.
  4. estado.jobs.baja(job_id), then app.emit("<name>://done", XDoneEvent { job_id, result, error }).
  5. Return job_id immediately.

Cancellation is cooperative: nothing is killed; the worker checks the flag. Both job_cancel and export_cancel call jobs.cancelar.

Events

Event Payload (src/bindings/) Emitted by Listened by
resolve://progress / resolve://done ResolveProgressEvent { jobId, progress } / ResolveDoneEvent { jobId, result, error } resolve_decklist features/decklist/acciones.ts via lanzarTrabajo
export://progress / export://done ExportProgressEvent { jobId, phase (prepare/compose/write), done, total } / ExportDoneEvent export_pdf features/export/acciones.ts
identify://progress / identify://done IdentifyProgressEvent { jobId, done, total, blob, … } / IdentifyDoneEvent { jobId, error } identify_batch features/variants/vincular-lote.tsx
design://progress / design://done DesignProgressEvent { jobId, done, total, … } / DesignDoneEvent { jobId, error } design_drafts_for_deck (diseno/lote.rs) features/disenar/lote-dialog.tsx
designlang://progress / designlang://done The same DesignProgressEvent (with the finished card) / DesignDoneEvent design_complete_lang (diseno/idioma.rs) features/disenar/renumerar.ts (useTrabajo type idioma)
designpng://progress / designpng://done DesignPngProgressEvent, DesignPngDoneEvent design_export_png (diseno/png.rs) features/disenar/png-dialog.tsx
mesa://progress / mesa://done MesaProgressEvent { jobId, mazo, mazos, hechas, total } / MesaDoneEvent { jobId, error } mesa_empezar, mesa_retomar, red_unirse, red_volver, red_empezar, salas_deck features/mesa/conexion.ts (conProgreso, also called from features/mesa/red/conexion-red.ts and features/salas/conexion-salas.ts)
original://progress { done, total } original_download features/disenar/marcos-originales.ts
packs://progress { id, done, total } pack_downloadable_download features/disenar/packs-descargables.ts
names://progress { fase, hecho, total } names_index_download features/settings/nombres-idiomas.tsx
update://progress { hecho, total } update_download stores/actualizacion-store.ts
ciclo://pausa / ciclo://vuelta none ciclo.rs (Android only): the app went to the background / came back lib/ciclo-de-vida.ts (saves the deck and the card being designed) / features/mesa/red/conexion-red.ts (the online game reconnects)
pbdeck://abierto string (a content://) abrir.rs, when another app opens a .pbdeck with this one features/library/abrir-pbdeck.ts
pasar://receptor, pasar://emisor, pasar://cerca EstadoReceptor, EstadoEmisor, EquipoCerca[] pasar/ features/pasar/estado.ts
pasar://imagenes-progress / pasar://imagenes-done ImagenesProgreso / ImagenesFin pasar_completar features/pasar/imagenes.ts
salas://enlace string (the code) enlace.rs, when a proxybuilder://unirse/… or tapete://unirse/… link reaches the running app features/salas/enlaces-entrantes.ts

original://, packs://, names:// and update://progress have no job id: they belong to a single awaited command (the invoke itself resolves when the download ends), are not cancelable, and are listened to directly with on().

Some events are not in Events and are listened to with listen from @tauri-apps/api/event, typed at the call:

Event Payload Emitted by Listened by
red://aviso AvisoRed red/mesa.rs (online game notices: room, start, connection, links) features/mesa/red/conexion-red.ts
salas://conexion, salas://playgroups, salas://sala, salas://enlaces, salas://host, salas://fuera boolean, Playgroup[], EstadoSala, EnlaceSala[], HostSala, MotivoFuera red/servidor/sala.rs features/salas/conexion-salas.ts
mesa://originales OriginalMesa (one per card) mesa_originales (commands/mesa.rs) features/mesa/originales.ts

Channels: the game table

The game table does not use events for its state: mesa_suscribir receives a tauri::ipc::Channel<MensajeMesa> created on the screen (new Channel() in src/features/mesa/conexion.ts, typed Channel<MensajeMesa> in Commands). A channel belongs to one subscriber and delivers in order, which a stream of patches needs (a lost or reordered patch would desynchronise the view). The session keeps it and sends Vista { vista, registro } (with the last 500 events that still count, Registro::vigentes(), REGISTRO_EN_VISTA in mesa/sesion.rs), Parche { parche, evento }, Cerrada, or, online, Rechazo { motivo } when the host did not accept an intention of this screen (it arrives later, not as the answer of mesa_accion); a patch whose version does not follow makes the screen ask mesa_vista. Subscribing again (after switching tabs) replaces the channel and starts with a full view. A spectator, who has no session of their own, subscribes to the view it watches (AppState.mirada, red/mirada.rs). See Game Table.

Front end: lanzarTrabajo

src/lib/trabajos.ts:

lanzarTrabajo(progreso, final, lanzar: () => Promise<number>, alProgreso)
  : Promise<{ jobId: number; fin: Promise<DoneEvent> }>

Invariants:

  1. Listen before launching. A short job may finish before invoke returns the id; its final event would be lost. So on(progreso) and on(final) are registered first.
  2. Events that arrive before the id is known are buffered (pendientesP, pendientesD) and replayed after the id arrives; later events are filtered by jobId.
  3. fin resolves with the done payload (even when it carries an error); both listeners are removed when it does. If lanzar() throws, listeners are removed and the error is rethrown.

Tested in src/lib/__tests__/trabajos.test.ts.

trabajo-store and the one-at-a-time rule

src/stores/trabajo-store.ts → useTrabajo holds the running resolve, export, design-language job or game table preparation (TipoTrabajo = "resolver" | "exportar" | "idioma" | "mesa"): { tipo, jobId, fase, hechas, total } with empezar, setId, avanzar, terminar. The footer of the left panel and the export button read it. resolver() and exportarA() throw { code: "Busy", params: {} } if actual is set: the one-job-at-a-time rule is enforced in the front end (Rust only counts running jobs for ocupado(); resolve_decklist itself does not refuse a second call). cancelarTrabajo() in acciones.ts calls export_cancel for exports and job_cancel otherwise. Designer batch, PNG export and bulk link use lanzarTrabajo from their own dialogs without useTrabajo.

File dialogs

Dialogs come from @tauri-apps/plugin-dialog (capability dialog:default), wrapped only by src/lib/dialogos.ts:

Function Behaviour
abrirParaImportar(opciones) open() starting in the last import folder; remembers the folder of the first chosen path. Returns string[] | null.
guardarParaExportar(opciones, nombre) save() starting in the last export folder with the suggested name; remembers the choice.
elegirCarpetaParaExportar() Folder picker for exporting loose images. Not on a phone (Unsupported, thrown on the screen): Android has no folder picker.
elegirDestinoDeImagenes({ titulo, nombre }) Where to put images that go into per-language folders: a folder on desktop, a ZIP on a phone.
recordar(tipo, ruta) Fire-and-forget carpeta_recordar; also used when files are dragged onto the app.

On Android the system pickers return a content:// URI instead of a path, so none of this applies there: nothing is remembered, defaultPath is only the proposed name, the filters become MIME types (filtrosParaAbrirEnMovil, filtrosParaGuardarEnMovil: Android does not know .pbdeck or .ttf, so an open filter containing an unknown extension is dropped and the person may pick anything, and a save filter with no known type falls back to application/octet-stream, with which Android keeps the name as typed) and the asking is esMovil() from src/lib/plataforma.ts. The mobile paths are in Paths and content URIs.

Import and export folders are remembered separately by Rust (commands/carpetas.rs, app_settings keys carpeta_importar and carpeta_exportar) so, for example, exporting a PDF does not move where "import images" opens. carpeta_inicial returns the suggested name alone if nothing is stored or the stored folder disappeared; a failing lookup falls back to the plain name in inicial(). CLAUDE.md requires all file dialogs to go through this module; src/lib/__tests__/dialogos.test.ts covers it.

Paths come back to the screen as plain strings (on Android, URIs) and go to Rust commands (export_pdf, deck_export_package, font_import…). Rust re-validates what it opens (exportado() in export.rs, .txt check in deck_list_save), because the WebView is not trusted to name arbitrary files. The capability file grants no fs permission at all.

Paths and content URIs

On desktop the screen hands Rust a path and Rust uses it. On Android a dialog returns a content:// URI instead: a permission the system gives to that one file, with no folder, often no extension in the URI and sometimes a provider that can only be read as a pipe. The rule is that every command that reads or writes a file the person chose goes through src-tauri/src/fichero.rs, never std::fs on a dialog path, and that on desktop the result is exactly what it was (a path is a path). Format and name come as parameters or from the name Android reports (DISPLAY_NAME), never from the URI.

Function What it does
es_uri(ruta) true for a string that starts with content://.
leer(app, ruta, nombre?) Puts the file within std::fs's reach: a path as it is; a URI copied into the cache (app_cache_dir/fichero/<uuid>/), because the .pbdeck is a ZIP and needs Seek, which a cloud provider's pipe does not give. The returned Copia deletes the folder when dropped.
leer_bytes(app, ruta, tope) The whole content; a URI over tope bytes is InvalidInput with motivo: fichero_grande.
escribir(app, ruta, nombre?, f) Gives f a local path to write: the real one, or a temporary file in the cache that is then copied to the URI. A URI cannot be renamed, so escribir_atomico (the .part then rename) is not available there.
escribir_bytes, copiar_a Write bytes / copy a local file to a path (in pieces, to a provisional name, then renamed) or to a URI.
temporal(app, nombre) A new empty path in the cache, removed on drop (Copia::conservar keeps it). Used for exports and for sharing.
para_compartir(app, ruta) Leaves the file where the Android project's FileProvider can see it (the cache) and returns the path and the MIME type (mime_de); the copy stays until the next sweep. Accepts only a URI or a file already inside the cache (compartir_fuera otherwise).
zip_de_carpeta, zip_de_lista Pack files into a ZIP (images, PDFs and ZIPs are stored, the rest deflated).
nombre_seguro, nombre_destino A name that can be created in any folder (no separators or control characters, not empty, 120 characters) / the name a write will have, the Android one for a URI.

Why a copy to the cache and not a streamed read: the importers (.pbdeck, ZIP packs, fonts) want a seekable file, and a copy turns the three kinds of source (path, local URI, cloud URI) into one. Why Acceso: the logic runs against the trait Acceso { abrir, nombre }, implemented over tauri-plugin-fs in the app (DeLaApp, which opens a URI with app.fs().open(..) and gets a real std::fs::File from the descriptor the system hands over) and over local files in the tests. tauri-plugin-fs is used only from Rust: no fs: permission is granted to the WebView. The cache folder is swept on use (anything older than 24 hours in fichero/ goes).

Opening a file from outside (another app, ACTION_VIEW) is src-tauri/src/abrir.rs: the Android plugin hands over a URI, which is kept (PENDIENTE) and announced with pbdeck://abierto; the screen asks pbdeck_pendiente once if it was not listening yet. Only content:// strings of at most 2048 characters count (valida). Rust does not import anything at that point.

Adding a command

  1. Write #[tauri::command] pub fn|async fn name(state: State<'_, AppState>, …) -> AppResult<T> in src-tauri/src/commands/<area>.rs; put real logic in a non-command function so tests can call it with an AppState built on tempfile dirs.
  2. Register it in generate_handler! in src-tauri/src/lib.rs.
  3. Add its entry to Commands in src/lib/ipc.ts (camelCase argument names).
  4. Derive Serialize, TS with #[ts(export)] on any new payload type; run pnpm bindings.
  5. Return errors as AppError with an existing ErrorCode; add a code only if the screen needs a distinct sentence (then add its text to TEXTOS and src/i18n/en/).
  6. For a long job: follow the five-step shape above, add both events to Events, call lanzarTrabajo from the screen.
  7. Run pnpm check, pnpm check:clippy, pnpm test:rust.

See also

Documentation · Documentación

Language: English · Español

Use and build · Uso y desarrollo

Architecture · Arquitectura

Data · Datos

Getting cards · Conseguir cartas

Layout, images, PDF · Maquetación, imagen y PDF

Card designer · Diseñador de cartas

Game table · Mesa de juego

Mobile · Móvil

Frontend · Frontend

Reference · Referencia

Clone this wiki locally