Compare commits
19 Commits
7d2f56a364
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 64c53f412d | |||
| 61fb56fa14 | |||
| 8039a3545d | |||
| fe0476218d | |||
| 1220ce1bb0 | |||
| 4bb2c6cc77 | |||
| 2841881a01 | |||
| 8b0f128313 | |||
| 2d9a1e64f9 | |||
| ebb648bb2d | |||
| df899bd1a8 | |||
| 75c85e7e0a | |||
| e34528c886 | |||
| 8b39c09484 | |||
| b8ba15d832 | |||
| d47e93b3c3 | |||
| cdafffaa70 | |||
| 6ae7a434c4 | |||
| 9eae97f1d8 |
@@ -0,0 +1,52 @@
|
|||||||
|
# Règles — Architecture (Pena-tauri)
|
||||||
|
|
||||||
|
## Principe
|
||||||
|
|
||||||
|
L'architecture est **Clean Architecture**. Toute nouvelle fonctionnalité doit respecter ce découpage sans exception.
|
||||||
|
|
||||||
|
## Structure Rust (`src-tauri/src/`)
|
||||||
|
|
||||||
|
```
|
||||||
|
domain/ — entités et traits purs, zéro dépendance externe
|
||||||
|
application/ — services métier, dépend uniquement de domain/
|
||||||
|
infrastructure/ — implémentations concrètes des traits de domain/
|
||||||
|
commands/ — points d'entrée Tauri (#[tauri::command]), orchestrent application/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Structure JS (`src/`)
|
||||||
|
|
||||||
|
```
|
||||||
|
services/ — appels Tauri (invoke), logique de données
|
||||||
|
state/ — état global de l'application
|
||||||
|
ui/ — composants d'affichage, sans logique métier
|
||||||
|
```
|
||||||
|
|
||||||
|
## Règles de dépendance
|
||||||
|
|
||||||
|
| Couche | Peut dépendre de | Ne peut PAS dépendre de |
|
||||||
|
|---|---|---|
|
||||||
|
| `domain` | rien | `application`, `infrastructure`, `commands` |
|
||||||
|
| `application` | `domain` | `infrastructure`, `commands` |
|
||||||
|
| `infrastructure` | `domain` | `application`, `commands` |
|
||||||
|
| `commands` | `application`, `domain` | `infrastructure` directement |
|
||||||
|
| `ui/` (JS) | `services/`, `state/` | rien d'externe hors Tauri |
|
||||||
|
| `services/` (JS) | invoke Tauri | `ui/` |
|
||||||
|
|
||||||
|
## Ce qui est interdit
|
||||||
|
|
||||||
|
- Instancier une implémentation concrète (`ComrakRenderer`, `FileRepository`, etc.) depuis `application/` ou `commands/` — passer par injection de dépendance ou par le point d'assemblage (`lib.rs`).
|
||||||
|
- Mettre de la logique métier dans `commands/` ou `ui/`.
|
||||||
|
- Mettre des appels Tauri (`invoke`) dans `ui/` — les déléguer à `services/`.
|
||||||
|
- Créer un module hors de ces quatre couches Rust sans justification explicite.
|
||||||
|
|
||||||
|
## Point d'assemblage
|
||||||
|
|
||||||
|
`lib.rs` est le seul endroit où les implémentations concrètes sont instanciées et câblées aux services.
|
||||||
|
|
||||||
|
## Nouveau code
|
||||||
|
|
||||||
|
Avant d'ajouter un fichier, se poser la question : **dans quelle couche appartient cette responsabilité ?**
|
||||||
|
- Règle métier / contrat → `domain/`
|
||||||
|
- Orchestration d'un cas d'usage → `application/`
|
||||||
|
- Accès I/O, filesystem, bibliothèque tierce → `infrastructure/`
|
||||||
|
- Commande exposée à Tauri → `commands/`
|
||||||
@@ -1,2 +1,10 @@
|
|||||||
src-tauri/target/
|
src-tauri/target/
|
||||||
src-tauri/gen/
|
src-tauri/gen/
|
||||||
|
|
||||||
|
# Artefacts de build Flatpak
|
||||||
|
flatpak/.build/
|
||||||
|
flatpak/.repo/
|
||||||
|
flatpak/.flatpak-builder/
|
||||||
|
flatpak/pena.deb
|
||||||
|
flatpak/icon.png
|
||||||
|
flatpak/*.flatpak
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Vue d'ensemble
|
||||||
|
|
||||||
|
## Qu'est-ce que Pena ?
|
||||||
|
|
||||||
|
Pena est un **lecteur Markdown de bureau**. Son objectif est d'offrir une lecture confortable
|
||||||
|
de documentation écrite en Markdown, qu'il s'agisse d'un fichier unique (un `README.md`) ou
|
||||||
|
d'un ensemble de fichiers organisés en arborescence (un « wiki » de documentation).
|
||||||
|
|
||||||
|
Contrairement à un éditeur, Pena est **focalisé sur la lecture** : il n'édite pas les fichiers,
|
||||||
|
il les affiche joliment, permet de naviguer entre eux, et de personnaliser entièrement
|
||||||
|
l'apparence du rendu.
|
||||||
|
|
||||||
|
## Pour qui ?
|
||||||
|
|
||||||
|
- Toute personne qui consulte régulièrement de la documentation Markdown en local
|
||||||
|
(wikis de projet, notes, documentation technique).
|
||||||
|
- Les utilisateurs qui veulent un rendu plus agréable et configurable que l'aperçu brut
|
||||||
|
d'un éditeur de texte.
|
||||||
|
- Ceux qui maintiennent une documentation multi-fichiers organisée en dossiers et
|
||||||
|
sous-dossiers, et qui veulent naviguer dedans comme dans un wiki.
|
||||||
|
|
||||||
|
## Deux modes d'ouverture
|
||||||
|
|
||||||
|
Pena propose deux façons d'ouvrir du contenu :
|
||||||
|
|
||||||
|
| Mode | Déclencheur | Comportement |
|
||||||
|
|---|---|---|
|
||||||
|
| **Fichier** | « Ouvrir un fichier Markdown » | Ouvre un `.md` isolé. La barre latérale liste tous les `.md` du dossier parent pour faciliter la navigation. |
|
||||||
|
| **Dossier (wiki)** | « Ouvrir un dossier wiki » | Ouvre un dossier complet. Tous les `.md` (y compris dans les sous-dossiers) sont listés en arborescence. Si un fichier `Home.md` existe, il est affiché en premier. |
|
||||||
|
|
||||||
|
## Principes de fonctionnement
|
||||||
|
|
||||||
|
- **Rendu côté Rust** — La conversion Markdown → HTML est faite par le backend Rust
|
||||||
|
(`comrak`), pas par une bibliothèque JS. Cela garantit un rendu rapide et cohérent.
|
||||||
|
- **CommonMark + extensions** — Les tableaux, le texte barré, les liens automatiques et les
|
||||||
|
listes de tâches sont pris en charge.
|
||||||
|
- **Coloration syntaxique** — Les blocs de code sont colorés via `syntect`, avec un thème
|
||||||
|
de coloration intégré (`base16-ocean.dark`).
|
||||||
|
- **Navigation entre documents** — Les liens Markdown relatifs entre fichiers
|
||||||
|
(ex. `[voir](../autre/page.md)`) sont fonctionnels : cliquer dessus charge le document
|
||||||
|
cible dans Pena.
|
||||||
|
- **Tout reste local** — Aucune connexion réseau n'est nécessaire. Les préférences
|
||||||
|
(récents, thème, CSS personnalisé) sont stockées localement dans le navigateur embarqué
|
||||||
|
(localStorage).
|
||||||
|
|
||||||
|
## Ce que Pena ne fait pas (encore)
|
||||||
|
|
||||||
|
- Pas d'édition de fichiers Markdown (lecture seule).
|
||||||
|
- Pas de rechargement automatique à la modification du fichier : le mécanisme de
|
||||||
|
surveillance (« watcher ») existe côté backend mais n'est pas branché côté interface.
|
||||||
|
Voir [Commandes Tauri](../2-technique/2.2-Commandes-Tauri.md#surveillance-de-fichiers-watch).
|
||||||
|
|
||||||
|
## Voir aussi
|
||||||
|
|
||||||
|
- [Fonctionnalités détaillées](1.2-Fonctionnalites.md)
|
||||||
|
- [Interface](1.3-Interface.md)
|
||||||
|
- [Thèmes et personnalisation](1.4-Themes-et-personnalisation.md)
|
||||||
|
- [Architecture technique](../2-technique/2.1-Architecture.md)
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# Fonctionnalités
|
||||||
|
|
||||||
|
Cette page liste l'ensemble des fonctionnalités visibles par l'utilisateur, regroupées par
|
||||||
|
thème.
|
||||||
|
|
||||||
|
## 1. Ouverture de documents
|
||||||
|
|
||||||
|
### Ouvrir un fichier Markdown
|
||||||
|
- Bouton **« Ouvrir un fichier Markdown »** sur l'écran d'accueil.
|
||||||
|
- Ouvre un sélecteur de fichiers natif, filtré sur l'extension `.md`.
|
||||||
|
- Une fois le fichier ouvert, la barre latérale liste **tous les `.md` du dossier parent**,
|
||||||
|
ce qui permet de basculer entre fichiers voisins.
|
||||||
|
|
||||||
|
### Ouvrir un dossier wiki
|
||||||
|
- Bouton **« Ouvrir un dossier wiki »** sur l'écran d'accueil.
|
||||||
|
- Ouvre un sélecteur de dossier natif.
|
||||||
|
- Tous les fichiers `.md` du dossier **et de ses sous-dossiers** sont listés (récursivement).
|
||||||
|
- Les fichiers et dossiers cachés (commençant par `.`) sont ignorés.
|
||||||
|
- Si un fichier `Home.md` ou `home.md` est présent à la racine, il est affiché en premier ;
|
||||||
|
sinon, c'est le premier fichier par ordre alphabétique.
|
||||||
|
|
||||||
|
## 2. Documents récents
|
||||||
|
|
||||||
|
- L'écran d'accueil affiche une section **« Récents »** (masquée si vide).
|
||||||
|
- Chaque entrée affiche une icône (📁 pour un dossier, 📄 pour un fichier), le nom et le
|
||||||
|
chemin parent.
|
||||||
|
- Un clic réouvre directement le fichier ou le dossier.
|
||||||
|
- Jusqu'à **8 entrées** sont conservées, sans doublon, dans le stockage local (localStorage,
|
||||||
|
clé `pena_recents`).
|
||||||
|
|
||||||
|
## 3. Navigation dans la documentation
|
||||||
|
|
||||||
|
### Barre latérale en arborescence
|
||||||
|
- Affiche la structure complète du dossier ouvert sous forme d'arbre.
|
||||||
|
- Les dossiers sont des sections repliables/dépliables (chevron).
|
||||||
|
- Fichiers et dossiers sont triés par **ordre naturel** : les préfixes numériques
|
||||||
|
sont comparés comme des nombres (`10` vient après `9`, et non après `1`).
|
||||||
|
- Dossiers et fichiers partagent la **même typographie** (taille et graisse) ;
|
||||||
|
seul le chevron ▶ distingue visuellement un dossier.
|
||||||
|
- Si un dossier contient un `home.md`, son nom devient un lien cliquable.
|
||||||
|
- Le document actuellement affiché est **surligné**, et les dossiers parents sont
|
||||||
|
automatiquement dépliés.
|
||||||
|
- Plusieurs niveaux de profondeur sont gérés avec une indentation progressive.
|
||||||
|
|
||||||
|
### Liens entre documents
|
||||||
|
- Les **liens Markdown relatifs** vers d'autres fichiers `.md`
|
||||||
|
(ex. `[autre page](../section/page.md)`) sont fonctionnels : un clic charge le document
|
||||||
|
cible dans Pena, sans quitter l'application. Les chemins relatifs (`../`, `./`) sont
|
||||||
|
résolus correctement.
|
||||||
|
- Les **liens externes** (`http://`, `https://`) s'ouvrent dans le navigateur par défaut
|
||||||
|
(nouvel onglet, avec `rel="noopener noreferrer"`).
|
||||||
|
- Les **ancres internes** (`#section`) restent des ancres de page.
|
||||||
|
|
||||||
|
## 4. Rendu Markdown
|
||||||
|
|
||||||
|
Le rendu prend en charge le standard **CommonMark** ainsi que les extensions suivantes :
|
||||||
|
|
||||||
|
| Extension | Exemple |
|
||||||
|
|---|---|
|
||||||
|
| Tableaux | `\| col A \| col B \|` |
|
||||||
|
| Texte barré | `~~barré~~` |
|
||||||
|
| Liens automatiques | une URL nue devient un lien cliquable |
|
||||||
|
| Listes de tâches | `- [ ] à faire` / `- [x] fait` |
|
||||||
|
|
||||||
|
Autres éléments rendus : titres `h1`–`h6`, code inline, blocs de code, citations
|
||||||
|
(blockquotes), listes, images (largeur adaptée), et liens.
|
||||||
|
|
||||||
|
## 5. Coloration syntaxique du code
|
||||||
|
|
||||||
|
- Les blocs de code délimités par triple backtick sont **colorés syntaxiquement** via
|
||||||
|
`syntect`.
|
||||||
|
- Le thème de coloration intégré est **`base16-ocean.dark`**.
|
||||||
|
- La syntaxe **TypeScript** est fournie en plus des syntaxes par défaut.
|
||||||
|
|
||||||
|
### Bouton « Copier » sur les blocs de code
|
||||||
|
- Chaque bloc de code affiche un bouton **« Copier »** en haut à droite.
|
||||||
|
- Un clic copie le contenu du bloc dans le presse-papiers.
|
||||||
|
- Un retour visuel temporaire (« Copié ! ») confirme l'action.
|
||||||
|
|
||||||
|
## 6. Personnalisation de l'apparence
|
||||||
|
|
||||||
|
Voir la page dédiée [Thèmes et personnalisation](1.4-Themes-et-personnalisation.md). En résumé :
|
||||||
|
|
||||||
|
- **6 thèmes rapides** prêts à l'emploi (Défaut, Mode sombre, Sépia, Grand texte,
|
||||||
|
Accent émeraude, Shell Indigo).
|
||||||
|
- Un **éditeur CSS** intégré, organisé en 4 onglets thématiques, permettant d'écrire son
|
||||||
|
propre CSS appliqué au rendu.
|
||||||
|
- Les choix sont **persistés** localement et réappliqués au démarrage suivant.
|
||||||
|
|
||||||
|
## 7. Fenêtre et barre de titre personnalisée
|
||||||
|
|
||||||
|
- L'application utilise une **barre de titre maison** (la décoration native est désactivée).
|
||||||
|
- Elle affiche le titre du document courant et propose les boutons **réduire**,
|
||||||
|
**agrandir / restaurer** et **fermer**.
|
||||||
|
- La fenêtre se déplace en glissant la barre de titre.
|
||||||
|
|
||||||
|
## 8. Rechargement automatique (live-reload)
|
||||||
|
|
||||||
|
- À l'ouverture d'un fichier ou d'un dossier, l'application **surveille** en continu son
|
||||||
|
contenu sur le disque.
|
||||||
|
- Quand le **document affiché** est modifié (par un éditeur externe, un `git pull`, etc.),
|
||||||
|
il est **rechargé automatiquement** — plus besoin de revenir à l'accueil et de rouvrir le
|
||||||
|
dossier.
|
||||||
|
- Quand un fichier `.md` est **ajouté ou supprimé** dans le dossier ouvert, la **sidebar**
|
||||||
|
se met à jour automatiquement pour refléter la nouvelle arborescence (le dossier du
|
||||||
|
document courant reste déplié).
|
||||||
|
- La surveillance s'arrête au retour à l'accueil et se réinitialise à chaque nouvelle
|
||||||
|
ouverture. Détails techniques :
|
||||||
|
[Surveillance de fichiers](../2-technique/2.2-Commandes-Tauri.md#surveillance-de-fichiers-watch).
|
||||||
|
|
||||||
|
## Voir aussi
|
||||||
|
|
||||||
|
- [Interface](1.3-Interface.md)
|
||||||
|
- [Thèmes et personnalisation](1.4-Themes-et-personnalisation.md)
|
||||||
|
- [Vue d'ensemble](1.1-Vue-d-ensemble.md)
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# Interface
|
||||||
|
|
||||||
|
L'interface de Pena se compose de deux vues principales — l'**accueil** et le **lecteur** —
|
||||||
|
surmontées d'une **barre de titre personnalisée** toujours visible.
|
||||||
|
|
||||||
|
## Barre de titre personnalisée
|
||||||
|
|
||||||
|
La décoration native de la fenêtre est désactivée ; Pena fournit sa propre barre de titre
|
||||||
|
en haut de la fenêtre (hauteur 36 px, fond sombre).
|
||||||
|
|
||||||
|
| Élément | Comportement |
|
||||||
|
|---|---|
|
||||||
|
| Titre | Affiche « Pena — Markdown Viewer » sur l'accueil, ou le titre du document ouvert dans le lecteur. |
|
||||||
|
| Zone de titre | Glisser-déposer pour déplacer la fenêtre. |
|
||||||
|
| Bouton **─** | Réduit la fenêtre. |
|
||||||
|
| Bouton **▢** | Agrandit la fenêtre, ou la restaure si elle est déjà agrandie. |
|
||||||
|
| Bouton **✕** | Ferme l'application (devient rouge au survol). |
|
||||||
|
|
||||||
|
## Vue d'accueil
|
||||||
|
|
||||||
|
C'est l'écran affiché au démarrage et quand on revient via « ← Accueil ».
|
||||||
|
|
||||||
|
Elle contient :
|
||||||
|
|
||||||
|
- Le **titre « Pena »** en grand.
|
||||||
|
- Deux boutons d'action :
|
||||||
|
- **« Ouvrir un fichier Markdown »**
|
||||||
|
- **« Ouvrir un dossier wiki »**
|
||||||
|
- Une section **« Récents »** (visible uniquement s'il y a des documents récents) listant les
|
||||||
|
derniers fichiers et dossiers ouverts, cliquables pour les rouvrir.
|
||||||
|
|
||||||
|
## Vue lecteur
|
||||||
|
|
||||||
|
Affichée dès qu'un document est ouvert. Elle est composée de deux zones :
|
||||||
|
|
||||||
|
### Barre latérale (gauche)
|
||||||
|
- Largeur fixe (264 px), fond sombre.
|
||||||
|
- En haut : le **nom du dossier** ouvert.
|
||||||
|
- Au centre : l'**arborescence des fichiers** `.md` (voir
|
||||||
|
[Navigation](1.2-Fonctionnalites.md#3-navigation-dans-la-documentation)).
|
||||||
|
- En bas (pied de barre) : deux boutons —
|
||||||
|
- **« Personnaliser CSS »** (ouvre la modale de thèmes / CSS) ;
|
||||||
|
- **« ← Accueil »** (revient à l'écran d'accueil).
|
||||||
|
|
||||||
|
### Zone de contenu (droite)
|
||||||
|
- Affiche le document Markdown rendu en HTML.
|
||||||
|
- Fond clair, typographie soignée, largeur de lecture limitée pour le confort.
|
||||||
|
- Chaque bloc de code possède un bouton **« Copier »**.
|
||||||
|
|
||||||
|
### Comportement responsive
|
||||||
|
- Sur les fenêtres larges, la barre latérale est visible en permanence à gauche.
|
||||||
|
- Sous ~1020 px de large, la barre latérale se rétracte et le contenu occupe toute la largeur.
|
||||||
|
- Sous ~540 px, les marges du contenu sont réduites.
|
||||||
|
|
||||||
|
## Modale de personnalisation CSS
|
||||||
|
|
||||||
|
Ouverte via le bouton « Personnaliser CSS » de la barre latérale. Voir la page dédiée
|
||||||
|
[Thèmes et personnalisation](1.4-Themes-et-personnalisation.md) pour le détail.
|
||||||
|
|
||||||
|
Elle se ferme via la croix, le bouton « Fermer », ou un clic en dehors de la modale.
|
||||||
|
|
||||||
|
## Palette visuelle
|
||||||
|
|
||||||
|
| Rôle | Usage |
|
||||||
|
|---|---|
|
||||||
|
| Fond sombre | Barre de titre, barre latérale |
|
||||||
|
| Fond clair | Zone de contenu |
|
||||||
|
| Accent rose/corail | Boutons, survols, liens d'action, bordures de tableaux |
|
||||||
|
| Accent indigo | Élément actif dans la barre latérale |
|
||||||
|
|
||||||
|
> L'ensemble de ces couleurs peut être surchargé par l'utilisateur via les thèmes rapides ou
|
||||||
|
> le CSS personnalisé.
|
||||||
|
|
||||||
|
## Voir aussi
|
||||||
|
|
||||||
|
- [Fonctionnalités](1.2-Fonctionnalites.md)
|
||||||
|
- [Thèmes et personnalisation](1.4-Themes-et-personnalisation.md)
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# Thèmes et personnalisation
|
||||||
|
|
||||||
|
Pena permet de modifier entièrement l'apparence du rendu, via deux mécanismes
|
||||||
|
complémentaires : les **thèmes rapides** (prêts à l'emploi) et l'**éditeur CSS** (sur mesure).
|
||||||
|
Tout se passe dans la **modale « Personnaliser CSS »**, ouverte depuis la barre latérale.
|
||||||
|
|
||||||
|
## Thèmes rapides
|
||||||
|
|
||||||
|
Six thèmes sont fournis et gérés par le backend. Chacun est un fichier CSS intégré à
|
||||||
|
l'application.
|
||||||
|
|
||||||
|
| Identifiant | Libellé affiché |
|
||||||
|
|---|---|
|
||||||
|
| `default` | Défaut |
|
||||||
|
| `dark` | Mode sombre |
|
||||||
|
| `sepia` | Sépia |
|
||||||
|
| `large-text` | Grand texte |
|
||||||
|
| `emerald` | Accent émeraude |
|
||||||
|
| `shell-indigo` | Shell Indigo |
|
||||||
|
|
||||||
|
### Utilisation
|
||||||
|
- Les thèmes apparaissent sous forme de **badges cliquables** dans la modale.
|
||||||
|
- Un clic applique le thème **immédiatement**.
|
||||||
|
- Le badge du thème actif est mis en évidence.
|
||||||
|
- Cliquer à nouveau sur le thème actif le **désactive** (retour au rendu de base).
|
||||||
|
- Le thème choisi est mémorisé (localStorage, clé `pena_quick_theme`) et réappliqué au
|
||||||
|
prochain démarrage.
|
||||||
|
|
||||||
|
## Éditeur CSS personnalisé
|
||||||
|
|
||||||
|
Pour aller plus loin qu'un thème, la modale propose un éditeur de CSS libre, organisé en
|
||||||
|
**4 onglets** thématiques. Chaque onglet conserve son propre CSS, indépendamment des autres :
|
||||||
|
|
||||||
|
| Onglet | Vocation |
|
||||||
|
|---|---|
|
||||||
|
| Arrière-scène | Styles généraux / d'ambiance |
|
||||||
|
| Police | Typographie |
|
||||||
|
| Contenu fond | Mise en forme du contenu |
|
||||||
|
| Développement | Réglages avancés / divers |
|
||||||
|
|
||||||
|
### Fonctionnement
|
||||||
|
- On écrit du **CSS standard** dans la zone de texte de l'onglet sélectionné.
|
||||||
|
- Le bouton **« ★ Enregistrer »** sauvegarde le CSS courant et l'applique aussitôt.
|
||||||
|
- Le bouton **« Réinitialiser »** efface **tout** le CSS personnalisé **et** désactive le
|
||||||
|
thème rapide actif.
|
||||||
|
- Le bouton **« Fermer »** ferme la modale sans enregistrer les changements en cours.
|
||||||
|
- Un champ permet de nommer la configuration.
|
||||||
|
|
||||||
|
### Application combinée
|
||||||
|
Le CSS finalement appliqué est la **concaténation** du CSS des 4 onglets, suivi du CSS du
|
||||||
|
thème rapide actif. L'ordre de cascade est donc :
|
||||||
|
|
||||||
|
1. CSS de base de l'application (`style.css`) ;
|
||||||
|
2. CSS personnalisé (les 4 onglets, concaténés) ;
|
||||||
|
3. CSS du thème rapide actif (s'il y en a un).
|
||||||
|
|
||||||
|
Ainsi, un thème rapide peut compléter un CSS personnalisé, et le CSS personnalisé peut
|
||||||
|
surcharger le rendu de base.
|
||||||
|
|
||||||
|
## Persistance des préférences
|
||||||
|
|
||||||
|
Toutes les préférences sont stockées localement (localStorage) et restaurées à chaque
|
||||||
|
ouverture :
|
||||||
|
|
||||||
|
| Donnée | Clé localStorage |
|
||||||
|
|---|---|
|
||||||
|
| Thème rapide actif | `pena_quick_theme` |
|
||||||
|
| CSS onglet « Arrière-scène » | `pena_css_general` |
|
||||||
|
| CSS onglet « Police » | `pena_css_police` |
|
||||||
|
| CSS onglet « Contenu fond » | `pena_css_contenu` |
|
||||||
|
| CSS onglet « Développement » | `pena_css_avance` |
|
||||||
|
|
||||||
|
> Ces données sont propres à la machine et à l'installation : elles ne sont pas synchronisées
|
||||||
|
> et ne quittent jamais l'ordinateur.
|
||||||
|
|
||||||
|
## Voir aussi
|
||||||
|
|
||||||
|
- [Interface](1.3-Interface.md)
|
||||||
|
- [Fonctionnalités](1.2-Fonctionnalites.md)
|
||||||
|
- [Commandes Tauri — Thèmes](../2-technique/2.2-Commandes-Tauri.md#thèmes)
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
Pena (`Pena-tauri`) est une application [Tauri 2](https://tauri.app/) : un **backend Rust**
|
||||||
|
expose des commandes à un **frontend web** (HTML/CSS/JS vanilla, sans bundler). Le frontend
|
||||||
|
est servi en fichiers statiques depuis `src/`, le backend est compilé dans `src-tauri/`.
|
||||||
|
|
||||||
|
L'ensemble suit une **Clean Architecture** stricte, côté Rust comme côté JS.
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────── Frontend (src/, JS vanilla) ────────────────────────────┐
|
||||||
|
│ ui/ — composants d'affichage (home, sidebar, reader, css-modal) │
|
||||||
|
│ state/ — état global minimal (app-state) │
|
||||||
|
│ services/ — appels Tauri via invoke (markdown, files, watcher, themes) │
|
||||||
|
│ router.js — navigation entre vues (accueil / lecteur) │
|
||||||
|
└──────────────────────────────────────┬───────────────────────────────────────────────┘
|
||||||
|
│ window.__TAURI__.core.invoke(...)
|
||||||
|
│ window.__TAURI__.event.listen(...)
|
||||||
|
┌──────────────────────────────────────┴──── Backend (src-tauri/src/, Rust) ───────────┐
|
||||||
|
│ commands/ — points d'entrée #[tauri::command] (render, watch, theme) │
|
||||||
|
│ application/ — services métier (render_service, file_service, theme_service) │
|
||||||
|
│ infrastructure/ — implémentations concrètes (comrak, notify, fs, thèmes statiques) │
|
||||||
|
│ domain/ — entités et traits purs (MarkdownRenderer, ThemeRepository…) │
|
||||||
|
│ lib.rs — point d'assemblage (wiring + enregistrement des commandes) │
|
||||||
|
└───────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backend Rust — couches
|
||||||
|
|
||||||
|
Le code Rust (`src-tauri/src/`) est découpé en quatre couches, avec des règles de dépendance
|
||||||
|
strictes.
|
||||||
|
|
||||||
|
| Couche | Rôle | Peut dépendre de | Ne doit PAS dépendre de |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `domain/` | Entités et traits purs, zéro dépendance externe | rien | `application`, `infrastructure`, `commands` |
|
||||||
|
| `application/` | Services métier (orchestration des cas d'usage) | `domain` | `infrastructure`, `commands` |
|
||||||
|
| `infrastructure/` | Implémentations concrètes des traits du domaine | `domain` | `application`, `commands` |
|
||||||
|
| `commands/` | Commandes exposées à Tauri | `application`, `domain` | `infrastructure` (directement) |
|
||||||
|
|
||||||
|
### `domain/`
|
||||||
|
Définit les contrats, sans aucune dépendance technique :
|
||||||
|
- `markdown.rs` — trait `MarkdownRenderer { fn render(&self, content: &str) -> String }`
|
||||||
|
et struct `RenderOptions` (tables, strikethrough, autolink, tasklist).
|
||||||
|
- `theme.rs` — struct `Theme { id, label }` et trait
|
||||||
|
`ThemeRepository { fn list() -> Vec<Theme>; fn get_css(id) -> Option<String> }`.
|
||||||
|
|
||||||
|
### `application/`
|
||||||
|
Orchestration métier, dépend uniquement du domaine :
|
||||||
|
- `render_service.rs` — `render_string()` (rendu d'une chaîne) et `render_file()`
|
||||||
|
(lecture du fichier puis rendu).
|
||||||
|
- `file_service.rs` — `list_markdown_files()` : valide le répertoire, collecte les `.md`,
|
||||||
|
trie alphabétiquement.
|
||||||
|
- `theme_service.rs` — `list_themes()` et `get_theme_css()`, délégués au repository.
|
||||||
|
|
||||||
|
### `infrastructure/`
|
||||||
|
Implémentations concrètes des traits du domaine :
|
||||||
|
- `comrak_renderer.rs` — `ComrakRenderer` (toutes les extensions activées, pour les
|
||||||
|
fichiers) et `ComrakPreviewRenderer` (sans extensions, pour les aperçus rapides). Fournit
|
||||||
|
aussi `syntax_css_for_theme()` pour générer le CSS de coloration `syntect`. La syntaxe
|
||||||
|
TypeScript est chargée depuis `resources/syntaxes/TypeScript.sublime-syntax`.
|
||||||
|
- `file_repository.rs` — `collect_md_files()` (parcours récursif, ignore les fichiers
|
||||||
|
cachés, ne retient que les `.md`) et `read_file()`.
|
||||||
|
- `notify_watcher.rs` — boucle de débounce (80 ms) qui filtre les événements de la crate
|
||||||
|
`notify` et émet l'événement Tauri `file-changed`.
|
||||||
|
- `theme_repository.rs` — `StaticThemeRepository` : les 6 thèmes et leur CSS sont intégrés
|
||||||
|
au binaire via `include_str!()` (fichiers `resources/themes/*.css`).
|
||||||
|
|
||||||
|
### `commands/`
|
||||||
|
Points d'entrée Tauri qui instancient les implémentations concrètes et appellent les
|
||||||
|
services. Voir la référence complète : [Commandes Tauri](2.2-Commandes-Tauri.md).
|
||||||
|
|
||||||
|
### `lib.rs` — point d'assemblage
|
||||||
|
Seul endroit où les implémentations concrètes sont câblées. Il :
|
||||||
|
- enregistre les plugins `tauri-plugin-fs` et `tauri-plugin-dialog` ;
|
||||||
|
- gère l'état global `WatcherState` (un seul watcher actif à la fois) ;
|
||||||
|
- enregistre les 8 commandes via `generate_handler![]`.
|
||||||
|
|
||||||
|
## Frontend JS — couches
|
||||||
|
|
||||||
|
Le code JS (`src/`) suit le même esprit de séparation :
|
||||||
|
|
||||||
|
| Dossier | Rôle | Peut dépendre de |
|
||||||
|
|---|---|---|
|
||||||
|
| `ui/` | Composants d'affichage, sans logique métier ni appel Tauri direct | `services/`, `state/` |
|
||||||
|
| `state/` | État global de l'application (`app-state.js`) | — |
|
||||||
|
| `services/` | Appels Tauri (`invoke`) et logique de données | API Tauri |
|
||||||
|
| `router.js` | Navigation entre les vues (accueil / lecteur), orchestrateur | `services/`, `ui/`, `state/` |
|
||||||
|
|
||||||
|
**Règle importante** : les composants `ui/` ne font **jamais** d'appel Tauri directement ;
|
||||||
|
ils passent toujours par un service de `services/`.
|
||||||
|
|
||||||
|
### Services frontend
|
||||||
|
- `services/markdown.js` — `render_markdown`, `convert_file`
|
||||||
|
- `services/files.js` — `list_md_files`
|
||||||
|
- `services/watcher.js` — `start_watch`, `stop_watch`, écoute de `file-changed`
|
||||||
|
(présent mais non utilisé par l'UI)
|
||||||
|
- `services/themes.js` — `list_themes`, `get_theme_css`
|
||||||
|
|
||||||
|
## Dépendances Rust principales
|
||||||
|
|
||||||
|
| Crate | Rôle |
|
||||||
|
|---|---|
|
||||||
|
| `tauri` (+ `tauri-plugin-fs`, `tauri-plugin-dialog`) | Framework applicatif, IPC, événements, dialogues |
|
||||||
|
| `comrak` | Rendu CommonMark + extensions |
|
||||||
|
| `syntect` | Coloration syntaxique des blocs de code |
|
||||||
|
| `notify` | Surveillance du système de fichiers |
|
||||||
|
| `serde` / `serde_json` | Sérialisation des DTO et payloads |
|
||||||
|
|
||||||
|
## Ressources intégrées au binaire
|
||||||
|
|
||||||
|
| Ressource | Emplacement | Usage |
|
||||||
|
|---|---|---|
|
||||||
|
| Thèmes rapides | `src-tauri/resources/themes/*.css` | Servis par `get_theme_css` |
|
||||||
|
| Syntaxe TypeScript | `src-tauri/resources/syntaxes/TypeScript.sublime-syntax` | Coloration des blocs TS |
|
||||||
|
|
||||||
|
Elles sont incluses à la compilation (`include_str!`), donc l'application n'a pas besoin de
|
||||||
|
fichiers externes pour fonctionner.
|
||||||
|
|
||||||
|
## Voir aussi
|
||||||
|
|
||||||
|
- [Commandes Tauri](2.2-Commandes-Tauri.md)
|
||||||
|
- [Build et compilation](../3-installation/3.3-Build.md)
|
||||||
|
- [Vue d'ensemble fonctionnelle](../1-fonctionnel/1.1-Vue-d-ensemble.md)
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# Commandes Tauri
|
||||||
|
|
||||||
|
Le backend Rust expose **8 commandes** au frontend, enregistrées dans `lib.rs` via
|
||||||
|
`generate_handler![]`. Le frontend les appelle avec
|
||||||
|
`window.__TAURI__.core.invoke('<nom>', { <args> })`.
|
||||||
|
|
||||||
|
Cette page sert de référence : signature, arguments, valeur de retour et comportement de
|
||||||
|
chaque commande.
|
||||||
|
|
||||||
|
## Rendu Markdown
|
||||||
|
|
||||||
|
### `render_markdown`
|
||||||
|
```
|
||||||
|
render_markdown(content: String) -> String
|
||||||
|
```
|
||||||
|
- **Args** : `content` — Markdown brut.
|
||||||
|
- **Retour** : HTML.
|
||||||
|
- **Comportement** : rendu via `ComrakPreviewRenderer` (sans extensions, aperçu rapide).
|
||||||
|
- **Frontend** : `services/markdown.js`.
|
||||||
|
|
||||||
|
### `convert_file`
|
||||||
|
```
|
||||||
|
convert_file(path: String) -> Result<String, String>
|
||||||
|
```
|
||||||
|
- **Args** : `path` — chemin absolu d'un fichier `.md`.
|
||||||
|
- **Retour** : `Ok(html)` ou `Err(message)`.
|
||||||
|
- **Comportement** : lit le fichier puis le rend via `ComrakRenderer` (toutes les extensions
|
||||||
|
activées : tables, strikethrough, autolink, tasklist + coloration syntaxique).
|
||||||
|
- **Frontend** : `services/markdown.js`. C'est la commande utilisée pour afficher un document
|
||||||
|
dans le lecteur.
|
||||||
|
|
||||||
|
### `get_syntax_highlight_css`
|
||||||
|
```
|
||||||
|
get_syntax_highlight_css() -> String
|
||||||
|
```
|
||||||
|
- **Args** : aucun.
|
||||||
|
- **Retour** : feuille CSS de coloration syntaxique.
|
||||||
|
- **Comportement** : génère le CSS du thème `syntect` `base16-ocean.dark`.
|
||||||
|
|
||||||
|
## Fichiers
|
||||||
|
|
||||||
|
### `list_md_files`
|
||||||
|
```
|
||||||
|
list_md_files(dir: String) -> Result<Vec<String>, String>
|
||||||
|
```
|
||||||
|
- **Args** : `dir` — chemin d'un répertoire.
|
||||||
|
- **Retour** : `Ok([chemins…])` (triés alphabétiquement) ou `Err(message)`.
|
||||||
|
- **Comportement** : parcourt le répertoire **récursivement**, ignore les fichiers/dossiers
|
||||||
|
cachés (commençant par `.`), ne retient que les `.md`.
|
||||||
|
- **Frontend** : `services/files.js`.
|
||||||
|
|
||||||
|
## Surveillance de fichiers (watch)
|
||||||
|
|
||||||
|
> Ces commandes sont implémentées et fonctionnelles côté backend, mais **l'interface ne les
|
||||||
|
> appelle pas encore**. Le rechargement automatique n'est donc pas actif. Voir
|
||||||
|
> [Fonctionnalités](../1-fonctionnel/1.2-Fonctionnalites.md#fonctionnalité-présente-mais-non-branchée).
|
||||||
|
|
||||||
|
### `start_watch`
|
||||||
|
```
|
||||||
|
start_watch(path: String) -> Result<(), String>
|
||||||
|
```
|
||||||
|
- **Args** : `path` — fichier ou répertoire à surveiller.
|
||||||
|
- **Comportement** :
|
||||||
|
- arrête le watcher précédent (un seul actif à la fois, stocké dans `WatcherState`) ;
|
||||||
|
- surveille en mode récursif si `path` est un répertoire, non récursif si c'est un
|
||||||
|
fichier ;
|
||||||
|
- lance une boucle de **débounce de 80 ms** qui ne retient que les fichiers `.md` non
|
||||||
|
cachés, et émet l'événement Tauri **`file-changed`** avec un payload `{ path }`.
|
||||||
|
- **Frontend** : `services/watcher.js` (`startWatch`).
|
||||||
|
|
||||||
|
### `stop_watch`
|
||||||
|
```
|
||||||
|
stop_watch() -> ()
|
||||||
|
```
|
||||||
|
- **Comportement** : arrête le watcher actif (le supprime de `WatcherState`, ce qui termine
|
||||||
|
le thread de surveillance).
|
||||||
|
- **Frontend** : `services/watcher.js` (`stopWatch`).
|
||||||
|
|
||||||
|
### Événement `file-changed`
|
||||||
|
- Émis par le backend pendant la surveillance.
|
||||||
|
- Payload : `{ path: String }` — chemin du fichier modifié.
|
||||||
|
- Écoutable côté frontend via
|
||||||
|
`window.__TAURI__.event.listen('file-changed', callback)` (`onFileChanged`).
|
||||||
|
- **Consommation** : `router.js` enregistre le listener au démarrage (`initWatcher`) et,
|
||||||
|
à chaque événement, recharge le document affiché s'il correspond au chemin modifié et
|
||||||
|
rafraîchit la sidebar si l'arborescence `.md` du dossier surveillé a changé. La
|
||||||
|
surveillance est démarrée dans `openPath` (via `startWatch`) et arrêtée dans `showHome`
|
||||||
|
(via `stopWatch`).
|
||||||
|
|
||||||
|
## Thèmes
|
||||||
|
|
||||||
|
### `list_themes`
|
||||||
|
```
|
||||||
|
list_themes() -> Vec<ThemeDto> // ThemeDto { id: String, label: String }
|
||||||
|
```
|
||||||
|
- **Args** : aucun.
|
||||||
|
- **Retour** : les 6 thèmes (`default`, `dark`, `sepia`, `large-text`, `emerald`,
|
||||||
|
`shell-indigo`) avec leur libellé.
|
||||||
|
- **Frontend** : `services/themes.js`.
|
||||||
|
|
||||||
|
### `get_theme_css`
|
||||||
|
```
|
||||||
|
get_theme_css(id: String) -> Option<String>
|
||||||
|
```
|
||||||
|
- **Args** : `id` — identifiant du thème.
|
||||||
|
- **Retour** : le CSS du thème (`Some`) ou `None` si l'identifiant est inconnu.
|
||||||
|
- **Comportement** : le CSS est intégré au binaire (`include_str!` sur
|
||||||
|
`resources/themes/<id>.css`).
|
||||||
|
- **Frontend** : `services/themes.js`.
|
||||||
|
|
||||||
|
## Récapitulatif
|
||||||
|
|
||||||
|
| Commande | Signature | Retour |
|
||||||
|
|---|---|---|
|
||||||
|
| `render_markdown` | `(content: String)` | `String` |
|
||||||
|
| `convert_file` | `(path: String)` | `Result<String, String>` |
|
||||||
|
| `get_syntax_highlight_css` | `()` | `String` |
|
||||||
|
| `list_md_files` | `(dir: String)` | `Result<Vec<String>, String>` |
|
||||||
|
| `start_watch` | `(path: String)` | `Result<(), String>` |
|
||||||
|
| `stop_watch` | `()` | `()` |
|
||||||
|
| `list_themes` | `()` | `Vec<ThemeDto>` |
|
||||||
|
| `get_theme_css` | `(id: String)` | `Option<String>` |
|
||||||
|
|
||||||
|
## Voir aussi
|
||||||
|
|
||||||
|
- [Architecture](2.1-Architecture.md)
|
||||||
|
- [Thèmes et personnalisation](../1-fonctionnel/1.4-Themes-et-personnalisation.md)
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# Installation (Linux générique / Fedora)
|
||||||
|
|
||||||
|
Cette page décrit l'installation de **Pena-tauri** sur une distribution Linux classique
|
||||||
|
(mutable), où l'on peut installer des paquets système avec le gestionnaire de la distribution.
|
||||||
|
|
||||||
|
- Pour un système **immuable** (Bazzite, Fedora Silverblue/Kinoite, openSUSE MicroOS…), voir
|
||||||
|
[Installation sur Bazzite OS](3.2-Installation-Bazzite.md).
|
||||||
|
- Pour le détail des commandes de compilation, voir [Build](3.3-Build.md).
|
||||||
|
|
||||||
|
## 1. Installer les prérequis
|
||||||
|
|
||||||
|
### Rust et Tauri CLI
|
||||||
|
```bash
|
||||||
|
# Rust (toolchain stable)
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||||
|
source "$HOME/.cargo/env"
|
||||||
|
|
||||||
|
# Tauri CLI v2
|
||||||
|
cargo install tauri-cli --version "^2"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dépendances système
|
||||||
|
|
||||||
|
**Fedora / RHEL :**
|
||||||
|
```bash
|
||||||
|
sudo dnf install webkit2gtk4.1-devel \
|
||||||
|
openssl-devel curl wget file libappindicator-gtk3-devel librsvg2-devel
|
||||||
|
sudo dnf group install "C Development Tools and Libraries"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Debian / Ubuntu :**
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file \
|
||||||
|
libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Récupérer les sources
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <url-du-depot> Pena
|
||||||
|
cd Pena/Pena-tauri
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Compiler et installer
|
||||||
|
|
||||||
|
### Lancer directement (développement)
|
||||||
|
```bash
|
||||||
|
cargo tauri dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Construire une release
|
||||||
|
```bash
|
||||||
|
cargo tauri build
|
||||||
|
```
|
||||||
|
|
||||||
|
L'exécutable est produit dans `src-tauri/target/release/`. Pour l'installer à l'échelle du
|
||||||
|
système, copiez-le dans un dossier du `PATH` :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo install -Dm755 src-tauri/target/release/pena-taury /usr/local/bin/pena
|
||||||
|
```
|
||||||
|
|
||||||
|
Vous pouvez ensuite lancer l'application avec `pena`.
|
||||||
|
|
||||||
|
> **Nom du binaire** : le binaire s'appelle `pena-taury` (nom du paquet Cargo). Renommez-le à
|
||||||
|
> votre convenance lors de l'installation, comme ci-dessus (`pena`).
|
||||||
|
|
||||||
|
## 4. (Optionnel) Raccourci d'application
|
||||||
|
|
||||||
|
Pour faire apparaître Pena dans le menu des applications, créez un fichier `.desktop` :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat > ~/.local/share/applications/pena.desktop <<'EOF'
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Pena
|
||||||
|
Comment=Lecteur Markdown
|
||||||
|
Exec=/usr/local/bin/pena
|
||||||
|
Terminal=false
|
||||||
|
Categories=Utility;Office;
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## Voir aussi
|
||||||
|
|
||||||
|
- [Build et compilation](3.3-Build.md)
|
||||||
|
- [Installation sur Bazzite OS](3.2-Installation-Bazzite.md)
|
||||||
|
- [Vue d'ensemble](../1-fonctionnel/1.1-Vue-d-ensemble.md)
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
# Installation sur Bazzite OS
|
||||||
|
|
||||||
|
[Bazzite](https://bazzite.gg/) est une distribution **immuable** basée sur Fedora Atomic
|
||||||
|
(rpm-ostree). Le système de fichiers racine est en lecture seule : on **n'installe pas** de
|
||||||
|
paquets de développement (`webkit2gtk-devel`, compilateurs…) directement sur l'hôte comme on
|
||||||
|
le ferait sur une Fedora classique.
|
||||||
|
|
||||||
|
Cette contrainte vaut aussi pour les autres systèmes atomiques (Fedora Silverblue, Kinoite,
|
||||||
|
Universal Blue, openSUSE MicroOS…).
|
||||||
|
|
||||||
|
La méthode recommandée est le **Flatpak** : on construit **une seule fois** un fichier
|
||||||
|
`.flatpak` (le « bundle »), on l'héberge (par ex. sur un dépôt Gitea), puis on l'installe sur
|
||||||
|
n'importe quelle machine **en une commande**, sans rien compiler ni installer de dépendance
|
||||||
|
sur l'hôte.
|
||||||
|
|
||||||
|
| Vous voulez… | Allez à… |
|
||||||
|
|---|---|
|
||||||
|
| **Installer Pena** depuis un `.flatpak` déjà construit (récupéré sur Gitea) | [1. Méthode simple](#1-méthode-simple--installer-le-paquet-pré-construit) |
|
||||||
|
| **Produire** le fichier `.flatpak` (une fois, sur une machine de build) | [2. Compiler](#2-compiler-le-binaire-une-fois) puis [3. Construire le bundle](#3-construire-le-paquet-flatpak-une-fois) |
|
||||||
|
| **Développer / itérer** sur le code sans empaqueter | [4. Alternative développeur (distrobox)](#4-alternative-développeur--lancer-via-distrobox) |
|
||||||
|
|
||||||
|
> Bazzite fournit **`flatpak`** (avec le dépôt Flathub), **`distrobox`** et **`podman`**
|
||||||
|
> préinstallés. Aucune surcouche rpm-ostree (`rpm-ostree install`) n'est nécessaire.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Méthode simple — installer le paquet pré-construit
|
||||||
|
|
||||||
|
C'est le scénario du quotidien : le fichier `pena.flatpak` a déjà été construit (voir
|
||||||
|
sections 2 et 3) et déposé sur votre Gitea. Sur la machine cible, il suffit de le récupérer et
|
||||||
|
de l'installer.
|
||||||
|
|
||||||
|
### 1.1 — Installer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Récupérer le bundle depuis Gitea (adaptez l'URL à votre dépôt)
|
||||||
|
curl -L -o pena.flatpak \
|
||||||
|
https://git.goutailler-olivier.com/<utilisateur>/<depot>/raw/branch/main/pena.flatpak
|
||||||
|
|
||||||
|
# Installer pour l'utilisateur courant (aucun droit root nécessaire)
|
||||||
|
flatpak install --user pena.flatpak
|
||||||
|
```
|
||||||
|
|
||||||
|
> Le runtime `org.gnome.Platform` (qui fournit WebKitGTK) est téléchargé automatiquement
|
||||||
|
> depuis Flathub lors de l'installation s'il n'est pas déjà présent. C'est la seule dépendance,
|
||||||
|
> et elle est gérée par Flatpak — rien à compiler ni à installer sur l'hôte.
|
||||||
|
|
||||||
|
### 1.2 — Lancer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flatpak run com.pena.app
|
||||||
|
```
|
||||||
|
|
||||||
|
Pena apparaît aussi dans le menu des applications de Bazzite.
|
||||||
|
|
||||||
|
### 1.3 — Mettre à jour / désinstaller
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Mettre à jour : récupérer le nouveau bundle puis réinstaller par-dessus
|
||||||
|
flatpak install --user --reinstall pena.flatpak
|
||||||
|
|
||||||
|
# Désinstaller
|
||||||
|
flatpak uninstall com.pena.app
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.4 — (Optionnel) Installation en une ligne
|
||||||
|
|
||||||
|
Vous pouvez déposer à côté du bundle, sur Gitea, un script `install.sh` qui automatise le
|
||||||
|
téléchargement et l'installation :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
URL="https://git.goutailler-olivier.com/<utilisateur>/<depot>/raw/branch/main/pena.flatpak"
|
||||||
|
TMP="$(mktemp --suffix=.flatpak)"
|
||||||
|
curl -L -o "$TMP" "$URL"
|
||||||
|
flatpak install --user -y "$TMP"
|
||||||
|
rm -f "$TMP"
|
||||||
|
echo "Pena installé. Lancez-le avec : flatpak run com.pena.app"
|
||||||
|
```
|
||||||
|
|
||||||
|
L'installation se résume alors à :
|
||||||
|
```bash
|
||||||
|
curl -L https://git.goutailler-olivier.com/<utilisateur>/<depot>/raw/branch/main/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Compiler le binaire (une fois)
|
||||||
|
|
||||||
|
Cette étape et la suivante se font **sur une machine de build** (la vôtre, dans un container).
|
||||||
|
Le résultat est un fichier `pena.flatpak` que vous n'aurez plus qu'à héberger.
|
||||||
|
|
||||||
|
Pena se compile dans un container Ubuntu via **distrobox**, pour ne rien installer sur l'hôte
|
||||||
|
immuable. WebKitGTK et le compilateur restent dans le container.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Créer et entrer dans un container Ubuntu
|
||||||
|
distrobox create --name pena-build --image ubuntu:24.04
|
||||||
|
distrobox enter pena-build
|
||||||
|
|
||||||
|
# 2. (dans le container) installer les dépendances de build
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y \
|
||||||
|
libwebkit2gtk-4.1-dev build-essential curl wget file \
|
||||||
|
libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev git
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
|
source "$HOME/.cargo/env"
|
||||||
|
cargo install tauri-cli --version "^2"
|
||||||
|
|
||||||
|
# 3. (dans le container) compiler
|
||||||
|
cd ~/Pena/Pena-tauri # adaptez le chemin vers les sources
|
||||||
|
cargo tauri build
|
||||||
|
```
|
||||||
|
|
||||||
|
Le binaire est produit dans `src-tauri/target/release/pena-taury`. Vous pouvez ensuite
|
||||||
|
quitter le container (`exit`) : la suite (section 3) se fait sur l'hôte, où `flatpak` est
|
||||||
|
disponible. Comme `$HOME` est partagé entre l'hôte et le container, le binaire compilé est
|
||||||
|
accessible des deux côtés.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Construire le paquet `.flatpak` (une fois)
|
||||||
|
|
||||||
|
À partir du binaire compilé, on produit le fichier unique `pena.flatpak`.
|
||||||
|
|
||||||
|
### 3.1 — Outillage Flatpak (sur l'hôte)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flatpak install -y flathub org.gnome.Platform//47 org.gnome.Sdk//47 org.flatpak.Builder
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 — Fichiers d'empaquetage
|
||||||
|
|
||||||
|
Depuis la racine `Pena-tauri/`, créez deux fichiers.
|
||||||
|
|
||||||
|
**`com.pena.app.desktop`** :
|
||||||
|
```ini
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Pena
|
||||||
|
Comment=Lecteur Markdown
|
||||||
|
Exec=pena
|
||||||
|
Icon=com.pena.app
|
||||||
|
Terminal=false
|
||||||
|
Categories=Utility;Office;
|
||||||
|
```
|
||||||
|
|
||||||
|
**`com.pena.app.yml`** (manifeste Flatpak) :
|
||||||
|
```yaml
|
||||||
|
id: com.pena.app
|
||||||
|
runtime: org.gnome.Platform
|
||||||
|
runtime-version: '47'
|
||||||
|
sdk: org.gnome.Sdk
|
||||||
|
command: pena
|
||||||
|
|
||||||
|
finish-args:
|
||||||
|
- --share=ipc
|
||||||
|
- --socket=wayland
|
||||||
|
- --socket=fallback-x11
|
||||||
|
- --device=dri
|
||||||
|
# Accès en lecture aux fichiers Markdown de l'utilisateur
|
||||||
|
- --filesystem=home:ro
|
||||||
|
|
||||||
|
modules:
|
||||||
|
- name: pena
|
||||||
|
buildsystem: simple
|
||||||
|
build-commands:
|
||||||
|
- install -Dm755 pena-taury /app/bin/pena
|
||||||
|
- install -Dm644 com.pena.app.desktop /app/share/applications/com.pena.app.desktop
|
||||||
|
- install -Dm644 icon.png /app/share/icons/hicolor/512x512/apps/com.pena.app.png
|
||||||
|
sources:
|
||||||
|
- type: file
|
||||||
|
path: src-tauri/target/release/pena-taury
|
||||||
|
- type: file
|
||||||
|
path: com.pena.app.desktop
|
||||||
|
- type: file
|
||||||
|
path: src-tauri/icons/icon.png
|
||||||
|
```
|
||||||
|
|
||||||
|
> L'identifiant `com.pena.app` correspond au champ `identifier` de
|
||||||
|
> `src-tauri/tauri.conf.json`. Le runtime `org.gnome.Platform` embarque WebKitGTK : l'app
|
||||||
|
> fonctionnera sans aucune dépendance installée sur la machine cible.
|
||||||
|
|
||||||
|
### 3.3 — Construire le bundle
|
||||||
|
|
||||||
|
Depuis `Pena-tauri/` :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Construire l'app dans un dépôt OSTree local (dossier "repo")
|
||||||
|
flatpak run org.flatpak.Builder --force-clean --repo=repo \
|
||||||
|
--install-deps-from=flathub build-dir com.pena.app.yml
|
||||||
|
|
||||||
|
# 2. Exporter en UN SEUL fichier .flatpak
|
||||||
|
flatpak build-bundle repo pena.flatpak com.pena.app \
|
||||||
|
--runtime-repo=https://flathub.org/repo/flathub.flatpakrepo
|
||||||
|
```
|
||||||
|
|
||||||
|
Vous obtenez **`pena.flatpak`** : c'est ce fichier unique, autoportant, que vous installez
|
||||||
|
partout (section 1). L'option `--runtime-repo` y inscrit la référence vers Flathub, pour que
|
||||||
|
le runtime soit récupéré automatiquement à l'installation.
|
||||||
|
|
||||||
|
### 3.4 — Héberger sur Gitea
|
||||||
|
|
||||||
|
Déposez `pena.flatpak` sur votre dépôt Gitea, soit :
|
||||||
|
- en l'ajoutant au dépôt (`git add pena.flatpak`) — simple, mais alourdit l'historique ;
|
||||||
|
- **de préférence**, en l'attachant à une **release** Gitea (onglet *Releases* → *New Release*
|
||||||
|
→ joindre le binaire). L'URL de téléchargement direct est alors stable et n'encombre pas le
|
||||||
|
dépôt.
|
||||||
|
|
||||||
|
Adaptez ensuite l'URL utilisée à la [section 1.1](#11--installer).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Alternative développeur — lancer via distrobox
|
||||||
|
|
||||||
|
Si vous **développez** sur Pena et n'avez pas besoin d'un paquet installable, vous pouvez
|
||||||
|
exécuter l'application directement depuis le container, sans passer par Flatpak.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Lancement direct (la fenêtre s'affiche sur le bureau de l'hôte)
|
||||||
|
distrobox enter pena-build -- bash -lc 'cd ~/Pena/Pena-tauri && cargo tauri dev'
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour exposer le binaire compilé à l'hôte comme une commande :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Depuis le container, après "cargo tauri build"
|
||||||
|
distrobox-export --bin ~/Pena/Pena-tauri/src-tauri/target/release/pena-taury \
|
||||||
|
--export-path ~/.local/bin
|
||||||
|
```
|
||||||
|
|
||||||
|
Le binaire devient lançable depuis l'hôte (`~/.local/bin` doit être dans le `PATH`). Cette
|
||||||
|
voie est pratique pour itérer, mais le Flatpak (sections 1–3) reste la méthode recommandée
|
||||||
|
pour une **installation** propre et reproductible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Note — AppImage
|
||||||
|
|
||||||
|
Une autre forme de « fichier unique à exécuter » est l'**AppImage** : un binaire autoportant
|
||||||
|
qui se lance sans installation (`chmod +x Pena.AppImage && ./Pena.AppImage`). Tauri sait en
|
||||||
|
produire, mais cela nécessite d'activer le bundler (`bundle.active: true` dans
|
||||||
|
`tauri.conf.json`, cible `appimage`) et l'outillage associé. Le **bundle Flatpak** décrit
|
||||||
|
ci-dessus est privilégié ici car il s'intègre au menu, se met à jour proprement et gère
|
||||||
|
automatiquement ses dépendances via Flathub.
|
||||||
|
|
||||||
|
## Voir aussi
|
||||||
|
|
||||||
|
- [Release et distribution](3.4-Release-et-distribution.md) — pourquoi le Flatpak, portabilité et trade-offs des formats
|
||||||
|
- [Build et compilation](3.3-Build.md)
|
||||||
|
- [Installation (Linux générique / Fedora)](3.1-Installation.md)
|
||||||
|
- [Architecture](../2-technique/2.1-Architecture.md)
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# Build et compilation
|
||||||
|
|
||||||
|
Cette page décrit comment compiler **Pena-tauri** depuis les sources, en développement comme
|
||||||
|
en production. Pour une installation sur un système immuable (Bazzite, Silverblue, etc.),
|
||||||
|
voir plutôt [Installation sur Bazzite OS](3.2-Installation-Bazzite.md).
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
| Outil | Détail |
|
||||||
|
|---|---|
|
||||||
|
| **Rust** | Toolchain stable, via [rustup](https://rustup.rs/) (édition 2021). |
|
||||||
|
| **Tauri CLI v2** | `cargo install tauri-cli --version "^2"` (fournit `cargo tauri`). |
|
||||||
|
| **Dépendances système** | WebKitGTK et libs associées (voir ci-dessous selon la distribution). |
|
||||||
|
|
||||||
|
Le frontend n'a **aucune dépendance Node** : c'est du JS vanilla servi statiquement depuis
|
||||||
|
`src/`. Il n'y a donc pas de `npm install` à faire pour `Pena-tauri`.
|
||||||
|
|
||||||
|
### Dépendances système Linux
|
||||||
|
|
||||||
|
**Fedora / RHEL :**
|
||||||
|
```bash
|
||||||
|
sudo dnf install webkit2gtk4.1-devel \
|
||||||
|
openssl-devel curl wget file libappindicator-gtk3-devel librsvg2-devel
|
||||||
|
sudo dnf group install "C Development Tools and Libraries"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Debian / Ubuntu :**
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file \
|
||||||
|
libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
> Sur une distribution immuable (Bazzite, Fedora Silverblue, Kinoite…), n'installez pas ces
|
||||||
|
> paquets sur l'hôte : utilisez un container ou un Flatpak. Voir
|
||||||
|
> [Installation sur Bazzite OS](3.2-Installation-Bazzite.md).
|
||||||
|
|
||||||
|
## Lancer en développement
|
||||||
|
|
||||||
|
Depuis `Pena-tauri/` :
|
||||||
|
```bash
|
||||||
|
cargo tauri dev
|
||||||
|
```
|
||||||
|
|
||||||
|
- Le frontend est servi directement depuis `src/` (fichiers statiques, sans bundler).
|
||||||
|
- La compilation Rust est lancée automatiquement et l'application s'ouvre.
|
||||||
|
- Toute modification du Rust déclenche une recompilation.
|
||||||
|
|
||||||
|
## Construire une release
|
||||||
|
|
||||||
|
Depuis `Pena-tauri/` :
|
||||||
|
```bash
|
||||||
|
cargo tauri build
|
||||||
|
```
|
||||||
|
|
||||||
|
L'exécutable est produit dans :
|
||||||
|
```
|
||||||
|
src-tauri/target/release/
|
||||||
|
```
|
||||||
|
|
||||||
|
### À propos du bundling
|
||||||
|
|
||||||
|
Dans `src-tauri/tauri.conf.json`, le bundling est **désactivé** :
|
||||||
|
```json
|
||||||
|
"bundle": { "active": false, "targets": "all", "icon": [] }
|
||||||
|
```
|
||||||
|
|
||||||
|
`cargo tauri build` produit donc l'**exécutable natif** (dans `target/release/`), mais ne
|
||||||
|
génère pas de paquets `.deb`, `.rpm` ou `.AppImage`. Pour activer la génération de ces
|
||||||
|
paquets, passer `bundle.active` à `true` et renseigner les icônes.
|
||||||
|
|
||||||
|
## Compiler le backend seul (sans Tauri CLI)
|
||||||
|
|
||||||
|
Pour de la compilation / des tests bas niveau, depuis `src-tauri/` :
|
||||||
|
```bash
|
||||||
|
cargo build # compilation debug
|
||||||
|
cargo build --release # compilation optimisée
|
||||||
|
cargo test # tests unitaires
|
||||||
|
cargo clippy -- -D warnings # lint (zéro warning toléré)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Qualité et couverture
|
||||||
|
|
||||||
|
Le dépôt `Pena-tauri` impose un hook `pre-commit` qui bloque le commit si :
|
||||||
|
- **Clippy** remonte le moindre warning (`-D warnings`) ;
|
||||||
|
- la **couverture de lignes** est inférieure à **60 %** (via `cargo-llvm-cov`).
|
||||||
|
|
||||||
|
Installer l'outil de couverture :
|
||||||
|
```bash
|
||||||
|
cargo install cargo-llvm-cov
|
||||||
|
cargo llvm-cov --summary-only
|
||||||
|
```
|
||||||
|
|
||||||
|
## Récapitulatif des commandes
|
||||||
|
|
||||||
|
| But | Commande (depuis `Pena-tauri/`) |
|
||||||
|
|---|---|
|
||||||
|
| Développement | `cargo tauri dev` |
|
||||||
|
| Release (exécutable) | `cargo tauri build` |
|
||||||
|
| Compiler le backend | `cargo build --release` (depuis `src-tauri/`) |
|
||||||
|
| Tests | `cargo test` (depuis `src-tauri/`) |
|
||||||
|
| Lint | `cargo clippy -- -D warnings` |
|
||||||
|
| Couverture | `cargo llvm-cov --summary-only` |
|
||||||
|
|
||||||
|
## Voir aussi
|
||||||
|
|
||||||
|
- [Release et distribution](3.4-Release-et-distribution.md) — portabilité du binaire et création d'un paquet Flatpak
|
||||||
|
- [Installation (Linux générique)](3.1-Installation.md)
|
||||||
|
- [Installation sur Bazzite OS](3.2-Installation-Bazzite.md)
|
||||||
|
- [Architecture](../2-technique/2.1-Architecture.md)
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
# Release et distribution
|
||||||
|
|
||||||
|
Cette page explique **comment fonctionne une release de Pena** : ce que produit réellement la
|
||||||
|
compilation, **si un binaire compilé sur une distribution Linux est utilisable sur n'importe
|
||||||
|
quelle autre**, et les différentes façons de **distribuer** l'application — avec un focus sur le
|
||||||
|
**paquet Flatpak** et ses avantages/inconvénients.
|
||||||
|
|
||||||
|
- Pour les commandes de compilation pures, voir [Build et compilation](3.3-Build.md).
|
||||||
|
- Pour la procédure pas-à-pas d'installation Flatpak sur système immuable, voir
|
||||||
|
[Installation sur Bazzite OS](3.2-Installation-Bazzite.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Que produit une release ?
|
||||||
|
|
||||||
|
`cargo tauri build` (depuis `Pena-tauri/`) produit un **exécutable natif** :
|
||||||
|
|
||||||
|
```
|
||||||
|
src-tauri/target/release/pena-taury
|
||||||
|
```
|
||||||
|
|
||||||
|
C'est un binaire ELF compilé pour l'architecture de la machine de build (typiquement
|
||||||
|
`x86_64-linux-gnu`). Le frontend (HTML/CSS/JS de `src/`) y est **embarqué** dans le binaire :
|
||||||
|
il n'y a pas de fichiers web à distribuer à côté.
|
||||||
|
|
||||||
|
> Dans `tauri.conf.json`, `bundle.active` vaut `false` : `cargo tauri build` ne génère donc
|
||||||
|
> **pas** de `.deb`/`.rpm`/`.AppImage` automatiquement. Il produit l'exécutable seul. La
|
||||||
|
> génération de paquets est traitée plus bas (section 4).
|
||||||
|
|
||||||
|
### Ce que le binaire contient — et ce qu'il ne contient pas
|
||||||
|
|
||||||
|
| Embarqué dans le binaire | **Pas** embarqué (fourni par le système) |
|
||||||
|
|---|---|
|
||||||
|
| Code Rust de Pena (commandes Tauri) | **glibc** (bibliothèque C) |
|
||||||
|
| Frontend (HTML/CSS/JS) | **WebKitGTK** (le moteur de rendu de la fenêtre) |
|
||||||
|
| `comrak` + `syntect` (rendu Markdown) | **GTK 3/4**, GLib, Cairo, Pango… |
|
||||||
|
| Plugins Tauri liés statiquement | Pilotes graphiques, serveur d'affichage (X11/Wayland) |
|
||||||
|
|
||||||
|
C'est ce tableau qui répond à la question de la portabilité.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Un binaire compilé sur Linux marche-t-il sur **n'importe quelle** distribution ?
|
||||||
|
|
||||||
|
**Réponse courte : non, pas de façon fiable.** Un binaire « brut » (le `pena-taury` produit
|
||||||
|
ci-dessus) n'est **pas universel**. Il dépend de bibliothèques présentes sur le système cible,
|
||||||
|
et il ne tournera ailleurs que si ces bibliothèques sont **présentes et compatibles**.
|
||||||
|
|
||||||
|
Trois raisons concrètes :
|
||||||
|
|
||||||
|
### 2.1 — Le lien dynamique avec WebKitGTK
|
||||||
|
|
||||||
|
C'est la contrainte la plus dure pour une app Tauri. Le binaire est lié à
|
||||||
|
**`libwebkit2gtk-4.1`**. Sur la machine cible, il faut :
|
||||||
|
|
||||||
|
- que WebKitGTK soit **installé** ;
|
||||||
|
- que ce soit la **bonne version d'ABI** : `webkit2gtk-4.0` et `webkit2gtk-4.1` ont des
|
||||||
|
*sonames* différents et **ne sont pas interchangeables**. Un binaire lié à `4.1` ne démarrera
|
||||||
|
pas sur une machine qui n'a que `4.0`, et inversement.
|
||||||
|
|
||||||
|
Sur une distribution récente, WebKitGTK 4.1 est généralement disponible ; sur une
|
||||||
|
distribution plus ancienne ou minimaliste, il peut manquer.
|
||||||
|
|
||||||
|
### 2.2 — La version de la glibc
|
||||||
|
|
||||||
|
Les binaires liés à la glibc sont **compatibles vers l'avant, pas vers l'arrière** :
|
||||||
|
|
||||||
|
- un binaire compilé sur une **vieille** glibc tourne sur une machine ayant une glibc **plus
|
||||||
|
récente** ✅ ;
|
||||||
|
- un binaire compilé sur une glibc **récente** échoue sur une machine ayant une glibc **plus
|
||||||
|
ancienne** ❌, avec une erreur du type `version 'GLIBC_2.38' not found`.
|
||||||
|
|
||||||
|
**Conséquence pratique :** pour maximiser la portabilité d'un binaire brut, compilez-le sur la
|
||||||
|
distribution la **plus ancienne** que vous comptez supporter (par ex. dans un container
|
||||||
|
Ubuntu 22.04), pas sur la plus récente.
|
||||||
|
|
||||||
|
### 2.3 — Les autres bibliothèques système
|
||||||
|
|
||||||
|
GTK, GLib, Cairo, Pango, librsvg, libappindicator… doivent aussi être présentes. Sur un poste
|
||||||
|
de bureau classique elles le sont presque toujours (ce sont des dépendances de l'environnement
|
||||||
|
de bureau), mais sur un serveur, un système minimal ou immuable, ce n'est pas garanti.
|
||||||
|
|
||||||
|
### Récapitulatif portabilité
|
||||||
|
|
||||||
|
| Cible | Le binaire brut marche-t-il ? |
|
||||||
|
|---|---|
|
||||||
|
| Même distribution / version que la machine de build | ✅ Oui |
|
||||||
|
| Distribution différente mais récente, avec `webkit2gtk-4.1` et glibc ≥ celle du build | ✅ Généralement |
|
||||||
|
| Distribution avec une glibc **plus ancienne** que le build | ❌ Non (`GLIBC_x.y not found`) |
|
||||||
|
| Distribution qui n'a que `webkit2gtk-4.0` | ❌ Non (soname incompatible) |
|
||||||
|
| Système immuable (Bazzite, Silverblue…) | ❌ Pas directement — voir Flatpak |
|
||||||
|
| Architecture différente (ARM vs x86_64) | ❌ Non — il faut recompiler pour l'archi |
|
||||||
|
|
||||||
|
**Conclusion :** « compilé sur Linux » ne veut pas dire « marche sur tout Linux ». Pour une
|
||||||
|
distribution **réellement universelle**, il faut un format qui **embarque ses dépendances** :
|
||||||
|
c'est le rôle du **Flatpak** (recommandé ici) ou de l'**AppImage**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Les formats de distribution possibles
|
||||||
|
|
||||||
|
| Format | Dépendances | Portabilité | Intégration bureau | Mise à jour |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **Binaire brut** | Fournies par l'hôte | Faible (voir §2) | Manuelle (`.desktop`) | Manuelle |
|
||||||
|
| **`.deb` / `.rpm`** | Déclarées, résolues par le gestionnaire de paquets | Bonne **sur la famille visée** (Debian *ou* Fedora) | Automatique | Via le gestionnaire |
|
||||||
|
| **AppImage** | **Embarquées** dans un fichier exécutable | Bonne (un seul fichier portable) | Partielle | Manuelle (re-télécharger) |
|
||||||
|
| **Flatpak** | **Embarquées** via un *runtime* partagé (Flathub) | **Excellente** (toute distro avec `flatpak`) | Complète (menu, icône) | `flatpak update` |
|
||||||
|
|
||||||
|
Pour Pena, le format **recommandé est le Flatpak** : il résout d'un coup les trois problèmes du
|
||||||
|
§2 (WebKitGTK, glibc, libs système) en fournissant un **runtime** complet et identique partout.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Créer un paquet Flatpak
|
||||||
|
|
||||||
|
Le Flatpak rend l'application **vraiment portable** : le moteur WebKitGTK et toutes les
|
||||||
|
bibliothèques système viennent du **runtime `org.gnome.Platform`** (téléchargé depuis Flathub),
|
||||||
|
pas de l'hôte. Le même `pena.flatpak` s'installe alors sur n'importe quelle distribution dotée
|
||||||
|
de `flatpak`, immuable ou non.
|
||||||
|
|
||||||
|
> La procédure complète (installation côté machine cible, hébergement sur Gitea, alternative
|
||||||
|
> développeur) est détaillée dans [Installation sur Bazzite OS](3.2-Installation-Bazzite.md).
|
||||||
|
> On résume ici les étapes de **production** du paquet.
|
||||||
|
|
||||||
|
### 4.1 — Principe en trois temps
|
||||||
|
|
||||||
|
```
|
||||||
|
[ cargo tauri build ] → binaire natif pena-taury
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
[ manifeste Flatpak ] → build dans un dépôt OSTree local "repo/"
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
[ flatpak build-bundle ] → fichier unique pena.flatpak
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 — Outillage (une fois)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flatpak install -y flathub org.gnome.Platform//47 org.gnome.Sdk//47 org.flatpak.Builder
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 — Le manifeste
|
||||||
|
|
||||||
|
Le manifeste décrit l'identifiant de l'app, le runtime qui fournit les dépendances, et comment
|
||||||
|
installer le binaire dans le sandbox. Depuis `Pena-tauri/`, créer **`com.pena.app.yml`** :
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
id: com.pena.app # = identifier de tauri.conf.json
|
||||||
|
runtime: org.gnome.Platform # fournit WebKitGTK + GTK + glibc du runtime
|
||||||
|
runtime-version: '47'
|
||||||
|
sdk: org.gnome.Sdk
|
||||||
|
command: pena
|
||||||
|
|
||||||
|
finish-args:
|
||||||
|
- --share=ipc
|
||||||
|
- --socket=wayland
|
||||||
|
- --socket=fallback-x11
|
||||||
|
- --device=dri
|
||||||
|
- --filesystem=home:ro # lecture des fichiers Markdown de l'utilisateur
|
||||||
|
|
||||||
|
modules:
|
||||||
|
- name: pena
|
||||||
|
buildsystem: simple
|
||||||
|
build-commands:
|
||||||
|
- install -Dm755 pena-taury /app/bin/pena
|
||||||
|
- install -Dm644 com.pena.app.desktop /app/share/applications/com.pena.app.desktop
|
||||||
|
- install -Dm644 icon.png /app/share/icons/hicolor/512x512/apps/com.pena.app.png
|
||||||
|
sources:
|
||||||
|
- type: file
|
||||||
|
path: src-tauri/target/release/pena-taury
|
||||||
|
- type: file
|
||||||
|
path: com.pena.app.desktop
|
||||||
|
- type: file
|
||||||
|
path: src-tauri/icons/icon.png
|
||||||
|
```
|
||||||
|
|
||||||
|
Et le fichier d'intégration au menu **`com.pena.app.desktop`** :
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Pena
|
||||||
|
Comment=Lecteur Markdown
|
||||||
|
Exec=pena
|
||||||
|
Icon=com.pena.app
|
||||||
|
Terminal=false
|
||||||
|
Categories=Utility;Office;
|
||||||
|
```
|
||||||
|
|
||||||
|
> Point clé sur la portabilité : `finish-args` définit le **sandbox**. Pena n'a besoin que de
|
||||||
|
> l'affichage (Wayland/X11), du GPU (`dri`) et d'un **accès en lecture seule** au `home`
|
||||||
|
> (`--filesystem=home:ro`) pour ouvrir les fichiers `.md`. C'est volontairement minimal.
|
||||||
|
|
||||||
|
### 4.4 — Construire le bundle
|
||||||
|
|
||||||
|
Depuis `Pena-tauri/`, après un `cargo tauri build` réussi :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Construire dans un dépôt OSTree local "repo/"
|
||||||
|
flatpak run org.flatpak.Builder --force-clean --repo=repo \
|
||||||
|
--install-deps-from=flathub build-dir com.pena.app.yml
|
||||||
|
|
||||||
|
# 2. Exporter en UN SEUL fichier .flatpak autoportant
|
||||||
|
flatpak build-bundle repo pena.flatpak com.pena.app \
|
||||||
|
--runtime-repo=https://flathub.org/repo/flathub.flatpakrepo
|
||||||
|
```
|
||||||
|
|
||||||
|
On obtient **`pena.flatpak`** : un fichier unique, installable partout par
|
||||||
|
`flatpak install --user pena.flatpak`. L'option `--runtime-repo` y inscrit la référence
|
||||||
|
Flathub, pour que le runtime soit récupéré automatiquement à l'installation s'il manque.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Flatpak : avantages et inconvénients
|
||||||
|
|
||||||
|
### Avantages
|
||||||
|
|
||||||
|
- **Portabilité réelle.** Le runtime fournit WebKitGTK, GTK et une glibc cohérente : les trois
|
||||||
|
blocages du §2 disparaissent. Le même fichier marche sur Fedora, Ubuntu, Arch, Bazzite,
|
||||||
|
Silverblue… sans recompiler.
|
||||||
|
- **Aucune dépendance à installer sur l'hôte.** Idéal pour les systèmes **immuables** (Bazzite,
|
||||||
|
Silverblue) où l'on ne peut pas faire `dnf install webkit2gtk-devel`.
|
||||||
|
- **Installation sans root** (`--user`).
|
||||||
|
- **Sandbox.** L'app n'a accès qu'à ce que `finish-args` autorise (ici : affichage + `home` en
|
||||||
|
lecture seule). Surface d'attaque réduite.
|
||||||
|
- **Intégration de bureau** automatique (entrée de menu, icône) et **mises à jour** par
|
||||||
|
`flatpak update`.
|
||||||
|
- **Reproductible.** Le runtime versionné (`//47`) garantit le même socle partout.
|
||||||
|
|
||||||
|
### Inconvénients
|
||||||
|
|
||||||
|
- **Taille.** Le **runtime** `org.gnome.Platform` pèse plusieurs centaines de Mo. Il n'est
|
||||||
|
téléchargé qu'une fois et partagé entre toutes les apps Flatpak, mais c'est lourd pour une app
|
||||||
|
aussi simple que Pena si c'est le seul Flatpak de la machine.
|
||||||
|
- **Premier lancement / première install** plus longs (téléchargement du runtime).
|
||||||
|
- **Le sandbox demande de la réflexion.** Tout accès fichier hors `home:ro` doit être déclaré
|
||||||
|
explicitement ; un oubli se traduit par une fonctionnalité qui « ne voit pas » les fichiers.
|
||||||
|
- **Dépendance à Flathub** pour récupérer le runtime (réseau requis à l'installation).
|
||||||
|
- **Outillage de build** supplémentaire (`flatpak-builder`, SDK) par rapport à un simple
|
||||||
|
`cargo tauri build`.
|
||||||
|
|
||||||
|
### Quand préférer une autre option
|
||||||
|
|
||||||
|
| Situation | Format conseillé |
|
||||||
|
|---|---|
|
||||||
|
| Diffusion large, multi-distributions, systèmes immuables | **Flatpak** |
|
||||||
|
| Un seul fichier à exécuter sans rien installer | **AppImage** (`bundle.active: true`, cible `appimage`) |
|
||||||
|
| Cible une seule famille (que Debian, ou que Fedora) avec gestion par paquets | **`.deb`** / **`.rpm`** |
|
||||||
|
| Usage perso sur sa propre machine, ou itération de dev | **Binaire brut** / `cargo tauri dev` |
|
||||||
|
|
||||||
|
> **AppImage** est l'autre format « un seul fichier ». Tauri sait en produire (activer le
|
||||||
|
> bundler dans `tauri.conf.json`), mais le **Flatpak** est privilégié ici pour son intégration
|
||||||
|
> au menu, ses mises à jour propres et la gestion automatique des dépendances via Flathub.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Liste de contrôle d'une release
|
||||||
|
|
||||||
|
1. Vérifier la version dans `src-tauri/tauri.conf.json` (`"version"`).
|
||||||
|
2. Compiler dans un environnement à **glibc ancienne** si l'on distribue un binaire brut
|
||||||
|
(container Ubuntu 22.04), sinon directement.
|
||||||
|
3. `cargo tauri build` → vérifier `src-tauri/target/release/pena-taury`.
|
||||||
|
4. Construire le `pena.flatpak` (section 4).
|
||||||
|
5. Tester l'install sur une machine **vierge** : `flatpak install --user pena.flatpak` puis
|
||||||
|
`flatpak run com.pena.app`.
|
||||||
|
6. Héberger le `.flatpak` sur **une release Gitea** (URL stable, n'alourdit pas le dépôt) —
|
||||||
|
voir [3.2 §3.4](3.2-Installation-Bazzite.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Voir aussi
|
||||||
|
|
||||||
|
- [Build et compilation](3.3-Build.md)
|
||||||
|
- [Installation sur Bazzite OS](3.2-Installation-Bazzite.md) — installation et hébergement du Flatpak
|
||||||
|
- [Installation (Linux générique / Fedora)](3.1-Installation.md)
|
||||||
|
- [Architecture](../2-technique/2.1-Architecture.md)
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# MarkdownRender (CLI `md-render`)
|
||||||
|
|
||||||
|
`MarkdownRender-nodejs` est un **outil en ligne de commande** indépendant de l'application
|
||||||
|
Tauri. Il convertit du Markdown en HTML et propose une prévisualisation live dans le
|
||||||
|
navigateur. Il est utile pour générer rapidement un `.html` ou prévisualiser un document sans
|
||||||
|
lancer Pena.
|
||||||
|
|
||||||
|
> C'est un utilitaire **annexe** ; le produit principal reste
|
||||||
|
> [Pena-tauri](../2-technique/2.1-Architecture.md).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Prérequis : **Node.js** (avec `npm`).
|
||||||
|
|
||||||
|
Depuis `MarkdownRender-nodejs/` :
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm link # rend la commande `md-render` disponible globalement
|
||||||
|
```
|
||||||
|
|
||||||
|
## Utilisation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Conversion simple (génère un fichier .html à côté de l'entrée)
|
||||||
|
md-render README.md
|
||||||
|
|
||||||
|
# Conversion vers un fichier de sortie précis
|
||||||
|
md-render README.md -o documentation.html
|
||||||
|
|
||||||
|
# Mode preview — ouvre le navigateur sans créer de fichier
|
||||||
|
md-render README.md --preview
|
||||||
|
|
||||||
|
# Preview d'un dossier entier (avec barre latérale)
|
||||||
|
md-render mon-wiki/ --preview
|
||||||
|
|
||||||
|
# Mode watch — sert http://localhost:3000 avec rechargement automatique
|
||||||
|
md-render README.md --watch
|
||||||
|
|
||||||
|
# Sur un port personnalisé
|
||||||
|
md-render README.md --watch --port 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
| Option | Alias | Description | Défaut |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `--watch` | `-w` | Démarre un serveur avec rechargement automatique | — |
|
||||||
|
| `--preview` | `-v` | Ouvre dans le navigateur sans créer de fichier | — |
|
||||||
|
| `--port <port>` | `-p` | Port du serveur (watch ou preview) | `3000` |
|
||||||
|
| `--output <fichier>` | `-o` | Fichier HTML de sortie | `<entrée>.html` |
|
||||||
|
| `--help` | `-h` | Affiche l'aide | — |
|
||||||
|
|
||||||
|
## Modes
|
||||||
|
|
||||||
|
### Mode preview (`--preview`)
|
||||||
|
Le Markdown est converti **en mémoire** et affiché directement dans le navigateur — aucun
|
||||||
|
fichier HTML n'est écrit sur le disque. Le navigateur s'ouvre automatiquement.
|
||||||
|
`Ctrl+C` arrête le serveur. Fonctionne sur un fichier unique ou un dossier entier.
|
||||||
|
|
||||||
|
### Mode watch (`--watch`)
|
||||||
|
Un serveur HTTP local est démarré. À chaque sauvegarde du fichier, la page se recharge
|
||||||
|
automatiquement via **Server-Sent Events (SSE)**. Les assets relatifs (images…) placés dans
|
||||||
|
le même répertoire que le `.md` sont servis automatiquement.
|
||||||
|
|
||||||
|
## Fonctionnalités
|
||||||
|
|
||||||
|
- 🎨 Coloration syntaxique du code ([highlight.js](https://highlightjs.org/))
|
||||||
|
- 📄 Rendu de style « GitHub-like »
|
||||||
|
- 👁 Prévisualisation instantanée sans fichier (`--preview`)
|
||||||
|
- 🔄 Rechargement automatique en mode watch (SSE)
|
||||||
|
- 🖼 Serveur de fichiers statiques pour les images locales
|
||||||
|
- ⚡ Débounce intelligent pour éviter les rechargements en rafale
|
||||||
|
- 🛡 Protection contre le path traversal
|
||||||
|
|
||||||
|
## Dépendances principales
|
||||||
|
|
||||||
|
| Paquet | Rôle |
|
||||||
|
|---|---|
|
||||||
|
| [`marked`](https://marked.js.org/) | Conversion Markdown → HTML |
|
||||||
|
| `marked-highlight` + [`highlight.js`](https://highlightjs.org/) | Coloration syntaxique |
|
||||||
|
| [`chokidar`](https://github.com/paulmillr/chokidar) | Surveillance de fichiers (mode watch) |
|
||||||
|
| [`minimist`](https://github.com/minimistjs/minimist) | Analyse des arguments CLI |
|
||||||
|
|
||||||
|
## Différences avec Pena-tauri
|
||||||
|
|
||||||
|
| | `md-render` (CLI) | Pena-tauri (app) |
|
||||||
|
|---|---|---|
|
||||||
|
| Plateforme | Terminal + navigateur | Application de bureau |
|
||||||
|
| Rendu Markdown | `marked` (JS) | `comrak` (Rust) |
|
||||||
|
| Coloration | highlight.js | syntect |
|
||||||
|
| Live-reload | ✅ (SSE) | implémenté mais non branché |
|
||||||
|
| Thèmes / CSS éditable | — | ✅ |
|
||||||
|
| Sortie fichier HTML | ✅ | — |
|
||||||
|
|
||||||
|
## Voir aussi
|
||||||
|
|
||||||
|
- [Vue d'ensemble de Pena](../1-fonctionnel/1.1-Vue-d-ensemble.md)
|
||||||
|
- [README de la documentation](../README.md)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Documentation Pena
|
||||||
|
|
||||||
|
**Pena** est une application de bureau de lecture de fichiers Markdown, construite avec
|
||||||
|
[Tauri 2](https://tauri.app/) (backend Rust) et du JavaScript vanilla (frontend, sans bundler).
|
||||||
|
Elle permet d'ouvrir un fichier `.md` isolé ou un dossier « wiki » complet, de naviguer entre
|
||||||
|
les documents, et de personnaliser entièrement le rendu via des thèmes et du CSS.
|
||||||
|
|
||||||
|
Le rendu Markdown → HTML est effectué côté Rust avec [`comrak`](https://github.com/kivikakk/comrak)
|
||||||
|
(CommonMark + extensions) et la coloration syntaxique via [`syntect`](https://github.com/trishume/syntect).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sommaire de la documentation
|
||||||
|
|
||||||
|
### Documentation fonctionnelle
|
||||||
|
- [Vue d'ensemble](1-fonctionnel/1.1-Vue-d-ensemble.md) — à quoi sert Pena, pour qui, dans quels cas
|
||||||
|
- [Fonctionnalités](1-fonctionnel/1.2-Fonctionnalites.md) — liste détaillée de tout ce que l'application sait faire
|
||||||
|
- [Interface](1-fonctionnel/1.3-Interface.md) — barre de titre, accueil, lecteur, barre latérale
|
||||||
|
- [Thèmes et personnalisation](1-fonctionnel/1.4-Themes-et-personnalisation.md) — thèmes rapides et éditeur CSS
|
||||||
|
|
||||||
|
### Documentation technique
|
||||||
|
- [Architecture](2-technique/2.1-Architecture.md) — Clean Architecture, découpage Rust et JS
|
||||||
|
- [Commandes Tauri](2-technique/2.2-Commandes-Tauri.md) — référence des commandes exposées au frontend
|
||||||
|
|
||||||
|
### Installation et build
|
||||||
|
- [Installation (Linux générique / Fedora)](3-installation/3.1-Installation.md)
|
||||||
|
- [Installation sur Bazzite OS](3-installation/3.2-Installation-Bazzite.md) — méthode simple : un fichier `.flatpak` pré-construit, hébergé sur Gitea, installable en une commande
|
||||||
|
- [Build et compilation](3-installation/3.3-Build.md)
|
||||||
|
- [Release et distribution](3-installation/3.4-Release-et-distribution.md) — portabilité d'un binaire Linux, formats de distribution, création d'un paquet Flatpak (avantages / inconvénients)
|
||||||
|
|
||||||
|
### Outils annexes
|
||||||
|
- [MarkdownRender (CLI Node.js)](4-outils/4.1-MarkdownRender-CLI.md) — convertisseur `md-render` en ligne de commande
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Composants du dépôt
|
||||||
|
|
||||||
|
| Composant | Techno | Rôle |
|
||||||
|
|---|---|---|
|
||||||
|
| `Pena-tauri/` | Tauri 2 + Rust + JS vanilla | Application de bureau principale (lecteur Markdown) |
|
||||||
|
| `MarkdownRender-nodejs/` | Node.js | Outil CLI annexe `md-render` (conversion + preview live dans le navigateur) |
|
||||||
|
|
||||||
|
> Le cœur du produit est **`Pena-tauri`**. `MarkdownRender-nodejs` est un utilitaire
|
||||||
|
> indépendant, documenté dans [outils/MarkdownRender-CLI.md](4-outils/4.1-MarkdownRender-CLI.md).
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Construit un paquet Flatpak de Pena à partir du .deb généré par Tauri.
|
||||||
|
#
|
||||||
|
# Étapes :
|
||||||
|
# 1. `cargo tauri build --bundles deb` -> produit le .deb autonome
|
||||||
|
# 2. Copie du .deb à côté du manifeste sous le nom `pena.deb`
|
||||||
|
# 3. `flatpak-builder` construit le bundle dans la sandbox GNOME
|
||||||
|
# 4. Installation locale (--user) ET export du fichier pena.flatpak
|
||||||
|
#
|
||||||
|
# Le fichier `pena.flatpak` est TOUJOURS exporté à côté du script.
|
||||||
|
#
|
||||||
|
# Usage :
|
||||||
|
# ./build-flatpak.sh # build + installation (--user) + pena.flatpak
|
||||||
|
# ./build-flatpak.sh --no-install # build + pena.flatpak seul (pas d'installation)
|
||||||
|
# ./build-flatpak.sh --no-deb # réutilise le .deb déjà présent (build rapide)
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
APP_ID="com.pena.app"
|
||||||
|
RUNTIME_VERSION="48"
|
||||||
|
RUST_CONTAINER="ubuntu-rust" # distrobox contenant cargo-tauri + libs GTK/WebKit
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")" # .../Pena-tauri
|
||||||
|
MANIFEST="$SCRIPT_DIR/${APP_ID}.yml"
|
||||||
|
BUILD_DIR="$SCRIPT_DIR/.build" # arbre de build flatpak-builder
|
||||||
|
REPO_DIR="$SCRIPT_DIR/.repo" # dépôt ostree temporaire
|
||||||
|
DEB_TARGET="$SCRIPT_DIR/pena.deb"
|
||||||
|
TAURI_CONF="$PROJECT_DIR/src-tauri/tauri.conf.json"
|
||||||
|
METAINFO="$SCRIPT_DIR/${APP_ID}.metainfo.xml"
|
||||||
|
|
||||||
|
DO_INSTALL=1
|
||||||
|
BUILD_DEB=1
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--no-install|--bundle) DO_INSTALL=0 ;; # --bundle : alias rétrocompatible
|
||||||
|
--no-deb) BUILD_DEB=0 ;;
|
||||||
|
-h|--help) sed -n '2,17p' "$0"; exit 0 ;;
|
||||||
|
*) echo "Argument inconnu : $arg" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Vérification des outils -------------------------------------------------
|
||||||
|
need() { command -v "$1" >/dev/null 2>&1 || { echo "Outil manquant : $1" >&2; exit 1; }; }
|
||||||
|
need flatpak
|
||||||
|
|
||||||
|
# flatpak-builder : commande native, sinon le Flatpak org.flatpak.Builder
|
||||||
|
# (cas des distributions atomiques type Bazzite/Silverblue).
|
||||||
|
if command -v flatpak-builder >/dev/null 2>&1; then
|
||||||
|
FLATPAK_BUILDER=(flatpak-builder)
|
||||||
|
elif flatpak info org.flatpak.Builder >/dev/null 2>&1; then
|
||||||
|
FLATPAK_BUILDER=(flatpak run org.flatpak.Builder)
|
||||||
|
else
|
||||||
|
echo "flatpak-builder est requis. Installez-le :" >&2
|
||||||
|
echo " flatpak install -y flathub org.flatpak.Builder" >&2
|
||||||
|
echo " ou sur Fedora classique : sudo dnf install flatpak-builder" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Remote flathub (scope --user) -------------------------------------------
|
||||||
|
# flatpak-builder est lancé en --user et installe les dépendances (runtime/SDK
|
||||||
|
# via --install-deps-from=flathub) dans cette même installation. Le remote
|
||||||
|
# flathub doit donc exister au niveau --user, ce qui n'est pas le cas par
|
||||||
|
# défaut sur les distributions atomiques (où flathub est un remote système).
|
||||||
|
echo ">> Vérification du remote flathub (--user)…"
|
||||||
|
flatpak remote-add --if-not-exists --user flathub \
|
||||||
|
https://flathub.org/repo/flathub.flatpakrepo
|
||||||
|
|
||||||
|
# --- 0. Version : source de vérité = tauri.conf.json -------------------------
|
||||||
|
# On lit la version applicative depuis tauri.conf.json et on la répercute dans
|
||||||
|
# le metainfo AppStream (release) afin que le Flatpak porte toujours la
|
||||||
|
# dernière version déclarée dans les sources.
|
||||||
|
need jq
|
||||||
|
VERSION="$(jq -r '.version' "$TAURI_CONF")"
|
||||||
|
if [[ -z "$VERSION" || "$VERSION" == "null" ]]; then
|
||||||
|
echo "Impossible de lire la version dans $TAURI_CONF" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
TODAY="$(date -u +%Y-%m-%d)"
|
||||||
|
echo ">> Version applicative : $VERSION (build du $TODAY)"
|
||||||
|
|
||||||
|
# Met à jour (ou insère) l'entrée <release> du metainfo pour cette version.
|
||||||
|
if grep -q "version=\"$VERSION\"" "$METAINFO"; then
|
||||||
|
echo ">> Release $VERSION déjà présente dans le metainfo."
|
||||||
|
else
|
||||||
|
echo ">> Ajout de la release $VERSION au metainfo…"
|
||||||
|
sed -i \
|
||||||
|
"s#<releases>#<releases>\n <release version=\"$VERSION\" date=\"$TODAY\"/>#" \
|
||||||
|
"$METAINFO"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 1. Build du .deb via Tauri ---------------------------------------------
|
||||||
|
if [[ "$BUILD_DEB" -eq 1 ]]; then
|
||||||
|
# La compilation Tauri exige les libs de dev GTK/WebKit, absentes de l'hôte
|
||||||
|
# (distribution atomique). On build donc dans le distrobox `$RUST_CONTAINER`,
|
||||||
|
# qui embarque cargo-tauri et ces libs. flatpak-builder reste sur l'hôte.
|
||||||
|
need distrobox
|
||||||
|
echo ">> Build du paquet .deb dans le distrobox $RUST_CONTAINER…"
|
||||||
|
distrobox enter "$RUST_CONTAINER" -- \
|
||||||
|
bash -lc "cd '$PROJECT_DIR' && cargo tauri build --bundles deb"
|
||||||
|
fi
|
||||||
|
|
||||||
|
DEB_SRC="$(find "$PROJECT_DIR/src-tauri/target/release/bundle/deb" -name '*.deb' 2>/dev/null | sort | tail -n1 || true)"
|
||||||
|
if [[ -z "$DEB_SRC" ]]; then
|
||||||
|
echo "Aucun .deb trouvé. Lancez le script sans --no-deb." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo ">> Utilisation du .deb : $DEB_SRC"
|
||||||
|
cp -f "$DEB_SRC" "$DEB_TARGET"
|
||||||
|
|
||||||
|
# Icône installée dans le Flatpak (128x128) depuis les sources du projet.
|
||||||
|
cp -f "$PROJECT_DIR/src-tauri/icons/128x128.png" "$SCRIPT_DIR/icon.png"
|
||||||
|
|
||||||
|
# --- 2. Build Flatpak --------------------------------------------------------
|
||||||
|
# Construit l'app dans un dépôt OSTree local ($REPO_DIR) ; les dépendances
|
||||||
|
# manquantes (runtime/SDK) sont récupérées depuis flathub.
|
||||||
|
echo ">> Construction du Flatpak…"
|
||||||
|
rm -rf "$BUILD_DIR"
|
||||||
|
FB_ARGS=(--force-clean --user --install-deps-from=flathub --repo="$REPO_DIR")
|
||||||
|
if [[ "$DO_INSTALL" -eq 1 ]]; then
|
||||||
|
FB_ARGS+=(--install)
|
||||||
|
fi
|
||||||
|
"${FLATPAK_BUILDER[@]}" "${FB_ARGS[@]}" "$BUILD_DIR" "$MANIFEST"
|
||||||
|
|
||||||
|
# --- 3. Export du fichier .flatpak (toujours) --------------------------------
|
||||||
|
# Le bundle autonome est systématiquement exporté à côté du manifeste.
|
||||||
|
OUT="$SCRIPT_DIR/pena.flatpak"
|
||||||
|
echo ">> Export du bundle : $OUT"
|
||||||
|
flatpak build-bundle "$REPO_DIR" "$OUT" "$APP_ID" \
|
||||||
|
--runtime-repo=https://flathub.org/repo/flathub.flatpakrepo
|
||||||
|
|
||||||
|
echo ">> Nettoyage du dépôt OSTree temporaire…"
|
||||||
|
rm -rf "$REPO_DIR"
|
||||||
|
|
||||||
|
if [[ "$DO_INSTALL" -eq 1 ]]; then
|
||||||
|
echo ">> Pena $VERSION installé (--user) et bundle exporté : $OUT"
|
||||||
|
echo ">> Lancer avec : flatpak run $APP_ID"
|
||||||
|
else
|
||||||
|
echo ">> Bundle exporté : $OUT"
|
||||||
|
echo ">> Installer avec : flatpak install --user $OUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ">> Terminé."
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<component type="desktop-application">
|
||||||
|
<id>com.pena.app</id>
|
||||||
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
|
<project_license>MIT</project_license>
|
||||||
|
<name>Pena</name>
|
||||||
|
<summary>Lecteur de fichiers Markdown</summary>
|
||||||
|
<description>
|
||||||
|
<p>
|
||||||
|
Pena est une application de bureau pour lire des fichiers Markdown.
|
||||||
|
La conversion Markdown vers HTML est réalisée côté Rust (comrak), avec
|
||||||
|
coloration syntaxique des blocs de code et plusieurs thèmes de lecture.
|
||||||
|
</p>
|
||||||
|
</description>
|
||||||
|
<launchable type="desktop-id">com.pena.app.desktop</launchable>
|
||||||
|
<categories>
|
||||||
|
<category>Utility</category>
|
||||||
|
<category>TextEditor</category>
|
||||||
|
</categories>
|
||||||
|
<content_rating type="oars-1.1"/>
|
||||||
|
<releases>
|
||||||
|
<release version="0.1.2" date="2026-07-05"/>
|
||||||
|
<release version="0.1.0" date="2026-06-23"/>
|
||||||
|
</releases>
|
||||||
|
</component>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
id: com.pena.app
|
||||||
|
runtime: org.gnome.Platform
|
||||||
|
runtime-version: '48'
|
||||||
|
sdk: org.gnome.Sdk
|
||||||
|
command: pena
|
||||||
|
|
||||||
|
finish-args:
|
||||||
|
# Affichage
|
||||||
|
- --socket=wayland
|
||||||
|
- --socket=fallback-x11
|
||||||
|
- --device=dri
|
||||||
|
- --share=ipc
|
||||||
|
# Lecture des fichiers Markdown ouverts par l'utilisateur
|
||||||
|
- --filesystem=home
|
||||||
|
|
||||||
|
modules:
|
||||||
|
- name: pena
|
||||||
|
buildsystem: simple
|
||||||
|
sources:
|
||||||
|
- type: file
|
||||||
|
path: pena.deb
|
||||||
|
- type: file
|
||||||
|
path: com.pena.app.metainfo.xml
|
||||||
|
- type: file
|
||||||
|
path: icon.png
|
||||||
|
build-commands:
|
||||||
|
- |
|
||||||
|
set -eux
|
||||||
|
# Extraction du .deb produit par `cargo tauri build`
|
||||||
|
ar -x pena.deb
|
||||||
|
tar -xf data.tar.gz
|
||||||
|
|
||||||
|
# Binaire principal -> /app/bin/pena (correspond à `command:`)
|
||||||
|
bin="$(find usr/bin -maxdepth 1 -type f | head -n1)"
|
||||||
|
install -Dm755 "$bin" /app/bin/pena
|
||||||
|
|
||||||
|
# Fichier .desktop : renommé en <app-id>.desktop (exigence Flatpak)
|
||||||
|
desktop="$(find usr/share/applications -name '*.desktop' | head -n1)"
|
||||||
|
install -Dm644 "$desktop" /app/share/applications/com.pena.app.desktop
|
||||||
|
sed -i \
|
||||||
|
-e 's/^Exec=.*/Exec=pena/' \
|
||||||
|
-e 's/^Icon=.*/Icon=com.pena.app/' \
|
||||||
|
/app/share/applications/com.pena.app.desktop
|
||||||
|
|
||||||
|
# Icône (128x128) fournie séparément à côté du manifeste.
|
||||||
|
install -Dm644 icon.png /app/share/icons/hicolor/128x128/apps/com.pena.app.png
|
||||||
|
|
||||||
|
# Métadonnées AppStream
|
||||||
|
install -Dm644 com.pena.app.metainfo.xml /app/share/metainfo/com.pena.app.metainfo.xml
|
||||||
|
After Width: | Height: | Size: 6.9 KiB |
@@ -2445,7 +2445,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pena-taury"
|
name = "pena-taury"
|
||||||
version = "0.1.0"
|
version = "0.1.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"comrak",
|
"comrak",
|
||||||
"notify",
|
"notify",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "pena-taury"
|
name = "pena-taury"
|
||||||
version = "0.1.0"
|
version = "0.1.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
@@ -17,5 +17,5 @@ tauri-plugin-dialog = "2"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
comrak = { version = "0.28", features = ["syntect"] }
|
comrak = { version = "0.28", features = ["syntect"] }
|
||||||
syntect = "5"
|
syntect = { version = "5", features = ["yaml-load"] }
|
||||||
notify = "7"
|
notify = "7"
|
||||||
|
|||||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#fff</color>
|
||||||
|
</resources>
|
||||||
|
After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 105 B After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 928 B |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 253 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
.content { background: #1e1e2e; color: #cdd6f4; }
|
||||||
|
.content h1, .content h2, .content h3, .content h4, .content h5, .content h6 { color: #cdd6f4; }
|
||||||
|
.content p, .content ul li, .content ol li { color: #cdd6f4; }
|
||||||
|
.content a { color: #89b4fa; }
|
||||||
|
.content a:visited { color: #cba6f7; }
|
||||||
|
.content code { background: #313244; color: #cba6f7; }
|
||||||
|
.content pre { background: #313244; }
|
||||||
|
.content blockquote { border-color: #6c7086; color: #a6adc8; }
|
||||||
|
.content table td, .content table th { color: #cdd6f4; }
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
/* ─────────────────────────────────────────────
|
||||||
|
Thème par défaut — basé sur Shell Indigo
|
||||||
|
Modifiez les valeurs pour personnaliser
|
||||||
|
l'apparence de Pena.
|
||||||
|
───────────────────────────────────────────── */
|
||||||
|
|
||||||
|
|
||||||
|
/* ── Page ──────────────────────────────────── */
|
||||||
|
|
||||||
|
/* Couleur de fond de la fenêtre (visible autour du contenu) */
|
||||||
|
body {
|
||||||
|
background: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Couleur de surbrillance lors d'une sélection de texte */
|
||||||
|
::selection {
|
||||||
|
background: #c7d2fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ── Zone de contenu ────────────────────────── */
|
||||||
|
|
||||||
|
/* Fond et couleur de texte principale de la zone de lecture */
|
||||||
|
.content {
|
||||||
|
background: #ffffff;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ── Liens ──────────────────────────────────── */
|
||||||
|
|
||||||
|
/* Lien normal (non visité) */
|
||||||
|
.content a {
|
||||||
|
color: #4f46e5;
|
||||||
|
border-bottom: 1px solid #c7d2fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lien au survol de la souris */
|
||||||
|
.content a:hover {
|
||||||
|
color: #3730a3;
|
||||||
|
border-bottom-color: #4f46e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lien déjà visité */
|
||||||
|
.content a:visited {
|
||||||
|
color: #6d28d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lien déjà visité au survol */
|
||||||
|
.content a:visited:hover {
|
||||||
|
color: #3730a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ── Titres ─────────────────────────────────── */
|
||||||
|
|
||||||
|
/* Couleur commune à tous les niveaux de titre (h1 à h6) */
|
||||||
|
.content h1,
|
||||||
|
.content h2,
|
||||||
|
.content h3,
|
||||||
|
.content h4,
|
||||||
|
.content h5,
|
||||||
|
.content h6 {
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ── Texte courant ──────────────────────────── */
|
||||||
|
|
||||||
|
/* Paragraphes */
|
||||||
|
.content p {
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Éléments de liste (à puces et numérotées) */
|
||||||
|
.content ul li,
|
||||||
|
.content ol li {
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Texte en gras */
|
||||||
|
.content strong {
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ligne de séparation horizontale (---) */
|
||||||
|
.content hr {
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ── Code ───────────────────────────────────── */
|
||||||
|
|
||||||
|
/* Code inline (entre backticks simples) */
|
||||||
|
.content :not(pre) > code {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #4f46e5;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bloc de code sans coloration syntaxique */
|
||||||
|
.content pre:not(.syntax-highlighting) {
|
||||||
|
background: #0f172a;
|
||||||
|
border: 1px solid #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bloc de code avec coloration syntaxique */
|
||||||
|
.content .syntax-highlighting {
|
||||||
|
border: 1px solid #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Texte dans un bloc sans coloration */
|
||||||
|
.content pre:not(.syntax-highlighting) code {
|
||||||
|
background: none;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ── Citation ───────────────────────────────── */
|
||||||
|
|
||||||
|
/* Bloc de citation (> texte) */
|
||||||
|
.content blockquote {
|
||||||
|
border-color: #818cf8;
|
||||||
|
background: #f5f3ff;
|
||||||
|
color: #4338ca;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ── Tableaux ───────────────────────────────── */
|
||||||
|
|
||||||
|
/* Conteneur : bordure extérieure et coins arrondis */
|
||||||
|
.content table {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ligne d'en-tête : fond gris clair */
|
||||||
|
.content thead {
|
||||||
|
background: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cellule d'en-tête : texte en majuscules espacées, séparateur vertical */
|
||||||
|
.content table th {
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
font-size: 0.77rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: #334155;
|
||||||
|
border-bottom: 2px solid #4f46e5;
|
||||||
|
border-right: 1px solid #cbd5e1;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pas de bordure droite sur la dernière colonne d'en-tête */
|
||||||
|
.content table th:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cellule de données : padding, séparateurs horizontaux et verticaux */
|
||||||
|
.content table td {
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
border-right: 1px solid #e2e8f0;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pas de bordure droite sur la dernière colonne */
|
||||||
|
.content table td:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pas de bordure basse sur la dernière ligne (évite le double bord avec le conteneur) */
|
||||||
|
.content tbody tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lignes paires : fond légèrement différent pour faciliter la lecture */
|
||||||
|
.content tbody tr:nth-child(even) {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ligne au survol : mise en évidence */
|
||||||
|
.content tbody tr:hover {
|
||||||
|
background: #eff6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ── Barre latérale ─────────────────────────── */
|
||||||
|
|
||||||
|
/* Fond de la barre latérale */
|
||||||
|
.sidebar {
|
||||||
|
background: #1a1b2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lien racine (premier niveau) au survol */
|
||||||
|
.nav-root-link:hover {
|
||||||
|
color: #818cf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lien racine (premier niveau) actif (page ouverte) */
|
||||||
|
.nav-root-link.active {
|
||||||
|
color: #818cf8;
|
||||||
|
background: rgba(129, 140, 248, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lien de dossier au survol */
|
||||||
|
.nav-folder-link:hover {
|
||||||
|
color: #818cf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lien de dossier actif */
|
||||||
|
.nav-folder-link.active {
|
||||||
|
color: #818cf8;
|
||||||
|
background: rgba(129, 140, 248, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lien de fichier (sous-niveaux) au survol */
|
||||||
|
.nav-file-link:hover {
|
||||||
|
color: #818cf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lien de fichier actif (page ouverte) */
|
||||||
|
.nav-file-link.active {
|
||||||
|
color: #818cf8;
|
||||||
|
background: rgba(129, 140, 248, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ── Barre de titre ─────────────────────────── */
|
||||||
|
|
||||||
|
/* Fond et bordure inférieure de la barre de titre */
|
||||||
|
#titlebar {
|
||||||
|
background: #1a1b2e;
|
||||||
|
/* border-bottom: 1px solid #1e293b; */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Texte du titre de la fenêtre */
|
||||||
|
#titlebar-title {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Boutons de contrôle (réduire, agrandir, fermer) au repos */
|
||||||
|
.titlebar-controls button {
|
||||||
|
color: rgba(255, 255, 255, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Boutons de contrôle au survol */
|
||||||
|
.titlebar-controls button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.content a:hover { color: #10b981; }
|
||||||
|
.content table th { border-bottom-color: #10b981; }
|
||||||
|
.content table td { border-bottom-color: rgba(16, 185, 129, 0.25); }
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.content { font-size: 18px; }
|
||||||
|
.content p { line-height: 1.8; }
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
.content { background: #f4ecd8; }
|
||||||
|
.content h1, .content h2, .content h3, .content h4, .content h5, .content h6 { color: #5c4a1e; }
|
||||||
|
.content p, .content ul li, .content ol li { color: #5c4a1e; }
|
||||||
|
.content a { color: #7a5c2e; }
|
||||||
|
.content code { background: #e8d5b0; color: #5c4a1e; }
|
||||||
|
.content pre { background: #e8d5b0; }
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
body { background: #f1f5f9; }
|
||||||
|
::selection { background: #c7d2fe; }
|
||||||
|
.content { background: #ffffff; color: #1e293b; }
|
||||||
|
.content a { color: #4f46e5; border-bottom: 1px solid #c7d2fe; }
|
||||||
|
.content a:hover { color: #3730a3; border-bottom-color: #4f46e5; }
|
||||||
|
.content a:visited { color: #6d28d9; }
|
||||||
|
.content a:visited:hover { color: #3730a3; }
|
||||||
|
.content h1, .content h2, .content h3, .content h4, .content h5, .content h6 { color: #0f172a; }
|
||||||
|
.content p { color: #1e293b; }
|
||||||
|
.content ul li, .content ol li { color: #1e293b; }
|
||||||
|
.content :not(pre) > code { background: #f1f5f9; color: #4f46e5; border: 1px solid #e2e8f0; }
|
||||||
|
.content pre:not(.syntax-highlighting) { background: #0f172a; border: 1px solid #1e293b; border-radius: 10px; }
|
||||||
|
.content .syntax-highlighting { border: 1px solid #1e293b; border-radius: 10px; }
|
||||||
|
.content pre:not(.syntax-highlighting) code { background: none; color: #e2e8f0; }
|
||||||
|
.content blockquote { border-color: #818cf8; background: #f5f3ff; color: #4338ca; }
|
||||||
|
.content table { color: #374151; }
|
||||||
|
.content table th { border-bottom-color: #4f46e5; color: #334155; }
|
||||||
|
.content table td { border-bottom-color: rgba(79, 70, 229, 0.25); }
|
||||||
|
.content strong { color: #0f172a; }
|
||||||
|
.content hr { border-color: #e2e8f0; }
|
||||||
|
.nav-root-link:hover { color: #818cf8; }
|
||||||
|
.nav-root-link.active { color: #818cf8; background: rgba(129, 140, 248, 0.08); }
|
||||||
|
.nav-folder-link:hover { color: #818cf8; }
|
||||||
|
.nav-folder-link.active { color: #818cf8; background: rgba(129, 140, 248, 0.08); }
|
||||||
|
.nav-file-link:hover { color: #818cf8; }
|
||||||
|
.nav-file-link.active { color: #818cf8; background: rgba(129, 140, 248, 0.08); }
|
||||||
|
#titlebar { background: #0f172a; border-bottom: 1px solid #1e293b; }
|
||||||
|
#titlebar-title { color: rgba(255,255,255,0.5); }
|
||||||
|
.titlebar-controls button { color: rgba(255,255,255,0.45); }
|
||||||
|
.titlebar-controls button:hover { background: rgba(255,255,255,0.08); color: rgba(255,255,255,0.9); }
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
use crate::infrastructure::file_repository;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub fn list_markdown_files(dir: &str) -> Result<Vec<String>, String> {
|
||||||
|
let path = Path::new(dir);
|
||||||
|
if !path.is_dir() {
|
||||||
|
return Err(format!("{dir} n'est pas un dossier"));
|
||||||
|
}
|
||||||
|
let mut files = Vec::new();
|
||||||
|
file_repository::collect_md_files(path, &mut files).map_err(|e| e.to_string())?;
|
||||||
|
files.sort();
|
||||||
|
Ok(files)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
fn tmpdir(name: &str) -> std::path::PathBuf {
|
||||||
|
std::env::temp_dir().join(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_markdown_files_sorted() {
|
||||||
|
let dir = tmpdir("pena_fs_sorted");
|
||||||
|
fs::create_dir_all(&dir).unwrap();
|
||||||
|
fs::write(dir.join("z.md"), "").unwrap();
|
||||||
|
fs::write(dir.join("a.md"), "").unwrap();
|
||||||
|
fs::write(dir.join("m.md"), "").unwrap();
|
||||||
|
let result = list_markdown_files(dir.to_str().unwrap()).unwrap();
|
||||||
|
fs::remove_dir_all(&dir).unwrap();
|
||||||
|
assert_eq!(result.len(), 3);
|
||||||
|
assert!(result[0] < result[1] && result[1] < result[2]);
|
||||||
|
assert!(result[0].ends_with("a.md"));
|
||||||
|
assert!(result[2].ends_with("z.md"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_markdown_files_ignores_non_md() {
|
||||||
|
let dir = tmpdir("pena_fs_non_md");
|
||||||
|
fs::create_dir_all(&dir).unwrap();
|
||||||
|
fs::write(dir.join("doc.md"), "").unwrap();
|
||||||
|
fs::write(dir.join("notes.txt"), "").unwrap();
|
||||||
|
let result = list_markdown_files(dir.to_str().unwrap()).unwrap();
|
||||||
|
fs::remove_dir_all(&dir).unwrap();
|
||||||
|
assert_eq!(result.len(), 1);
|
||||||
|
assert!(result[0].ends_with("doc.md"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_markdown_files_not_a_dir() {
|
||||||
|
let result = list_markdown_files("/nonexistent/pena_fs_dir");
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_markdown_files_empty_dir() {
|
||||||
|
let dir = tmpdir("pena_fs_empty");
|
||||||
|
fs::create_dir_all(&dir).unwrap();
|
||||||
|
let result = list_markdown_files(dir.to_str().unwrap()).unwrap();
|
||||||
|
fs::remove_dir_all(&dir).unwrap();
|
||||||
|
assert_eq!(result.len(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod file_service;
|
||||||
|
pub mod render_service;
|
||||||
|
pub mod theme_service;
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
use crate::domain::MarkdownRenderer;
|
||||||
|
use crate::infrastructure::file_repository;
|
||||||
|
|
||||||
|
pub fn render_string(renderer: &dyn MarkdownRenderer, content: &str) -> String {
|
||||||
|
renderer.render(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_file(renderer: &dyn MarkdownRenderer, path: &str) -> Result<String, String> {
|
||||||
|
let content = file_repository::read_file(path)?;
|
||||||
|
Ok(renderer.render(&content))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
struct MockRenderer;
|
||||||
|
|
||||||
|
impl MarkdownRenderer for MockRenderer {
|
||||||
|
fn render(&self, content: &str) -> String {
|
||||||
|
format!("<mock>{content}</mock>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_string_delegates_to_renderer() {
|
||||||
|
let renderer = MockRenderer;
|
||||||
|
let result = render_string(&renderer, "hello");
|
||||||
|
assert_eq!(result, "<mock>hello</mock>");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_string_empty_content() {
|
||||||
|
let renderer = MockRenderer;
|
||||||
|
let result = render_string(&renderer, "");
|
||||||
|
assert_eq!(result, "<mock></mock>");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_file_success() {
|
||||||
|
let dir = std::env::temp_dir().join("pena_rs_render_file");
|
||||||
|
fs::create_dir_all(&dir).unwrap();
|
||||||
|
let file = dir.join("test.md");
|
||||||
|
fs::write(&file, "world").unwrap();
|
||||||
|
let renderer = MockRenderer;
|
||||||
|
let result = render_file(&renderer, file.to_str().unwrap());
|
||||||
|
fs::remove_dir_all(&dir).unwrap();
|
||||||
|
assert_eq!(result.unwrap(), "<mock>world</mock>");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_file_not_found() {
|
||||||
|
let renderer = MockRenderer;
|
||||||
|
let result = render_file(&renderer, "/nonexistent/pena_rs_file.md");
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
use crate::domain::theme::{Theme, ThemeRepository};
|
||||||
|
|
||||||
|
pub fn list_themes(repo: &dyn ThemeRepository) -> Vec<Theme> {
|
||||||
|
repo.list()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_theme_css(repo: &dyn ThemeRepository, id: &str) -> Option<String> {
|
||||||
|
repo.get_css(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::domain::theme::{Theme, ThemeRepository};
|
||||||
|
|
||||||
|
struct MockRepo {
|
||||||
|
themes: Vec<(&'static str, &'static str, &'static str)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ThemeRepository for MockRepo {
|
||||||
|
fn list(&self) -> Vec<Theme> {
|
||||||
|
self.themes.iter().map(|(id, label, _)| Theme { id: id.to_string(), label: label.to_string() }).collect()
|
||||||
|
}
|
||||||
|
fn get_css(&self, id: &str) -> Option<String> {
|
||||||
|
self.themes.iter().find(|(i, _, _)| *i == id).map(|(_, _, css)| css.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mock_repo() -> MockRepo {
|
||||||
|
MockRepo {
|
||||||
|
themes: vec![
|
||||||
|
("dark", "Mode sombre", ".content{background:#000}"),
|
||||||
|
("light", "Clair", ".content{background:#fff}"),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_themes_delegates_to_repo() {
|
||||||
|
let repo = mock_repo();
|
||||||
|
let themes = list_themes(&repo);
|
||||||
|
assert_eq!(themes.len(), 2);
|
||||||
|
assert_eq!(themes[0].id, "dark");
|
||||||
|
assert_eq!(themes[1].label, "Clair");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_theme_css_returns_css_for_known_id() {
|
||||||
|
let repo = mock_repo();
|
||||||
|
let css = get_theme_css(&repo, "dark");
|
||||||
|
assert_eq!(css, Some(".content{background:#000}".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_theme_css_returns_none_for_unknown_id() {
|
||||||
|
let repo = mock_repo();
|
||||||
|
assert!(get_theme_css(&repo, "unknown").is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod render;
|
||||||
|
pub mod theme;
|
||||||
|
pub mod watch;
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
use crate::application::{file_service, render_service};
|
||||||
|
use crate::infrastructure::comrak_renderer::{
|
||||||
|
syntax_css_for_theme, ComrakPreviewRenderer, ComrakRenderer,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn render_markdown(content: String) -> String {
|
||||||
|
let renderer = ComrakPreviewRenderer;
|
||||||
|
render_service::render_string(&renderer, &content)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn convert_file(path: String) -> Result<String, String> {
|
||||||
|
let renderer = ComrakRenderer;
|
||||||
|
render_service::render_file(&renderer, &path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_syntax_highlight_css() -> String {
|
||||||
|
syntax_css_for_theme("base16-ocean.dark")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn list_md_files(dir: String) -> Result<Vec<String>, String> {
|
||||||
|
file_service::list_markdown_files(&dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
fn tmpdir(name: &str) -> std::path::PathBuf {
|
||||||
|
std::env::temp_dir().join(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_markdown_heading() {
|
||||||
|
let html = render_markdown("# Hello\n".to_string());
|
||||||
|
assert!(html.contains("<h1>"));
|
||||||
|
assert!(html.contains("Hello"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_markdown_code_block() {
|
||||||
|
let html = render_markdown("```rust\nfn main() {}\n```\n".to_string());
|
||||||
|
assert!(html.contains("syntax-highlighting"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_markdown_empty() {
|
||||||
|
let html = render_markdown(String::new());
|
||||||
|
assert!(!html.contains("<h1>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn convert_file_success() {
|
||||||
|
let dir = tmpdir("pena_convert_success");
|
||||||
|
fs::create_dir_all(&dir).unwrap();
|
||||||
|
let file = dir.join("test.md");
|
||||||
|
fs::write(&file, "# Title\n\nParagraph.").unwrap();
|
||||||
|
let result = convert_file(file.to_string_lossy().to_string());
|
||||||
|
fs::remove_dir_all(&dir).unwrap();
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert!(result.unwrap().contains("<h1>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn convert_file_with_table() {
|
||||||
|
let dir = tmpdir("pena_convert_table");
|
||||||
|
fs::create_dir_all(&dir).unwrap();
|
||||||
|
let file = dir.join("table.md");
|
||||||
|
fs::write(&file, "| A | B |\n|---|---|\n| 1 | 2 |").unwrap();
|
||||||
|
let result = convert_file(file.to_string_lossy().to_string());
|
||||||
|
fs::remove_dir_all(&dir).unwrap();
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert!(result.unwrap().contains("<table>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn convert_file_not_found() {
|
||||||
|
let result = convert_file("/nonexistent/path/does/not/exist.md".to_string());
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_syntax_highlight_css_returns_non_empty() {
|
||||||
|
let css = get_syntax_highlight_css();
|
||||||
|
assert!(!css.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_syntax_highlight_css_contains_wrapper_class() {
|
||||||
|
let css = get_syntax_highlight_css();
|
||||||
|
assert!(css.contains(".syntax-highlighting {"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_md_files_sorted() {
|
||||||
|
let dir = tmpdir("pena_list_sorted");
|
||||||
|
fs::create_dir_all(&dir).unwrap();
|
||||||
|
fs::write(dir.join("z.md"), "").unwrap();
|
||||||
|
fs::write(dir.join("a.md"), "").unwrap();
|
||||||
|
fs::write(dir.join("m.md"), "").unwrap();
|
||||||
|
let result = list_md_files(dir.to_string_lossy().to_string()).unwrap();
|
||||||
|
fs::remove_dir_all(&dir).unwrap();
|
||||||
|
assert_eq!(result.len(), 3);
|
||||||
|
assert!(result[0] < result[1] && result[1] < result[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_md_files_not_a_dir() {
|
||||||
|
let result = list_md_files("/nonexistent/does/not/exist".to_string());
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
use crate::application::theme_service;
|
||||||
|
use crate::infrastructure::theme_repository::StaticThemeRepository;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct ThemeDto {
|
||||||
|
pub id: String,
|
||||||
|
pub label: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn list_themes() -> Vec<ThemeDto> {
|
||||||
|
let repo = StaticThemeRepository;
|
||||||
|
theme_service::list_themes(&repo)
|
||||||
|
.into_iter()
|
||||||
|
.map(|t| ThemeDto { id: t.id, label: t.label })
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_theme_css(id: String) -> Option<String> {
|
||||||
|
let repo = StaticThemeRepository;
|
||||||
|
theme_service::get_theme_css(&repo, &id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_themes_returns_non_empty() {
|
||||||
|
let themes = list_themes();
|
||||||
|
assert!(!themes.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_themes_ids_are_non_empty() {
|
||||||
|
for theme in list_themes() {
|
||||||
|
assert!(!theme.id.is_empty());
|
||||||
|
assert!(!theme.label.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_theme_css_known() {
|
||||||
|
assert!(get_theme_css("dark".into()).is_some());
|
||||||
|
assert!(get_theme_css("shell-indigo".into()).is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_theme_css_unknown_returns_none() {
|
||||||
|
assert!(get_theme_css("does-not-exist".into()).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
use crate::domain::MarkdownRenderer;
|
|
||||||
use crate::infrastructure::comrak_renderer::{ComrakPreviewRenderer, ComrakRenderer};
|
|
||||||
use crate::infrastructure::file_repository;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
pub fn render_markdown(content: String) -> String {
|
|
||||||
ComrakPreviewRenderer {
|
|
||||||
theme: "base16-ocean.dark".to_string(),
|
|
||||||
}
|
|
||||||
.render(&content)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn convert_file(path: String) -> Result<String, String> {
|
|
||||||
let content = file_repository::read_file(&path)?;
|
|
||||||
Ok(ComrakRenderer {
|
|
||||||
theme: "InspiredGitHub".to_string(),
|
|
||||||
}
|
|
||||||
.render(&content))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn collect_md_files(dir: &Path, result: &mut Vec<String>) -> std::io::Result<()> {
|
|
||||||
file_repository::collect_md_files(dir, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list_md_files(dir: String) -> Result<Vec<String>, String> {
|
|
||||||
let path = Path::new(&dir);
|
|
||||||
if !path.is_dir() {
|
|
||||||
return Err(format!("{dir} n'est pas un dossier"));
|
|
||||||
}
|
|
||||||
let mut files = Vec::new();
|
|
||||||
collect_md_files(path, &mut files).map_err(|e| e.to_string())?;
|
|
||||||
files.sort();
|
|
||||||
Ok(files)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use std::fs;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
fn tmpdir(name: &str) -> PathBuf {
|
|
||||||
std::env::temp_dir().join(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn render_markdown_heading() {
|
|
||||||
let html = render_markdown("# Hello\n".to_string());
|
|
||||||
assert!(html.contains("<h1>"));
|
|
||||||
assert!(html.contains("Hello"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn render_markdown_code_block() {
|
|
||||||
let html = render_markdown("```rust\nfn main() {}\n```\n".to_string());
|
|
||||||
assert!(html.contains("<code") || html.contains("<pre>"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn render_markdown_empty() {
|
|
||||||
let html = render_markdown(String::new());
|
|
||||||
assert!(!html.contains("<h1>"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn convert_file_success() {
|
|
||||||
let dir = tmpdir("pena_convert_success");
|
|
||||||
fs::create_dir_all(&dir).unwrap();
|
|
||||||
let file = dir.join("test.md");
|
|
||||||
fs::write(&file, "# Title\n\nParagraph.").unwrap();
|
|
||||||
let result = convert_file(file.to_string_lossy().to_string());
|
|
||||||
fs::remove_dir_all(&dir).unwrap();
|
|
||||||
assert!(result.is_ok());
|
|
||||||
assert!(result.unwrap().contains("<h1>"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn convert_file_with_table() {
|
|
||||||
let dir = tmpdir("pena_convert_table");
|
|
||||||
fs::create_dir_all(&dir).unwrap();
|
|
||||||
let file = dir.join("table.md");
|
|
||||||
fs::write(&file, "| A | B |\n|---|---|\n| 1 | 2 |").unwrap();
|
|
||||||
let result = convert_file(file.to_string_lossy().to_string());
|
|
||||||
fs::remove_dir_all(&dir).unwrap();
|
|
||||||
assert!(result.is_ok());
|
|
||||||
assert!(result.unwrap().contains("<table>"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn convert_file_not_found() {
|
|
||||||
let result = convert_file("/nonexistent/path/does/not/exist.md".to_string());
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn collect_md_flat() {
|
|
||||||
let dir = tmpdir("pena_collect_flat");
|
|
||||||
fs::create_dir_all(&dir).unwrap();
|
|
||||||
fs::write(dir.join("a.md"), "").unwrap();
|
|
||||||
fs::write(dir.join("b.md"), "").unwrap();
|
|
||||||
fs::write(dir.join("c.txt"), "").unwrap();
|
|
||||||
let mut result = Vec::new();
|
|
||||||
collect_md_files(&dir, &mut result).unwrap();
|
|
||||||
result.sort();
|
|
||||||
fs::remove_dir_all(&dir).unwrap();
|
|
||||||
assert_eq!(result.len(), 2);
|
|
||||||
assert!(result[0].ends_with("a.md"));
|
|
||||||
assert!(result[1].ends_with("b.md"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn collect_md_recursive() {
|
|
||||||
let dir = tmpdir("pena_collect_recursive");
|
|
||||||
let sub = dir.join("docs");
|
|
||||||
fs::create_dir_all(&sub).unwrap();
|
|
||||||
fs::write(dir.join("Home.md"), "").unwrap();
|
|
||||||
fs::write(sub.join("Page.md"), "").unwrap();
|
|
||||||
let mut result = Vec::new();
|
|
||||||
collect_md_files(&dir, &mut result).unwrap();
|
|
||||||
fs::remove_dir_all(&dir).unwrap();
|
|
||||||
assert_eq!(result.len(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn collect_md_ignores_hidden_dir() {
|
|
||||||
let dir = tmpdir("pena_collect_hidden_dir");
|
|
||||||
let hidden = dir.join(".hidden");
|
|
||||||
fs::create_dir_all(&hidden).unwrap();
|
|
||||||
fs::write(dir.join("visible.md"), "").unwrap();
|
|
||||||
fs::write(hidden.join("secret.md"), "").unwrap();
|
|
||||||
let mut result = Vec::new();
|
|
||||||
collect_md_files(&dir, &mut result).unwrap();
|
|
||||||
fs::remove_dir_all(&dir).unwrap();
|
|
||||||
assert_eq!(result.len(), 1);
|
|
||||||
assert!(result[0].ends_with("visible.md"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn collect_md_ignores_hidden_file() {
|
|
||||||
let dir = tmpdir("pena_collect_hidden_file");
|
|
||||||
fs::create_dir_all(&dir).unwrap();
|
|
||||||
fs::write(dir.join("visible.md"), "").unwrap();
|
|
||||||
fs::write(dir.join(".hidden.md"), "").unwrap();
|
|
||||||
let mut result = Vec::new();
|
|
||||||
collect_md_files(&dir, &mut result).unwrap();
|
|
||||||
fs::remove_dir_all(&dir).unwrap();
|
|
||||||
assert_eq!(result.len(), 1);
|
|
||||||
assert!(result[0].ends_with("visible.md"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn collect_md_nonexistent_dir() {
|
|
||||||
let mut result = Vec::new();
|
|
||||||
let err = collect_md_files(Path::new("/nonexistent/path/for/pena"), &mut result);
|
|
||||||
assert!(err.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn list_md_files_sorted() {
|
|
||||||
let dir = tmpdir("pena_list_sorted");
|
|
||||||
fs::create_dir_all(&dir).unwrap();
|
|
||||||
fs::write(dir.join("z.md"), "").unwrap();
|
|
||||||
fs::write(dir.join("a.md"), "").unwrap();
|
|
||||||
fs::write(dir.join("m.md"), "").unwrap();
|
|
||||||
let result = list_md_files(dir.to_string_lossy().to_string()).unwrap();
|
|
||||||
fs::remove_dir_all(&dir).unwrap();
|
|
||||||
assert_eq!(result.len(), 3);
|
|
||||||
assert!(result[0] < result[1] && result[1] < result[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn list_md_files_not_a_dir() {
|
|
||||||
let result = list_md_files("/nonexistent/does/not/exist".to_string());
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
mod markdown;
|
mod markdown;
|
||||||
|
pub mod theme;
|
||||||
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
pub use markdown::{MarkdownRenderer, RenderOptions};
|
pub use markdown::{MarkdownRenderer, RenderOptions};
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
pub struct Theme {
|
||||||
|
pub id: String,
|
||||||
|
pub label: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ThemeRepository: Send + Sync {
|
||||||
|
fn list(&self) -> Vec<Theme>;
|
||||||
|
fn get_css(&self, id: &str) -> Option<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
struct StubRepo;
|
||||||
|
impl ThemeRepository for StubRepo {
|
||||||
|
fn list(&self) -> Vec<Theme> {
|
||||||
|
vec![Theme { id: "a".into(), label: "A".into() }]
|
||||||
|
}
|
||||||
|
fn get_css(&self, id: &str) -> Option<String> {
|
||||||
|
if id == "a" { Some(".x{}".into()) } else { None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn theme_fields_accessible() {
|
||||||
|
let t = Theme { id: "dark".into(), label: "Mode sombre".into() };
|
||||||
|
assert_eq!(t.id, "dark");
|
||||||
|
assert_eq!(t.label, "Mode sombre");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn repository_trait_object_works() {
|
||||||
|
let repo: Box<dyn ThemeRepository> = Box::new(StubRepo);
|
||||||
|
assert_eq!(repo.list().len(), 1);
|
||||||
|
assert!(repo.get_css("a").is_some());
|
||||||
|
assert!(repo.get_css("unknown").is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,31 @@
|
|||||||
use comrak::plugins::syntect::SyntectAdapterBuilder;
|
use comrak::plugins::syntect::SyntectAdapterBuilder;
|
||||||
use comrak::{markdown_to_html_with_plugins, Options, Plugins};
|
use comrak::{markdown_to_html_with_plugins, Options, Plugins};
|
||||||
|
use syntect::highlighting::ThemeSet;
|
||||||
|
use syntect::html::{css_for_theme_with_class_style, ClassStyle};
|
||||||
|
use syntect::parsing::SyntaxDefinition;
|
||||||
|
|
||||||
use crate::domain::MarkdownRenderer;
|
use crate::domain::MarkdownRenderer;
|
||||||
|
|
||||||
pub struct ComrakRenderer {
|
const TYPESCRIPT_SYNTAX: &str =
|
||||||
pub theme: String,
|
include_str!("../../resources/syntaxes/TypeScript.sublime-syntax");
|
||||||
|
|
||||||
|
fn build_syntax_set() -> syntect::parsing::SyntaxSet {
|
||||||
|
let mut builder = syntect::parsing::SyntaxSet::load_defaults_nonewlines().into_builder();
|
||||||
|
if let Ok(def) = SyntaxDefinition::load_from_str(TYPESCRIPT_SYNTAX, true, None) {
|
||||||
|
builder.add(def);
|
||||||
|
}
|
||||||
|
builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ComrakPreviewRenderer {
|
pub struct ComrakRenderer;
|
||||||
pub theme: String,
|
pub struct ComrakPreviewRenderer;
|
||||||
}
|
|
||||||
|
|
||||||
impl MarkdownRenderer for ComrakRenderer {
|
impl MarkdownRenderer for ComrakRenderer {
|
||||||
fn render(&self, content: &str) -> String {
|
fn render(&self, content: &str) -> String {
|
||||||
let adapter = SyntectAdapterBuilder::new().theme(&self.theme).build();
|
let adapter = SyntectAdapterBuilder::new()
|
||||||
|
.syntax_set(build_syntax_set())
|
||||||
|
.css()
|
||||||
|
.build();
|
||||||
let mut options = Options::default();
|
let mut options = Options::default();
|
||||||
options.extension.table = true;
|
options.extension.table = true;
|
||||||
options.extension.strikethrough = true;
|
options.extension.strikethrough = true;
|
||||||
@@ -27,10 +39,70 @@ impl MarkdownRenderer for ComrakRenderer {
|
|||||||
|
|
||||||
impl MarkdownRenderer for ComrakPreviewRenderer {
|
impl MarkdownRenderer for ComrakPreviewRenderer {
|
||||||
fn render(&self, content: &str) -> String {
|
fn render(&self, content: &str) -> String {
|
||||||
let adapter = SyntectAdapterBuilder::new().theme(&self.theme).build();
|
let adapter = SyntectAdapterBuilder::new()
|
||||||
|
.syntax_set(build_syntax_set())
|
||||||
|
.css()
|
||||||
|
.build();
|
||||||
let options = Options::default();
|
let options = Options::default();
|
||||||
let mut plugins = Plugins::default();
|
let mut plugins = Plugins::default();
|
||||||
plugins.render.codefence_syntax_highlighter = Some(&adapter);
|
plugins.render.codefence_syntax_highlighter = Some(&adapter);
|
||||||
markdown_to_html_with_plugins(content, &options, &plugins)
|
markdown_to_html_with_plugins(content, &options, &plugins)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn syntax_css_for_theme(syntect_theme_name: &str) -> String {
|
||||||
|
let ts = ThemeSet::load_defaults();
|
||||||
|
let theme = match ts.themes.get(syntect_theme_name) {
|
||||||
|
Some(t) => t,
|
||||||
|
None => return String::new(),
|
||||||
|
};
|
||||||
|
match css_for_theme_with_class_style(theme, ClassStyle::Spaced) {
|
||||||
|
Ok(css) => css.replace(".code {", ".syntax-highlighting {"),
|
||||||
|
Err(_) => String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_outputs_syntax_highlighting_class() {
|
||||||
|
let r = ComrakRenderer;
|
||||||
|
let html = r.render("```rust\nfn main() {}\n```\n");
|
||||||
|
assert!(html.contains("syntax-highlighting"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_outputs_classed_spans() {
|
||||||
|
let r = ComrakRenderer;
|
||||||
|
let html = r.render("```rust\nfn main() {}\n```\n");
|
||||||
|
assert!(html.contains("<span class="));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn typescript_syntax_is_highlighted() {
|
||||||
|
let r = ComrakRenderer;
|
||||||
|
let html = r.render("```typescript\ninterface Foo { id: string; }\n```\n");
|
||||||
|
assert!(html.contains("<span class="), "TypeScript should produce highlighted spans");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn syntax_css_known_theme_contains_wrapper() {
|
||||||
|
let css = syntax_css_for_theme("base16-ocean.dark");
|
||||||
|
assert!(!css.is_empty());
|
||||||
|
assert!(css.contains(".syntax-highlighting {"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn syntax_css_known_theme_contains_token_rules() {
|
||||||
|
let css = syntax_css_for_theme("base16-ocean.dark");
|
||||||
|
assert!(css.contains("color:") || css.contains("color :"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn syntax_css_unknown_theme_returns_empty() {
|
||||||
|
let css = syntax_css_for_theme("nonexistent-theme-xyz");
|
||||||
|
assert!(css.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,3 +23,76 @@ pub fn collect_md_files(dir: &Path, result: &mut Vec<String>) -> std::io::Result
|
|||||||
pub fn read_file(path: &str) -> Result<String, String> {
|
pub fn read_file(path: &str) -> Result<String, String> {
|
||||||
fs::read_to_string(path).map_err(|e| e.to_string())
|
fs::read_to_string(path).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn tmpdir(name: &str) -> PathBuf {
|
||||||
|
std::env::temp_dir().join(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn collect_md_flat() {
|
||||||
|
let dir = tmpdir("pena_collect_flat");
|
||||||
|
fs::create_dir_all(&dir).unwrap();
|
||||||
|
fs::write(dir.join("a.md"), "").unwrap();
|
||||||
|
fs::write(dir.join("b.md"), "").unwrap();
|
||||||
|
fs::write(dir.join("c.txt"), "").unwrap();
|
||||||
|
let mut result = Vec::new();
|
||||||
|
collect_md_files(&dir, &mut result).unwrap();
|
||||||
|
result.sort();
|
||||||
|
fs::remove_dir_all(&dir).unwrap();
|
||||||
|
assert_eq!(result.len(), 2);
|
||||||
|
assert!(result[0].ends_with("a.md"));
|
||||||
|
assert!(result[1].ends_with("b.md"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn collect_md_recursive() {
|
||||||
|
let dir = tmpdir("pena_collect_recursive");
|
||||||
|
let sub = dir.join("docs");
|
||||||
|
fs::create_dir_all(&sub).unwrap();
|
||||||
|
fs::write(dir.join("Home.md"), "").unwrap();
|
||||||
|
fs::write(sub.join("Page.md"), "").unwrap();
|
||||||
|
let mut result = Vec::new();
|
||||||
|
collect_md_files(&dir, &mut result).unwrap();
|
||||||
|
fs::remove_dir_all(&dir).unwrap();
|
||||||
|
assert_eq!(result.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn collect_md_ignores_hidden_dir() {
|
||||||
|
let dir = tmpdir("pena_collect_hidden_dir");
|
||||||
|
let hidden = dir.join(".hidden");
|
||||||
|
fs::create_dir_all(&hidden).unwrap();
|
||||||
|
fs::write(dir.join("visible.md"), "").unwrap();
|
||||||
|
fs::write(hidden.join("secret.md"), "").unwrap();
|
||||||
|
let mut result = Vec::new();
|
||||||
|
collect_md_files(&dir, &mut result).unwrap();
|
||||||
|
fs::remove_dir_all(&dir).unwrap();
|
||||||
|
assert_eq!(result.len(), 1);
|
||||||
|
assert!(result[0].ends_with("visible.md"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn collect_md_ignores_hidden_file() {
|
||||||
|
let dir = tmpdir("pena_collect_hidden_file");
|
||||||
|
fs::create_dir_all(&dir).unwrap();
|
||||||
|
fs::write(dir.join("visible.md"), "").unwrap();
|
||||||
|
fs::write(dir.join(".hidden.md"), "").unwrap();
|
||||||
|
let mut result = Vec::new();
|
||||||
|
collect_md_files(&dir, &mut result).unwrap();
|
||||||
|
fs::remove_dir_all(&dir).unwrap();
|
||||||
|
assert_eq!(result.len(), 1);
|
||||||
|
assert!(result[0].ends_with("visible.md"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn collect_md_nonexistent_dir() {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
let err = collect_md_files(Path::new("/nonexistent/path/for/pena"), &mut result);
|
||||||
|
assert!(err.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod comrak_renderer;
|
pub mod comrak_renderer;
|
||||||
pub mod file_repository;
|
pub mod file_repository;
|
||||||
pub mod notify_watcher;
|
pub mod notify_watcher;
|
||||||
|
pub mod theme_repository;
|
||||||
|
|||||||
@@ -64,3 +64,44 @@ fn is_relevant_path(path: &Path) -> bool {
|
|||||||
}
|
}
|
||||||
path.extension().is_some_and(|e| e == "md")
|
path.extension().is_some_and(|e| e == "md")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relevant_path_md_file() {
|
||||||
|
assert!(is_relevant_path(Path::new("/docs/notes.md")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relevant_path_nested_md() {
|
||||||
|
assert!(is_relevant_path(Path::new("/wiki/section/page.md")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn irrelevant_path_txt_extension() {
|
||||||
|
assert!(!is_relevant_path(Path::new("/docs/readme.txt")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn irrelevant_path_no_extension() {
|
||||||
|
assert!(!is_relevant_path(Path::new("/docs/makefile")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn irrelevant_path_hidden_file() {
|
||||||
|
assert!(!is_relevant_path(Path::new("/docs/.hidden.md")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn irrelevant_path_hidden_dir() {
|
||||||
|
assert!(!is_relevant_path(Path::new("/docs/.git/file.md")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn irrelevant_path_hidden_dir_at_root() {
|
||||||
|
assert!(!is_relevant_path(Path::new(".hidden/file.md")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
use crate::domain::theme::{Theme, ThemeRepository};
|
||||||
|
|
||||||
|
const DEFAULT_CSS: &str = include_str!("../../resources/themes/default.css");
|
||||||
|
const DARK_CSS: &str = include_str!("../../resources/themes/dark.css");
|
||||||
|
const SEPIA_CSS: &str = include_str!("../../resources/themes/sepia.css");
|
||||||
|
const LARGE_TEXT_CSS: &str = include_str!("../../resources/themes/large-text.css");
|
||||||
|
const EMERALD_CSS: &str = include_str!("../../resources/themes/emerald.css");
|
||||||
|
const SHELL_INDIGO_CSS: &str = include_str!("../../resources/themes/shell-indigo.css");
|
||||||
|
|
||||||
|
pub struct StaticThemeRepository;
|
||||||
|
|
||||||
|
impl ThemeRepository for StaticThemeRepository {
|
||||||
|
fn list(&self) -> Vec<Theme> {
|
||||||
|
vec![
|
||||||
|
Theme { id: "default".into(), label: "Défaut".into() },
|
||||||
|
Theme { id: "dark".into(), label: "Mode sombre".into() },
|
||||||
|
Theme { id: "sepia".into(), label: "Sépia".into() },
|
||||||
|
Theme { id: "large-text".into(), label: "Grand texte".into() },
|
||||||
|
Theme { id: "emerald".into(), label: "Accent émeraude".into() },
|
||||||
|
Theme { id: "shell-indigo".into(), label: "Shell Indigo".into() },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_css(&self, id: &str) -> Option<String> {
|
||||||
|
match id {
|
||||||
|
"default" => Some(DEFAULT_CSS.to_string()),
|
||||||
|
"dark" => Some(DARK_CSS.to_string()),
|
||||||
|
"sepia" => Some(SEPIA_CSS.to_string()),
|
||||||
|
"large-text" => Some(LARGE_TEXT_CSS.to_string()),
|
||||||
|
"emerald" => Some(EMERALD_CSS.to_string()),
|
||||||
|
"shell-indigo" => Some(SHELL_INDIGO_CSS.to_string()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::domain::theme::ThemeRepository;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_returns_all_themes() {
|
||||||
|
let repo = StaticThemeRepository;
|
||||||
|
let themes = repo.list();
|
||||||
|
assert_eq!(themes.len(), 6);
|
||||||
|
let ids: Vec<&str> = themes.iter().map(|t| t.id.as_str()).collect();
|
||||||
|
assert!(ids.contains(&"default"));
|
||||||
|
assert!(ids.contains(&"dark"));
|
||||||
|
assert!(ids.contains(&"shell-indigo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_css_known_themes() {
|
||||||
|
let repo = StaticThemeRepository;
|
||||||
|
for id in ["default", "dark", "sepia", "large-text", "emerald", "shell-indigo"] {
|
||||||
|
let css = repo.get_css(id);
|
||||||
|
assert!(css.is_some(), "CSS manquant pour le thème {id}");
|
||||||
|
assert!(!css.unwrap().is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_css_unknown_returns_none() {
|
||||||
|
let repo = StaticThemeRepository;
|
||||||
|
assert!(repo.get_css("nonexistent").is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,37 +1,25 @@
|
|||||||
mod core;
|
mod application;
|
||||||
|
mod commands;
|
||||||
mod domain;
|
mod domain;
|
||||||
mod infrastructure;
|
mod infrastructure;
|
||||||
mod watcher;
|
|
||||||
|
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
fn render_markdown(content: String) -> String {
|
|
||||||
core::render_markdown(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
fn convert_file(path: String) -> Result<String, String> {
|
|
||||||
core::convert_file(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
fn list_md_files(dir: String) -> Result<Vec<String>, String> {
|
|
||||||
core::list_md_files(dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_fs::init())
|
.plugin(tauri_plugin_fs::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.manage(watcher::WatcherState(Mutex::new(None)))
|
.manage(commands::watch::WatcherState(Mutex::new(None)))
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
render_markdown,
|
commands::render::render_markdown,
|
||||||
convert_file,
|
commands::render::convert_file,
|
||||||
list_md_files,
|
commands::render::list_md_files,
|
||||||
watcher::start_watch,
|
commands::render::get_syntax_highlight_css,
|
||||||
watcher::stop_watch,
|
commands::watch::start_watch,
|
||||||
|
commands::watch::stop_watch,
|
||||||
|
commands::theme::list_themes,
|
||||||
|
commands::theme::get_theme_css,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("Erreur lors du démarrage de l'application Tauri");
|
.expect("Erreur lors du démarrage de l'application Tauri");
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Pena",
|
"productName": "Pena",
|
||||||
"version": "0.1.0",
|
"version": "0.1.2",
|
||||||
"identifier": "com.pena.app",
|
"identifier": "com.pena.app",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../src"
|
"frontendDist": "../src"
|
||||||
@@ -24,6 +24,12 @@
|
|||||||
"bundle": {
|
"bundle": {
|
||||||
"active": false,
|
"active": false,
|
||||||
"targets": "all",
|
"targets": "all",
|
||||||
"icon": []
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 993 B After Width: | Height: | Size: 993 B |
@@ -6,8 +6,7 @@
|
|||||||
<title>Pena — Markdown Viewer</title>
|
<title>Pena — Markdown Viewer</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&family=Roboto+Mono&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&family=Roboto+Mono&display=swap">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
<link rel="stylesheet" href="style.css" />
|
||||||
<link rel="stylesheet" href="style.css" />
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@@ -20,7 +19,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="app-content">
|
||||||
|
|
||||||
<div id="view-home">
|
<div id="view-home">
|
||||||
|
<img class="home-logo" src="assets/icon.png" alt="Pena" />
|
||||||
<h1 class="home-title">Pena</h1>
|
<h1 class="home-title">Pena</h1>
|
||||||
<div class="home-buttons">
|
<div class="home-buttons">
|
||||||
<button id="btn-open-file">Ouvrir un fichier Markdown</button>
|
<button id="btn-open-file">Ouvrir un fichier Markdown</button>
|
||||||
@@ -33,12 +35,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="view-reader" class="hidden">
|
<div id="view-reader" class="hidden">
|
||||||
<aside id="sidebar" class="sidebar">
|
<div class="sidebar">
|
||||||
|
<aside id="sidebar" class="sidebar__scroll">
|
||||||
<!-- contenu généré par JS -->
|
<!-- contenu généré par JS -->
|
||||||
</aside>
|
</aside>
|
||||||
<div id="content" class="content">
|
<div class="sidebar__footer">
|
||||||
<!-- contenu généré par JS -->
|
|
||||||
</div>
|
|
||||||
<button id="btn-customize-css" class="btn-customize-css">
|
<button id="btn-customize-css" class="btn-customize-css">
|
||||||
<svg class="btn-customize-css__icon" width="15" height="15" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg class="btn-customize-css__icon" width="15" height="15" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M15.5 8.5H15.51M10.5 7.5H10.51M7.5 11.5H7.51M12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12C21 13.6569 19.6569 15 18 15H17.4C17.0284 15 16.8426 15 16.6871 15.0246C15.8313 15.1602 15.1602 15.8313 15.0246 16.6871C15 16.8426 15 17.0284 15 17.4V18C15 19.6569 13.6569 21 12 21ZM16 8.5C16 8.77614 15.7761 9 15.5 9C15.2239 9 15 8.77614 15 8.5C15 8.22386 15.2239 8 15.5 8C15.7761 8 16 8.22386 16 8.5ZM11 7.5C11 7.77614 10.7761 8 10.5 8C10.2239 8 10 7.77614 10 7.5C10 7.22386 10.2239 7 10.5 7C10.7761 7 11 7.22386 11 7.5ZM8 11.5C8 11.7761 7.77614 12 7.5 12C7.22386 12 7 11.7761 7 11.5C7 11.2239 7.22386 11 7.5 11C7.77614 11 8 11.2239 8 11.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M15.5 8.5H15.51M10.5 7.5H10.51M7.5 11.5H7.51M12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12C21 13.6569 19.6569 15 18 15H17.4C17.0284 15 16.8426 15 16.6871 15.0246C15.8313 15.1602 15.1602 15.8313 15.0246 16.6871C15 16.8426 15 17.0284 15 17.4V18C15 19.6569 13.6569 21 12 21ZM16 8.5C16 8.77614 15.7761 9 15.5 9C15.2239 9 15 8.77614 15 8.5C15 8.22386 15.2239 8 15.5 8C15.7761 8 16 8.22386 16 8.5ZM11 7.5C11 7.77614 10.7761 8 10.5 8C10.2239 8 10 7.77614 10 7.5C10 7.22386 10.2239 7 10.5 7C10.7761 7 11 7.22386 11 7.5ZM8 11.5C8 11.7761 7.77614 12 7.5 12C7.22386 12 7 11.7761 7 11.5C7 11.2239 7.22386 11 7.5 11C7.77614 11 8 11.2239 8 11.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
@@ -47,6 +48,14 @@
|
|||||||
</button>
|
</button>
|
||||||
<button id="btn-back" class="btn-back">← Accueil</button>
|
<button id="btn-back" class="btn-back">← Accueil</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="sidebar-resizer" class="sidebar__resizer" title="Redimensionner"></div>
|
||||||
|
</div>
|
||||||
|
<div id="content" class="content">
|
||||||
|
<!-- contenu généré par JS -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- #app-content -->
|
||||||
|
|
||||||
<!-- Modale CSS -->
|
<!-- Modale CSS -->
|
||||||
<div id="css-modal-overlay" class="css-modal-overlay hidden">
|
<div id="css-modal-overlay" class="css-modal-overlay hidden">
|
||||||
@@ -66,6 +75,10 @@
|
|||||||
<button class="css-modal__tab" data-tab="avance">Développement</button>
|
<button class="css-modal__tab" data-tab="avance">Développement</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="css-modal__body">
|
<div class="css-modal__body">
|
||||||
|
<div class="css-modal__quick-themes">
|
||||||
|
<p class="css-modal__quick-themes-label">Thèmes rapides</p>
|
||||||
|
<div id="quick-themes-list" class="css-modal__quick-themes-list"></div>
|
||||||
|
</div>
|
||||||
<p class="css-modal__description">Personnalisez l'apparence de vos pages en modifiant le CSS ci-dessous. Les styles s'appliquent à l'onglet actif.</p>
|
<p class="css-modal__description">Personnalisez l'apparence de vos pages en modifiant le CSS ci-dessous. Les styles s'appliquent à l'onglet actif.</p>
|
||||||
<textarea id="css-editor" class="css-modal__editor" placeholder="/* Entrez votre CSS ici */ /* Exemple : .content { font-size: 18px; } */"></textarea>
|
<textarea id="css-editor" class="css-modal__editor" placeholder="/* Entrez votre CSS ici */ /* Exemple : .content { font-size: 18px; } */"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||