19 Commits

Author SHA1 Message Date
Gato 64c53f412d docs: documentation fonctionnelle, technique et installation 2026-07-05 10:58:29 +02:00
Gato 61fb56fa14 chore: icônes de bundle, packaging Flatpak et bump version 0.1.2
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 06:17:56 +02:00
Gato 8039a3545d feat: rechargement automatique des documents et sidebar redimensionnable
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 06:17:12 +02:00
Gato fe0476218d feat: export systematique de pena.flatpak lors du build
Le mode par defaut installe (--user) ET exporte le bundle pena.flatpak a cote
du script. Le flag --bundle devient --no-install (bundle seul, alias conserve).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 08:11:27 +02:00
Gato 1220ce1bb0 fix: tri naturel de la sidebar et police uniforme dossiers/fichiers
Les entrees a prefixe numerique etaient triees lexicographiquement (10 avant 2).
Passage a un tri naturel via Intl.Collator numeric. Les libelles de dossiers
adoptent desormais la meme taille et graisse que les fichiers (chevron seul
marqueur visuel).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 07:54:19 +02:00
Gato 4bb2c6cc77 feat: build flatpak dans distrobox ubuntu-rust + sync version metainfo
Le build cargo tauri echouait sur l hote atomique (libs GTK/WebKit absentes) ;
il tourne desormais dans le distrobox ubuntu-rust. La version applicative est
lue depuis tauri.conf.json et repercutee dans le metainfo AppStream.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 07:37:17 +02:00
Gato 2841881a01 feat: script de packaging Flatpak
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 07:02:32 +02:00
Gato 8b0f128313 (feat) copy code content
Signed-off-by: Gato <cedric@goutailler-olivier.fr>
2026-06-22 21:33:38 +02:00
Gato 2d9a1e64f9 fix: inclure le CSS syntect statiquement dans style.css
L'injection dynamique via invoke() échouait silencieusement.
Le CSS base16-ocean.dark est maintenant scopé (.syntax-highlighting .keyword
etc.) et inclus directement dans style.css — aucun appel runtime requis.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 15:49:26 +02:00
Gato ebb648bb2d fix: isoler le style du code inline pour ne pas affecter les blocs pre
.content code dans les thèmes ciblait aussi le <code> dans <pre>,
écrasant les couleurs syntect par héritage et ajoutant une bordure
indésirable. Changé en .content :not(pre) > code + reset explicite
dans style.css (border: none, color: inherit).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 15:44:00 +02:00
Gato df899bd1a8 feat: coloration syntaxique via CSS classes (themeable)
Remplace les styles inline syntect par des classes CSS. Le CSS est
généré côté Rust (base16-ocean.dark) et injecté au démarrage via
get_syntax_highlight_css. Les thèmes peuvent surcharger .syntax-highlighting
et les classes de tokens. Supprime le lien CDN highlight.js inutilisé.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 15:36:08 +02:00
Gato 75c85e7e0a fix: corriger les classes CSS mortes de la sidebar dans les thèmes
Les thèmes référençaient d'anciennes classes (sidebar__menu__*__link)
jamais générées par le JS. Remplacement par les vraies classes nav-tree
(nav-root-link, nav-folder-link, nav-file-link) et ajout du fond
sidebar et des états actifs configurables par thème.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 15:20:55 +02:00
Gato e34528c886 feat: thèmes rapides gérés par le backend via fichiers CSS
Les thèmes ne sont plus codés en dur dans le JS. Chaque thème est
un fichier CSS dans src-tauri/resources/themes/, embarqué à la
compilation via include_str!. Le backend expose list_themes et
get_theme_css comme commandes Tauri ; le frontend charge et met en
cache le CSS à la demande.

Ajout du thème "Défaut" (basé sur Shell Indigo) avec commentaires
sur chaque règle et rendu complet des tableaux (en-têtes en
majuscules, bordure extérieure, séparateurs verticaux, lignes
alternées) aligné sur MarkdownRender-nodejs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 15:04:11 +02:00
Gato 8b39c09484 feat: ajout des badges thèmes rapides dans le modal CSS
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 14:39:24 +02:00
Gato b8ba15d832 feat: navigation entre documents via les liens Markdown
Intercepte les clics sur les liens dans le contenu rendu : les liens
internes (.md) chargent le document dans l'app, les liens HTTP s'ouvrent
dans le navigateur système. Restructure aussi la sidebar avec un footer
fixe pour les boutons Personnaliser/Accueil.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 14:35:50 +02:00
Gato d47e93b3c3 docs: règle architecture Clean Architecture pour Pena-tauri 2026-06-19 14:27:25 +02:00
Gato cdafffaa70 refactor: migration architecture clean (Rust + JS)
Côté JS : décompose main.js en modules ES6 (state, services, ui, router)
pour séparer les responsabilités et faciliter la maintenance.
Côté Rust : ajoute 7 tests sur is_relevant_path (notify_watcher)
pour couvrir les cas cachés/non-md ; couverture totale 77 % > seuil 60 %.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 14:23:30 +02:00
Gato 6ae7a434c4 refactor: migration Clean Architecture — module commands avec render et watch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 14:13:30 +02:00
Gato 9eae97f1d8 feat: module application avec render_service et file_service
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 14:09:08 +02:00
114 changed files with 6964 additions and 577 deletions
+52
View File
@@ -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/`
+8
View File
@@ -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
+58
View File
@@ -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)
+115
View File
@@ -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)
+77
View File
@@ -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)
+124
View File
@@ -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)
+127
View File
@@ -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)
+89
View File
@@ -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 13) 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)
+110
View File
@@ -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)
+98
View File
@@ -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)
+44
View File
@@ -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).
+144
View File
@@ -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é."
+25
View File
@@ -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>
+49
View File
@@ -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
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

+1 -1
View File
@@ -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",
+2 -2
View File
@@ -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"
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

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>
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 B

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because it is too large Load Diff
+9
View File
@@ -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; }
+255
View File
@@ -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);
}
+3
View File
@@ -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; }
+6
View File
@@ -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); }
+65
View File
@@ -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);
}
}
+3
View File
@@ -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());
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod render;
pub mod theme;
pub mod watch;
+116
View File
@@ -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());
}
}
+54
View File
@@ -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());
}
}
-176
View File
@@ -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
View File
@@ -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};
+39
View File
@@ -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
View File
@@ -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());
}
}
+11 -23
View File
@@ -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");
+8 -2
View File
@@ -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"
]
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Before

Width:  |  Height:  |  Size: 993 B

After

Width:  |  Height:  |  Size: 993 B

+19 -6
View File
@@ -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 */&#10;/* Exemple : .content { font-size: 18px; } */"></textarea> <textarea id="css-editor" class="css-modal__editor" placeholder="/* Entrez votre CSS ici */&#10;/* Exemple : .content { font-size: 18px; } */"></textarea>
</div> </div>

Some files were not shown because too many files have changed in this diff Show More