feat: Windows notifications, benchmarks, GPG signing, docs site, one-liner installers
This commit is contained in:
+22
-2
@@ -99,6 +99,19 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: artifacts/
|
path: artifacts/
|
||||||
|
|
||||||
|
- name: Import GPG key
|
||||||
|
if: ${{ secrets.GPG_PRIVATE_KEY != '' }}
|
||||||
|
env:
|
||||||
|
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
run: echo "$GPG_PRIVATE_KEY" | gpg --batch --yes --import
|
||||||
|
|
||||||
|
- name: Sign artifacts
|
||||||
|
if: ${{ secrets.GPG_PRIVATE_KEY != '' }}
|
||||||
|
run: |
|
||||||
|
for f in artifacts/*/clip-sync-*; do
|
||||||
|
gpg --batch --yes --armor --detach-sign "$f"
|
||||||
|
done
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
uses: gitea/action-gitea-release@v1
|
uses: gitea/action-gitea-release@v1
|
||||||
with:
|
with:
|
||||||
@@ -109,9 +122,16 @@ jobs:
|
|||||||
|
|
||||||
See [CHANGELOG.md](https://git.dracodev.net/Projets/clip-sync/src/branch/main/CHANGELOG.md) for details.
|
See [CHANGELOG.md](https://git.dracodev.net/Projets/clip-sync/src/branch/main/CHANGELOG.md) for details.
|
||||||
|
|
||||||
### Install
|
### Install — Linux / macOS
|
||||||
```bash
|
```bash
|
||||||
curl -sSL https://git.dracodev.net/Projets/clip-sync/raw/branch/main/install.sh | bash
|
curl -fsSL https://git.dracodev.net/Projets/clip-sync/raw/branch/main/install.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Install — Windows (PowerShell)
|
||||||
|
```powershell
|
||||||
|
irm https://git.dracodev.net/Projets/clip-sync/raw/branch/main/install.ps1 | iex
|
||||||
|
```
|
||||||
|
|
||||||
|
Binaries are signed with GPG (`.asc` files) where a signing key is configured.
|
||||||
files: |
|
files: |
|
||||||
artifacts/**/clip-sync-*
|
artifacts/**/clip-sync-*
|
||||||
|
|||||||
+1
-1
@@ -111,7 +111,7 @@ Boucle principale :
|
|||||||
### 9. Découverte (`internal/discovery/`) et notifications (`internal/notify/`)
|
### 9. Découverte (`internal/discovery/`) et notifications (`internal/notify/`)
|
||||||
|
|
||||||
- mDNS `_clip-sync._tcp` (hashicorp/mdns), activation via `daemon.discovery`
|
- mDNS `_clip-sync._tcp` (hashicorp/mdns), activation via `daemon.discovery`
|
||||||
- Notifications desktop : notify-send (Linux), osascript (macOS), no-op (Windows)
|
- Notifications desktop : notify-send (Linux), osascript (macOS), Shell_NotifyIcon (Windows)
|
||||||
|
|
||||||
## Dépendances
|
## Dépendances
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Notifications desktop (`daemon.notify`)
|
- Notifications desktop (`daemon.notify`)
|
||||||
- Flag `--config <path>` pour une configuration personnalisée
|
- Flag `--config <path>` pour une configuration personnalisée
|
||||||
- Fichier LICENSE (MIT)
|
- Fichier LICENSE (MIT)
|
||||||
|
- Notifications desktop Windows (Shell_NotifyIcon, plus de no-op)
|
||||||
|
- Benchmarks (`make bench`) : dedup, serveur, conversion DIB↔PNG
|
||||||
|
- Signature GPG des releases (CI `GPG_PRIVATE_KEY` + `make sign`)
|
||||||
|
- Site de documentation statique (`docs/index.html`)
|
||||||
|
- Installateur Windows en une ligne (`install.ps1`, `irm | iex`)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Lecture du presse-papiers Windows bornée à la taille réelle (GlobalSize)
|
- Lecture du presse-papiers Windows bornée à la taille réelle (GlobalSize)
|
||||||
|
|||||||
@@ -54,6 +54,22 @@ test-cover:
|
|||||||
go test ./... -coverprofile=coverage.out -count=1 -timeout 60s
|
go test ./... -coverprofile=coverage.out -count=1 -timeout 60s
|
||||||
go tool cover -func=coverage.out
|
go tool cover -func=coverage.out
|
||||||
|
|
||||||
|
# ── Benchmark ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.PHONY: bench
|
||||||
|
bench:
|
||||||
|
go test ./... -bench=. -benchmem -run=^$$ -count=1
|
||||||
|
|
||||||
|
# ── Release signing ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.PHONY: sign
|
||||||
|
sign:
|
||||||
|
@for f in $(BUILD_DIR)/clip-sync-*; do \
|
||||||
|
echo "Signing $$f"; \
|
||||||
|
gpg --batch --yes --armor --detach-sign "$$f"; \
|
||||||
|
done
|
||||||
|
@echo "Signed binaries in $(BUILD_DIR)/ (alongside .asc signatures)"
|
||||||
|
|
||||||
# ── Lint ───────────────────────────────────────────────────────────────
|
# ── Lint ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
@@ -84,6 +100,8 @@ help:
|
|||||||
@echo " test-verbose Run tests with verbose output"
|
@echo " test-verbose Run tests with verbose output"
|
||||||
@echo " test-race Run tests with race detector"
|
@echo " test-race Run tests with race detector"
|
||||||
@echo " test-cover Run tests with coverage report"
|
@echo " test-cover Run tests with coverage report"
|
||||||
|
@echo " bench Run benchmarks"
|
||||||
|
@echo " sign GPG-sign release binaries in bin/"
|
||||||
@echo " lint Run golangci-lint"
|
@echo " lint Run golangci-lint"
|
||||||
@echo " clean Remove build artifacts"
|
@echo " clean Remove build artifacts"
|
||||||
@echo " install Install binary to ~/.local/bin/"
|
@echo " install Install binary to ~/.local/bin/"
|
||||||
|
|||||||
@@ -32,13 +32,19 @@ Copier sur une machine → coller sur une autre, automatiquement et instantaném
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Méthode 1 : Script automatique (tous OS)
|
### Méthode 1 : Script automatique en une ligne (recommandé)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -sSL https://git.dracodev.net/Projets/clip-sync/raw/branch/main/install.sh | bash
|
curl -fsSL https://git.dracodev.net/Projets/clip-sync/raw/branch/main/install.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
Le script détecte OS + architecture, télécharge le binaire, l'installe dans `~/.local/bin/` et crée une config par défaut.
|
**Windows (PowerShell) :**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
irm https://git.dracodev.net/Projets/clip-sync/raw/branch/main/install.ps1 | iex
|
||||||
|
```
|
||||||
|
|
||||||
|
Le script détecte OS + architecture, télécharge le binaire, l'installe dans le répertoire utilisateur et crée une config par défaut. Pour épingler une version : `CLIP_SYNC_VERSION=v0.2.0` (Linux/macOS) ou `$env:CLIP_SYNC_VERSION='v0.2.0'` (Windows).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -351,6 +357,18 @@ curl -X POST http://192.168.1.20:9137/clip \
|
|||||||
|
|
||||||
L'interface de monitoring est disponible sur `http://localhost:9137/` après le démarrage du daemon.
|
L'interface de monitoring est disponible sur `http://localhost:9137/` après le démarrage du daemon.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Site de documentation statique : [`docs/index.html`](docs/index.html).
|
||||||
|
|
||||||
|
Commandes de développement utiles :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make bench # benchmarks
|
||||||
|
make sign # signer les binaires de release avec GPG
|
||||||
|
make test-race # tests avec détecteur de race
|
||||||
|
```
|
||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
MIT © 2026
|
MIT © 2026
|
||||||
|
|||||||
+6
-5
@@ -30,11 +30,12 @@
|
|||||||
macOS (`daemon.sync_images`) — la lecture macOS via AppleScript est
|
macOS (`daemon.sync_images`) — la lecture macOS via AppleScript est
|
||||||
best-effort et dépend de la version du système
|
best-effort et dépend de la version du système
|
||||||
|
|
||||||
## v1.0.0 🌐 Stable (partiellement réalisée)
|
## v1.0.0 🌐 Stable (réalisée)
|
||||||
|
|
||||||
- [x] Découverte automatique des pairs via mDNS (`daemon.discovery`)
|
- [x] Découverte automatique des pairs via mDNS (`daemon.discovery`)
|
||||||
- [x] Interface web locale de monitoring (`http://localhost:9137/`)
|
- [x] Interface web locale de monitoring (`http://localhost:9137/`)
|
||||||
- [x] Notification desktop (DBus/Linux, Notification Center/macOS ; no-op Windows)
|
- [x] Notification desktop (notify-send/Linux, osascript/macOS, Shell_NotifyIcon/Windows)
|
||||||
- [ ] Tests de performance et benchmark
|
- [x] Tests de performance et benchmark (`make bench`)
|
||||||
- [ ] Documentation utilisateur finale (site statique ou wiki)
|
- [x] Documentation utilisateur finale (site statique `docs/index.html`)
|
||||||
- [ ] Signature GPG des releases
|
- [x] Signature GPG des releases (CI + `make sign`)
|
||||||
|
- [x] Installation en une ligne (Linux/macOS `curl|bash`, Windows `irm|iex`)
|
||||||
|
|||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>clip-sync — documentation</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #ffffff; --fg: #1a202c; --muted: #64748b;
|
||||||
|
--accent: #0969da; --code-bg: #f6f8fa; --border: #d0d7de;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin: 0; font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; color: var(--fg); line-height: 1.6; }
|
||||||
|
header { background: #0d1117; color: #fff; padding: 3rem 1rem; }
|
||||||
|
header .wrap, main, footer { max-width: 860px; margin: 0 auto; padding: 0 1rem; }
|
||||||
|
header h1 { margin: 0 0 .5rem; font-size: 2rem; }
|
||||||
|
header p { margin: 0; color: #8b949e; }
|
||||||
|
main { padding-bottom: 3rem; }
|
||||||
|
h2 { border-bottom: 1px solid var(--border); padding-bottom: .3rem; margin-top: 2.5rem; }
|
||||||
|
code { background: var(--code-bg); border: 1px solid var(--border); border-radius: 4px; padding: .1rem .35rem; font-family: ui-monospace, "Cascadia Code", Consolas, monospace; font-size: .9em; }
|
||||||
|
pre { background: var(--code-bg); border: 1px solid var(--border); border-radius: 6px; padding: .8rem 1rem; overflow-x: auto; }
|
||||||
|
pre code { background: none; border: none; padding: 0; }
|
||||||
|
table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
|
||||||
|
th, td { border: 1px solid var(--border); padding: .4rem .6rem; text-align: left; font-size: .92rem; }
|
||||||
|
th { background: var(--code-bg); }
|
||||||
|
.badge { display: inline-block; background: var(--accent); color: #fff; border-radius: 4px; padding: .1rem .5rem; font-size: .75rem; font-weight: 600; }
|
||||||
|
footer { color: var(--muted); font-size: .85rem; padding: 1rem; border-top: 1px solid var(--border); }
|
||||||
|
a { color: var(--accent); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>clip-sync</h1>
|
||||||
|
<p>Daemon de synchronisation bidirectionnelle du presse-papiers sur réseau local. Copier sur une machine → coller sur une autre.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
|
||||||
|
<h2>Installation en une ligne</h2>
|
||||||
|
<p>Rapide, sans dépendance à compiler : un binaire unique téléchargé puis exécutable.</p>
|
||||||
|
|
||||||
|
<h3>Linux / macOS</h3>
|
||||||
|
<pre><code>curl -fsSL https://git.dracodev.net/Projets/clip-sync/raw/branch/main/install.sh | bash</code></pre>
|
||||||
|
|
||||||
|
<h3>Windows (PowerShell)</h3>
|
||||||
|
<pre><code>irm https://git.dracodev.net/Projets/clip-sync/raw/branch/main/install.ps1 | iex</code></pre>
|
||||||
|
|
||||||
|
<p>Le script détecte OS + architecture, télécharge le binaire, l'installe dans
|
||||||
|
votre répertoire utilisateur et crée une configuration par défaut. Pour choisir
|
||||||
|
une version précise : <code>CLIP_SYNC_VERSION=v0.2.0</code> (Linux/macOS) ou
|
||||||
|
<code>$env:CLIP_SYNC_VERSION='v0.2.0'</code> (Windows).</p>
|
||||||
|
|
||||||
|
<h2>Démarrage rapide</h2>
|
||||||
|
<ol>
|
||||||
|
<li>Éditez <code>~/.config/clip-sync/peers.toml</code> et ajoutez vos machines (voir ci-dessous).</li>
|
||||||
|
<li>Lancez <code>clip-sync</code> sur chaque machine.</li>
|
||||||
|
<li>Copiez du texte ou une image (Ctrl+C), collez sur l'autre machine (Ctrl+V).</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Configuration minimale</h3>
|
||||||
|
<pre><code># ~/.config/clip-sync/peers.toml
|
||||||
|
[daemon]
|
||||||
|
port = 9137
|
||||||
|
poll_interval_ms = 500
|
||||||
|
|
||||||
|
[[peers]]
|
||||||
|
name = "portable"
|
||||||
|
addr = "192.168.1.20:9137"</code></pre>
|
||||||
|
|
||||||
|
<p>Sur l'autre machine, inversez l'adresse. Vous pouvez aussi activer la
|
||||||
|
découverte automatique (<code>discovery = true</code>) pour ne plus configurer
|
||||||
|
d'adresse du tout.</p>
|
||||||
|
|
||||||
|
<h2>Commandes</h2>
|
||||||
|
<table>
|
||||||
|
<tr><th>Commande</th><th>Description</th></tr>
|
||||||
|
<tr><td><code>clip-sync</code></td><td>Lancer le daemon</td></tr>
|
||||||
|
<tr><td><code>clip-sync --config <path></code></td><td>Config personnalisée</td></tr>
|
||||||
|
<tr><td><code>clip-sync --version</code></td><td>Afficher la version</td></tr>
|
||||||
|
<tr><td><code>clip-sync history</code></td><td>Afficher l'historique local</td></tr>
|
||||||
|
<tr><td><code>clip-sync --one-shot [text]</code></td><td>Envoyer le presse-papiers (ou texte) une fois</td></tr>
|
||||||
|
<tr><td><code>clip-sync send-file <path></code></td><td>Envoyer un fichier aux pairs</td></tr>
|
||||||
|
<tr><td><code>clip-sync --generate-key</code></td><td>Générer une clé partagée</td></tr>
|
||||||
|
<tr><td><code>clip-sync --generate-cert</code></td><td>Générer un certificat TLS auto-signé</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Configuration</h2>
|
||||||
|
<table>
|
||||||
|
<tr><th>Clé</th><th>Défaut</th><th>Description</th></tr>
|
||||||
|
<tr><td><code>daemon.port</code></td><td><code>9137</code></td><td>Port d'écoute HTTP(S)</td></tr>
|
||||||
|
<tr><td><code>daemon.poll_interval_ms</code></td><td><code>500</code></td><td>Intervalle de scrutation (ms)</td></tr>
|
||||||
|
<tr><td><code>daemon.max_body_bytes</code></td><td><code>10485760</code></td><td>Taille max d'une requête</td></tr>
|
||||||
|
<tr><td><code>daemon.history_size</code></td><td><code>50</code></td><td>Entrées d'historique conservées</td></tr>
|
||||||
|
<tr><td><code>daemon.discovery</code></td><td><code>false</code></td><td>Découverte mDNS des pairs</td></tr>
|
||||||
|
<tr><td><code>daemon.notify</code></td><td><code>false</code></td><td>Notifications desktop</td></tr>
|
||||||
|
<tr><td><code>daemon.sync_images</code></td><td><code>false</code></td><td>Sync d'images (PNG)</td></tr>
|
||||||
|
<tr><td><code>daemon.receive_files</code></td><td><code>false</code></td><td>Accepter les transferts de fichiers</td></tr>
|
||||||
|
<tr><td><code>security.shared_key</code></td><td>—</td><td>Clé partagée (Bearer token)</td></tr>
|
||||||
|
<tr><td><code>security.allowed_origins</code></td><td>—</td><td>Hôtes autorisés</td></tr>
|
||||||
|
<tr><td><code>security.tls</code></td><td><code>false</code></td><td>Chiffrement TLS entre pairs</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>Variables d'environnement : <code>CLIP_SYNC_PORT</code>,
|
||||||
|
<code>CLIP_SYNC_POLL_MS</code>, <code>CLIP_SYNC_KEY</code>,
|
||||||
|
<code>CLIP_SYNC_MAX_BODY_BYTES</code>.</p>
|
||||||
|
|
||||||
|
<h2>Sécurité</h2>
|
||||||
|
<p>Sur un réseau partagé, activez au minimum une clé partagée :</p>
|
||||||
|
<pre><code>clip-sync --generate-key # copier la clé sur toutes les machines</code></pre>
|
||||||
|
<pre><code>[security]
|
||||||
|
shared_key = "LA_CLÉ_GÉNÉRÉE"</code></pre>
|
||||||
|
<p>Pour chiffrer le trafic, générez un certificat (<code>clip-sync --generate-cert</code>),
|
||||||
|
copiez <code>cert.pem</code>/<code>key.pem</code> sur chaque machine, puis
|
||||||
|
<code>tls = true</code> et <code>insecure_skip_verify = true</code>.</p>
|
||||||
|
|
||||||
|
<h2>Fonctionnalités</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Sync bidirectionnelle (texte + images PNG)</li>
|
||||||
|
<li>Anti-boucle (hash SHA-256 + timestamp + origine)</li>
|
||||||
|
<li>Clé partagée, validation d'origine, TLS optionnel</li>
|
||||||
|
<li>Découverte mDNS, historique, transfert de fichiers</li>
|
||||||
|
<li>Interface web locale : <code>http://localhost:9137/</code></li>
|
||||||
|
<li>Linux (X11/Wayland), Windows, macOS — binaire unique, zéro runtime</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Service au démarrage</h2>
|
||||||
|
<p><strong>Linux (systemd)</strong> : voir le fichier <code>clip-sync.service</code>.
|
||||||
|
<strong>Windows</strong> : utiliser NSSM. <strong>macOS</strong> : un LaunchAgent
|
||||||
|
(voir le README pour les détails complets).</p>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="wrap">clip-sync — MIT · <a href="https://git.dracodev.net/Projets/clip-sync">Code source</a></div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+88
@@ -0,0 +1,88 @@
|
|||||||
|
# clip-sync install.ps1
|
||||||
|
# One-liner install on Windows (PowerShell):
|
||||||
|
#
|
||||||
|
# irm https://git.dracodev.net/Projets/clip-sync/raw/branch/main/install.ps1 | iex
|
||||||
|
#
|
||||||
|
# Detects architecture, downloads the binary, installs to
|
||||||
|
# %LOCALAPPDATA%\clip-sync\bin, adds it to PATH, and creates a default config.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$Version = if ($env:CLIP_SYNC_VERSION) { $env:CLIP_SYNC_VERSION } else { 'v0.2.0' }
|
||||||
|
$Repo = 'https://git.dracodev.net/Projets/clip-sync'
|
||||||
|
|
||||||
|
# ── Detect architecture ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
$arch = switch ($env:PROCESSOR_ARCHITECTURE) {
|
||||||
|
'AMD64' { 'amd64' }
|
||||||
|
'ARM64' { 'arm64' }
|
||||||
|
'x86' { 'amd64' } # 32-bit Windows runs the amd64 binary under WoW64/emulation
|
||||||
|
default { throw "clip-sync: unsupported architecture: $env:PROCESSOR_ARCHITECTURE" }
|
||||||
|
}
|
||||||
|
|
||||||
|
$binName = "clip-sync-windows-$arch.exe"
|
||||||
|
$url = "$Repo/releases/download/$Version/$binName"
|
||||||
|
|
||||||
|
$installDir = Join-Path $env:LOCALAPPDATA 'clip-sync\bin'
|
||||||
|
$exePath = Join-Path $installDir 'clip-sync.exe'
|
||||||
|
|
||||||
|
# ── Download ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Force -Path $installDir | Out-Null
|
||||||
|
Write-Host "clip-sync: downloading $url"
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-WebRequest -Uri $url -OutFile $exePath -UseBasicParsing
|
||||||
|
} catch {
|
||||||
|
Write-Host "clip-sync: download failed. Try building from source:" -ForegroundColor Red
|
||||||
|
Write-Host " git clone $Repo && cd clip-sync && make install"
|
||||||
|
throw
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Add to PATH ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||||
|
if ($userPath -split ';' -notcontains $installDir) {
|
||||||
|
[Environment]::SetEnvironmentVariable('Path', "$userPath;$installDir", 'User')
|
||||||
|
$env:Path += ";$installDir"
|
||||||
|
Write-Host "clip-sync: added $installDir to PATH (restart your terminal to use it)"
|
||||||
|
} else {
|
||||||
|
$env:Path += ";$installDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Create default config ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
$configDir = Join-Path $env:USERPROFILE '.config\clip-sync'
|
||||||
|
$configFile = Join-Path $configDir 'peers.toml'
|
||||||
|
|
||||||
|
if (-not (Test-Path $configFile)) {
|
||||||
|
New-Item -ItemType Directory -Force -Path $configDir | Out-Null
|
||||||
|
@'
|
||||||
|
# clip-sync peers configuration
|
||||||
|
# %USERPROFILE%\.config\clip-sync\peers.toml
|
||||||
|
|
||||||
|
[daemon]
|
||||||
|
port = 9137
|
||||||
|
poll_interval_ms = 500
|
||||||
|
# discovery = true # enable mDNS auto-discovery of peers
|
||||||
|
# notify = true # enable desktop notifications
|
||||||
|
# sync_images = true # sync images from the clipboard (PNG)
|
||||||
|
# receive_files = true # accept incoming file transfers
|
||||||
|
|
||||||
|
[security]
|
||||||
|
# shared_key = "..." # generate with: clip-sync --generate-key
|
||||||
|
|
||||||
|
# Add your peers here:
|
||||||
|
# [[peers]]
|
||||||
|
# name = "example"
|
||||||
|
# addr = "192.168.1.x:9137"
|
||||||
|
'@ | Set-Content -Path $configFile -Encoding UTF8
|
||||||
|
Write-Host "clip-sync: created default config at $configFile"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "clip-sync: installation complete!" -ForegroundColor Green
|
||||||
|
Write-Host " 1. Edit $configFile to add your peers"
|
||||||
|
Write-Host " 2. Run: clip-sync"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " For service installation, see README.md"
|
||||||
@@ -70,3 +70,22 @@ func TestDIBRejectsUnsupportedCompression(t *testing.T) {
|
|||||||
t.Error("expected error for unsupported compression, got nil")
|
t.Error("expected error for unsupported compression, got nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkDIBRoundTrip(b *testing.B) {
|
||||||
|
img := image.NewRGBA(image.Rect(0, 0, 256, 256))
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_ = png.Encode(&buf, img)
|
||||||
|
pngData := buf.Bytes()
|
||||||
|
|
||||||
|
dib, err := pngToDIB(pngData)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("pngToDIB: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
if _, err := dibToPNG(dib); err != nil {
|
||||||
|
b.Fatalf("dibToPNG: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package dedup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkShouldIgnoreNewContent(b *testing.B) {
|
||||||
|
f := NewFilter("machine-a")
|
||||||
|
payload := Payload{
|
||||||
|
Text: "benchmark content string",
|
||||||
|
Ts: time.Now().UnixNano(),
|
||||||
|
Origin: "machine-b",
|
||||||
|
}
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = f.ShouldIgnore(payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkShouldIgnoreImage(b *testing.B) {
|
||||||
|
f := NewFilter("machine-a")
|
||||||
|
payload := Payload{
|
||||||
|
Mime: "image/png",
|
||||||
|
Data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
||||||
|
Ts: time.Now().UnixNano(),
|
||||||
|
Origin: "machine-b",
|
||||||
|
}
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = f.ShouldIgnore(payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMarkWritten(b *testing.B) {
|
||||||
|
f := NewFilter("machine-a")
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
f.MarkWritten("some clipboard text")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,161 @@
|
|||||||
|
|
||||||
package notify
|
package notify
|
||||||
|
|
||||||
// Notifier is a no-op on Windows. Native toast notifications require a
|
import (
|
||||||
// registered, packaged app (or a third-party module such as BurntToast), which
|
"fmt"
|
||||||
// is out of scope for a single self-contained binary.
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// notifier shows balloon notifications via Shell_NotifyIconW. This is a
|
||||||
|
// self-contained approach (no Start Menu shortcut or AppID registration
|
||||||
|
// required), unlike WinRT toast notifications. The balloon appears in the
|
||||||
|
// notification area and a small tray icon is kept for the daemon's lifetime.
|
||||||
type notifier struct{}
|
type notifier struct{}
|
||||||
|
|
||||||
// New returns a no-op Windows notifier.
|
// New returns a Windows notifier using Shell_NotifyIcon balloon tips.
|
||||||
func New() Notifier { return notifier{} }
|
func New() Notifier { return notifier{} }
|
||||||
|
|
||||||
func (notifier) Notify(string, string) error { return nil }
|
const (
|
||||||
|
nifInfo = 0x00000010
|
||||||
|
nimAdd = 0x00000000
|
||||||
|
nimModify = 0x00000001
|
||||||
|
niiInfo = 0x00000001
|
||||||
|
)
|
||||||
|
|
||||||
|
// hwndMessage is the HWND_MESSAGE (-3) pseudo-handle for a message-only window.
|
||||||
|
const hwndMessage = ^uintptr(2)
|
||||||
|
|
||||||
|
var (
|
||||||
|
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
user32 = syscall.NewLazyDLL("user32.dll")
|
||||||
|
shell32 = syscall.NewLazyDLL("shell32.dll")
|
||||||
|
|
||||||
|
procGetModuleHandleW = kernel32.NewProc("GetModuleHandleW")
|
||||||
|
procRegisterClassExW = user32.NewProc("RegisterClassExW")
|
||||||
|
procCreateWindowExW = user32.NewProc("CreateWindowExW")
|
||||||
|
procDefWindowProcW = user32.NewProc("DefWindowProcW")
|
||||||
|
procShellNotifyIconW = shell32.NewProc("Shell_NotifyIconW")
|
||||||
|
)
|
||||||
|
|
||||||
|
type wndClassExW struct {
|
||||||
|
cbSize uint32
|
||||||
|
style uint32
|
||||||
|
lpfnWndProc uintptr
|
||||||
|
cbClsExtra int32
|
||||||
|
cbWndExtra int32
|
||||||
|
hInstance uintptr
|
||||||
|
hIcon uintptr
|
||||||
|
hCursor uintptr
|
||||||
|
hbrBackground uintptr
|
||||||
|
lpszMenuName *uint16
|
||||||
|
lpszClassName *uint16
|
||||||
|
hIconSm uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
type notifyIconDataW struct {
|
||||||
|
cbSize uint32
|
||||||
|
hWnd uintptr
|
||||||
|
uID uint32
|
||||||
|
uFlags uint32
|
||||||
|
uCallbackMessage uint32
|
||||||
|
hIcon uintptr
|
||||||
|
szTip [128]uint16
|
||||||
|
dwState uint32
|
||||||
|
dwStateMask uint32
|
||||||
|
szInfo [256]uint16
|
||||||
|
uTimeout uint32
|
||||||
|
szInfoTitle [64]uint16
|
||||||
|
dwInfoFlags uint32
|
||||||
|
guidItem [16]byte
|
||||||
|
hBalloonIcon uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
notifyOnce sync.Once
|
||||||
|
notifyHwnd uintptr
|
||||||
|
notifyErr error
|
||||||
|
|
||||||
|
iconMu sync.Mutex
|
||||||
|
iconAdded bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func setup() {
|
||||||
|
hInstance, _, _ := procGetModuleHandleW.Call(0)
|
||||||
|
if hInstance == 0 {
|
||||||
|
notifyErr = syscall.EINVAL
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
className, _ := syscall.UTF16PtrFromString("clip-sync-notify")
|
||||||
|
wc := wndClassExW{
|
||||||
|
cbSize: uint32(unsafe.Sizeof(wndClassExW{})),
|
||||||
|
lpfnWndProc: procDefWindowProcW.Addr(),
|
||||||
|
hInstance: hInstance,
|
||||||
|
lpszClassName: className,
|
||||||
|
}
|
||||||
|
// Registration may fail if already registered (e.g. re-run) — that's fine.
|
||||||
|
_, _, _ = procRegisterClassExW.Call(uintptr(unsafe.Pointer(&wc)))
|
||||||
|
|
||||||
|
hwnd, _, _ := procCreateWindowExW.Call(
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(className)),
|
||||||
|
uintptr(unsafe.Pointer(className)),
|
||||||
|
0,
|
||||||
|
0, 0, 0, 0,
|
||||||
|
hwndMessage,
|
||||||
|
0, hInstance, 0,
|
||||||
|
)
|
||||||
|
notifyHwnd = hwnd
|
||||||
|
if hwnd == 0 {
|
||||||
|
notifyErr = fmt.Errorf("notify: CreateWindowExW failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (notifier) Notify(title, body string) error {
|
||||||
|
notifyOnce.Do(setup)
|
||||||
|
if notifyErr != nil {
|
||||||
|
return notifyErr
|
||||||
|
}
|
||||||
|
|
||||||
|
var nid notifyIconDataW
|
||||||
|
nid.cbSize = uint32(unsafe.Sizeof(nid))
|
||||||
|
nid.hWnd = notifyHwnd
|
||||||
|
nid.uID = 1
|
||||||
|
nid.uFlags = nifInfo
|
||||||
|
nid.uTimeout = 5000
|
||||||
|
nid.dwInfoFlags = niiInfo
|
||||||
|
|
||||||
|
if info, err := syscall.UTF16FromString(body); err == nil {
|
||||||
|
copy(nid.szInfo[:], info)
|
||||||
|
}
|
||||||
|
if title16, err := syscall.UTF16FromString(title); err == nil {
|
||||||
|
copy(nid.szInfoTitle[:], title16)
|
||||||
|
}
|
||||||
|
|
||||||
|
iconMu.Lock()
|
||||||
|
defer iconMu.Unlock()
|
||||||
|
|
||||||
|
cmd := uintptr(nimAdd)
|
||||||
|
if iconAdded {
|
||||||
|
cmd = nimModify
|
||||||
|
}
|
||||||
|
r, _, _ := procShellNotifyIconW.Call(cmd, uintptr(unsafe.Pointer(&nid)))
|
||||||
|
if r != 0 {
|
||||||
|
iconAdded = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to the other command on failure (e.g. icon already/not yet present).
|
||||||
|
other := uintptr(nimAdd)
|
||||||
|
if cmd == nimAdd {
|
||||||
|
other = nimModify
|
||||||
|
}
|
||||||
|
r, _, _ = procShellNotifyIconW.Call(other, uintptr(unsafe.Pointer(&nid)))
|
||||||
|
if r != 0 {
|
||||||
|
iconAdded = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("notify: Shell_NotifyIconW failed")
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.dracodev.net/Projets/clip-sync/internal/dedup"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkHandleClipText(b *testing.B) {
|
||||||
|
cb := &mockClipboard{}
|
||||||
|
srv := newTestServer(cb, Options{MaxBodyBytes: 1 << 20})
|
||||||
|
|
||||||
|
body, _ := json.Marshal(dedup.Payload{
|
||||||
|
Text: "benchmark clipboard text", Ts: 1690000000000000000, Origin: "machine-b",
|
||||||
|
})
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/clip", bytes.NewReader(body))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
srv.handleClip(w, req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkHandleClipImage(b *testing.B) {
|
||||||
|
cb := &mockClipboard{}
|
||||||
|
srv := newTestServer(cb, Options{MaxBodyBytes: 1 << 20})
|
||||||
|
|
||||||
|
body := []byte(`{"mime":"image/png","data":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==","ts":1,"origin":"machine-b"}`)
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/clip", bytes.NewReader(body))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
srv.handleClip(w, req)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user