Repository navigation
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.
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_idisjobId,scryfall_idisscryfallId,type_filteristypeFilter,highres_onlyishighresOnly. Struct fields inside payloads follow each struct's#[serde(rename_all = "camelCase")]. -
Unit results become
null(result: nullinCommands), andOption<T>becomesT | null. -
Commandsis 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 aCommandsentry compiles fine and simply cannot be called from typed code. Today the 177 names ingenerate_handler!and the 177 keys ofCommandsmatch exactly (checked by diffing both lists). -
Direct Tauri imports outside
ipc.tsexist 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), theChannelclass of@tauri-apps/api/core(src/features/mesa/conexion.ts, see Channels), andlistenfrom@tauri-apps/api/eventfor the events that are not inEvents(src/features/mesa/red/conexion-red.ts,src/features/salas/conexion-salas.ts,src/features/mesa/originales.ts).
// src-tauri/src/lib.rs
.invoke_handler(tauri::generate_handler![
commands::sistema::ping,
commands::carpetas::carpeta_recordar,
// ... 177 entries
])Each area is a module in src-tauri/src/commands/ (mod.rs lists them): actualizar, cards, carpetas, decklist, diseno, export, fuentes, identify, imagen, layout, library, mesa, red, salas, sistema, stats, temas. The two link commands (enlace_pendiente, enlace_invitacion) live outside that folder, in src-tauri/src/enlace.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.
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. |
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. |
Updater (actualizar.rs; logic in src-tauri/src/actualizar.rs, see Releases and Auto Updates)
| Command | Purpose |
|---|---|
update_status |
Current version and how the app was installed. |
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). |
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[]. |
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. |
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). |
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 (several files if the deck splits). Events export://progress, export://done. |
export_estimate |
Approximate PDF size (bytes, images, pages). |
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. |
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. |
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. |
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_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. |
Invitation links (src-tauri/src/enlace.rs)
| Command | Purpose |
|---|---|
enlace_pendiente |
The code of the proxybuilder://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. |
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.
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 |
scryfall/client.rs (red(), 5xx after retries, estado param), diseno/letras_libres.rs, diseno/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 |
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) |
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 |
ImagingError::Unsupported, ProbeError::Unsupported; mesa/guardada.rs with motivo: version for a saved game or replay from a newer catalogue |
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, update_install, red_crear, red_unirse, 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: the Spanish sentences in src/lib/errores.ts contain no {placeholders}, so these params are currently not shown to the user; they are for logs, tests and future texts.
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.
src/lib/errores.ts:
-
TEXTOS: Record<ErrorCode, string>has one Spanish source sentence per code, wrapped inparaTraducir(...)so the i18n tooling (pnpm idiomas) sees it; English comes fromsrc/i18n/en/. TheRecord<ErrorCode, …>type makes the compiler fail if Rust adds a code the screen does not cover (afterpnpm bindings). -
esAppError(e): object whosecodeis a key ofTEXTOS. Rejections frominvokeareunknown, and a plain JS object such as{ code: "Busy", params: {} }qualifies, which is how the front end raisesBusyitself. -
MOTIVOS: Partial<Record<ErrorCode, Record<string, string>>>: a sentence of its own for known reasons of a code, looked up byparams.motivoor, failing that,params.code. TodayRejectedhas one perRechazovariant plus the host's refusals (see below) andpartida_abierta,sin_registro,sin_terminarandrepeticion(game history and replay);InvalidInputhas the game list reasons (falta_lista,lista,lista_sin_nombre) andcodigo,deck,identidad;Unsupportedhasversion;Busyhaspartida(«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); andServerhas 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) ismensajeDeErrorbound to the activet. Screens callmensaje(e)incatchblocks 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.
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").
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.
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 bindingsrunscargo test --manifest-path src-tauri/Cargo.toml --workspace export_bindings. ts-rs generates oneexport_bindings_<Type>test per exported type, and running the tests writes the files. - The output directory is set by
.cargo/config.tomlat the repo root:TS_RS_EXPORT_DIR = { value = "src/bindings", relative = true }. It must live at the root, not insrc-tauri/.cargo/, because Cargo reads config from the directory it is launched in, and the pnpm scripts launch from the root. -
u64/i64fields use#[ts(type = "number")](e.g.ExportResult.bytes,SaveResult.updatedAt) to avoidbigint. - Doc comments on Rust types are copied into the
.tsfiles (note the Spanish text inAppError.ts). -
ErrorCodeis a string union;AppError.paramsis{ [key in string]: string }. - CI runs
pnpm check(which includestsc --noEmit), so a stale binding that no longer matchesCommandsfails the build. See Testing and CI.
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_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):
let (job_id, cancelado) = state.jobs.alta();- Spawn (
tauri::async_runtime::spawnorspawn_blocking), holdlet _marca = estado.trabajo(); - Run, calling
app.emit("<name>://progress", XProgressEvent { job_id, … }), checkingcancelado.load(...)between steps and returningCancelled. -
estado.jobs.baja(job_id), thenapp.emit("<name>://done", XDoneEvent { job_id, result, error }). - Return
job_idimmediately.
Cancellation is cooperative: nothing is killed; the worker checks the flag. Both job_cancel and export_cancel call jobs.cancelar.
| 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_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 |
salas://enlace |
string (the code) |
enlace.rs, when a proxybuilder://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 |
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.
src/lib/trabajos.ts:
lanzarTrabajo(progreso, final, lanzar: () => Promise<number>, alProgreso)
: Promise<{ jobId: number; fin: Promise<DoneEvent> }>Invariants:
-
Listen before launching. A short job may finish before
invokereturns the id; its final event would be lost. Soon(progreso)andon(final)are registered first. - Events that arrive before the id is known are buffered (
pendientesP,pendientesD) and replayed after the id arrives; later events are filtered byjobId. -
finresolves with the done payload (even when it carries an error); both listeners are removed when it does. Iflanzar()throws, listeners are removed and the error is rethrown.
Tested in src/lib/__tests__/trabajos.test.ts.
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.
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. |
recordar(tipo, ruta) |
Fire-and-forget carpeta_recordar; also used when files are dragged onto the app. |
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 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.
- Write
#[tauri::command] pub fn|async fn name(state: State<'_, AppState>, …) -> AppResult<T>insrc-tauri/src/commands/<area>.rs; put real logic in a non-command function so tests can call it with anAppStatebuilt ontempfiledirs. - Register it in
generate_handler!insrc-tauri/src/lib.rs. - Add its entry to
Commandsinsrc/lib/ipc.ts(camelCase argument names). - Derive
Serialize, TSwith#[ts(export)]on any new payload type; runpnpm bindings. - Return errors as
AppErrorwith an existingErrorCode; add a code only if the screen needs a distinct sentence (then add its text toTEXTOSandsrc/i18n/en/). - For a long job: follow the five-step shape above, add both events to
Events, calllanzarTrabajofrom the screen. - Run
pnpm check,pnpm check:clippy,pnpm test:rust.
Use and build · Uso y desarrollo
- User-Guide · Guía de usuario
- Development-Setup · Preparación del entorno de desarrollo
- Testing-and-CI · Tests y CI
- Releases-and-Auto-Updates · Versiones y actualizaciones automáticas
- Conventions · Convenciones
Architecture · Arquitectura
- Architecture · Arquitectura
- IPC-and-Error-Handling · IPC y manejo de errores
- Rust-App-Layer · Capa de aplicación Rust
Data · Datos
- Deck-Model · Modelo del mazo
- Storage-and-Blobs · Almacenamiento y blobs
- Deck-Package-Format · Formato del paquete de mazo
- Deck-Library · Biblioteca de mazos
Getting cards · Conseguir cartas
- Decklist-Parsing · Parseo de la lista del mazo
- Scryfall-Integration · Integración con Scryfall
- External-Sources · Fuentes externas
- Card-Identification-and-OCR · Identificación de cartas y OCR
Layout, images, PDF · Maquetación, imagen y PDF
- Layout-Engine · Motor de maquetación
- Imaging-and-PDF · Imagen y PDF
- Export · Exportación
- Live-Preview · Vista previa en vivo
- Statistics · Estadísticas
Card designer · Diseñador de cartas
- Designer-Overview · Visión general del diseñador
- Designer-Engine · Motor del diseñador
- Designer-Composition · Composición del diseñador
- Frame-Packs · Packs de marcos
- Designer-App-Layer · Capa de aplicación del diseñador
- Designer-UI · Interfaz del diseñador
- Card-Editor-Dialog · Diálogo Editar carta
Game table · Mesa de juego
Frontend · Frontend
- Frontend-Architecture · Arquitectura del frontend
- State-Stores · Stores de estado
- Internationalization · Internacionalización
- Themes-and-Skins · Temas y skins
- Settings-and-Shell · Ajustes y shell
- Tutorials · Tutoriales
- Decklist-Panel · Panel de la lista del mazo
- Add-Card-and-Variants · Añadir carta y variantes
- Set-Browser · Explorador de sets
Reference · Referencia