Engine seam (ScanEngine trait) + Tauri GUI
ClamAV is a temporary dependency. Everything ClamAV-specific moves
behind a 4-method trait in crates/houndd/src/engine.rs:
trait ScanEngine { name; probe; scan; update }
struct ClamAvEngine // today
const ENGINE // one-line flip when the native engine lands
- parse_clamscan() is now a pure fn with unit tests (OK/FOUND/INCOMPLETE,
dedup, malformed lines)
- main.rs drops all clamscan/clamd/freshclam knowledge; Status reports
engine='clamav' via the trait
- README documents the seam and the wire contract that stays stable
GUI (gui/, Tauri 2, standalone workspace, vanilla JS premium-dark shell):
- system-tray sentinel on the 4-state dog-head ladder (green/amber/red/gray)
with tooltips; clicks open the window / scan / update
- window: protection hero, path + recursive scan, progress bar, results
table, signature update log; state mirrored to the tray
- compiles clean (cargo build, 0 warnings) on Mint 22.3 + webkit2gtk-4.1
All 11 workspace tests pass, incl. the E2E EICAR scan over the real
Unix-socket daemon (clamscan finds the planted EICAR).
71
README.md
|
|
@ -14,7 +14,7 @@ antivirus/
|
||||||
├── Cargo.toml # Rust workspace
|
├── Cargo.toml # Rust workspace
|
||||||
├── crates/
|
├── crates/
|
||||||
│ ├── hound-api/ # shared wire types + socket client (daemon/CLI/GUI all use it)
|
│ ├── hound-api/ # shared wire types + socket client (daemon/CLI/GUI all use it)
|
||||||
│ ├── houndd/ # the daemon: Unix-socket API over ClamAV
|
│ ├── houndd/ # the daemon: Unix-socket API over a pluggable engine
|
||||||
│ └── hound/ # CLI client
|
│ └── hound/ # CLI client
|
||||||
├── assets/icons/ # dog-head brand mark + 4-state tray ladder
|
├── assets/icons/ # dog-head brand mark + 4-state tray ladder
|
||||||
└── gui/ # Tauri 2 desktop app (system tray + scan UI)
|
└── gui/ # Tauri 2 desktop app (system tray + scan UI)
|
||||||
|
|
@ -25,19 +25,41 @@ antivirus/
|
||||||
```
|
```
|
||||||
houndd (Rust daemon — the engine)
|
houndd (Rust daemon — the engine)
|
||||||
┌──────────────────────────────────┐
|
┌──────────────────────────────────┐
|
||||||
│ L1 ClamAV signatures │
|
│ ScanEngine trait │
|
||||||
│ L2 Curated threat packs (Pro) │
|
│ ├─ L1 ClamAV signatures (now) │
|
||||||
│ L3 Behavioral monitor (Pro) │
|
│ ├─ L2 Curated threat packs(Pro)│
|
||||||
│ L4 Supply-chain checks (Pro) │
|
│ ├─ L3 Behavioral monitor (Pro) │
|
||||||
|
│ └─ L4 Supply-chain checks(Pro) │
|
||||||
└──────────────┬───────────────────┘
|
└──────────────┬───────────────────┘
|
||||||
Unix socket (JSON-RPC, line-delimited)
|
Unix socket (JSON-RPC, line-delimited)
|
||||||
┌───────────┼───────────┐
|
┌───────────┼───────────┐
|
||||||
hound CLI GUI (Tauri) future modules
|
hound CLI GUI (Tauri) future modules
|
||||||
```
|
```
|
||||||
|
|
||||||
The daemon is the only process that touches ClamAV. CLI and GUI are thin
|
The daemon is the only process that touches a scanning engine. CLI and GUI
|
||||||
clients — so future suite tools (firewall, updater, …) plug into the same
|
are thin clients — so future suite tools (firewall, updater, …) plug into
|
||||||
socket.
|
the same socket.
|
||||||
|
|
||||||
|
### Swapping the engine (the ClamAV seam)
|
||||||
|
|
||||||
|
ClamAV is a **temporary** dependency. Everything ClamAV-specific — version
|
||||||
|
probe, signature freshness, the `clamscan` subprocess + output parsing,
|
||||||
|
`freshclam` — lives in one file behind a four-method trait:
|
||||||
|
|
||||||
|
```
|
||||||
|
crates/houndd/src/engine.rs
|
||||||
|
trait ScanEngine { name; probe; scan; update }
|
||||||
|
struct ClamAvEngine // today
|
||||||
|
const ENGINE: ClamAvEngine // ← flip this line when the native
|
||||||
|
// engine lands; nothing else in the
|
||||||
|
// daemon, CLI, GUI, or wire API moves
|
||||||
|
```
|
||||||
|
|
||||||
|
The wire stays engine-agnostic: `Status.engine` names the implementation
|
||||||
|
(`"clamav"` today) and `Status.db` carries what any signature store has —
|
||||||
|
a file name and a timestamp. When our own Rust engine ships, it's a new
|
||||||
|
`ScanEngine` implementation, a one-const flip, and the tray/CLI/GUI simply
|
||||||
|
start reporting the new engine name.
|
||||||
|
|
||||||
## Icon system
|
## Icon system
|
||||||
|
|
||||||
|
|
@ -89,6 +111,39 @@ cargo run -p hound -- scan /tmp/eicar.com
|
||||||
# expect: exit code 1, "Eicar-Test-Signature FOUND"
|
# expect: exit code 1, "Eicar-Test-Signature FOUND"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Updating signatures
|
||||||
|
|
||||||
|
`hound update` wraps `freshclam` (trying `sudo freshclam` first, since
|
||||||
|
plain-user runs can't write `/var/lib/clamav` and `/var/log/clamav`). The
|
||||||
|
GUI's "Update Signatures" button drives the same RPC and shows the log.
|
||||||
|
```sh
|
||||||
|
cargo run -p hound -- update # or: hound update --json
|
||||||
|
```
|
||||||
|
|
||||||
|
## The GUI (`gui/`)
|
||||||
|
|
||||||
|
A Tauri 2 desktop app — a thin view over the *same* `houndd` socket the
|
||||||
|
CLI uses (via the shared `hound-api` client), so the window and the
|
||||||
|
command line never disagree about your security state.
|
||||||
|
|
||||||
|
```
|
||||||
|
gui/
|
||||||
|
├── dist/ # the front-end (vanilla HTML/CSS/JS, premium dark shell)
|
||||||
|
└── src-tauri/ # Tauri 2 shell + system-tray sentinel
|
||||||
|
```
|
||||||
|
|
||||||
|
The tray sentinel swaps the 4-state icons (green/amber/red/gray) as your
|
||||||
|
state changes; the window shows a live protection hero, a scan progress
|
||||||
|
bar, a results table, and the signature-update log.
|
||||||
|
|
||||||
|
Build it:
|
||||||
|
```sh
|
||||||
|
cd gui
|
||||||
|
npm install
|
||||||
|
npm run tauri dev # dev with hot reload
|
||||||
|
npm run tauri build # → .deb in src-tauri/target/release/bundle/
|
||||||
|
```
|
||||||
|
|
||||||
## Git conventions
|
## Git conventions
|
||||||
|
|
||||||
- Branch `main` is deployable; small, focused commits.
|
- Branch `main` is deployable; small, focused commits.
|
||||||
|
|
|
||||||
6
gui/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
node_modules/
|
||||||
|
dist/assets/
|
||||||
|
src-tauri/target/
|
||||||
|
src-tauri/gen/
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
17
gui/package.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"name": "hound-gui",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Hound Antivirus — desktop app",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"tauri": "tauri",
|
||||||
|
"dev": "tauri dev",
|
||||||
|
"build": "tauri build"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^2.5.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
5200
gui/src-tauri/Cargo.lock
generated
Normal file
26
gui/src-tauri/Cargo.toml
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
[package]
|
||||||
|
name = "hound-gui"
|
||||||
|
description = "Hound Antivirus desktop app (Tauri 2)"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://git.joelovestech.com/Hound/Antivirus"
|
||||||
|
|
||||||
|
# Standalone crate: the GUI has its own workspace (different dep set from
|
||||||
|
# the CLI/daemon) and is built via `cargo tauri build`, not the root build.
|
||||||
|
[workspace]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
hound-api = { path = "../../crates/hound-api" }
|
||||||
|
tauri = { version = "2", features = ["tray-icon", "image-png"] }
|
||||||
|
tauri-plugin-dialog = "2"
|
||||||
|
tauri-plugin-opener = "2"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
anyhow = "1"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = "2"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
strip = true
|
||||||
3
gui/src-tauri/build.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
BIN
gui/src-tauri/icons/hound-48.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
gui/src-tauri/icons/state-paused-16.png
Normal file
|
After Width: | Height: | Size: 610 B |
BIN
gui/src-tauri/icons/state-paused-22.png
Normal file
|
After Width: | Height: | Size: 889 B |
BIN
gui/src-tauri/icons/state-paused-24.png
Normal file
|
After Width: | Height: | Size: 905 B |
BIN
gui/src-tauri/icons/state-paused-32.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
gui/src-tauri/icons/state-paused-48.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
gui/src-tauri/icons/state-protected-16.png
Normal file
|
After Width: | Height: | Size: 640 B |
BIN
gui/src-tauri/icons/state-protected-22.png
Normal file
|
After Width: | Height: | Size: 918 B |
BIN
gui/src-tauri/icons/state-protected-24.png
Normal file
|
After Width: | Height: | Size: 938 B |
BIN
gui/src-tauri/icons/state-protected-32.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
gui/src-tauri/icons/state-protected-48.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
gui/src-tauri/icons/state-scanning-16.png
Normal file
|
After Width: | Height: | Size: 611 B |
BIN
gui/src-tauri/icons/state-scanning-22.png
Normal file
|
After Width: | Height: | Size: 862 B |
BIN
gui/src-tauri/icons/state-scanning-24.png
Normal file
|
After Width: | Height: | Size: 922 B |
BIN
gui/src-tauri/icons/state-scanning-32.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
gui/src-tauri/icons/state-scanning-48.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
gui/src-tauri/icons/state-threat-16.png
Normal file
|
After Width: | Height: | Size: 625 B |
BIN
gui/src-tauri/icons/state-threat-22.png
Normal file
|
After Width: | Height: | Size: 881 B |
BIN
gui/src-tauri/icons/state-threat-24.png
Normal file
|
After Width: | Height: | Size: 919 B |
BIN
gui/src-tauri/icons/state-threat-32.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
gui/src-tauri/icons/state-threat-48.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
222
gui/src-tauri/src/main.rs
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
//! `hound-gui` — the Tauri 2 desktop app for Hound Antivirus.
|
||||||
|
//!
|
||||||
|
//! The window is a thin view over the same `houndd` Unix socket the CLI uses.
|
||||||
|
//! The system-tray sentinel swaps between the four state icons:
|
||||||
|
//!
|
||||||
|
//! protected (green) / scanning (amber) / threat (red) / paused (gray)
|
||||||
|
|
||||||
|
use hound_api::{Client, ScanResult, Status, UpdateResult};
|
||||||
|
use serde_json::json;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use tauri::image::Image;
|
||||||
|
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
|
||||||
|
use tauri::tray::{TrayIconBuilder, TrayIconEvent};
|
||||||
|
use tauri::{Emitter, Manager, State};
|
||||||
|
|
||||||
|
type R<T> = anyhow::Result<T>;
|
||||||
|
|
||||||
|
const TRAY_ID: &str = "hound-tray";
|
||||||
|
|
||||||
|
/// States the tray can render. Anything unknown falls back to `protected`.
|
||||||
|
const STATES: [&str; 4] = ["protected", "scanning", "threat", "paused"];
|
||||||
|
|
||||||
|
/// The four preloaded state icons, managed so tray swaps never hit disk.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct TrayIcons(HashMap<String, Image<'static>>);
|
||||||
|
|
||||||
|
fn client() -> Client {
|
||||||
|
Client::default_path()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Commands (window → daemon) ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn status() -> Result<Status, String> {
|
||||||
|
let c = client();
|
||||||
|
tauri::async_runtime::spawn_blocking(move || c.status())
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn scan(path: String, recursive: bool) -> Result<ScanResult, String> {
|
||||||
|
let c = client();
|
||||||
|
tauri::async_runtime::spawn_blocking(move || c.scan(&path, recursive))
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn update() -> Result<UpdateResult, String> {
|
||||||
|
let c = client();
|
||||||
|
tauri::async_runtime::spawn_blocking(move || c.update())
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Swap the tray icon + tooltip. The window calls this to keep the sentinel
|
||||||
|
/// in step with what the user is doing (e.g. it starts a scan).
|
||||||
|
#[tauri::command]
|
||||||
|
fn set_state(
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
icons: State<'_, TrayIcons>,
|
||||||
|
state: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let state = if STATES.contains(&state.as_str()) {
|
||||||
|
state
|
||||||
|
} else {
|
||||||
|
"protected".into()
|
||||||
|
};
|
||||||
|
let img = icons.0.get(&state).cloned().unwrap_or_else(|| {
|
||||||
|
icons
|
||||||
|
.0
|
||||||
|
.get("protected")
|
||||||
|
.cloned()
|
||||||
|
.expect("protected icon always loaded")
|
||||||
|
});
|
||||||
|
if let Some(tray) = app.tray_by_id(TRAY_ID) {
|
||||||
|
let _ = tray.set_icon(Some(img));
|
||||||
|
let _ = tray.set_tooltip(Some(tooltip_for(&state)));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tooltip_for(state: &str) -> String {
|
||||||
|
match state {
|
||||||
|
"threat" => "Hound — threat found".into(),
|
||||||
|
"scanning" => "Hound — scanning…".into(),
|
||||||
|
"paused" => "Hound — paused".into(),
|
||||||
|
_ => "Hound — protected".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Icon resolution ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn icon_dir(app: &tauri::AppHandle) -> PathBuf {
|
||||||
|
// Packaged: resources dir. Dev: the repo's assets/icons.
|
||||||
|
if let Ok(dir) = app.path().resource_dir() {
|
||||||
|
let d = dir.join("icons");
|
||||||
|
if d.join("state-protected-22.png").exists() {
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for candidate in [
|
||||||
|
PathBuf::from("icons"),
|
||||||
|
PathBuf::from("../src-tauri/icons"),
|
||||||
|
PathBuf::from("../assets/icons"),
|
||||||
|
] {
|
||||||
|
if candidate.join("state-protected-22.png").exists() {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PathBuf::from("icons")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state_icons(app: &tauri::AppHandle) -> R<HashMap<String, Image<'static>>> {
|
||||||
|
let dir = icon_dir(app);
|
||||||
|
let mut icons = HashMap::new();
|
||||||
|
for state in STATES {
|
||||||
|
let path = dir.join(format!("state-{state}-22.png"));
|
||||||
|
icons.insert(state.to_string(), load_icon(&path)?);
|
||||||
|
}
|
||||||
|
Ok(icons)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load a PNG tray icon; Tauri decodes and converts to RGBA for us.
|
||||||
|
fn load_icon(path: &Path) -> R<Image<'static>> {
|
||||||
|
Image::from_path(path).map_err(|e| anyhow::anyhow!("loading tray icon {}: {e}", path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── App ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub fn run() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_dialog::init())
|
||||||
|
.plugin(tauri_plugin_opener::init())
|
||||||
|
.invoke_handler(tauri::generate_handler![status, scan, update, set_state])
|
||||||
|
.setup(|app| {
|
||||||
|
let handle = app.handle().clone();
|
||||||
|
let icons = TrayIcons(load_state_icons(&handle)?);
|
||||||
|
let initial_icon = icons
|
||||||
|
.0
|
||||||
|
.get("protected")
|
||||||
|
.expect("protected icon loaded")
|
||||||
|
.clone();
|
||||||
|
app.manage(icons);
|
||||||
|
|
||||||
|
let open = MenuItem::with_id(&handle, "open", "Open Hound", true, None::<&str>)?;
|
||||||
|
let scan_home =
|
||||||
|
MenuItem::with_id(&handle, "scan-home", "Scan Home Folder", true, None::<&str>)?;
|
||||||
|
let scan_downloads = MenuItem::with_id(
|
||||||
|
&handle,
|
||||||
|
"scan-downloads",
|
||||||
|
"Scan Downloads",
|
||||||
|
true,
|
||||||
|
None::<&str>,
|
||||||
|
)?;
|
||||||
|
let update_sig =
|
||||||
|
MenuItem::with_id(&handle, "update", "Update Signatures", true, None::<&str>)?;
|
||||||
|
let sep = PredefinedMenuItem::separator(&handle)?;
|
||||||
|
let quit = MenuItem::with_id(&handle, "quit", "Quit", true, None::<&str>)?;
|
||||||
|
let menu = Menu::with_items(
|
||||||
|
&handle,
|
||||||
|
&[&open, &scan_home, &scan_downloads, &update_sig, &sep, &quit],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let _ = TrayIconBuilder::with_id(TRAY_ID)
|
||||||
|
.icon(initial_icon)
|
||||||
|
.tooltip("Hound — protected")
|
||||||
|
.menu(&menu)
|
||||||
|
.show_menu_on_left_click(false)
|
||||||
|
.on_menu_event(|app, event| {
|
||||||
|
let id = event.id().as_ref();
|
||||||
|
let window = match app.get_webview_window("main") {
|
||||||
|
Some(w) => w,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
match id {
|
||||||
|
"open" => {
|
||||||
|
let _ = window.show();
|
||||||
|
let _ = window.set_focus();
|
||||||
|
}
|
||||||
|
"quit" => app.exit(0),
|
||||||
|
"scan-home" => {
|
||||||
|
let _ =
|
||||||
|
window.emit("tray-event", json!({ "action": "scan", "path": "~" }));
|
||||||
|
}
|
||||||
|
"scan-downloads" => {
|
||||||
|
let _ = window.emit(
|
||||||
|
"tray-event",
|
||||||
|
json!({ "action": "scan", "path": "~/Downloads" }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"update" => {
|
||||||
|
let _ = window.emit("tray-event", json!({ "action": "update" }));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_tray_icon_event(|tray, event| {
|
||||||
|
if let TrayIconEvent::Click { .. } = event {
|
||||||
|
let app = tray.app_handle();
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
let _ = window.show();
|
||||||
|
let _ = window.set_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build(&handle)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running Hound");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
run()
|
||||||
|
}
|
||||||
43
gui/src-tauri/tauri.conf.json
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "Hound Antivirus",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"identifier": "com.joelovestech.hound",
|
||||||
|
"build": {
|
||||||
|
"frontendDist": "../dist",
|
||||||
|
"devUrl": "http://localhost:1420",
|
||||||
|
"beforeDevCommand": "",
|
||||||
|
"beforeBuildCommand": ""
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "Hound Antivirus",
|
||||||
|
"width": 940,
|
||||||
|
"height": 640,
|
||||||
|
"minWidth": 760,
|
||||||
|
"minHeight": 520,
|
||||||
|
"resizable": true,
|
||||||
|
"center": true,
|
||||||
|
"fullscreen": false,
|
||||||
|
"decorations": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
},
|
||||||
|
"trayIcon": {
|
||||||
|
"iconPath": "icons/state-protected-22.png",
|
||||||
|
"iconAsTemplate": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": ["app", "deb"],
|
||||||
|
"icon": [
|
||||||
|
"icons/hound-48.png",
|
||||||
|
"icons/state-protected-32.png"
|
||||||
|
],
|
||||||
|
"resources": ["icons/state-protected-22.png", "icons/state-scanning-22.png", "icons/state-threat-22.png", "icons/state-paused-22.png"]
|
||||||
|
}
|
||||||
|
}
|
||||||