chore: initialiser le projet imagod-web (Rust)

- Structure du projet Cargo avec dépendances (clap, axum, reqwest, rusqlite, arboard, tray-icon)
- CLI avec sous-commandes start, serve, stop, status, config, install, uninstall, logs
- Module config: parsing TOML, chemins Windows/Linux
- Module dedup: dédoublonnage SHA-256 avec cache SQLite
- Module imago_client: client HTTP pour API Imago (upload, search, get)
- Module watcher: boucle de polling stub
- Module server: serveur web axum avec routes API REST stub
- Platform-specific: clipboard, tray, service, window_info (Windows)
- Frontend placeholder HTML
- Config par défaut (imagod.toml)
- README.md, LICENSE (MIT), .gitignore
This commit is contained in:
2026-06-22 16:44:47 -04:00
commit a3b853de04
19 changed files with 923 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
# Build
/target/
.cargo/
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Local data
imagod.db
imagod.log
*.local.toml
+71
View File
@@ -0,0 +1,71 @@
[package]
name = "imagod-web"
version = "0.1.0"
edition = "2021"
description = "Imago Daemon + Dashboard — Service de capture automatique de screenshots + Dashboard web embarqué"
authors = ["Bruno Charest <[email protected]>"]
license = "MIT"
repository = "https://git.dracodev.net/Projets/imagod-web"
keywords = ["screenshot", "clipboard", "imago", "dashboard", "ai"]
categories = ["command-line-utilities", "web-programming"]
[dependencies]
# CLI
clap = { version = "4", features = ["derive", "env"] }
# Async runtime
tokio = { version = "1", features = ["full"] }
# HTTP Client (API Imago)
reqwest = { version = "0.12", features = ["json", "multipart", "stream"] }
# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# Config
toml = "0.8"
# Hashing
sha2 = "0.10"
# SQLite
rusqlite = { version = "0.32", features = ["bundled"] }
# Time
chrono = { version = "0.4", features = ["serde"] }
# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
# Error handling
anyhow = "1"
thiserror = "2"
# Async trait
async-trait = "0.1"
# UUID for unique IDs
uuid = { version = "1", features = ["v4"] }
# Web server (axum + tower)
axum = { version = "0.8", features = ["ws"] }
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "fs", "trace"] }
# Frontend embedding
rust-embed = "8"
# Platform-specific — Windows
[target.'cfg(windows)'.dependencies]
# Clipboard access
arboard = "3"
# System tray
tray-icon = "0.19"
# Windows API (pour titre fenêtre active)
windows-sys = { version = "0.59", features = [
"Win32_UI_WindowsAndMessaging",
"Win32_Foundation",
"Win32_System_Threading",
"Win32_UI_TextServices",
] }
# Platform-specific — Linux
[target.'cfg(unix)'.dependencies]
# Notify pour watcher filesystem (Linux)
notify = { version = "7", features = ["macos_kqueue"] }
[profile.release]
opt-level = "z" # Optimize for size
lto = true # Link-time optimization
strip = true # Strip symbols
codegen-units = 1 # Better optimization
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Bruno Charest
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+92
View File
@@ -0,0 +1,92 @@
# imagod-web
**Imago Daemon + Dashboard** — Service de capture automatique de screenshots + Dashboard web embarqué.
Un binaire **Rust** unique qui remplit deux rôles :
| Mode | Plateforme | Rôle |
|------|-----------|------|
| `imagod-web start` | Windows | Watcher clipboard + Dashboard web :9090 |
| `imagod-web serve` | Linux/Raspi | Dashboard web uniquement (sans watcher) |
## Fonctionnalités
- **Capture automatique** : surveille le clipboard Windows, détecte les nouveaux screenshots
- **Dédoublonnage** : SHA-256 pour éviter les doublons
- **Upload Imago** : envoi vers le backend Imago pour OCR + AI Vision + Tags
- **Dashboard web embarqué** : galerie, recherche, preview, copie/partage
- **Cross-platform** : Windows (watcher+dashboard), Linux/Raspi (dashboard)
- **System tray** : icône native Windows avec menu contextuel
- **Service Windows** : installation via NSSM pour démarrage automatique
## Prérequis
- [Rust](https://rustup.rs) 1.80+
- Un serveur [Imago](https://git.dracodev.net/Projets/Imago) fonctionnel
- Une clé API client Imago
## Installation
```bash
# Compiler depuis les sources
git clone https://git.dracodev.net/Projets/imagod-web.git
cd imagod-web
cargo build --release
# Le binaire est dans target/release/imagod-web (ou imagod-web.exe)
```
### Cross-compilation
```bash
# Windows
cargo build --release --target x86_64-pc-windows-msvc
# Linux x86_64 (openclaw-1)
cargo build --release --target x86_64-unknown-linux-gnu
# Linux ARM64 (Raspberry Pi)
cargo build --release --target aarch64-unknown-linux-gnu
```
## Utilisation
```bash
# Windows — lancer le watcher + dashboard
imagod-web start --port 9090 --open-browser
# Linux — dashboard uniquement
imagod-web serve --port 9090 --host 0.0.0.0 --no-watcher
# Voir le statut
imagod-web status
# Configurer
imagod-web config show
imagod-web config set imago.api_key "ik_..."
# Installer comme service Windows (admin requis)
imagod-web install
```
## Configuration
Fichier `imagod.toml` dans `%APPDATA%/imagod-web/` (Windows) ou `~/.config/imagod-web/` (Linux).
```toml
[imago]
url = "http://imago.dracodev.net:8000"
api_key = "CHANGE_ME"
[watcher]
capture_images = true
poll_interval_ms = 500
```
## Architecture
Voir [[imagod-web-architecture|Document d'architecture complet]] dans le vault Obsidian IT.
## Licence
MIT — Copyright (c) 2026 Bruno Charest
+30
View File
@@ -0,0 +1,30 @@
[imago]
# URL du serveur Imago
url = "http://imago.dracodev.net:8000"
# Clé API client Imago (à configurer)
api_key = "CHANGE_ME"
[watcher]
capture_images = true
poll_interval_ms = 500
[watcher.filters]
# Applications à capturer (vide = tout)
only_apps = []
# Applications à exclure
exclude_apps = ["mstsc"]
[dedup]
# Méthode de dédoublonnage : sha256 | perceptual_hash
method = "sha256"
# Nombre de hashes en cache
cache_size = 10000
[ui]
show_notifications = true
show_tray_icon = true
notification_timeout_sec = 3
[logging]
# Niveau de log : trace | debug | info | warn | error
level = "info"
+80
View File
@@ -0,0 +1,80 @@
//! Parsing CLI avec clap
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(
name = "imagod-web",
version = env!("CARGO_PKG_VERSION"),
about = "Imago Daemon + Dashboard — Capture automatique de screenshots + Dashboard web embarqué",
long_about = "Service de capture automatique de screenshots vers Imago avec dashboard web embarqué.\n\n\
Modes :\n \
Windows : imagod-web start → watcher clipboard + dashboard :9090\n \
Linux : imagod-web serve --port 9090 --no-watcher → dashboard uniquement"
)]
pub struct Args {
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand)]
pub enum Command {
/// Lancer le watcher + dashboard (Windows uniquement)
Start {
/// Port du dashboard web
#[arg(long, default_value = "9090")]
port: u16,
/// Ouvrir le dashboard dans le navigateur
#[arg(long)]
open_browser: bool,
},
/// Lancer le dashboard web uniquement (Linux/serveur)
Serve {
/// Port du dashboard web
#[arg(long, default_value = "9090")]
port: u16,
/// Interface d'écoute
#[arg(long, default_value = "127.0.0.1")]
host: String,
/// Désactiver le watcher clipboard
#[arg(long)]
no_watcher: bool,
/// Ouvrir le dashboard dans le navigateur
#[arg(long)]
open_browser: bool,
},
/// Arrêter le service
Stop,
/// Afficher l'état du service
Status,
/// Gérer la configuration
Config {
#[command(subcommand)]
action: ConfigAction,
},
/// Installer comme service Windows
Install,
/// Désinstaller le service Windows
Uninstall,
/// Afficher les derniers logs
Logs,
}
#[derive(Subcommand)]
pub enum ConfigAction {
/// Afficher la configuration actuelle
Show,
/// Modifier un paramètre
Set {
/// Clé de configuration (ex: imago.url)
key: String,
/// Valeur
value: String,
},
/// Réinitialiser la configuration par défaut
Reset,
}
pub fn parse() -> Args {
Args::parse()
}
+16
View File
@@ -0,0 +1,16 @@
//! Clipboard — Abstraction arboard (Windows)
//!
//! Phase 3: Lecture du clipboard Windows, détection d'images
use anyhow::Result;
/// Vérifie si le clipboard contient une image
pub fn has_image() -> Result<bool> {
// Phase 3: Implémentation arboard
todo!("Phase 3: Clipboard detection")
}
/// Récupère l'image du clipboard en bytes
pub fn get_image_bytes() -> Result<Vec<u8>> {
todo!("Phase 3: Clipboard image extraction")
}
+132
View File
@@ -0,0 +1,132 @@
//! Gestion de la configuration TOML
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
/// Configuration complète d'imagod-web
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
#[serde(default)]
pub imago: ImagoConfig,
#[serde(default)]
pub watcher: WatcherConfig,
#[serde(default)]
pub dedup: DedupConfig,
#[serde(default)]
pub ui: UiConfig,
#[serde(default)]
pub logging: LoggingConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImagoConfig {
pub url: String,
pub api_key: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatcherConfig {
pub capture_images: bool,
pub poll_interval_ms: u64,
pub filters: WatcherFilters,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatcherFilters {
pub only_apps: Vec<String>,
pub exclude_apps: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DedupConfig {
pub method: String,
pub cache_size: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UiConfig {
pub show_notifications: bool,
pub show_tray_icon: bool,
pub notification_timeout_sec: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
pub level: String,
}
impl Default for Config {
fn default() -> Self {
Self {
imago: ImagoConfig {
url: "http://imago.dracodev.net:8000".into(),
api_key: String::new(),
},
watcher: WatcherConfig {
capture_images: true,
poll_interval_ms: 500,
filters: WatcherFilters {
only_apps: vec![],
exclude_apps: vec!["mstsc".into()],
},
},
dedup: DedupConfig {
method: "sha256".into(),
cache_size: 10000,
},
ui: UiConfig {
show_notifications: true,
show_tray_icon: true,
notification_timeout_sec: 3,
},
logging: LoggingConfig {
level: "info".into(),
},
}
}
}
impl Config {
/// Charge la configuration depuis `%APPDATA%/imagod-web/imagod.toml`
pub fn load() -> Result<Self> {
let path = Self::config_path()?;
if path.exists() {
let content = std::fs::read_to_string(&path)
.with_context(|| format!("Impossible de lire {}", path.display()))?;
let config: Config = toml::from_str(&content)
.with_context(|| "Erreur de parsing du fichier de configuration")?;
Ok(config)
} else {
// Premier lancement — créer la config par défaut
let config = Config::default();
config.save()?;
Ok(config)
}
}
/// Sauvegarde la configuration
pub fn save(&self) -> Result<()> {
let path = Self::config_path()?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let content = toml::to_string_pretty(self)?;
std::fs::write(&path, content)?;
Ok(())
}
/// Chemin du fichier de configuration
fn config_path() -> Result<PathBuf> {
let base = if cfg!(target_os = "windows") {
let appdata = std::env::var("APPDATA")
.context("Variable APPDATA non définie")?;
PathBuf::from(appdata)
} else {
let home = std::env::var("HOME")
.context("Variable HOME non définie")?;
PathBuf::from(home).join(".config")
};
Ok(base.join("imagod-web").join("imagod.toml"))
}
}
+71
View File
@@ -0,0 +1,71 @@
//! Dédoublonnage des images par hash SHA-256
use anyhow::Result;
use rusqlite::Connection;
use sha2::{Digest, Sha256};
use std::path::Path;
/// Gestionnaire de dédoublonnage
pub struct Dedup {
conn: Connection,
cache_size: usize,
}
impl Dedup {
/// Ouvre ou crée la base de dédoublonnage locale
pub fn open(db_path: &Path, cache_size: usize) -> Result<Self> {
let conn = Connection::open(db_path)?;
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS hashes (
hash BLOB PRIMARY KEY,
image_id TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_hashes_created ON hashes(created_at);"
)?;
Ok(Self { conn, cache_size })
}
/// Calcule le hash SHA-256 d'un buffer
pub fn hash_bytes(data: &[u8]) -> Vec<u8> {
let mut hasher = Sha256::new();
hasher.update(data);
hasher.finalize().to_vec()
}
/// Vérifie si un hash a déjà été vu
pub fn already_seen(&self, hash: &[u8]) -> Result<bool> {
let exists: bool = self.conn.query_row(
"SELECT COUNT(*) > 0 FROM hashes WHERE hash = ?1",
rusqlite::params![hash],
|row| row.get(0),
)?;
Ok(exists)
}
/// Marque un hash comme vu
pub fn mark_seen(&self, hash: &[u8], image_id: &str) -> Result<()> {
self.conn.execute(
"INSERT OR IGNORE INTO hashes (hash, image_id) VALUES (?1, ?2)",
rusqlite::params![hash, image_id],
)?;
// Nettoyage périodique si dépassement du cache
self.conn.execute(
"DELETE FROM hashes WHERE rowid NOT IN (
SELECT rowid FROM hashes ORDER BY created_at DESC LIMIT ?1
)",
rusqlite::params![self.cache_size],
)?;
Ok(())
}
/// Nombre de hashes en cache
pub fn count(&self) -> Result<usize> {
let count: usize = self.conn.query_row(
"SELECT COUNT(*) FROM hashes",
[],
|row| row.get(0),
)?;
Ok(count)
}
}
+38
View File
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Imago Dashboard</title>
<style>
/* Phase 9: Dashboard Svelte — ici juste un placeholder */
:root {
--bg: #0d1117;
--fg: #c9d1d9;
--accent: #58a6ff;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg);
color: var(--fg);
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.placeholder {
text-align: center;
padding: 2rem;
}
h1 { color: var(--accent); margin-bottom: 1rem; }
p { color: #8b949e; }
</style>
</head>
<body>
<div class="placeholder">
<h1>🖼️ Imago Dashboard</h1>
<p>Dashboard web en construction — Phase 9</p>
</div>
</body>
</html>
+113
View File
@@ -0,0 +1,113 @@
//! Client HTTP pour l'API Imago
use anyhow::{Context, Result};
use reqwest::{Client, multipart};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct UploadResponse {
pub id: String,
pub filename: String,
pub status: String,
}
#[derive(Debug, Deserialize)]
pub struct ImageInfo {
pub id: String,
pub filename: String,
pub tags: Vec<String>,
pub description: Option<String>,
pub ocr_text: Option<String>,
pub created_at: String,
pub width: Option<u32>,
pub height: Option<u32>,
pub size_bytes: Option<u64>,
}
#[derive(Debug, Deserialize)]
pub struct SearchResponse {
pub results: Vec<ImageInfo>,
pub total: usize,
}
/// Client pour l'API Imago
pub struct ImagoClient {
client: Client,
base_url: String,
api_key: String,
}
impl ImagoClient {
pub fn new(base_url: String, api_key: String) -> Result<Self> {
let client = Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()?;
Ok(Self { client, base_url, api_key })
}
/// Upload une image vers Imago
pub async fn upload_image(
&self,
data: Vec<u8>,
filename: &str,
window_title: Option<&str>,
) -> Result<UploadResponse> {
let part = multipart::Part::bytes(data)
.file_name(filename.to_string())
.mime_str("image/png")?;
let mut form = multipart::Form::new()
.part("file", part);
if let Some(title) = window_title {
form = form.text("window_title", title.to_string());
}
let response = self.client
.post(format!("{}/images/upload", self.base_url))
.header("Authorization", format!("Bearer {}", self.api_key))
.multipart(form)
.send()
.await?;
let upload: UploadResponse = response
.error_for_status()?
.json()
.await?;
Ok(upload)
}
/// Recherche des images
pub async fn search(&self, query: &str, limit: usize) -> Result<SearchResponse> {
let response = self.client
.get(format!("{}/images/search", self.base_url))
.header("Authorization", format!("Bearer {}", self.api_key))
.query(&[("q", query), ("limit", &limit.to_string())])
.send()
.await?;
let result: SearchResponse = response
.error_for_status()?
.json()
.await?;
Ok(result)
}
/// Récupère les infos d'une image
pub async fn get_image(&self, id: &str) -> Result<ImageInfo> {
let response = self.client
.get(format!("{}/images/{}", self.base_url, id))
.header("Authorization", format!("Bearer {}", self.api_key))
.send()
.await?;
let info: ImageInfo = response
.error_for_status()?
.json()
.await?;
Ok(info)
}
}
+87
View File
@@ -0,0 +1,87 @@
//! imagod-web — Imago Daemon + Dashboard
//!
//! Point d'entrée CLI. Dispatch vers les sous-commandes :
//! - `start` : Lance le watcher clipboard + dashboard web (Windows)
//! - `serve` : Lance le dashboard web uniquement (Linux/serveur)
//! - `stop` : Arrête le service
//! - `status`: Affiche l'état
//! - `config`: Gère la configuration
//! - `install` / `uninstall`: Service Windows
mod cli;
mod config;
mod dedup;
mod imago_client;
mod watcher;
#[cfg(target_os = "windows")]
mod clipboard;
#[cfg(target_os = "windows")]
mod tray;
#[cfg(target_os = "windows")]
mod service;
#[cfg(target_os = "windows")]
mod window_info;
mod server;
use anyhow::Result;
use tracing_subscriber::{fmt, EnvFilter};
fn setup_logging() {
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info"));
fmt()
.with_env_filter(filter)
.with_target(true)
.init();
}
#[tokio::main]
async fn main() -> Result<()> {
setup_logging();
let args = cli::parse();
match args.command {
cli::Command::Start { port, open_browser } => {
tracing::info!("Démarrage imagod-web — watcher + dashboard");
// Lance le watcher et le serveur web en parallèle
// ...
todo!("Phase 6-7: Watcher + serveur web")
}
cli::Command::Serve { port, host, no_watcher, open_browser } => {
tracing::info!("Démarrage dashboard web uniquement sur {}:{}", host, port);
// Lance uniquement le serveur web
// ...
todo!("Phase 9: Dashboard web")
}
cli::Command::Stop => {
tracing::info!("Arrêt du service");
todo!("Phase 7: Stop")
}
cli::Command::Status => {
tracing::info!("Statut du service");
todo!("Phase 7: Status")
}
cli::Command::Config { action } => {
tracing::info!("Configuration: {:?}", action);
todo!("Phase 2: Config")
}
cli::Command::Install => {
tracing::info!("Installation comme service Windows");
todo!("Phase 10: Service install")
}
cli::Command::Uninstall => {
tracing::info!("Désinstallation du service Windows");
todo!("Phase 10: Service uninstall")
}
cli::Command::Logs => {
tracing::info!("Logs");
todo!("Phase 7: Logs")
}
}
Ok(())
}
+24
View File
@@ -0,0 +1,24 @@
//! Serveur web — Dashboard embarqué (axum)
//!
//! Phase 9: API REST + WebSocket + frontend Svelte embarqué via rust-embed
pub mod routes;
pub mod ws;
use axum::Router;
use std::net::SocketAddr;
/// Démarre le serveur web
pub async fn serve(host: &str, port: u16) -> anyhow::Result<()> {
let app = Router::new()
.merge(routes::api_routes())
.merge(routes::static_routes());
let addr: SocketAddr = format!("{}:{}", host, port).parse()?;
tracing::info!("Dashboard web démarré sur http://{}", addr);
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
Ok(())
}
+37
View File
@@ -0,0 +1,37 @@
//! Routes API REST du dashboard
use axum::{Router, routing::get};
/// Routes de l'API REST
pub fn api_routes() -> Router {
Router::new()
.route("/api/health", get(health_check))
.route("/api/images", get(list_images))
.route("/api/images/search", get(search_images))
.route("/api/stats", get(stats))
}
/// Routes statiques (frontend embarqué)
pub fn static_routes() -> Router {
// Phase 9: Servir le frontend Svelte via rust-embed
Router::new()
}
async fn health_check() -> &'static str {
"OK"
}
async fn list_images() -> &'static str {
// Phase 9: Liste paginée des images
"[]"
}
async fn search_images() -> &'static str {
// Phase 9: Recherche full-text
"[]"
}
async fn stats() -> &'static str {
// Phase 9: Statistiques
r#"{"total_images":0,"total_storage_bytes":0}"#
}
+5
View File
@@ -0,0 +1,5 @@
//! WebSocket — Mise à jour en temps réel du dashboard
//!
//! Phase 9: Notifie le frontend quand un nouveau screenshot est uploadé
// Stub — à implémenter dans Phase 9
+13
View File
@@ -0,0 +1,13 @@
//! Service Windows (NSSM / windows-service)
//!
//! Phase 10: Installation comme service Windows
use anyhow::Result;
pub fn install() -> Result<()> {
todo!("Phase 10: Service install via NSSM")
}
pub fn uninstall() -> Result<()> {
todo!("Phase 10: Service uninstall")
}
+10
View File
@@ -0,0 +1,10 @@
//! System Tray (Windows)
//!
//! Phase 8: Icône system tray avec menu contextuel
use anyhow::Result;
pub async fn run() -> Result<()> {
// Phase 8: Icône tray avec Start/Stop/Status/Quit
todo!("Phase 8: System tray")
}
+54
View File
@@ -0,0 +1,54 @@
//! Watcher — Boucle principale de polling du clipboard
//!
//! Surveille le clipboard à intervalle régulier, détecte les nouvelles images,
//! les dédoublonne et les envoie à Imago.
use anyhow::Result;
use crate::config::Config;
use crate::dedup::Dedup;
use crate::imago_client::ImagoClient;
use std::path::PathBuf;
use std::time::Duration;
pub struct Watcher {
config: Config,
dedup: Dedup,
imago: ImagoClient,
}
impl Watcher {
pub async fn new(config: Config) -> Result<Self> {
let db_path = PathBuf::from("imagod.db");
let dedup = Dedup::open(&db_path, config.dedup.cache_size)?;
let imago = ImagoClient::new(
config.imago.url.clone(),
config.imago.api_key.clone(),
)?;
Ok(Self { config, dedup, imago })
}
/// Lance la boucle de polling
pub async fn run(&self) -> Result<()> {
let interval = Duration::from_millis(self.config.watcher.poll_interval_ms);
tracing::info!(
"Watcher démarré — polling toutes les {}ms",
self.config.watcher.poll_interval_ms
);
// TODO: Phase 6 — Boucle principale
// loop {
// 1. Vérifier le clipboard (Windows: arboard, Linux: N/A)
// 2. Si nouvelle image → hash SHA-256
// 3. Vérifier dédoublonnage
// 4. Si nouveau → upload vers Imago
// 5. Marquer comme vu
// sleep(interval)
// }
tracing::warn!("Watcher::run() — implémentation Phase 6 à venir");
tokio::time::sleep(Duration::from_secs(1)).await;
Ok(())
}
}
+11
View File
@@ -0,0 +1,11 @@
//! Window Info — Titre de la fenêtre active (Windows API)
//!
//! Phase 6: Capture du contexte (titre fenêtre) pour enrichir les tags
use anyhow::Result;
/// Récupère le titre de la fenêtre active
pub fn get_active_window_title() -> Result<Option<String>> {
// Phase 6: Windows API — GetForegroundWindow + GetWindowText
todo!("Phase 6: Active window title")
}