Compare commits

2 Commits

Author SHA1 Message Date
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
11 changed files with 3682 additions and 6 deletions
+7
View File
@@ -1,2 +1,9 @@
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/*.flatpak
+92
View File
@@ -0,0 +1,92 @@
#!/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/ou export en fichier .flatpak
#
# Usage :
# ./build-flatpak.sh # build + installation locale (--user)
# ./build-flatpak.sh --bundle # build + export d'un fichier Pena.flatpak
# ./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"
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"
DO_BUNDLE=0
BUILD_DEB=1
for arg in "$@"; do
case "$arg" in
--bundle) DO_BUNDLE=1 ;;
--no-deb) BUILD_DEB=0 ;;
-h|--help) sed -n '2,16p' "$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
if ! command -v flatpak-builder >/dev/null 2>&1; then
echo "flatpak-builder est requis. Installez-le :" >&2
echo " flatpak install -y flathub org.flatpak.Builder" >&2
echo " (puis lancez ce script via : flatpak run org.flatpak.Builder ...)" >&2
echo " ou sur Fedora : sudo dnf install flatpak-builder" >&2
exit 1
fi
# --- Runtime / SDK GNOME -----------------------------------------------------
echo ">> Vérification du runtime GNOME ${RUNTIME_VERSION}"
if ! flatpak info "org.gnome.Platform//${RUNTIME_VERSION}" >/dev/null 2>&1; then
flatpak install -y --user flathub \
"org.gnome.Platform//${RUNTIME_VERSION}" \
"org.gnome.Sdk//${RUNTIME_VERSION}"
fi
# --- 1. Build du .deb via Tauri ---------------------------------------------
if [[ "$BUILD_DEB" -eq 1 ]]; then
echo ">> Build du paquet .deb (cargo tauri build --bundles deb)…"
( 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"
# --- 2. Build Flatpak --------------------------------------------------------
echo ">> Construction du Flatpak…"
rm -rf "$BUILD_DIR"
FB_ARGS=(--force-clean --user --repo="$REPO_DIR")
if [[ "$DO_BUNDLE" -eq 0 ]]; then
FB_ARGS+=(--install)
fi
flatpak-builder "${FB_ARGS[@]}" "$BUILD_DIR" "$MANIFEST"
# --- 3. Export d'un fichier .flatpak (optionnel) -----------------------------
if [[ "$DO_BUNDLE" -eq 1 ]]; then
OUT="$SCRIPT_DIR/Pena.flatpak"
echo ">> Export du bundle : $OUT"
flatpak build-bundle "$REPO_DIR" "$OUT" "$APP_ID" "$RUNTIME_VERSION" 2>/dev/null \
|| flatpak build-bundle "$REPO_DIR" "$OUT" "$APP_ID"
echo ">> Installer avec : flatpak install --user $OUT"
else
echo ">> Installé. Lancer avec : flatpak run $APP_ID"
fi
echo ">> Terminé."
+24
View File
@@ -0,0 +1,24 @@
<?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.0" date="2026-06-23"/>
</releases>
</component>
+48
View File
@@ -0,0 +1,48 @@
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
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 : on prend la plus grande disponible
icon="$(find usr/share/icons -name '*.png' | sort | tail -n1)"
install -Dm644 "$icon" /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
+1 -1
View File
@@ -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"
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());
} }
} }

Before

Width:  |  Height:  |  Size: 993 B

After

Width:  |  Height:  |  Size: 993 B

+23
View File
@@ -455,6 +455,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;
+32
View File
@@ -35,10 +35,42 @@ 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);
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, '');