# LibreOffice

Un modèle pilote LibreOffice 25.2.3.2 par le Hub pour construire dans Calc une feuille de ventes, avec des formules et un en-tête, sans aucun document ouvert, et l'enregistrer en fichier .ods. Le premier script a fonctionné. Cette page donne le texte, le script, les réponses du Hub et la capture.

*Une lettre du Hub aux cinq applications, mise en page dans Writer par un script, avec le logo au-dessus du titre.*

## Faits

- Version essayée : 25.2.3.2
- Mode : `uno`

## Démarrer LibreOffice et son pont

LibreOffice et son pont ne démarrent jamais sur la parole du modèle : l'opérateur démarre les deux. Un seul lanceur le fait, d'après le [guide](https://github.com/RonanDavalan/LibreModular-MCP/blob/main/docs/modules/libreoffice.md), et rend la main aussitôt :

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

L'exécution n'avait pas d'écran, un serveur X virtuel en a tenu lieu, et elle a réglé dans `config.toml` les ports 2102 et 9314, et non les valeurs par défaut 2002 et 9304. Le lanceur a imprimé :

```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 s'est mis à l'écoute d'UNO 1,3 s après le lancement, et le Hub a été démarré ensuite. `get_state` sur `hub` a listé les trois cibles qui partagent le pont, `libreoffice-writer`, `libreoffice-calc` et `libreoffice-impress`, connectées, avec la version que LibreOffice déclarait :

**Réponse du 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": []
}
```


## Texte donné au modèle

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


Avant d'écrire, le modèle a appelé `get_state` sur `libreoffice-calc`. `active` valait `null` et `documents` était vide, avec un avertissement disant que LibreOffice n'avait aucun document Calc courant. Ce n'est pas une erreur : un script s'exécute alors avec `doc` à `None` et peut créer le premier document.

## Script écrit

Un modèle, Claude Sonnet 5.5, a écrit ce script à partir du guide et de cet état. Ce n'est pas un script tiré des tests du projet. Il crée le document quand `doc` vaut `None`, et il enregistre avec `storeToURL` dans le répertoire de travail.

**Script écrit**

```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)}
```


## La réponse du Hub

**Réponse du 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
}
```


Le total général, calculé par la formule, est 1 450 : 670 pour la tente, 480 pour le réchaud et 300 pour la lanterne. Le fichier `.ods` que le script a enregistré fait 13 472 octets. `get_state` a ensuite décrit la feuille et renvoyé une capture :

**Réponse du 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": []
}
```


## L'image produite

La capture est une page exportée par le filtre PNG de LibreOffice, de 828 par 1 170 pixels, avec le tableau en haut et le reste blanc. Cette image garde ses 230 pixels du haut. L'en-tête est en blanc gras sur `#1c385c`, et la dernière ligne et la dernière colonne contiennent les totaux.

![Un tableau intitulé Sales, avec l'en-tête Product, Q1 à Q4 et Total sur une bande bleu foncé, trois lignes de produits et une ligne Total en gras.](https://libremodular.davalan.fr/images/applications/libreoffice-sales.png)

*Le haut de la capture de la feuille Sales, telle que le Hub l'a exportée.*


*Le tableau des ventes de la recette et son graphique en barres, faits par le même script dans Calc.*


> **Limite**
>
> Seule la cible Calc a été pilotée. Non essayé : une vraie session de bureau, la capture d'un document de plusieurs feuilles ou pages, et un LibreOffice plus lent à démarrer que le pont n'attend. Quand le Hub se connecte avant que LibreOffice se mette à l'écoute, il affiche la version `unknown`, comme le consigne la page de [compatibilité](https://libremodular.davalan.fr/fr/compatibility/index.md#libreoffice). Un LibreOffice à vous déjà ouvert arrête le lanceur, qui ne s'attache jamais à une instance susceptible de contenir du travail non enregistré.

[Guide de référence dans le dépôt](https://github.com/RonanDavalan/LibreModular-MCP/blob/main/docs/modules/libreoffice.md)

