# LibreOffice

Un modelo maneja LibreOffice 25.2.3.2 a través del Hub para construir en Calc una hoja de ventas, con fórmulas y una fila de encabezado, sin ningún documento abierto, y guardarla como archivo .ods. El primer script funcionó. Esta página da el texto, el script, las respuestas del Hub y la captura.

*Una carta del Hub a las cinco aplicaciones, maquetada en Writer por un script, con el logotipo sobre el título.*

## Datos

- Versión probada: 25.2.3.2
- Modo: `uno`

## Iniciar LibreOffice y su puente

LibreOffice y su puente nunca se inician por la palabra del modelo: los inicia el operador. Un lanzador lo hace todo, según la [guía](https://github.com/RonanDavalan/LibreModular-MCP/blob/main/docs/modules/libreoffice.md), y vuelve al instante:

```sh
libremodular-launch libreoffice --bridge-file /usr/share/libremodular-mcp/bridges/uno_bridge.py
```

La ejecución no tenía pantalla, así que una pantalla X virtual hizo sus veces, y fijó los puertos en 2102 y 9314 en `config.toml`, no los predeterminados 2002 y 9304. El lanzador imprimió:

```text
LibreOffice started (process 612876), listening for UNO on 127.0.0.1:2102
bridge started (process 612877), listening on 127.0.0.1:9314
a script runs on the current document of its target's family, or with doc set to None when none is open
stop the bridge with 'libremodular-launch libreoffice --stop'; LibreOffice is yours to close
```

LibreOffice empezó a escuchar UNO 1,3 s después del lanzamiento, y el Hub se inició después. `get_state` sobre `hub` enumeró los tres destinos que comparten el puente, `libreoffice-writer`, `libreoffice-calc` y `libreoffice-impress`, conectados, con la versión que informó LibreOffice:

**Respuesta del Hub**

```json
{
  "app": "hub",
  "app_version": "0.1.0",
  "bridge_version": null,
  "captured_at": "2026-10-21T12:48:38.198Z",
  "connected": true,
  "content_origin": "hub",
  "screenshot": null,
  "state": {
    "modules": [
      {
        "app_version": "25.2.3.2",
        "bridge_version": "1",
        "connected": true,
        "mode": "uno",
        "name": "libreoffice-writer"
      },
      {
        "app_version": "25.2.3.2",
        "bridge_version": "1",
        "connected": true,
        "mode": "uno",
        "name": "libreoffice-calc"
      },
      {
        "app_version": "25.2.3.2",
        "bridge_version": "1",
        "connected": true,
        "mode": "uno",
        "name": "libreoffice-impress"
      }
    ]
  },
  "warnings": []
}
```


## Texto dado al modelo

```text
In LibreOffice Calc, build a quarterly sales sheet named "Sales". Rows: Tent (120, 150, 310, 90), Stove (80, 60, 140, 200), Lantern (45, 70, 65, 120), for the columns Q1 to Q4. Add a Total column and a Total row, computed with formulas. Header row in bold white on dark blue (#1c385c). Save it as sales.ods in your working directory.
```


Antes de escribir, el modelo llamó a `get_state` sobre `libreoffice-calc`. `active` era `null` y `documents` estaba vacío, con una advertencia de que LibreOffice no tenía ningún documento de Calc actual. Eso no es un error: un script se ejecuta entonces con `doc` en `None` y puede crear el primer documento.

## Script escrito

Un modelo, Claude Sonnet 5.5, escribió este script a partir de la guía y de ese estado. No es un script tomado de las pruebas del proyecto. Crea el documento cuando `doc` es `None` y guarda con `storeToURL` dentro del directorio de trabajo.

**Script escrito**

```python
import os

import uno

if doc is None:
    doc = desktop.loadComponentFromURL("private:factory/scalc", "_blank", 0, ())
sheet = doc.Sheets.getByIndex(0)
sheet.Name = "Sales"

header = ["Product", "Q1", "Q2", "Q3", "Q4", "Total"]
data = [("Tent", 120, 150, 310, 90), ("Stove", 80, 60, 140, 200), ("Lantern", 45, 70, 65, 120)]
for column, title in enumerate(header):
    cell = sheet.getCellByPosition(column, 0)
    cell.setString(title)
    cell.CharWeight = 150.0
    cell.CharColor = 0xFFFFFF
    cell.CellBackColor = 0x1C385C
    if column > 0:
        cell.HoriJustify = uno.Enum("com.sun.star.table.CellHoriJustify", "RIGHT")
for row, (name, *values) in enumerate(data, start=1):
    sheet.getCellByPosition(0, row).setString(name)
    for column, value in enumerate(values, start=1):
        sheet.getCellByPosition(column, row).setValue(value)
    sheet.getCellByPosition(5, row).setFormula(f"=SUM(B{row + 1}:E{row + 1})")
last = len(data) + 1
sheet.getCellByPosition(0, last).setString("Total")
for column in range(1, 6):
    letter = "ABCDEF"[column]
    sheet.getCellByPosition(column, last).setFormula(f"=SUM({letter}2:{letter}{last})")
for column in range(0, 6):
    sheet.getCellByPosition(column, last).CharWeight = 150.0
    sheet.Columns.getByIndex(column).Width = 2600

path = os.path.join(LMM_WORK_DIR, "sales.ods")
properties = (uno.createUnoStruct("com.sun.star.beans.PropertyValue"),)
properties[0].Name = "FilterName"
properties[0].Value = "calc8"
doc.storeToURL(uno.systemPathToFileUrl(path), properties)
result = {"sheet": sheet.Name, "grand_total": sheet.getCellByPosition(5, last).getValue(), "ods_bytes": os.path.getsize(path)}
```


## Respuesta del Hub

**Respuesta del Hub**

```json
{
  "app": "libreoffice-calc",
  "duration_ms": 555,
  "error": null,
  "result": {
    "grand_total": 1450.0,
    "ods_bytes": 13472,
    "sheet": "Sales"
  },
  "status": "ok",
  "stderr": "",
  "stdout": "",
  "truncated": false
}
```


El total general, calculado por la fórmula, es 1450: 670 de la tienda de campaña, 480 del hornillo y 300 de la linterna. El archivo `.ods` que guardó el script pesa 13 472 bytes. `get_state` describió entonces la hoja y devolvió una captura:

**Respuesta del Hub**

```json
{
  "app": "libreoffice-calc",
  "app_version": "25.2.3.2",
  "bridge_version": "1",
  "captured_at": "2026-10-21T12:48:39.480Z",
  "connected": true,
  "content_origin": "document",
  "screenshot": "$XDG_RUNTIME_DIR/libremodular-mcp/captures/libreoffice-calc-20261021T124838664Z.png",
  "state": {
    "active": {
      "sheets": [
        {
          "last_column": 5,
          "last_row": 4,
          "name": "Sales"
        }
      ]
    },
    "documents": [
      {
        "family": "calc",
        "title": "Untitled 1"
      }
    ]
  },
  "warnings": []
}
```


## La imagen producida

La captura es una página exportada por el filtro PNG de LibreOffice, de 828 por 1170 píxeles, con la tabla arriba y el resto en blanco. Esta imagen conserva sus 230 píxeles superiores. El encabezado está en negrita blanca sobre `#1c385c`, y la última fila y la última columna contienen los totales.

![Una tabla titulada Sales con el encabezado Product, Q1 a Q4 y Total sobre una banda azul oscuro, tres filas de productos y una fila Total en negrita.](https://libremodular.davalan.fr/images/applications/libreoffice-sales.png)

*La parte superior de la captura de la hoja Sales, tal como la exportó el Hub.*


*La tabla de ventas de la receta y su gráfico de barras, hechos por el mismo script en Calc.*


> **Límite**
>
> Solo se manejó el destino Calc. No se probó: una sesión de escritorio real, la captura de un documento con varias hojas o páginas y un LibreOffice más lento en iniciarse de lo que espera el puente. Cuando el Hub se conecta antes de que LibreOffice escuche, muestra la versión como `unknown`, como registra la [página de compatibilidad](https://libremodular.davalan.fr/es/compatibility/index.md#libreoffice). Un LibreOffice suyo que ya esté abierto detiene el lanzador, que nunca se conecta a una instancia que pueda contener trabajo sin guardar.

[Guía de referencia en el repositorio](https://github.com/RonanDavalan/LibreModular-MCP/blob/main/docs/modules/libreoffice.md)

