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