8 Commits

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 07:37:17 +02:00
Gato 2841881a01 feat: script de packaging Flatpak
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 07:02:32 +02:00
Gato 8b0f128313 (feat) copy code content
Signed-off-by: Gato <cedric@goutailler-olivier.fr>
2026-06-22 21:33:38 +02:00
85 changed files with 5390 additions and 38 deletions
+8
View File
@@ -1,2 +1,10 @@
src-tauri/target/ src-tauri/target/
src-tauri/gen/ src-tauri/gen/
# Artefacts de build Flatpak
flatpak/.build/
flatpak/.repo/
flatpak/.flatpak-builder/
flatpak/pena.deb
flatpak/icon.png
flatpak/*.flatpak
+58
View File
@@ -0,0 +1,58 @@
# Vue d'ensemble
## Qu'est-ce que Pena ?
Pena est un **lecteur Markdown de bureau**. Son objectif est d'offrir une lecture confortable
de documentation écrite en Markdown, qu'il s'agisse d'un fichier unique (un `README.md`) ou
d'un ensemble de fichiers organisés en arborescence (un « wiki » de documentation).
Contrairement à un éditeur, Pena est **focalisé sur la lecture** : il n'édite pas les fichiers,
il les affiche joliment, permet de naviguer entre eux, et de personnaliser entièrement
l'apparence du rendu.
## Pour qui ?
- Toute personne qui consulte régulièrement de la documentation Markdown en local
(wikis de projet, notes, documentation technique).
- Les utilisateurs qui veulent un rendu plus agréable et configurable que l'aperçu brut
d'un éditeur de texte.
- Ceux qui maintiennent une documentation multi-fichiers organisée en dossiers et
sous-dossiers, et qui veulent naviguer dedans comme dans un wiki.
## Deux modes d'ouverture
Pena propose deux façons d'ouvrir du contenu :
| Mode | Déclencheur | Comportement |
|---|---|---|
| **Fichier** | « Ouvrir un fichier Markdown » | Ouvre un `.md` isolé. La barre latérale liste tous les `.md` du dossier parent pour faciliter la navigation. |
| **Dossier (wiki)** | « Ouvrir un dossier wiki » | Ouvre un dossier complet. Tous les `.md` (y compris dans les sous-dossiers) sont listés en arborescence. Si un fichier `Home.md` existe, il est affiché en premier. |
## Principes de fonctionnement
- **Rendu côté Rust** — La conversion Markdown → HTML est faite par le backend Rust
(`comrak`), pas par une bibliothèque JS. Cela garantit un rendu rapide et cohérent.
- **CommonMark + extensions** — Les tableaux, le texte barré, les liens automatiques et les
listes de tâches sont pris en charge.
- **Coloration syntaxique** — Les blocs de code sont colorés via `syntect`, avec un thème
de coloration intégré (`base16-ocean.dark`).
- **Navigation entre documents** — Les liens Markdown relatifs entre fichiers
(ex. `[voir](../autre/page.md)`) sont fonctionnels : cliquer dessus charge le document
cible dans Pena.
- **Tout reste local** — Aucune connexion réseau n'est nécessaire. Les préférences
(récents, thème, CSS personnalisé) sont stockées localement dans le navigateur embarqué
(localStorage).
## Ce que Pena ne fait pas (encore)
- Pas d'édition de fichiers Markdown (lecture seule).
- Pas de rechargement automatique à la modification du fichier : le mécanisme de
surveillance (« watcher ») existe côté backend mais n'est pas branché côté interface.
Voir [Commandes Tauri](../2-technique/2.2-Commandes-Tauri.md#surveillance-de-fichiers-watch).
## Voir aussi
- [Fonctionnalités détaillées](1.2-Fonctionnalites.md)
- [Interface](1.3-Interface.md)
- [Thèmes et personnalisation](1.4-Themes-et-personnalisation.md)
- [Architecture technique](../2-technique/2.1-Architecture.md)
+115
View File
@@ -0,0 +1,115 @@
# Fonctionnalités
Cette page liste l'ensemble des fonctionnalités visibles par l'utilisateur, regroupées par
thème.
## 1. Ouverture de documents
### Ouvrir un fichier Markdown
- Bouton **« Ouvrir un fichier Markdown »** sur l'écran d'accueil.
- Ouvre un sélecteur de fichiers natif, filtré sur l'extension `.md`.
- Une fois le fichier ouvert, la barre latérale liste **tous les `.md` du dossier parent**,
ce qui permet de basculer entre fichiers voisins.
### Ouvrir un dossier wiki
- Bouton **« Ouvrir un dossier wiki »** sur l'écran d'accueil.
- Ouvre un sélecteur de dossier natif.
- Tous les fichiers `.md` du dossier **et de ses sous-dossiers** sont listés (récursivement).
- Les fichiers et dossiers cachés (commençant par `.`) sont ignorés.
- Si un fichier `Home.md` ou `home.md` est présent à la racine, il est affiché en premier ;
sinon, c'est le premier fichier par ordre alphabétique.
## 2. Documents récents
- L'écran d'accueil affiche une section **« Récents »** (masquée si vide).
- Chaque entrée affiche une icône (📁 pour un dossier, 📄 pour un fichier), le nom et le
chemin parent.
- Un clic réouvre directement le fichier ou le dossier.
- Jusqu'à **8 entrées** sont conservées, sans doublon, dans le stockage local (localStorage,
clé `pena_recents`).
## 3. Navigation dans la documentation
### Barre latérale en arborescence
- Affiche la structure complète du dossier ouvert sous forme d'arbre.
- Les dossiers sont des sections repliables/dépliables (chevron).
- Fichiers et dossiers sont triés par **ordre naturel** : les préfixes numériques
sont comparés comme des nombres (`10` vient après `9`, et non après `1`).
- Dossiers et fichiers partagent la **même typographie** (taille et graisse) ;
seul le chevron ▶ distingue visuellement un dossier.
- Si un dossier contient un `home.md`, son nom devient un lien cliquable.
- Le document actuellement affiché est **surligné**, et les dossiers parents sont
automatiquement dépliés.
- Plusieurs niveaux de profondeur sont gérés avec une indentation progressive.
### Liens entre documents
- Les **liens Markdown relatifs** vers d'autres fichiers `.md`
(ex. `[autre page](../section/page.md)`) sont fonctionnels : un clic charge le document
cible dans Pena, sans quitter l'application. Les chemins relatifs (`../`, `./`) sont
résolus correctement.
- Les **liens externes** (`http://`, `https://`) s'ouvrent dans le navigateur par défaut
(nouvel onglet, avec `rel="noopener noreferrer"`).
- Les **ancres internes** (`#section`) restent des ancres de page.
## 4. Rendu Markdown
Le rendu prend en charge le standard **CommonMark** ainsi que les extensions suivantes :
| Extension | Exemple |
|---|---|
| Tableaux | `\| col A \| col B \|` |
| Texte barré | `~~barré~~` |
| Liens automatiques | une URL nue devient un lien cliquable |
| Listes de tâches | `- [ ] à faire` / `- [x] fait` |
Autres éléments rendus : titres `h1``h6`, code inline, blocs de code, citations
(blockquotes), listes, images (largeur adaptée), et liens.
## 5. Coloration syntaxique du code
- Les blocs de code délimités par triple backtick sont **colorés syntaxiquement** via
`syntect`.
- Le thème de coloration intégré est **`base16-ocean.dark`**.
- La syntaxe **TypeScript** est fournie en plus des syntaxes par défaut.
### Bouton « Copier » sur les blocs de code
- Chaque bloc de code affiche un bouton **« Copier »** en haut à droite.
- Un clic copie le contenu du bloc dans le presse-papiers.
- Un retour visuel temporaire (« Copié ! ») confirme l'action.
## 6. Personnalisation de l'apparence
Voir la page dédiée [Thèmes et personnalisation](1.4-Themes-et-personnalisation.md). En résumé :
- **6 thèmes rapides** prêts à l'emploi (Défaut, Mode sombre, Sépia, Grand texte,
Accent émeraude, Shell Indigo).
- Un **éditeur CSS** intégré, organisé en 4 onglets thématiques, permettant d'écrire son
propre CSS appliqué au rendu.
- Les choix sont **persistés** localement et réappliqués au démarrage suivant.
## 7. Fenêtre et barre de titre personnalisée
- L'application utilise une **barre de titre maison** (la décoration native est désactivée).
- Elle affiche le titre du document courant et propose les boutons **réduire**,
**agrandir / restaurer** et **fermer**.
- La fenêtre se déplace en glissant la barre de titre.
## 8. Rechargement automatique (live-reload)
- À l'ouverture d'un fichier ou d'un dossier, l'application **surveille** en continu son
contenu sur le disque.
- Quand le **document affiché** est modifié (par un éditeur externe, un `git pull`, etc.),
il est **rechargé automatiquement** — plus besoin de revenir à l'accueil et de rouvrir le
dossier.
- Quand un fichier `.md` est **ajouté ou supprimé** dans le dossier ouvert, la **sidebar**
se met à jour automatiquement pour refléter la nouvelle arborescence (le dossier du
document courant reste déplié).
- La surveillance s'arrête au retour à l'accueil et se réinitialise à chaque nouvelle
ouverture. Détails techniques :
[Surveillance de fichiers](../2-technique/2.2-Commandes-Tauri.md#surveillance-de-fichiers-watch).
## Voir aussi
- [Interface](1.3-Interface.md)
- [Thèmes et personnalisation](1.4-Themes-et-personnalisation.md)
- [Vue d'ensemble](1.1-Vue-d-ensemble.md)
+77
View File
@@ -0,0 +1,77 @@
# Interface
L'interface de Pena se compose de deux vues principales — l'**accueil** et le **lecteur**
surmontées d'une **barre de titre personnalisée** toujours visible.
## Barre de titre personnalisée
La décoration native de la fenêtre est désactivée ; Pena fournit sa propre barre de titre
en haut de la fenêtre (hauteur 36 px, fond sombre).
| Élément | Comportement |
|---|---|
| Titre | Affiche « Pena — Markdown Viewer » sur l'accueil, ou le titre du document ouvert dans le lecteur. |
| Zone de titre | Glisser-déposer pour déplacer la fenêtre. |
| Bouton **─** | Réduit la fenêtre. |
| Bouton **▢** | Agrandit la fenêtre, ou la restaure si elle est déjà agrandie. |
| Bouton **✕** | Ferme l'application (devient rouge au survol). |
## Vue d'accueil
C'est l'écran affiché au démarrage et quand on revient via « ← Accueil ».
Elle contient :
- Le **titre « Pena »** en grand.
- Deux boutons d'action :
- **« Ouvrir un fichier Markdown »**
- **« Ouvrir un dossier wiki »**
- Une section **« Récents »** (visible uniquement s'il y a des documents récents) listant les
derniers fichiers et dossiers ouverts, cliquables pour les rouvrir.
## Vue lecteur
Affichée dès qu'un document est ouvert. Elle est composée de deux zones :
### Barre latérale (gauche)
- Largeur fixe (264 px), fond sombre.
- En haut : le **nom du dossier** ouvert.
- Au centre : l'**arborescence des fichiers** `.md` (voir
[Navigation](1.2-Fonctionnalites.md#3-navigation-dans-la-documentation)).
- En bas (pied de barre) : deux boutons —
- **« Personnaliser CSS »** (ouvre la modale de thèmes / CSS) ;
- **« ← Accueil »** (revient à l'écran d'accueil).
### Zone de contenu (droite)
- Affiche le document Markdown rendu en HTML.
- Fond clair, typographie soignée, largeur de lecture limitée pour le confort.
- Chaque bloc de code possède un bouton **« Copier »**.
### Comportement responsive
- Sur les fenêtres larges, la barre latérale est visible en permanence à gauche.
- Sous ~1020 px de large, la barre latérale se rétracte et le contenu occupe toute la largeur.
- Sous ~540 px, les marges du contenu sont réduites.
## Modale de personnalisation CSS
Ouverte via le bouton « Personnaliser CSS » de la barre latérale. Voir la page dédiée
[Thèmes et personnalisation](1.4-Themes-et-personnalisation.md) pour le détail.
Elle se ferme via la croix, le bouton « Fermer », ou un clic en dehors de la modale.
## Palette visuelle
| Rôle | Usage |
|---|---|
| Fond sombre | Barre de titre, barre latérale |
| Fond clair | Zone de contenu |
| Accent rose/corail | Boutons, survols, liens d'action, bordures de tableaux |
| Accent indigo | Élément actif dans la barre latérale |
> L'ensemble de ces couleurs peut être surchargé par l'utilisateur via les thèmes rapides ou
> le CSS personnalisé.
## Voir aussi
- [Fonctionnalités](1.2-Fonctionnalites.md)
- [Thèmes et personnalisation](1.4-Themes-et-personnalisation.md)
@@ -0,0 +1,80 @@
# Thèmes et personnalisation
Pena permet de modifier entièrement l'apparence du rendu, via deux mécanismes
complémentaires : les **thèmes rapides** (prêts à l'emploi) et l'**éditeur CSS** (sur mesure).
Tout se passe dans la **modale « Personnaliser CSS »**, ouverte depuis la barre latérale.
## Thèmes rapides
Six thèmes sont fournis et gérés par le backend. Chacun est un fichier CSS intégré à
l'application.
| Identifiant | Libellé affiché |
|---|---|
| `default` | Défaut |
| `dark` | Mode sombre |
| `sepia` | Sépia |
| `large-text` | Grand texte |
| `emerald` | Accent émeraude |
| `shell-indigo` | Shell Indigo |
### Utilisation
- Les thèmes apparaissent sous forme de **badges cliquables** dans la modale.
- Un clic applique le thème **immédiatement**.
- Le badge du thème actif est mis en évidence.
- Cliquer à nouveau sur le thème actif le **désactive** (retour au rendu de base).
- Le thème choisi est mémorisé (localStorage, clé `pena_quick_theme`) et réappliqué au
prochain démarrage.
## Éditeur CSS personnalisé
Pour aller plus loin qu'un thème, la modale propose un éditeur de CSS libre, organisé en
**4 onglets** thématiques. Chaque onglet conserve son propre CSS, indépendamment des autres :
| Onglet | Vocation |
|---|---|
| Arrière-scène | Styles généraux / d'ambiance |
| Police | Typographie |
| Contenu fond | Mise en forme du contenu |
| Développement | Réglages avancés / divers |
### Fonctionnement
- On écrit du **CSS standard** dans la zone de texte de l'onglet sélectionné.
- Le bouton **« ★ Enregistrer »** sauvegarde le CSS courant et l'applique aussitôt.
- Le bouton **« Réinitialiser »** efface **tout** le CSS personnalisé **et** désactive le
thème rapide actif.
- Le bouton **« Fermer »** ferme la modale sans enregistrer les changements en cours.
- Un champ permet de nommer la configuration.
### Application combinée
Le CSS finalement appliqué est la **concaténation** du CSS des 4 onglets, suivi du CSS du
thème rapide actif. L'ordre de cascade est donc :
1. CSS de base de l'application (`style.css`) ;
2. CSS personnalisé (les 4 onglets, concaténés) ;
3. CSS du thème rapide actif (s'il y en a un).
Ainsi, un thème rapide peut compléter un CSS personnalisé, et le CSS personnalisé peut
surcharger le rendu de base.
## Persistance des préférences
Toutes les préférences sont stockées localement (localStorage) et restaurées à chaque
ouverture :
| Donnée | Clé localStorage |
|---|---|
| Thème rapide actif | `pena_quick_theme` |
| CSS onglet « Arrière-scène » | `pena_css_general` |
| CSS onglet « Police » | `pena_css_police` |
| CSS onglet « Contenu fond » | `pena_css_contenu` |
| CSS onglet « Développement » | `pena_css_avance` |
> Ces données sont propres à la machine et à l'installation : elles ne sont pas synchronisées
> et ne quittent jamais l'ordinateur.
## Voir aussi
- [Interface](1.3-Interface.md)
- [Fonctionnalités](1.2-Fonctionnalites.md)
- [Commandes Tauri — Thèmes](../2-technique/2.2-Commandes-Tauri.md#thèmes)
+124
View File
@@ -0,0 +1,124 @@
# Architecture
Pena (`Pena-tauri`) est une application [Tauri 2](https://tauri.app/) : un **backend Rust**
expose des commandes à un **frontend web** (HTML/CSS/JS vanilla, sans bundler). Le frontend
est servi en fichiers statiques depuis `src/`, le backend est compilé dans `src-tauri/`.
L'ensemble suit une **Clean Architecture** stricte, côté Rust comme côté JS.
## Vue d'ensemble
```
┌──────────────────────────── Frontend (src/, JS vanilla) ────────────────────────────┐
│ ui/ — composants d'affichage (home, sidebar, reader, css-modal) │
│ state/ — état global minimal (app-state) │
│ services/ — appels Tauri via invoke (markdown, files, watcher, themes) │
│ router.js — navigation entre vues (accueil / lecteur) │
└──────────────────────────────────────┬───────────────────────────────────────────────┘
│ window.__TAURI__.core.invoke(...)
│ window.__TAURI__.event.listen(...)
┌──────────────────────────────────────┴──── Backend (src-tauri/src/, Rust) ───────────┐
│ commands/ — points d'entrée #[tauri::command] (render, watch, theme) │
│ application/ — services métier (render_service, file_service, theme_service) │
│ infrastructure/ — implémentations concrètes (comrak, notify, fs, thèmes statiques) │
│ domain/ — entités et traits purs (MarkdownRenderer, ThemeRepository…) │
│ lib.rs — point d'assemblage (wiring + enregistrement des commandes) │
└───────────────────────────────────────────────────────────────────────────────────────┘
```
## Backend Rust — couches
Le code Rust (`src-tauri/src/`) est découpé en quatre couches, avec des règles de dépendance
strictes.
| Couche | Rôle | Peut dépendre de | Ne doit PAS dépendre de |
|---|---|---|---|
| `domain/` | Entités et traits purs, zéro dépendance externe | rien | `application`, `infrastructure`, `commands` |
| `application/` | Services métier (orchestration des cas d'usage) | `domain` | `infrastructure`, `commands` |
| `infrastructure/` | Implémentations concrètes des traits du domaine | `domain` | `application`, `commands` |
| `commands/` | Commandes exposées à Tauri | `application`, `domain` | `infrastructure` (directement) |
### `domain/`
Définit les contrats, sans aucune dépendance technique :
- `markdown.rs` — trait `MarkdownRenderer { fn render(&self, content: &str) -> String }`
et struct `RenderOptions` (tables, strikethrough, autolink, tasklist).
- `theme.rs` — struct `Theme { id, label }` et trait
`ThemeRepository { fn list() -> Vec<Theme>; fn get_css(id) -> Option<String> }`.
### `application/`
Orchestration métier, dépend uniquement du domaine :
- `render_service.rs``render_string()` (rendu d'une chaîne) et `render_file()`
(lecture du fichier puis rendu).
- `file_service.rs``list_markdown_files()` : valide le répertoire, collecte les `.md`,
trie alphabétiquement.
- `theme_service.rs``list_themes()` et `get_theme_css()`, délégués au repository.
### `infrastructure/`
Implémentations concrètes des traits du domaine :
- `comrak_renderer.rs``ComrakRenderer` (toutes les extensions activées, pour les
fichiers) et `ComrakPreviewRenderer` (sans extensions, pour les aperçus rapides). Fournit
aussi `syntax_css_for_theme()` pour générer le CSS de coloration `syntect`. La syntaxe
TypeScript est chargée depuis `resources/syntaxes/TypeScript.sublime-syntax`.
- `file_repository.rs``collect_md_files()` (parcours récursif, ignore les fichiers
cachés, ne retient que les `.md`) et `read_file()`.
- `notify_watcher.rs` — boucle de débounce (80 ms) qui filtre les événements de la crate
`notify` et émet l'événement Tauri `file-changed`.
- `theme_repository.rs``StaticThemeRepository` : les 6 thèmes et leur CSS sont intégrés
au binaire via `include_str!()` (fichiers `resources/themes/*.css`).
### `commands/`
Points d'entrée Tauri qui instancient les implémentations concrètes et appellent les
services. Voir la référence complète : [Commandes Tauri](2.2-Commandes-Tauri.md).
### `lib.rs` — point d'assemblage
Seul endroit où les implémentations concrètes sont câblées. Il :
- enregistre les plugins `tauri-plugin-fs` et `tauri-plugin-dialog` ;
- gère l'état global `WatcherState` (un seul watcher actif à la fois) ;
- enregistre les 8 commandes via `generate_handler![]`.
## Frontend JS — couches
Le code JS (`src/`) suit le même esprit de séparation :
| Dossier | Rôle | Peut dépendre de |
|---|---|---|
| `ui/` | Composants d'affichage, sans logique métier ni appel Tauri direct | `services/`, `state/` |
| `state/` | État global de l'application (`app-state.js`) | — |
| `services/` | Appels Tauri (`invoke`) et logique de données | API Tauri |
| `router.js` | Navigation entre les vues (accueil / lecteur), orchestrateur | `services/`, `ui/`, `state/` |
**Règle importante** : les composants `ui/` ne font **jamais** d'appel Tauri directement ;
ils passent toujours par un service de `services/`.
### Services frontend
- `services/markdown.js``render_markdown`, `convert_file`
- `services/files.js``list_md_files`
- `services/watcher.js``start_watch`, `stop_watch`, écoute de `file-changed`
(présent mais non utilisé par l'UI)
- `services/themes.js``list_themes`, `get_theme_css`
## Dépendances Rust principales
| Crate | Rôle |
|---|---|
| `tauri` (+ `tauri-plugin-fs`, `tauri-plugin-dialog`) | Framework applicatif, IPC, événements, dialogues |
| `comrak` | Rendu CommonMark + extensions |
| `syntect` | Coloration syntaxique des blocs de code |
| `notify` | Surveillance du système de fichiers |
| `serde` / `serde_json` | Sérialisation des DTO et payloads |
## Ressources intégrées au binaire
| Ressource | Emplacement | Usage |
|---|---|---|
| Thèmes rapides | `src-tauri/resources/themes/*.css` | Servis par `get_theme_css` |
| Syntaxe TypeScript | `src-tauri/resources/syntaxes/TypeScript.sublime-syntax` | Coloration des blocs TS |
Elles sont incluses à la compilation (`include_str!`), donc l'application n'a pas besoin de
fichiers externes pour fonctionner.
## Voir aussi
- [Commandes Tauri](2.2-Commandes-Tauri.md)
- [Build et compilation](../3-installation/3.3-Build.md)
- [Vue d'ensemble fonctionnelle](../1-fonctionnel/1.1-Vue-d-ensemble.md)
+127
View File
@@ -0,0 +1,127 @@
# Commandes Tauri
Le backend Rust expose **8 commandes** au frontend, enregistrées dans `lib.rs` via
`generate_handler![]`. Le frontend les appelle avec
`window.__TAURI__.core.invoke('<nom>', { <args> })`.
Cette page sert de référence : signature, arguments, valeur de retour et comportement de
chaque commande.
## Rendu Markdown
### `render_markdown`
```
render_markdown(content: String) -> String
```
- **Args** : `content` — Markdown brut.
- **Retour** : HTML.
- **Comportement** : rendu via `ComrakPreviewRenderer` (sans extensions, aperçu rapide).
- **Frontend** : `services/markdown.js`.
### `convert_file`
```
convert_file(path: String) -> Result<String, String>
```
- **Args** : `path` — chemin absolu d'un fichier `.md`.
- **Retour** : `Ok(html)` ou `Err(message)`.
- **Comportement** : lit le fichier puis le rend via `ComrakRenderer` (toutes les extensions
activées : tables, strikethrough, autolink, tasklist + coloration syntaxique).
- **Frontend** : `services/markdown.js`. C'est la commande utilisée pour afficher un document
dans le lecteur.
### `get_syntax_highlight_css`
```
get_syntax_highlight_css() -> String
```
- **Args** : aucun.
- **Retour** : feuille CSS de coloration syntaxique.
- **Comportement** : génère le CSS du thème `syntect` `base16-ocean.dark`.
## Fichiers
### `list_md_files`
```
list_md_files(dir: String) -> Result<Vec<String>, String>
```
- **Args** : `dir` — chemin d'un répertoire.
- **Retour** : `Ok([chemins…])` (triés alphabétiquement) ou `Err(message)`.
- **Comportement** : parcourt le répertoire **récursivement**, ignore les fichiers/dossiers
cachés (commençant par `.`), ne retient que les `.md`.
- **Frontend** : `services/files.js`.
## Surveillance de fichiers (watch)
> Ces commandes sont implémentées et fonctionnelles côté backend, mais **l'interface ne les
> appelle pas encore**. Le rechargement automatique n'est donc pas actif. Voir
> [Fonctionnalités](../1-fonctionnel/1.2-Fonctionnalites.md#fonctionnalité-présente-mais-non-branchée).
### `start_watch`
```
start_watch(path: String) -> Result<(), String>
```
- **Args** : `path` — fichier ou répertoire à surveiller.
- **Comportement** :
- arrête le watcher précédent (un seul actif à la fois, stocké dans `WatcherState`) ;
- surveille en mode récursif si `path` est un répertoire, non récursif si c'est un
fichier ;
- lance une boucle de **débounce de 80 ms** qui ne retient que les fichiers `.md` non
cachés, et émet l'événement Tauri **`file-changed`** avec un payload `{ path }`.
- **Frontend** : `services/watcher.js` (`startWatch`).
### `stop_watch`
```
stop_watch() -> ()
```
- **Comportement** : arrête le watcher actif (le supprime de `WatcherState`, ce qui termine
le thread de surveillance).
- **Frontend** : `services/watcher.js` (`stopWatch`).
### Événement `file-changed`
- Émis par le backend pendant la surveillance.
- Payload : `{ path: String }` — chemin du fichier modifié.
- Écoutable côté frontend via
`window.__TAURI__.event.listen('file-changed', callback)` (`onFileChanged`).
- **Consommation** : `router.js` enregistre le listener au démarrage (`initWatcher`) et,
à chaque événement, recharge le document affiché s'il correspond au chemin modifié et
rafraîchit la sidebar si l'arborescence `.md` du dossier surveillé a changé. La
surveillance est démarrée dans `openPath` (via `startWatch`) et arrêtée dans `showHome`
(via `stopWatch`).
## Thèmes
### `list_themes`
```
list_themes() -> Vec<ThemeDto> // ThemeDto { id: String, label: String }
```
- **Args** : aucun.
- **Retour** : les 6 thèmes (`default`, `dark`, `sepia`, `large-text`, `emerald`,
`shell-indigo`) avec leur libellé.
- **Frontend** : `services/themes.js`.
### `get_theme_css`
```
get_theme_css(id: String) -> Option<String>
```
- **Args** : `id` — identifiant du thème.
- **Retour** : le CSS du thème (`Some`) ou `None` si l'identifiant est inconnu.
- **Comportement** : le CSS est intégré au binaire (`include_str!` sur
`resources/themes/<id>.css`).
- **Frontend** : `services/themes.js`.
## Récapitulatif
| Commande | Signature | Retour |
|---|---|---|
| `render_markdown` | `(content: String)` | `String` |
| `convert_file` | `(path: String)` | `Result<String, String>` |
| `get_syntax_highlight_css` | `()` | `String` |
| `list_md_files` | `(dir: String)` | `Result<Vec<String>, String>` |
| `start_watch` | `(path: String)` | `Result<(), String>` |
| `stop_watch` | `()` | `()` |
| `list_themes` | `()` | `Vec<ThemeDto>` |
| `get_theme_css` | `(id: String)` | `Option<String>` |
## Voir aussi
- [Architecture](2.1-Architecture.md)
- [Thèmes et personnalisation](../1-fonctionnel/1.4-Themes-et-personnalisation.md)
+89
View File
@@ -0,0 +1,89 @@
# Installation (Linux générique / Fedora)
Cette page décrit l'installation de **Pena-tauri** sur une distribution Linux classique
(mutable), où l'on peut installer des paquets système avec le gestionnaire de la distribution.
- Pour un système **immuable** (Bazzite, Fedora Silverblue/Kinoite, openSUSE MicroOS…), voir
[Installation sur Bazzite OS](3.2-Installation-Bazzite.md).
- Pour le détail des commandes de compilation, voir [Build](3.3-Build.md).
## 1. Installer les prérequis
### Rust et Tauri CLI
```bash
# Rust (toolchain stable)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
# Tauri CLI v2
cargo install tauri-cli --version "^2"
```
### Dépendances système
**Fedora / RHEL :**
```bash
sudo dnf install webkit2gtk4.1-devel \
openssl-devel curl wget file libappindicator-gtk3-devel librsvg2-devel
sudo dnf group install "C Development Tools and Libraries"
```
**Debian / Ubuntu :**
```bash
sudo apt update
sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file \
libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev
```
## 2. Récupérer les sources
```bash
git clone <url-du-depot> Pena
cd Pena/Pena-tauri
```
## 3. Compiler et installer
### Lancer directement (développement)
```bash
cargo tauri dev
```
### Construire une release
```bash
cargo tauri build
```
L'exécutable est produit dans `src-tauri/target/release/`. Pour l'installer à l'échelle du
système, copiez-le dans un dossier du `PATH` :
```bash
sudo install -Dm755 src-tauri/target/release/pena-taury /usr/local/bin/pena
```
Vous pouvez ensuite lancer l'application avec `pena`.
> **Nom du binaire** : le binaire s'appelle `pena-taury` (nom du paquet Cargo). Renommez-le à
> votre convenance lors de l'installation, comme ci-dessus (`pena`).
## 4. (Optionnel) Raccourci d'application
Pour faire apparaître Pena dans le menu des applications, créez un fichier `.desktop` :
```bash
cat > ~/.local/share/applications/pena.desktop <<'EOF'
[Desktop Entry]
Type=Application
Name=Pena
Comment=Lecteur Markdown
Exec=/usr/local/bin/pena
Terminal=false
Categories=Utility;Office;
EOF
```
## Voir aussi
- [Build et compilation](3.3-Build.md)
- [Installation sur Bazzite OS](3.2-Installation-Bazzite.md)
- [Vue d'ensemble](../1-fonctionnel/1.1-Vue-d-ensemble.md)
@@ -0,0 +1,253 @@
# Installation sur Bazzite OS
[Bazzite](https://bazzite.gg/) est une distribution **immuable** basée sur Fedora Atomic
(rpm-ostree). Le système de fichiers racine est en lecture seule : on **n'installe pas** de
paquets de développement (`webkit2gtk-devel`, compilateurs…) directement sur l'hôte comme on
le ferait sur une Fedora classique.
Cette contrainte vaut aussi pour les autres systèmes atomiques (Fedora Silverblue, Kinoite,
Universal Blue, openSUSE MicroOS…).
La méthode recommandée est le **Flatpak** : on construit **une seule fois** un fichier
`.flatpak` (le « bundle »), on l'héberge (par ex. sur un dépôt Gitea), puis on l'installe sur
n'importe quelle machine **en une commande**, sans rien compiler ni installer de dépendance
sur l'hôte.
| Vous voulez… | Allez à… |
|---|---|
| **Installer Pena** depuis un `.flatpak` déjà construit (récupéré sur Gitea) | [1. Méthode simple](#1-méthode-simple--installer-le-paquet-pré-construit) |
| **Produire** le fichier `.flatpak` (une fois, sur une machine de build) | [2. Compiler](#2-compiler-le-binaire-une-fois) puis [3. Construire le bundle](#3-construire-le-paquet-flatpak-une-fois) |
| **Développer / itérer** sur le code sans empaqueter | [4. Alternative développeur (distrobox)](#4-alternative-développeur--lancer-via-distrobox) |
> Bazzite fournit **`flatpak`** (avec le dépôt Flathub), **`distrobox`** et **`podman`**
> préinstallés. Aucune surcouche rpm-ostree (`rpm-ostree install`) n'est nécessaire.
---
## 1. Méthode simple — installer le paquet pré-construit
C'est le scénario du quotidien : le fichier `pena.flatpak` a déjà été construit (voir
sections 2 et 3) et déposé sur votre Gitea. Sur la machine cible, il suffit de le récupérer et
de l'installer.
### 1.1 — Installer
```bash
# Récupérer le bundle depuis Gitea (adaptez l'URL à votre dépôt)
curl -L -o pena.flatpak \
https://git.goutailler-olivier.com/<utilisateur>/<depot>/raw/branch/main/pena.flatpak
# Installer pour l'utilisateur courant (aucun droit root nécessaire)
flatpak install --user pena.flatpak
```
> Le runtime `org.gnome.Platform` (qui fournit WebKitGTK) est téléchargé automatiquement
> depuis Flathub lors de l'installation s'il n'est pas déjà présent. C'est la seule dépendance,
> et elle est gérée par Flatpak — rien à compiler ni à installer sur l'hôte.
### 1.2 — Lancer
```bash
flatpak run com.pena.app
```
Pena apparaît aussi dans le menu des applications de Bazzite.
### 1.3 — Mettre à jour / désinstaller
```bash
# Mettre à jour : récupérer le nouveau bundle puis réinstaller par-dessus
flatpak install --user --reinstall pena.flatpak
# Désinstaller
flatpak uninstall com.pena.app
```
### 1.4 — (Optionnel) Installation en une ligne
Vous pouvez déposer à côté du bundle, sur Gitea, un script `install.sh` qui automatise le
téléchargement et l'installation :
```bash
#!/usr/bin/env bash
set -euo pipefail
URL="https://git.goutailler-olivier.com/<utilisateur>/<depot>/raw/branch/main/pena.flatpak"
TMP="$(mktemp --suffix=.flatpak)"
curl -L -o "$TMP" "$URL"
flatpak install --user -y "$TMP"
rm -f "$TMP"
echo "Pena installé. Lancez-le avec : flatpak run com.pena.app"
```
L'installation se résume alors à :
```bash
curl -L https://git.goutailler-olivier.com/<utilisateur>/<depot>/raw/branch/main/install.sh | bash
```
---
## 2. Compiler le binaire (une fois)
Cette étape et la suivante se font **sur une machine de build** (la vôtre, dans un container).
Le résultat est un fichier `pena.flatpak` que vous n'aurez plus qu'à héberger.
Pena se compile dans un container Ubuntu via **distrobox**, pour ne rien installer sur l'hôte
immuable. WebKitGTK et le compilateur restent dans le container.
```bash
# 1. Créer et entrer dans un container Ubuntu
distrobox create --name pena-build --image ubuntu:24.04
distrobox enter pena-build
# 2. (dans le container) installer les dépendances de build
sudo apt update
sudo apt install -y \
libwebkit2gtk-4.1-dev build-essential curl wget file \
libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev git
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source "$HOME/.cargo/env"
cargo install tauri-cli --version "^2"
# 3. (dans le container) compiler
cd ~/Pena/Pena-tauri # adaptez le chemin vers les sources
cargo tauri build
```
Le binaire est produit dans `src-tauri/target/release/pena-taury`. Vous pouvez ensuite
quitter le container (`exit`) : la suite (section 3) se fait sur l'hôte, où `flatpak` est
disponible. Comme `$HOME` est partagé entre l'hôte et le container, le binaire compilé est
accessible des deux côtés.
---
## 3. Construire le paquet `.flatpak` (une fois)
À partir du binaire compilé, on produit le fichier unique `pena.flatpak`.
### 3.1 — Outillage Flatpak (sur l'hôte)
```bash
flatpak install -y flathub org.gnome.Platform//47 org.gnome.Sdk//47 org.flatpak.Builder
```
### 3.2 — Fichiers d'empaquetage
Depuis la racine `Pena-tauri/`, créez deux fichiers.
**`com.pena.app.desktop`** :
```ini
[Desktop Entry]
Type=Application
Name=Pena
Comment=Lecteur Markdown
Exec=pena
Icon=com.pena.app
Terminal=false
Categories=Utility;Office;
```
**`com.pena.app.yml`** (manifeste Flatpak) :
```yaml
id: com.pena.app
runtime: org.gnome.Platform
runtime-version: '47'
sdk: org.gnome.Sdk
command: pena
finish-args:
- --share=ipc
- --socket=wayland
- --socket=fallback-x11
- --device=dri
# Accès en lecture aux fichiers Markdown de l'utilisateur
- --filesystem=home:ro
modules:
- name: pena
buildsystem: simple
build-commands:
- install -Dm755 pena-taury /app/bin/pena
- install -Dm644 com.pena.app.desktop /app/share/applications/com.pena.app.desktop
- install -Dm644 icon.png /app/share/icons/hicolor/512x512/apps/com.pena.app.png
sources:
- type: file
path: src-tauri/target/release/pena-taury
- type: file
path: com.pena.app.desktop
- type: file
path: src-tauri/icons/icon.png
```
> L'identifiant `com.pena.app` correspond au champ `identifier` de
> `src-tauri/tauri.conf.json`. Le runtime `org.gnome.Platform` embarque WebKitGTK : l'app
> fonctionnera sans aucune dépendance installée sur la machine cible.
### 3.3 — Construire le bundle
Depuis `Pena-tauri/` :
```bash
# 1. Construire l'app dans un dépôt OSTree local (dossier "repo")
flatpak run org.flatpak.Builder --force-clean --repo=repo \
--install-deps-from=flathub build-dir com.pena.app.yml
# 2. Exporter en UN SEUL fichier .flatpak
flatpak build-bundle repo pena.flatpak com.pena.app \
--runtime-repo=https://flathub.org/repo/flathub.flatpakrepo
```
Vous obtenez **`pena.flatpak`** : c'est ce fichier unique, autoportant, que vous installez
partout (section 1). L'option `--runtime-repo` y inscrit la référence vers Flathub, pour que
le runtime soit récupéré automatiquement à l'installation.
### 3.4 — Héberger sur Gitea
Déposez `pena.flatpak` sur votre dépôt Gitea, soit :
- en l'ajoutant au dépôt (`git add pena.flatpak`) — simple, mais alourdit l'historique ;
- **de préférence**, en l'attachant à une **release** Gitea (onglet *Releases**New Release*
→ joindre le binaire). L'URL de téléchargement direct est alors stable et n'encombre pas le
dépôt.
Adaptez ensuite l'URL utilisée à la [section 1.1](#11--installer).
---
## 4. Alternative développeur — lancer via distrobox
Si vous **développez** sur Pena et n'avez pas besoin d'un paquet installable, vous pouvez
exécuter l'application directement depuis le container, sans passer par Flatpak.
```bash
# Lancement direct (la fenêtre s'affiche sur le bureau de l'hôte)
distrobox enter pena-build -- bash -lc 'cd ~/Pena/Pena-tauri && cargo tauri dev'
```
Pour exposer le binaire compilé à l'hôte comme une commande :
```bash
# Depuis le container, après "cargo tauri build"
distrobox-export --bin ~/Pena/Pena-tauri/src-tauri/target/release/pena-taury \
--export-path ~/.local/bin
```
Le binaire devient lançable depuis l'hôte (`~/.local/bin` doit être dans le `PATH`). Cette
voie est pratique pour itérer, mais le Flatpak (sections 13) reste la méthode recommandée
pour une **installation** propre et reproductible.
---
## Note — AppImage
Une autre forme de « fichier unique à exécuter » est l'**AppImage** : un binaire autoportant
qui se lance sans installation (`chmod +x Pena.AppImage && ./Pena.AppImage`). Tauri sait en
produire, mais cela nécessite d'activer le bundler (`bundle.active: true` dans
`tauri.conf.json`, cible `appimage`) et l'outillage associé. Le **bundle Flatpak** décrit
ci-dessus est privilégié ici car il s'intègre au menu, se met à jour proprement et gère
automatiquement ses dépendances via Flathub.
## Voir aussi
- [Release et distribution](3.4-Release-et-distribution.md) — pourquoi le Flatpak, portabilité et trade-offs des formats
- [Build et compilation](3.3-Build.md)
- [Installation (Linux générique / Fedora)](3.1-Installation.md)
- [Architecture](../2-technique/2.1-Architecture.md)
+110
View File
@@ -0,0 +1,110 @@
# Build et compilation
Cette page décrit comment compiler **Pena-tauri** depuis les sources, en développement comme
en production. Pour une installation sur un système immuable (Bazzite, Silverblue, etc.),
voir plutôt [Installation sur Bazzite OS](3.2-Installation-Bazzite.md).
## Prérequis
| Outil | Détail |
|---|---|
| **Rust** | Toolchain stable, via [rustup](https://rustup.rs/) (édition 2021). |
| **Tauri CLI v2** | `cargo install tauri-cli --version "^2"` (fournit `cargo tauri`). |
| **Dépendances système** | WebKitGTK et libs associées (voir ci-dessous selon la distribution). |
Le frontend n'a **aucune dépendance Node** : c'est du JS vanilla servi statiquement depuis
`src/`. Il n'y a donc pas de `npm install` à faire pour `Pena-tauri`.
### Dépendances système Linux
**Fedora / RHEL :**
```bash
sudo dnf install webkit2gtk4.1-devel \
openssl-devel curl wget file libappindicator-gtk3-devel librsvg2-devel
sudo dnf group install "C Development Tools and Libraries"
```
**Debian / Ubuntu :**
```bash
sudo apt update
sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file \
libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev
```
> Sur une distribution immuable (Bazzite, Fedora Silverblue, Kinoite…), n'installez pas ces
> paquets sur l'hôte : utilisez un container ou un Flatpak. Voir
> [Installation sur Bazzite OS](3.2-Installation-Bazzite.md).
## Lancer en développement
Depuis `Pena-tauri/` :
```bash
cargo tauri dev
```
- Le frontend est servi directement depuis `src/` (fichiers statiques, sans bundler).
- La compilation Rust est lancée automatiquement et l'application s'ouvre.
- Toute modification du Rust déclenche une recompilation.
## Construire une release
Depuis `Pena-tauri/` :
```bash
cargo tauri build
```
L'exécutable est produit dans :
```
src-tauri/target/release/
```
### À propos du bundling
Dans `src-tauri/tauri.conf.json`, le bundling est **désactivé** :
```json
"bundle": { "active": false, "targets": "all", "icon": [] }
```
`cargo tauri build` produit donc l'**exécutable natif** (dans `target/release/`), mais ne
génère pas de paquets `.deb`, `.rpm` ou `.AppImage`. Pour activer la génération de ces
paquets, passer `bundle.active` à `true` et renseigner les icônes.
## Compiler le backend seul (sans Tauri CLI)
Pour de la compilation / des tests bas niveau, depuis `src-tauri/` :
```bash
cargo build # compilation debug
cargo build --release # compilation optimisée
cargo test # tests unitaires
cargo clippy -- -D warnings # lint (zéro warning toléré)
```
## Qualité et couverture
Le dépôt `Pena-tauri` impose un hook `pre-commit` qui bloque le commit si :
- **Clippy** remonte le moindre warning (`-D warnings`) ;
- la **couverture de lignes** est inférieure à **60 %** (via `cargo-llvm-cov`).
Installer l'outil de couverture :
```bash
cargo install cargo-llvm-cov
cargo llvm-cov --summary-only
```
## Récapitulatif des commandes
| But | Commande (depuis `Pena-tauri/`) |
|---|---|
| Développement | `cargo tauri dev` |
| Release (exécutable) | `cargo tauri build` |
| Compiler le backend | `cargo build --release` (depuis `src-tauri/`) |
| Tests | `cargo test` (depuis `src-tauri/`) |
| Lint | `cargo clippy -- -D warnings` |
| Couverture | `cargo llvm-cov --summary-only` |
## Voir aussi
- [Release et distribution](3.4-Release-et-distribution.md) — portabilité du binaire et création d'un paquet Flatpak
- [Installation (Linux générique)](3.1-Installation.md)
- [Installation sur Bazzite OS](3.2-Installation-Bazzite.md)
- [Architecture](../2-technique/2.1-Architecture.md)
@@ -0,0 +1,277 @@
# Release et distribution
Cette page explique **comment fonctionne une release de Pena** : ce que produit réellement la
compilation, **si un binaire compilé sur une distribution Linux est utilisable sur n'importe
quelle autre**, et les différentes façons de **distribuer** l'application — avec un focus sur le
**paquet Flatpak** et ses avantages/inconvénients.
- Pour les commandes de compilation pures, voir [Build et compilation](3.3-Build.md).
- Pour la procédure pas-à-pas d'installation Flatpak sur système immuable, voir
[Installation sur Bazzite OS](3.2-Installation-Bazzite.md).
---
## 1. Que produit une release ?
`cargo tauri build` (depuis `Pena-tauri/`) produit un **exécutable natif** :
```
src-tauri/target/release/pena-taury
```
C'est un binaire ELF compilé pour l'architecture de la machine de build (typiquement
`x86_64-linux-gnu`). Le frontend (HTML/CSS/JS de `src/`) y est **embarqué** dans le binaire :
il n'y a pas de fichiers web à distribuer à côté.
> Dans `tauri.conf.json`, `bundle.active` vaut `false` : `cargo tauri build` ne génère donc
> **pas** de `.deb`/`.rpm`/`.AppImage` automatiquement. Il produit l'exécutable seul. La
> génération de paquets est traitée plus bas (section 4).
### Ce que le binaire contient — et ce qu'il ne contient pas
| Embarqué dans le binaire | **Pas** embarqué (fourni par le système) |
|---|---|
| Code Rust de Pena (commandes Tauri) | **glibc** (bibliothèque C) |
| Frontend (HTML/CSS/JS) | **WebKitGTK** (le moteur de rendu de la fenêtre) |
| `comrak` + `syntect` (rendu Markdown) | **GTK 3/4**, GLib, Cairo, Pango… |
| Plugins Tauri liés statiquement | Pilotes graphiques, serveur d'affichage (X11/Wayland) |
C'est ce tableau qui répond à la question de la portabilité.
---
## 2. Un binaire compilé sur Linux marche-t-il sur **n'importe quelle** distribution ?
**Réponse courte : non, pas de façon fiable.** Un binaire « brut » (le `pena-taury` produit
ci-dessus) n'est **pas universel**. Il dépend de bibliothèques présentes sur le système cible,
et il ne tournera ailleurs que si ces bibliothèques sont **présentes et compatibles**.
Trois raisons concrètes :
### 2.1 — Le lien dynamique avec WebKitGTK
C'est la contrainte la plus dure pour une app Tauri. Le binaire est lié à
**`libwebkit2gtk-4.1`**. Sur la machine cible, il faut :
- que WebKitGTK soit **installé** ;
- que ce soit la **bonne version d'ABI** : `webkit2gtk-4.0` et `webkit2gtk-4.1` ont des
*sonames* différents et **ne sont pas interchangeables**. Un binaire lié à `4.1` ne démarrera
pas sur une machine qui n'a que `4.0`, et inversement.
Sur une distribution récente, WebKitGTK 4.1 est généralement disponible ; sur une
distribution plus ancienne ou minimaliste, il peut manquer.
### 2.2 — La version de la glibc
Les binaires liés à la glibc sont **compatibles vers l'avant, pas vers l'arrière** :
- un binaire compilé sur une **vieille** glibc tourne sur une machine ayant une glibc **plus
récente** ✅ ;
- un binaire compilé sur une glibc **récente** échoue sur une machine ayant une glibc **plus
ancienne** ❌, avec une erreur du type `version 'GLIBC_2.38' not found`.
**Conséquence pratique :** pour maximiser la portabilité d'un binaire brut, compilez-le sur la
distribution la **plus ancienne** que vous comptez supporter (par ex. dans un container
Ubuntu 22.04), pas sur la plus récente.
### 2.3 — Les autres bibliothèques système
GTK, GLib, Cairo, Pango, librsvg, libappindicator… doivent aussi être présentes. Sur un poste
de bureau classique elles le sont presque toujours (ce sont des dépendances de l'environnement
de bureau), mais sur un serveur, un système minimal ou immuable, ce n'est pas garanti.
### Récapitulatif portabilité
| Cible | Le binaire brut marche-t-il ? |
|---|---|
| Même distribution / version que la machine de build | ✅ Oui |
| Distribution différente mais récente, avec `webkit2gtk-4.1` et glibc ≥ celle du build | ✅ Généralement |
| Distribution avec une glibc **plus ancienne** que le build | ❌ Non (`GLIBC_x.y not found`) |
| Distribution qui n'a que `webkit2gtk-4.0` | ❌ Non (soname incompatible) |
| Système immuable (Bazzite, Silverblue…) | ❌ Pas directement — voir Flatpak |
| Architecture différente (ARM vs x86_64) | ❌ Non — il faut recompiler pour l'archi |
**Conclusion :** « compilé sur Linux » ne veut pas dire « marche sur tout Linux ». Pour une
distribution **réellement universelle**, il faut un format qui **embarque ses dépendances** :
c'est le rôle du **Flatpak** (recommandé ici) ou de l'**AppImage**.
---
## 3. Les formats de distribution possibles
| Format | Dépendances | Portabilité | Intégration bureau | Mise à jour |
|---|---|---|---|---|
| **Binaire brut** | Fournies par l'hôte | Faible (voir §2) | Manuelle (`.desktop`) | Manuelle |
| **`.deb` / `.rpm`** | Déclarées, résolues par le gestionnaire de paquets | Bonne **sur la famille visée** (Debian *ou* Fedora) | Automatique | Via le gestionnaire |
| **AppImage** | **Embarquées** dans un fichier exécutable | Bonne (un seul fichier portable) | Partielle | Manuelle (re-télécharger) |
| **Flatpak** | **Embarquées** via un *runtime* partagé (Flathub) | **Excellente** (toute distro avec `flatpak`) | Complète (menu, icône) | `flatpak update` |
Pour Pena, le format **recommandé est le Flatpak** : il résout d'un coup les trois problèmes du
§2 (WebKitGTK, glibc, libs système) en fournissant un **runtime** complet et identique partout.
---
## 4. Créer un paquet Flatpak
Le Flatpak rend l'application **vraiment portable** : le moteur WebKitGTK et toutes les
bibliothèques système viennent du **runtime `org.gnome.Platform`** (téléchargé depuis Flathub),
pas de l'hôte. Le même `pena.flatpak` s'installe alors sur n'importe quelle distribution dotée
de `flatpak`, immuable ou non.
> La procédure complète (installation côté machine cible, hébergement sur Gitea, alternative
> développeur) est détaillée dans [Installation sur Bazzite OS](3.2-Installation-Bazzite.md).
> On résume ici les étapes de **production** du paquet.
### 4.1 — Principe en trois temps
```
[ cargo tauri build ] → binaire natif pena-taury
[ manifeste Flatpak ] → build dans un dépôt OSTree local "repo/"
[ flatpak build-bundle ] → fichier unique pena.flatpak
```
### 4.2 — Outillage (une fois)
```bash
flatpak install -y flathub org.gnome.Platform//47 org.gnome.Sdk//47 org.flatpak.Builder
```
### 4.3 — Le manifeste
Le manifeste décrit l'identifiant de l'app, le runtime qui fournit les dépendances, et comment
installer le binaire dans le sandbox. Depuis `Pena-tauri/`, créer **`com.pena.app.yml`** :
```yaml
id: com.pena.app # = identifier de tauri.conf.json
runtime: org.gnome.Platform # fournit WebKitGTK + GTK + glibc du runtime
runtime-version: '47'
sdk: org.gnome.Sdk
command: pena
finish-args:
- --share=ipc
- --socket=wayland
- --socket=fallback-x11
- --device=dri
- --filesystem=home:ro # lecture des fichiers Markdown de l'utilisateur
modules:
- name: pena
buildsystem: simple
build-commands:
- install -Dm755 pena-taury /app/bin/pena
- install -Dm644 com.pena.app.desktop /app/share/applications/com.pena.app.desktop
- install -Dm644 icon.png /app/share/icons/hicolor/512x512/apps/com.pena.app.png
sources:
- type: file
path: src-tauri/target/release/pena-taury
- type: file
path: com.pena.app.desktop
- type: file
path: src-tauri/icons/icon.png
```
Et le fichier d'intégration au menu **`com.pena.app.desktop`** :
```ini
[Desktop Entry]
Type=Application
Name=Pena
Comment=Lecteur Markdown
Exec=pena
Icon=com.pena.app
Terminal=false
Categories=Utility;Office;
```
> Point clé sur la portabilité : `finish-args` définit le **sandbox**. Pena n'a besoin que de
> l'affichage (Wayland/X11), du GPU (`dri`) et d'un **accès en lecture seule** au `home`
> (`--filesystem=home:ro`) pour ouvrir les fichiers `.md`. C'est volontairement minimal.
### 4.4 — Construire le bundle
Depuis `Pena-tauri/`, après un `cargo tauri build` réussi :
```bash
# 1. Construire dans un dépôt OSTree local "repo/"
flatpak run org.flatpak.Builder --force-clean --repo=repo \
--install-deps-from=flathub build-dir com.pena.app.yml
# 2. Exporter en UN SEUL fichier .flatpak autoportant
flatpak build-bundle repo pena.flatpak com.pena.app \
--runtime-repo=https://flathub.org/repo/flathub.flatpakrepo
```
On obtient **`pena.flatpak`** : un fichier unique, installable partout par
`flatpak install --user pena.flatpak`. L'option `--runtime-repo` y inscrit la référence
Flathub, pour que le runtime soit récupéré automatiquement à l'installation s'il manque.
---
## 5. Flatpak : avantages et inconvénients
### Avantages
- **Portabilité réelle.** Le runtime fournit WebKitGTK, GTK et une glibc cohérente : les trois
blocages du §2 disparaissent. Le même fichier marche sur Fedora, Ubuntu, Arch, Bazzite,
Silverblue… sans recompiler.
- **Aucune dépendance à installer sur l'hôte.** Idéal pour les systèmes **immuables** (Bazzite,
Silverblue) où l'on ne peut pas faire `dnf install webkit2gtk-devel`.
- **Installation sans root** (`--user`).
- **Sandbox.** L'app n'a accès qu'à ce que `finish-args` autorise (ici : affichage + `home` en
lecture seule). Surface d'attaque réduite.
- **Intégration de bureau** automatique (entrée de menu, icône) et **mises à jour** par
`flatpak update`.
- **Reproductible.** Le runtime versionné (`//47`) garantit le même socle partout.
### Inconvénients
- **Taille.** Le **runtime** `org.gnome.Platform` pèse plusieurs centaines de Mo. Il n'est
téléchargé qu'une fois et partagé entre toutes les apps Flatpak, mais c'est lourd pour une app
aussi simple que Pena si c'est le seul Flatpak de la machine.
- **Premier lancement / première install** plus longs (téléchargement du runtime).
- **Le sandbox demande de la réflexion.** Tout accès fichier hors `home:ro` doit être déclaré
explicitement ; un oubli se traduit par une fonctionnalité qui « ne voit pas » les fichiers.
- **Dépendance à Flathub** pour récupérer le runtime (réseau requis à l'installation).
- **Outillage de build** supplémentaire (`flatpak-builder`, SDK) par rapport à un simple
`cargo tauri build`.
### Quand préférer une autre option
| Situation | Format conseillé |
|---|---|
| Diffusion large, multi-distributions, systèmes immuables | **Flatpak** |
| Un seul fichier à exécuter sans rien installer | **AppImage** (`bundle.active: true`, cible `appimage`) |
| Cible une seule famille (que Debian, ou que Fedora) avec gestion par paquets | **`.deb`** / **`.rpm`** |
| Usage perso sur sa propre machine, ou itération de dev | **Binaire brut** / `cargo tauri dev` |
> **AppImage** est l'autre format « un seul fichier ». Tauri sait en produire (activer le
> bundler dans `tauri.conf.json`), mais le **Flatpak** est privilégié ici pour son intégration
> au menu, ses mises à jour propres et la gestion automatique des dépendances via Flathub.
---
## 6. Liste de contrôle d'une release
1. Vérifier la version dans `src-tauri/tauri.conf.json` (`"version"`).
2. Compiler dans un environnement à **glibc ancienne** si l'on distribue un binaire brut
(container Ubuntu 22.04), sinon directement.
3. `cargo tauri build` → vérifier `src-tauri/target/release/pena-taury`.
4. Construire le `pena.flatpak` (section 4).
5. Tester l'install sur une machine **vierge** : `flatpak install --user pena.flatpak` puis
`flatpak run com.pena.app`.
6. Héberger le `.flatpak` sur **une release Gitea** (URL stable, n'alourdit pas le dépôt) —
voir [3.2 §3.4](3.2-Installation-Bazzite.md).
---
## Voir aussi
- [Build et compilation](3.3-Build.md)
- [Installation sur Bazzite OS](3.2-Installation-Bazzite.md) — installation et hébergement du Flatpak
- [Installation (Linux générique / Fedora)](3.1-Installation.md)
- [Architecture](../2-technique/2.1-Architecture.md)
+98
View File
@@ -0,0 +1,98 @@
# MarkdownRender (CLI `md-render`)
`MarkdownRender-nodejs` est un **outil en ligne de commande** indépendant de l'application
Tauri. Il convertit du Markdown en HTML et propose une prévisualisation live dans le
navigateur. Il est utile pour générer rapidement un `.html` ou prévisualiser un document sans
lancer Pena.
> C'est un utilitaire **annexe** ; le produit principal reste
> [Pena-tauri](../2-technique/2.1-Architecture.md).
## Installation
Prérequis : **Node.js** (avec `npm`).
Depuis `MarkdownRender-nodejs/` :
```bash
npm install
npm link # rend la commande `md-render` disponible globalement
```
## Utilisation
```bash
# Conversion simple (génère un fichier .html à côté de l'entrée)
md-render README.md
# Conversion vers un fichier de sortie précis
md-render README.md -o documentation.html
# Mode preview — ouvre le navigateur sans créer de fichier
md-render README.md --preview
# Preview d'un dossier entier (avec barre latérale)
md-render mon-wiki/ --preview
# Mode watch — sert http://localhost:3000 avec rechargement automatique
md-render README.md --watch
# Sur un port personnalisé
md-render README.md --watch --port 8080
```
## Options
| Option | Alias | Description | Défaut |
|---|---|---|---|
| `--watch` | `-w` | Démarre un serveur avec rechargement automatique | — |
| `--preview` | `-v` | Ouvre dans le navigateur sans créer de fichier | — |
| `--port <port>` | `-p` | Port du serveur (watch ou preview) | `3000` |
| `--output <fichier>` | `-o` | Fichier HTML de sortie | `<entrée>.html` |
| `--help` | `-h` | Affiche l'aide | — |
## Modes
### Mode preview (`--preview`)
Le Markdown est converti **en mémoire** et affiché directement dans le navigateur — aucun
fichier HTML n'est écrit sur le disque. Le navigateur s'ouvre automatiquement.
`Ctrl+C` arrête le serveur. Fonctionne sur un fichier unique ou un dossier entier.
### Mode watch (`--watch`)
Un serveur HTTP local est démarré. À chaque sauvegarde du fichier, la page se recharge
automatiquement via **Server-Sent Events (SSE)**. Les assets relatifs (images…) placés dans
le même répertoire que le `.md` sont servis automatiquement.
## Fonctionnalités
- 🎨 Coloration syntaxique du code ([highlight.js](https://highlightjs.org/))
- 📄 Rendu de style « GitHub-like »
- 👁 Prévisualisation instantanée sans fichier (`--preview`)
- 🔄 Rechargement automatique en mode watch (SSE)
- 🖼 Serveur de fichiers statiques pour les images locales
- ⚡ Débounce intelligent pour éviter les rechargements en rafale
- 🛡 Protection contre le path traversal
## Dépendances principales
| Paquet | Rôle |
|---|---|
| [`marked`](https://marked.js.org/) | Conversion Markdown → HTML |
| `marked-highlight` + [`highlight.js`](https://highlightjs.org/) | Coloration syntaxique |
| [`chokidar`](https://github.com/paulmillr/chokidar) | Surveillance de fichiers (mode watch) |
| [`minimist`](https://github.com/minimistjs/minimist) | Analyse des arguments CLI |
## Différences avec Pena-tauri
| | `md-render` (CLI) | Pena-tauri (app) |
|---|---|---|
| Plateforme | Terminal + navigateur | Application de bureau |
| Rendu Markdown | `marked` (JS) | `comrak` (Rust) |
| Coloration | highlight.js | syntect |
| Live-reload | ✅ (SSE) | implémenté mais non branché |
| Thèmes / CSS éditable | — | ✅ |
| Sortie fichier HTML | ✅ | — |
## Voir aussi
- [Vue d'ensemble de Pena](../1-fonctionnel/1.1-Vue-d-ensemble.md)
- [README de la documentation](../README.md)
+44
View File
@@ -0,0 +1,44 @@
# Documentation Pena
**Pena** est une application de bureau de lecture de fichiers Markdown, construite avec
[Tauri 2](https://tauri.app/) (backend Rust) et du JavaScript vanilla (frontend, sans bundler).
Elle permet d'ouvrir un fichier `.md` isolé ou un dossier « wiki » complet, de naviguer entre
les documents, et de personnaliser entièrement le rendu via des thèmes et du CSS.
Le rendu Markdown → HTML est effectué côté Rust avec [`comrak`](https://github.com/kivikakk/comrak)
(CommonMark + extensions) et la coloration syntaxique via [`syntect`](https://github.com/trishume/syntect).
---
## Sommaire de la documentation
### Documentation fonctionnelle
- [Vue d'ensemble](1-fonctionnel/1.1-Vue-d-ensemble.md) — à quoi sert Pena, pour qui, dans quels cas
- [Fonctionnalités](1-fonctionnel/1.2-Fonctionnalites.md) — liste détaillée de tout ce que l'application sait faire
- [Interface](1-fonctionnel/1.3-Interface.md) — barre de titre, accueil, lecteur, barre latérale
- [Thèmes et personnalisation](1-fonctionnel/1.4-Themes-et-personnalisation.md) — thèmes rapides et éditeur CSS
### Documentation technique
- [Architecture](2-technique/2.1-Architecture.md) — Clean Architecture, découpage Rust et JS
- [Commandes Tauri](2-technique/2.2-Commandes-Tauri.md) — référence des commandes exposées au frontend
### Installation et build
- [Installation (Linux générique / Fedora)](3-installation/3.1-Installation.md)
- [Installation sur Bazzite OS](3-installation/3.2-Installation-Bazzite.md) — méthode simple : un fichier `.flatpak` pré-construit, hébergé sur Gitea, installable en une commande
- [Build et compilation](3-installation/3.3-Build.md)
- [Release et distribution](3-installation/3.4-Release-et-distribution.md) — portabilité d'un binaire Linux, formats de distribution, création d'un paquet Flatpak (avantages / inconvénients)
### Outils annexes
- [MarkdownRender (CLI Node.js)](4-outils/4.1-MarkdownRender-CLI.md) — convertisseur `md-render` en ligne de commande
---
## Composants du dépôt
| Composant | Techno | Rôle |
|---|---|---|
| `Pena-tauri/` | Tauri 2 + Rust + JS vanilla | Application de bureau principale (lecteur Markdown) |
| `MarkdownRender-nodejs/` | Node.js | Outil CLI annexe `md-render` (conversion + preview live dans le navigateur) |
> Le cœur du produit est **`Pena-tauri`**. `MarkdownRender-nodejs` est un utilitaire
> indépendant, documenté dans [outils/MarkdownRender-CLI.md](4-outils/4.1-MarkdownRender-CLI.md).
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env bash
#
# Construit un paquet Flatpak de Pena à partir du .deb généré par Tauri.
#
# Étapes :
# 1. `cargo tauri build --bundles deb` -> produit le .deb autonome
# 2. Copie du .deb à côté du manifeste sous le nom `pena.deb`
# 3. `flatpak-builder` construit le bundle dans la sandbox GNOME
# 4. Installation locale (--user) ET export du fichier pena.flatpak
#
# Le fichier `pena.flatpak` est TOUJOURS exporté à côté du script.
#
# Usage :
# ./build-flatpak.sh # build + installation (--user) + pena.flatpak
# ./build-flatpak.sh --no-install # build + pena.flatpak seul (pas d'installation)
# ./build-flatpak.sh --no-deb # réutilise le .deb déjà présent (build rapide)
#
set -euo pipefail
APP_ID="com.pena.app"
RUNTIME_VERSION="48"
RUST_CONTAINER="ubuntu-rust" # distrobox contenant cargo-tauri + libs GTK/WebKit
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")" # .../Pena-tauri
MANIFEST="$SCRIPT_DIR/${APP_ID}.yml"
BUILD_DIR="$SCRIPT_DIR/.build" # arbre de build flatpak-builder
REPO_DIR="$SCRIPT_DIR/.repo" # dépôt ostree temporaire
DEB_TARGET="$SCRIPT_DIR/pena.deb"
TAURI_CONF="$PROJECT_DIR/src-tauri/tauri.conf.json"
METAINFO="$SCRIPT_DIR/${APP_ID}.metainfo.xml"
DO_INSTALL=1
BUILD_DEB=1
for arg in "$@"; do
case "$arg" in
--no-install|--bundle) DO_INSTALL=0 ;; # --bundle : alias rétrocompatible
--no-deb) BUILD_DEB=0 ;;
-h|--help) sed -n '2,17p' "$0"; exit 0 ;;
*) echo "Argument inconnu : $arg" >&2; exit 1 ;;
esac
done
# --- Vérification des outils -------------------------------------------------
need() { command -v "$1" >/dev/null 2>&1 || { echo "Outil manquant : $1" >&2; exit 1; }; }
need flatpak
# flatpak-builder : commande native, sinon le Flatpak org.flatpak.Builder
# (cas des distributions atomiques type Bazzite/Silverblue).
if command -v flatpak-builder >/dev/null 2>&1; then
FLATPAK_BUILDER=(flatpak-builder)
elif flatpak info org.flatpak.Builder >/dev/null 2>&1; then
FLATPAK_BUILDER=(flatpak run org.flatpak.Builder)
else
echo "flatpak-builder est requis. Installez-le :" >&2
echo " flatpak install -y flathub org.flatpak.Builder" >&2
echo " ou sur Fedora classique : sudo dnf install flatpak-builder" >&2
exit 1
fi
# --- Remote flathub (scope --user) -------------------------------------------
# flatpak-builder est lancé en --user et installe les dépendances (runtime/SDK
# via --install-deps-from=flathub) dans cette même installation. Le remote
# flathub doit donc exister au niveau --user, ce qui n'est pas le cas par
# défaut sur les distributions atomiques (où flathub est un remote système).
echo ">> Vérification du remote flathub (--user)…"
flatpak remote-add --if-not-exists --user flathub \
https://flathub.org/repo/flathub.flatpakrepo
# --- 0. Version : source de vérité = tauri.conf.json -------------------------
# On lit la version applicative depuis tauri.conf.json et on la répercute dans
# le metainfo AppStream (release) afin que le Flatpak porte toujours la
# dernière version déclarée dans les sources.
need jq
VERSION="$(jq -r '.version' "$TAURI_CONF")"
if [[ -z "$VERSION" || "$VERSION" == "null" ]]; then
echo "Impossible de lire la version dans $TAURI_CONF" >&2
exit 1
fi
TODAY="$(date -u +%Y-%m-%d)"
echo ">> Version applicative : $VERSION (build du $TODAY)"
# Met à jour (ou insère) l'entrée <release> du metainfo pour cette version.
if grep -q "version=\"$VERSION\"" "$METAINFO"; then
echo ">> Release $VERSION déjà présente dans le metainfo."
else
echo ">> Ajout de la release $VERSION au metainfo…"
sed -i \
"s#<releases>#<releases>\n <release version=\"$VERSION\" date=\"$TODAY\"/>#" \
"$METAINFO"
fi
# --- 1. Build du .deb via Tauri ---------------------------------------------
if [[ "$BUILD_DEB" -eq 1 ]]; then
# La compilation Tauri exige les libs de dev GTK/WebKit, absentes de l'hôte
# (distribution atomique). On build donc dans le distrobox `$RUST_CONTAINER`,
# qui embarque cargo-tauri et ces libs. flatpak-builder reste sur l'hôte.
need distrobox
echo ">> Build du paquet .deb dans le distrobox $RUST_CONTAINER"
distrobox enter "$RUST_CONTAINER" -- \
bash -lc "cd '$PROJECT_DIR' && cargo tauri build --bundles deb"
fi
DEB_SRC="$(find "$PROJECT_DIR/src-tauri/target/release/bundle/deb" -name '*.deb' 2>/dev/null | sort | tail -n1 || true)"
if [[ -z "$DEB_SRC" ]]; then
echo "Aucun .deb trouvé. Lancez le script sans --no-deb." >&2
exit 1
fi
echo ">> Utilisation du .deb : $DEB_SRC"
cp -f "$DEB_SRC" "$DEB_TARGET"
# Icône installée dans le Flatpak (128x128) depuis les sources du projet.
cp -f "$PROJECT_DIR/src-tauri/icons/128x128.png" "$SCRIPT_DIR/icon.png"
# --- 2. Build Flatpak --------------------------------------------------------
# Construit l'app dans un dépôt OSTree local ($REPO_DIR) ; les dépendances
# manquantes (runtime/SDK) sont récupérées depuis flathub.
echo ">> Construction du Flatpak…"
rm -rf "$BUILD_DIR"
FB_ARGS=(--force-clean --user --install-deps-from=flathub --repo="$REPO_DIR")
if [[ "$DO_INSTALL" -eq 1 ]]; then
FB_ARGS+=(--install)
fi
"${FLATPAK_BUILDER[@]}" "${FB_ARGS[@]}" "$BUILD_DIR" "$MANIFEST"
# --- 3. Export du fichier .flatpak (toujours) --------------------------------
# Le bundle autonome est systématiquement exporté à côté du manifeste.
OUT="$SCRIPT_DIR/pena.flatpak"
echo ">> Export du bundle : $OUT"
flatpak build-bundle "$REPO_DIR" "$OUT" "$APP_ID" \
--runtime-repo=https://flathub.org/repo/flathub.flatpakrepo
echo ">> Nettoyage du dépôt OSTree temporaire…"
rm -rf "$REPO_DIR"
if [[ "$DO_INSTALL" -eq 1 ]]; then
echo ">> Pena $VERSION installé (--user) et bundle exporté : $OUT"
echo ">> Lancer avec : flatpak run $APP_ID"
else
echo ">> Bundle exporté : $OUT"
echo ">> Installer avec : flatpak install --user $OUT"
fi
echo ">> Terminé."
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>com.pena.app</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>MIT</project_license>
<name>Pena</name>
<summary>Lecteur de fichiers Markdown</summary>
<description>
<p>
Pena est une application de bureau pour lire des fichiers Markdown.
La conversion Markdown vers HTML est réalisée côté Rust (comrak), avec
coloration syntaxique des blocs de code et plusieurs thèmes de lecture.
</p>
</description>
<launchable type="desktop-id">com.pena.app.desktop</launchable>
<categories>
<category>Utility</category>
<category>TextEditor</category>
</categories>
<content_rating type="oars-1.1"/>
<releases>
<release version="0.1.2" date="2026-07-05"/>
<release version="0.1.0" date="2026-06-23"/>
</releases>
</component>
+49
View File
@@ -0,0 +1,49 @@
id: com.pena.app
runtime: org.gnome.Platform
runtime-version: '48'
sdk: org.gnome.Sdk
command: pena
finish-args:
# Affichage
- --socket=wayland
- --socket=fallback-x11
- --device=dri
- --share=ipc
# Lecture des fichiers Markdown ouverts par l'utilisateur
- --filesystem=home
modules:
- name: pena
buildsystem: simple
sources:
- type: file
path: pena.deb
- type: file
path: com.pena.app.metainfo.xml
- type: file
path: icon.png
build-commands:
- |
set -eux
# Extraction du .deb produit par `cargo tauri build`
ar -x pena.deb
tar -xf data.tar.gz
# Binaire principal -> /app/bin/pena (correspond à `command:`)
bin="$(find usr/bin -maxdepth 1 -type f | head -n1)"
install -Dm755 "$bin" /app/bin/pena
# Fichier .desktop : renommé en <app-id>.desktop (exigence Flatpak)
desktop="$(find usr/share/applications -name '*.desktop' | head -n1)"
install -Dm644 "$desktop" /app/share/applications/com.pena.app.desktop
sed -i \
-e 's/^Exec=.*/Exec=pena/' \
-e 's/^Icon=.*/Icon=com.pena.app/' \
/app/share/applications/com.pena.app.desktop
# Icône (128x128) fournie séparément à côté du manifeste.
install -Dm644 icon.png /app/share/icons/hicolor/128x128/apps/com.pena.app.png
# Métadonnées AppStream
install -Dm644 com.pena.app.metainfo.xml /app/share/metainfo/com.pena.app.metainfo.xml
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

+1 -1
View File
@@ -2445,7 +2445,7 @@ dependencies = [
[[package]] [[package]]
name = "pena-taury" name = "pena-taury"
version = "0.1.0" version = "0.1.2"
dependencies = [ dependencies = [
"comrak", "comrak",
"notify", "notify",
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "pena-taury" name = "pena-taury"
version = "0.1.0" version = "0.1.2"
edition = "2021" edition = "2021"
[lib] [lib]
@@ -17,5 +17,5 @@ tauri-plugin-dialog = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
comrak = { version = "0.28", features = ["syntect"] } comrak = { version = "0.28", features = ["syntect"] }
syntect = "5" syntect = { version = "5", features = ["yaml-load"] }
notify = "7" notify = "7"
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 B

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because it is too large Load Diff
-2
View File
@@ -102,13 +102,11 @@ body {
.content pre:not(.syntax-highlighting) { .content pre:not(.syntax-highlighting) {
background: #0f172a; background: #0f172a;
border: 1px solid #1e293b; border: 1px solid #1e293b;
border-radius: 10px;
} }
/* Bloc de code avec coloration syntaxique */ /* Bloc de code avec coloration syntaxique */
.content .syntax-highlighting { .content .syntax-highlighting {
border: 1px solid #1e293b; border: 1px solid #1e293b;
border-radius: 10px;
} }
/* Texte dans un bloc sans coloration */ /* Texte dans un bloc sans coloration */
@@ -2,15 +2,30 @@ use comrak::plugins::syntect::SyntectAdapterBuilder;
use comrak::{markdown_to_html_with_plugins, Options, Plugins}; use comrak::{markdown_to_html_with_plugins, Options, Plugins};
use syntect::highlighting::ThemeSet; use syntect::highlighting::ThemeSet;
use syntect::html::{css_for_theme_with_class_style, ClassStyle}; use syntect::html::{css_for_theme_with_class_style, ClassStyle};
use syntect::parsing::SyntaxDefinition;
use crate::domain::MarkdownRenderer; 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 ComrakRenderer;
pub struct ComrakPreviewRenderer; pub struct ComrakPreviewRenderer;
impl MarkdownRenderer for ComrakRenderer { impl MarkdownRenderer for ComrakRenderer {
fn render(&self, content: &str) -> String { fn render(&self, content: &str) -> String {
let adapter = SyntectAdapterBuilder::new().css().build(); let adapter = SyntectAdapterBuilder::new()
.syntax_set(build_syntax_set())
.css()
.build();
let mut options = Options::default(); let mut options = Options::default();
options.extension.table = true; options.extension.table = true;
options.extension.strikethrough = true; options.extension.strikethrough = true;
@@ -24,7 +39,10 @@ impl MarkdownRenderer for ComrakRenderer {
impl MarkdownRenderer for ComrakPreviewRenderer { impl MarkdownRenderer for ComrakPreviewRenderer {
fn render(&self, content: &str) -> String { fn render(&self, content: &str) -> String {
let adapter = SyntectAdapterBuilder::new().css().build(); let adapter = SyntectAdapterBuilder::new()
.syntax_set(build_syntax_set())
.css()
.build();
let options = Options::default(); let options = Options::default();
let mut plugins = Plugins::default(); let mut plugins = Plugins::default();
plugins.render.codefence_syntax_highlighter = Some(&adapter); plugins.render.codefence_syntax_highlighter = Some(&adapter);
@@ -62,6 +80,13 @@ mod tests {
assert!(html.contains("<span class=")); 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] #[test]
fn syntax_css_known_theme_contains_wrapper() { fn syntax_css_known_theme_contains_wrapper() {
let css = syntax_css_for_theme("base16-ocean.dark"); let css = syntax_css_for_theme("base16-ocean.dark");
@@ -80,5 +105,4 @@ mod tests {
let css = syntax_css_for_theme("nonexistent-theme-xyz"); let css = syntax_css_for_theme("nonexistent-theme-xyz");
assert!(css.is_empty()); assert!(css.is_empty());
} }
} }
+8 -2
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "Pena", "productName": "Pena",
"version": "0.1.0", "version": "0.1.2",
"identifier": "com.pena.app", "identifier": "com.pena.app",
"build": { "build": {
"frontendDist": "../src" "frontendDist": "../src"
@@ -24,6 +24,12 @@
"bundle": { "bundle": {
"active": false, "active": false,
"targets": "all", "targets": "all",
"icon": [] "icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Before

Width:  |  Height:  |  Size: 993 B

After

Width:  |  Height:  |  Size: 993 B

+2
View File
@@ -22,6 +22,7 @@
<div id="app-content"> <div id="app-content">
<div id="view-home"> <div id="view-home">
<img class="home-logo" src="assets/icon.png" alt="Pena" />
<h1 class="home-title">Pena</h1> <h1 class="home-title">Pena</h1>
<div class="home-buttons"> <div class="home-buttons">
<button id="btn-open-file">Ouvrir un fichier Markdown</button> <button id="btn-open-file">Ouvrir un fichier Markdown</button>
@@ -47,6 +48,7 @@
</button> </button>
<button id="btn-back" class="btn-back">← Accueil</button> <button id="btn-back" class="btn-back">← Accueil</button>
</div> </div>
<div id="sidebar-resizer" class="sidebar__resizer" title="Redimensionner"></div>
</div> </div>
<div id="content" class="content"> <div id="content" class="content">
<!-- contenu généré par JS --> <!-- contenu généré par JS -->
+4 -1
View File
@@ -1,5 +1,6 @@
import { showHome, openPath } from './router.js'; import { showHome, openPath, initWatcher } from './router.js';
import { initCssModal } from './ui/css-modal.js'; import { initCssModal } from './ui/css-modal.js';
import { initSidebarResize } from './ui/sidebar-resize.js';
const win = window.__TAURI__.window.getCurrentWindow(); const win = window.__TAURI__.window.getCurrentWindow();
@@ -33,4 +34,6 @@ document.getElementById('btn-open-dir').addEventListener('click', async () => {
document.getElementById('btn-back').addEventListener('click', showHome); document.getElementById('btn-back').addEventListener('click', showHome);
await initCssModal(); await initCssModal();
initSidebarResize();
await initWatcher();
showHome(); showHome();
+55 -2
View File
@@ -1,5 +1,6 @@
import { appState } from './state/app-state.js'; import { appState } from './state/app-state.js';
import { listMdFiles } from './services/files.js'; import { listMdFiles } from './services/files.js';
import { startWatch, stopWatch, onFileChanged } from './services/watcher.js';
import { renderSidebar } from './ui/sidebar.js'; import { renderSidebar } from './ui/sidebar.js';
import { loadPage } from './ui/reader.js'; import { loadPage } from './ui/reader.js';
import { saveRecent, renderRecents } from './ui/home.js'; import { saveRecent, renderRecents } from './ui/home.js';
@@ -11,9 +12,15 @@ const content = document.querySelector('#content');
const win = window.__TAURI__.window.getCurrentWindow(); const win = window.__TAURI__.window.getCurrentWindow();
const titlebarTitle = document.getElementById('titlebar-title'); const titlebarTitle = document.getElementById('titlebar-title');
const navigate = filePath => loadPage(filePath, appState.currentMode, sidebarEl);
export function showHome() { export function showHome() {
stopWatch();
appState.currentPath = null; appState.currentPath = null;
appState.currentMode = null; appState.currentMode = null;
appState.baseDir = null;
appState.currentFile = null;
appState.files = [];
sidebarEl.innerHTML = ''; sidebarEl.innerHTML = '';
win.setTitle('Pena — Markdown Viewer'); win.setTitle('Pena — Markdown Viewer');
titlebarTitle.textContent = 'Pena — Markdown Viewer'; titlebarTitle.textContent = 'Pena — Markdown Viewer';
@@ -38,15 +45,21 @@ export async function openPath(path, mode) {
const parentDir = path.split(sep).slice(0, -1).join(sep); const parentDir = path.split(sep).slice(0, -1).join(sep);
try { try {
const files = await listMdFiles(parentDir); const files = await listMdFiles(parentDir);
appState.baseDir = parentDir;
appState.files = files;
sidebarEl.classList.remove('hidden'); sidebarEl.classList.remove('hidden');
content.style.marginLeft = ''; content.style.marginLeft = '';
renderSidebar(parentDir, files, path, filePath => loadPage(filePath, appState.currentMode, sidebarEl)); renderSidebar(parentDir, files, path, navigate);
} catch { } catch {
// Le dossier parent est illisible : on surveille au moins le fichier ouvert.
appState.baseDir = path;
appState.files = [];
sidebarEl.innerHTML = ''; sidebarEl.innerHTML = '';
sidebarEl.classList.add('hidden'); sidebarEl.classList.add('hidden');
content.style.marginLeft = '0'; content.style.marginLeft = '0';
} }
await loadPage(path, appState.currentMode, sidebarEl); await loadPage(path, appState.currentMode, sidebarEl);
startWatch(appState.baseDir);
return; return;
} }
@@ -56,6 +69,8 @@ export async function openPath(path, mode) {
try { try {
const files = await listMdFiles(path); const files = await listMdFiles(path);
appState.baseDir = path;
appState.files = files;
showReader(); showReader();
const sep = path.includes('\\') ? '\\' : '/'; const sep = path.includes('\\') ? '\\' : '/';
@@ -65,10 +80,48 @@ export async function openPath(path, mode) {
}); });
const firstFile = home ?? files[0]; const firstFile = home ?? files[0];
renderSidebar(path, files, firstFile, filePath => loadPage(filePath, appState.currentMode, sidebarEl)); renderSidebar(path, files, firstFile, navigate);
await loadPage(firstFile, appState.currentMode, sidebarEl); await loadPage(firstFile, appState.currentMode, sidebarEl);
startWatch(path);
} catch (err) { } catch (err) {
showReader(); showReader();
content.innerHTML = `<p class="error">Impossible d'ouvrir le dossier : ${err}</p>`; content.innerHTML = `<p class="error">Impossible d'ouvrir le dossier : ${err}</p>`;
} }
} }
// Compare deux listes de fichiers indépendamment de l'ordre.
function sameFiles(a, b) {
if (a.length !== b.length) return false;
const key = arr => arr.slice().sort().join('\n');
return key(a) === key(b);
}
// Réaction à un changement filesystem signalé par le backend (événement débouncé,
// filtré sur les fichiers .md).
async function handleFileChanged(changedPath) {
if (!appState.baseDir || !appState.currentMode) return;
// Rafraîchir la sidebar uniquement si l'arborescence a changé (ajout/suppression),
// pour ne pas replier les dossiers ouverts à chaque simple modification de contenu.
try {
const files = await listMdFiles(appState.baseDir);
if (!sameFiles(files, appState.files)) {
appState.files = files;
renderSidebar(appState.baseDir, files, appState.currentFile, navigate);
}
} catch {
// baseDir supprimé/illisible : on garde la sidebar en l'état.
}
// Recharger le document affiché si c'est lui qui vient d'être modifié.
if (changedPath === appState.currentFile) {
await loadPage(appState.currentFile, appState.currentMode, sidebarEl);
}
}
// À appeler une fois au démarrage : enregistre le listener global de changements.
export function initWatcher() {
return onFileChanged(event => {
handleFileChanged(event.payload.path);
});
}
+12 -1
View File
@@ -1 +1,12 @@
export const appState = { currentPath: null, currentMode: null }; export const appState = {
currentPath: null,
currentMode: null,
// Dossier réellement surveillé (racine de la sidebar) : le dossier ouvert en
// mode « dir », ou le dossier parent du fichier ouvert en mode « file ».
baseDir: null,
// Fichier actuellement affiché dans le lecteur.
currentFile: null,
// Dernière liste de fichiers .md connue de `baseDir`, pour détecter les
// ajouts/suppressions et ne rafraîchir la sidebar que si l'arborescence change.
files: [],
};
+65 -19
View File
@@ -1,4 +1,5 @@
/* ── Reset ── */ /* ── Reset ── */
:root { --sidebar-width: 264px; }
*, *::before, *::after { box-sizing: border-box; } *, *::before, *::after { box-sizing: border-box; }
::selection { background: #ffc3c3; } ::selection { background: #ffc3c3; }
html, body { height: 100%; margin: 0; } html, body { height: 100%; margin: 0; }
@@ -91,6 +92,13 @@ body {
background: #1a1b2e; background: #1a1b2e;
} }
.home-logo {
width: 160px;
height: 160px;
object-fit: contain;
margin: 0 0 16px;
}
.home-title { .home-title {
color: #fff; color: #fff;
font-size: 4rem; font-size: 4rem;
@@ -206,7 +214,7 @@ body {
position: fixed; position: fixed;
top: 36px; top: 36px;
left: 0; left: 0;
width: 264px; width: var(--sidebar-width);
height: calc(100vh - 36px); height: calc(100vh - 36px);
background: #1a1b2e; background: #1a1b2e;
display: flex; display: flex;
@@ -215,6 +223,22 @@ body {
overflow: hidden; overflow: hidden;
} }
/* Poignée de redimensionnement */
.sidebar__resizer {
position: absolute;
top: 0;
right: 0;
width: 6px;
height: 100%;
cursor: col-resize;
z-index: 100;
background: transparent;
transition: background 0.15s;
}
.sidebar__resizer:hover,
.sidebar__resizer.is-dragging { background: rgba(129, 140, 248, 0.4); }
body.is-resizing-sidebar { cursor: col-resize; user-select: none; }
.sidebar__scroll { .sidebar__scroll {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
@@ -315,10 +339,10 @@ body {
.nav-root-link { .nav-root-link {
display: block; display: block;
padding: 7px 24px; padding: 6px 24px;
color: rgba(255,255,255,0.85); color: rgba(255,255,255,0.85);
text-decoration: none; text-decoration: none;
font-size: 16px; font-size: 14px;
font-weight: 400; font-weight: 400;
line-height: 1.4; line-height: 1.4;
transition: color 0.2s; transition: color 0.2s;
@@ -358,33 +382,32 @@ body {
.nav-arrow:hover { opacity: 1; background: rgba(255,255,255,0.08); } .nav-arrow:hover { opacity: 1; background: rgba(255,255,255,0.08); }
.nav-folder[open] > summary .nav-arrow { transform: rotate(90deg); opacity: 0.8; } .nav-folder[open] > summary .nav-arrow { transform: rotate(90deg); opacity: 0.8; }
/* Les libellés de dossiers partagent la même police que les fichiers :
même taille (14px), même graisse (400), sans majuscules. Seule la flèche ▶
distingue visuellement un dossier d'un fichier. */
.nav-folder-link, .nav-folder-name { .nav-folder-link, .nav-folder-name {
flex: 1; flex: 1;
display: block; display: block;
padding: 6px 24px 6px 0; padding: 6px 24px 6px 0;
font-size: 11px; font-size: 14px;
font-weight: 700; font-weight: 400;
text-transform: uppercase; line-height: 1.4;
letter-spacing: 0.08em; color: rgba(255,255,255,0.85);
color: rgba(255,255,255,0.35);
text-decoration: none; text-decoration: none;
transition: color 0.2s; transition: color 0.2s;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.nav-folder-link:hover { color: rgba(255,255,255,0.85); } .nav-folder-link:hover { color: #ff5577; }
.nav-folder-link.active { color: #ff5577; } .nav-folder-link.active { color: #ff5577; }
/* Dossiers imbriqués : même style que les fichiers imbriqués (couleur atténuée). */
.nav-folder .nav-folder > summary .nav-folder-link, .nav-folder .nav-folder > summary .nav-folder-link,
.nav-folder .nav-folder > summary .nav-folder-name { .nav-folder .nav-folder > summary .nav-folder-name {
font-size: 13px; color: rgba(255,255,255,0.55);
font-weight: 500;
text-transform: none;
letter-spacing: 0;
color: rgba(255,255,255,0.45);
} }
.nav-folder .nav-folder > summary .nav-folder-link:hover { color: rgba(255,255,255,0.9); } .nav-folder .nav-folder > summary .nav-folder-link:hover { color: #ff5577; }
.nav-file-link { .nav-file-link {
display: block; display: block;
@@ -402,16 +425,16 @@ body {
font-weight: 500; font-weight: 500;
} }
.nav-depth-1 > .nav-file-link { padding: 5px 24px 5px 52px; } .nav-depth-1 > .nav-file-link { padding: 6px 24px 6px 52px; }
.nav-depth-1 > .nav-folder > summary { padding-left: 24px; } .nav-depth-1 > .nav-folder > summary { padding-left: 24px; }
.nav-depth-2 > .nav-file-link { padding: 4px 24px 4px 68px; } .nav-depth-2 > .nav-file-link { padding: 6px 24px 6px 68px; }
.nav-depth-2 > .nav-folder > summary { padding-left: 40px; } .nav-depth-2 > .nav-folder > summary { padding-left: 40px; }
.nav-depth-3 > .nav-file-link { padding: 4px 24px 4px 84px; } .nav-depth-3 > .nav-file-link { padding: 6px 24px 6px 84px; }
.nav-depth-3 > .nav-folder > summary { padding-left: 56px; } .nav-depth-3 > .nav-folder > summary { padding-left: 56px; }
/* ── Content ── */ /* ── Content ── */
.content { .content {
margin-left: 264px; margin-left: var(--sidebar-width);
padding-top: 48px; padding-top: 48px;
padding-right: 72px; padding-right: 72px;
padding-bottom: 96px; padding-bottom: 96px;
@@ -455,6 +478,29 @@ body {
.content pre:not(.syntax-highlighting) { background: #f4f4f7; } .content pre:not(.syntax-highlighting) { background: #f4f4f7; }
.content pre code { background: none; padding: 0; font-weight: 400; font-size: 13px; border: none; border-radius: 0; color: inherit; } .content pre code { background: none; padding: 0; font-weight: 400; font-size: 13px; border: none; border-radius: 0; color: inherit; }
/* ── Bouton « copier » des blocs de code ── */
.content .code-block { position: relative; }
.content .code-block .copy-code-btn {
position: absolute;
top: 24px;
right: 12px;
padding: 4px 10px;
font-family: inherit;
font-size: 12px;
line-height: 1.2;
color: #c0c5ce;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 4px;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s ease, background 0.15s ease;
}
.content .code-block:hover .copy-code-btn,
.content .code-block .copy-code-btn:focus-visible { opacity: 1; }
.content .code-block .copy-code-btn:hover { background: rgba(255, 255, 255, 0.18); }
.content .code-block .copy-code-btn.copied { color: #a3be8c; border-color: rgba(163, 190, 140, 0.5); }
/* ── Coloration syntaxique (base16-ocean.dark) ── */ /* ── Coloration syntaxique (base16-ocean.dark) ── */
.syntax-highlighting { .syntax-highlighting {
color: #c0c5ce; color: #c0c5ce;
+34
View File
@@ -1,4 +1,5 @@
import { convertFile } from '../services/markdown.js'; import { convertFile } from '../services/markdown.js';
import { appState } from '../state/app-state.js';
const win = window.__TAURI__.window.getCurrentWindow(); const win = window.__TAURI__.window.getCurrentWindow();
const titlebarTitle = document.getElementById('titlebar-title'); const titlebarTitle = document.getElementById('titlebar-title');
@@ -35,10 +36,43 @@ function wireLinks(filePath, mode, sidebarEl) {
}); });
} }
function addCopyButtons() {
content.querySelectorAll('pre').forEach(pre => {
if (pre.parentElement?.classList.contains('code-block')) return;
const wrapper = document.createElement('div');
wrapper.className = 'code-block';
pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(pre);
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'copy-code-btn';
btn.textContent = 'Copier';
btn.addEventListener('click', async () => {
const code = (pre.querySelector('code') ?? pre).textContent;
try {
await navigator.clipboard.writeText(code);
btn.textContent = 'Copié !';
btn.classList.add('copied');
} catch {
btn.textContent = 'Erreur';
}
setTimeout(() => {
btn.textContent = 'Copier';
btn.classList.remove('copied');
}, 1500);
});
wrapper.appendChild(btn);
});
}
export async function loadPage(filePath, mode, sidebarEl) { export async function loadPage(filePath, mode, sidebarEl) {
try { try {
const html = await convertFile(filePath); const html = await convertFile(filePath);
appState.currentFile = filePath;
content.innerHTML = html; content.innerHTML = html;
addCopyButtons();
const basename = filePath.split('/').pop().split('\\').pop(); const basename = filePath.split('/').pop().split('\\').pop();
const title = basename.replace(/\.md$/i, ''); const title = basename.replace(/\.md$/i, '');
+49
View File
@@ -0,0 +1,49 @@
const STORAGE_KEY = 'pena.sidebarWidth';
const MIN_WIDTH = 180;
const MAX_WIDTH = 520;
function clamp(width) {
return Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, width));
}
function applyWidth(width) {
document.documentElement.style.setProperty('--sidebar-width', `${width}px`);
}
export function initSidebarResize() {
const handle = document.getElementById('sidebar-resizer');
if (!handle) return;
const stored = parseInt(localStorage.getItem(STORAGE_KEY), 10);
if (!Number.isNaN(stored)) applyWidth(clamp(stored));
let dragging = false;
const onMove = (e) => {
if (!dragging) return;
applyWidth(clamp(e.clientX));
};
const onUp = () => {
if (!dragging) return;
dragging = false;
handle.classList.remove('is-dragging');
document.body.classList.remove('is-resizing-sidebar');
const width = parseInt(
getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width'),
10,
);
if (!Number.isNaN(width)) localStorage.setItem(STORAGE_KEY, String(width));
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onUp);
};
handle.addEventListener('mousedown', (e) => {
e.preventDefault();
dragging = true;
handle.classList.add('is-dragging');
document.body.classList.add('is-resizing-sidebar');
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
});
}
+16 -5
View File
@@ -1,5 +1,13 @@
import { escapeHtml } from '../utils.js'; import { escapeHtml } from '../utils.js';
// Tri « naturel » : les préfixes numériques sont comparés comme des nombres
// (10 vient après 9, pas après 1).
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
function cleanLabel(name) {
return name.replace(/\.md$/i, '').replace(/-/g, ' ');
}
function buildTree(relPaths, sep) { function buildTree(relPaths, sep) {
const tree = { _files: [], _dirs: {} }; const tree = { _files: [], _dirs: {} };
relPaths.forEach((rel) => { relPaths.forEach((rel) => {
@@ -19,8 +27,11 @@ function renderTree(node, prefix, sep, currentRel, depth) {
let html = ''; let html = '';
const depthClass = depth > 0 ? ` class="nav-depth-${Math.min(depth, 3)}"` : ''; const depthClass = depth > 0 ? ` class="nav-depth-${Math.min(depth, 3)}"` : '';
node._files.slice().sort().forEach((rel) => { node._files
const label = rel.split(sep).pop().replace(/\.md$/i, '').replace(/-/g, ' '); .slice()
.sort((a, b) => collator.compare(a.split(sep).pop(), b.split(sep).pop()))
.forEach((rel) => {
const label = cleanLabel(rel.split(sep).pop());
const abs = prefix + rel; const abs = prefix + rel;
const cls = rel === currentRel ? ' active' : ''; const cls = rel === currentRel ? ' active' : '';
if (depth === 0) { if (depth === 0) {
@@ -30,7 +41,7 @@ function renderTree(node, prefix, sep, currentRel, depth) {
} }
}); });
Object.keys(node._dirs).sort().forEach((dirName) => { Object.keys(node._dirs).sort((a, b) => collator.compare(a, b)).forEach((dirName) => {
const child = node._dirs[dirName]; const child = node._dirs[dirName];
const isOpen = currentRel && currentRel.split(sep).includes(dirName); const isOpen = currentRel && currentRel.split(sep).includes(dirName);
const openAttr = isOpen ? ' open' : ''; const openAttr = isOpen ? ' open' : '';
@@ -40,9 +51,9 @@ function renderTree(node, prefix, sep, currentRel, depth) {
if (homeFile) { if (homeFile) {
const abs = prefix + homeFile; const abs = prefix + homeFile;
const cls = homeFile === currentRel ? ' active' : ''; const cls = homeFile === currentRel ? ' active' : '';
folderLabel = `<a href="#" class="nav-folder-link${cls}" data-path="${escapeHtml(abs)}" onclick="event.stopPropagation()">${escapeHtml(dirName)}</a>`; folderLabel = `<a href="#" class="nav-folder-link${cls}" data-path="${escapeHtml(abs)}" onclick="event.stopPropagation()">${escapeHtml(cleanLabel(dirName))}</a>`;
} else { } else {
folderLabel = `<span class="nav-folder-name">${escapeHtml(dirName)}</span>`; folderLabel = `<span class="nav-folder-name">${escapeHtml(cleanLabel(dirName))}</span>`;
} }
const arrow = `<span class="nav-arrow" onclick="event.preventDefault();event.stopPropagation();var d=this.closest('details');d.open=!d.open">▶</span>`; const arrow = `<span class="nav-arrow" onclick="event.preventDefault();event.stopPropagation();var d=this.closest('details');d.open=!d.open">▶</span>`;