feat: Windows notifications, benchmarks, GPG signing, docs site, one-liner installers

This commit is contained in:
2026-08-14 10:57:20 -04:00
parent 448d2c4ead
commit 35c6b98aec
12 changed files with 555 additions and 16 deletions
+22 -2
View File
@@ -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
View File
@@ -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
+5
View File
@@ -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)
+18
View File
@@ -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/"
+21 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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 &lt;path&gt;</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 &lt;path&gt;</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
View File
@@ -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"
+19
View File
@@ -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)
}
}
}
+41
View File
@@ -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")
}
}
+154 -5
View File
@@ -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")
}
+41
View File
@@ -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)
}
}