mirror of
https://github.com/PR0M3TH3AN/Marlin.git
synced 2025-09-08 07:08:44 +00:00

Enhance the GitHub Actions CI workflow by introducing a comprehensive test job that runs a new script (`run_all_tests.sh`) for building, testing, and benchmarking. Update dependencies to use the latest actions and ensure consistent environment variables. Remove the previous build-and-test and benchmark jobs, consolidating functionality for improved clarity and efficiency.
671 lines
619 KiB
HTML
671 lines
619 KiB
HTML
<!doctype html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<style>html, body {
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
.app {
|
||
margin: 10px;
|
||
padding: 0;
|
||
}
|
||
|
||
.files-list {
|
||
margin: 10px 0 0;
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
.files-list__head {
|
||
border: 1px solid #999;
|
||
}
|
||
.files-list__head > tr > th {
|
||
padding: 10px;
|
||
border: 1px solid #999;
|
||
text-align: left;
|
||
font-weight: normal;
|
||
background: #ddd;
|
||
}
|
||
.files-list__body {
|
||
}
|
||
.files-list__file {
|
||
cursor: pointer;
|
||
}
|
||
.files-list__file:hover {
|
||
background: #ccf;
|
||
}
|
||
.files-list__file > td {
|
||
padding: 10px;
|
||
border: 1px solid #999;
|
||
}
|
||
.files-list__file > td:first-child::before {
|
||
content: '\01F4C4';
|
||
margin-right: 1em;
|
||
}
|
||
.files-list__file_low {
|
||
background: #fcc;
|
||
}
|
||
.files-list__file_medium {
|
||
background: #ffc;
|
||
}
|
||
.files-list__file_high {
|
||
background: #cfc;
|
||
}
|
||
.files-list__file_folder > td:first-child::before {
|
||
content: '\01F4C1';
|
||
margin-right: 1em;
|
||
}
|
||
|
||
.file-header {
|
||
border: 1px solid #999;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
position: sticky;
|
||
top: 0;
|
||
background: white;
|
||
}
|
||
|
||
.file-header__back {
|
||
margin: 10px;
|
||
cursor: pointer;
|
||
flex-shrink: 0;
|
||
flex-grow: 0;
|
||
text-decoration: underline;
|
||
color: #338;
|
||
}
|
||
|
||
.file-header__name {
|
||
margin: 10px;
|
||
flex-shrink: 2;
|
||
flex-grow: 2;
|
||
}
|
||
|
||
.file-header__stat {
|
||
margin: 10px;
|
||
flex-shrink: 0;
|
||
flex-grow: 0;
|
||
}
|
||
|
||
.file-content {
|
||
margin: 10px 0 0;
|
||
border: 1px solid #999;
|
||
padding: 10px;
|
||
counter-reset: line;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.code-line::before {
|
||
content: counter(line);
|
||
margin-right: 10px;
|
||
}
|
||
.code-line {
|
||
margin: 0;
|
||
padding: 0.3em;
|
||
height: 1em;
|
||
counter-increment: line;
|
||
}
|
||
.code-line_covered {
|
||
background: #cfc;
|
||
}
|
||
.code-line_uncovered {
|
||
background: #fcc;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="root"></div>
|
||
<script>
|
||
var data = {"files":[{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","build.rs"],"content":"// cli-bin/build.rs\n//\n// The CLI currently needs no build-time code-generation, but Cargo\n// insists on rerunning any build-script each compile. Tell it to\n// rebuild only if this file itself changes.\n\nfn main() {\n // If you later add code-gen (e.g. embed completions or YAML), add\n // further `cargo:rerun-if-changed=\u003cpath\u003e` lines here.\n println!(\"cargo:rerun-if-changed=build.rs\");\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","src","cli","annotate.rs"],"content":"// src/cli/annotate.rs\nuse clap::{Subcommand, Args};\nuse rusqlite::Connection;\nuse crate::cli::Format;\n\n#[derive(Subcommand, Debug)]\npub enum AnnotateCmd {\n Add (ArgsAdd),\n List(ArgsList),\n}\n\n#[derive(Args, Debug)]\npub struct ArgsAdd {\n pub file: String,\n pub note: String,\n #[arg(long)] pub range: Option\u003cString\u003e,\n #[arg(long)] pub highlight: bool,\n}\n\n#[derive(Args, Debug)]\npub struct ArgsList { pub file_pattern: String }\n\npub fn run(cmd: \u0026AnnotateCmd, _conn: \u0026mut Connection, _format: Format) -\u003e anyhow::Result\u003c()\u003e {\n match cmd {\n AnnotateCmd::Add(a) =\u003e todo!(\"annotate add {:?}\", a),\n AnnotateCmd::List(a) =\u003e todo!(\"annotate list {:?}\", a),\n }\n}\n","traces":[{"line":23,"address":[2450848],"length":1,"stats":{"Line":0}},{"line":24,"address":[2450887],"length":1,"stats":{"Line":0}}],"covered":0,"coverable":2},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","src","cli","coll.rs"],"content":"//! `marlin coll …` – named collections of files (simple “playlists”).\n\nuse clap::{Args, Subcommand};\nuse rusqlite::Connection;\n\nuse crate::cli::Format; // local enum for text / json output\nuse libmarlin::db; // core DB helpers from the library crate\n\n#[derive(Subcommand, Debug)]\npub enum CollCmd {\n /// Create an empty collection\n Create(CreateArgs),\n /// Add files (glob) to a collection\n Add(AddArgs),\n /// List files inside a collection\n List(ListArgs),\n}\n\n#[derive(Args, Debug)]\npub struct CreateArgs {\n pub name: String,\n}\n\n#[derive(Args, Debug)]\npub struct AddArgs {\n pub name: String,\n pub file_pattern: String,\n}\n\n#[derive(Args, Debug)]\npub struct ListArgs {\n pub name: String,\n}\n\n/// Look-up an existing collection **without** implicitly creating it.\n///\n/// Returns the collection ID or an error if it doesn’t exist.\nfn lookup_collection_id(conn: \u0026Connection, name: \u0026str) -\u003e anyhow::Result\u003ci64\u003e {\n conn.query_row(\n \"SELECT id FROM collections WHERE name = ?1\",\n [name],\n |r| r.get(0),\n )\n .map_err(|_| anyhow::anyhow!(\"collection not found: {}\", name))\n}\n\npub fn run(cmd: \u0026CollCmd, conn: \u0026mut Connection, fmt: Format) -\u003e anyhow::Result\u003c()\u003e {\n match cmd {\n /* ── coll create ──────────────────────────────────────────── */\n CollCmd::Create(a) =\u003e {\n db::ensure_collection(conn, \u0026a.name)?;\n if matches!(fmt, Format::Text) {\n println!(\"Created collection '{}'\", a.name);\n }\n }\n\n /* ── coll add ─────────────────────────────────────────────── */\n CollCmd::Add(a) =\u003e {\n // Fail if the target collection does not yet exist\n let coll_id = lookup_collection_id(conn, \u0026a.name)?;\n\n let like = a.file_pattern.replace('*', \"%\");\n let mut stmt = conn.prepare(\"SELECT id FROM files WHERE path LIKE ?1\")?;\n let ids: Vec\u003ci64\u003e = stmt\n .query_map([\u0026like], |r| r.get::\u003c_, i64\u003e(0))?\n .collect::\u003cResult\u003c_, _\u003e\u003e()?;\n\n for fid in \u0026ids {\n db::add_file_to_collection(conn, coll_id, *fid)?;\n }\n\n match fmt {\n Format::Text =\u003e println!(\"Added {} file(s) → '{}'\", ids.len(), a.name),\n Format::Json =\u003e {\n #[cfg(feature = \"json\")]\n {\n println!(\n \"{{\\\"collection\\\":\\\"{}\\\",\\\"added\\\":{}}}\",\n a.name,\n ids.len()\n );\n }\n }\n }\n }\n\n /* ── coll list ────────────────────────────────────────────── */\n CollCmd::List(a) =\u003e {\n let files = db::list_collection(conn, \u0026a.name)?;\n match fmt {\n Format::Text =\u003e {\n for f in files {\n println!(\"{f}\");\n }\n }\n Format::Json =\u003e {\n #[cfg(feature = \"json\")]\n {\n println!(\"{}\", serde_json::to_string(\u0026files)?);\n }\n }\n }\n }\n }\n Ok(())\n}\n","traces":[{"line":38,"address":[2169520],"length":1,"stats":{"Line":0}},{"line":39,"address":[2169565],"length":1,"stats":{"Line":0}},{"line":41,"address":[2169545],"length":1,"stats":{"Line":0}},{"line":42,"address":[2296672,2296656],"length":1,"stats":{"Line":0}},{"line":44,"address":[2296704,2296721],"length":1,"stats":{"Line":0}},{"line":47,"address":[2169632,2171921,2171991],"length":1,"stats":{"Line":0}},{"line":48,"address":[2169683],"length":1,"stats":{"Line":0}},{"line":50,"address":[2169766],"length":1,"stats":{"Line":0}},{"line":51,"address":[2169783,2170057],"length":1,"stats":{"Line":0}},{"line":52,"address":[2170122],"length":1,"stats":{"Line":0}},{"line":53,"address":[2170138],"length":1,"stats":{"Line":0}},{"line":58,"address":[2169858],"length":1,"stats":{"Line":0}},{"line":60,"address":[2170255,2169866],"length":1,"stats":{"Line":0}},{"line":62,"address":[2170320],"length":1,"stats":{"Line":0}},{"line":63,"address":[2170443,2170372,2171981],"length":1,"stats":{"Line":0}},{"line":64,"address":[2170775,2171932,2170882,2170990,2171195],"length":1,"stats":{"Line":0}},{"line":65,"address":[2170767,2170926],"length":1,"stats":{"Line":0}},{"line":68,"address":[2171293,2171376],"length":1,"stats":{"Line":0}},{"line":69,"address":[2171486,2171767],"length":1,"stats":{"Line":0}},{"line":72,"address":[2171515],"length":1,"stats":{"Line":0}},{"line":73,"address":[2171558],"length":1,"stats":{"Line":0}},{"line":88,"address":[2169940],"length":1,"stats":{"Line":0}},{"line":89,"address":[2169952,2172004,2172061],"length":1,"stats":{"Line":0}},{"line":90,"address":[2172165],"length":1,"stats":{"Line":0}},{"line":92,"address":[2172197,2172440,2172305],"length":1,"stats":{"Line":0}},{"line":93,"address":[2172509,2172602],"length":1,"stats":{"Line":0}},{"line":105,"address":[2170227],"length":1,"stats":{"Line":0}}],"covered":0,"coverable":27},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","src","cli","event.rs"],"content":"// src/cli/event.rs\nuse clap::{Subcommand, Args};\nuse rusqlite::Connection;\nuse crate::cli::Format;\n\n#[derive(Subcommand, Debug)]\npub enum EventCmd {\n Add (ArgsAdd),\n Timeline,\n}\n\n#[derive(Args, Debug)]\npub struct ArgsAdd {\n pub file: String,\n pub date: String,\n pub description: String,\n}\n\npub fn run(cmd: \u0026EventCmd, _conn: \u0026mut Connection, _format: Format) -\u003e anyhow::Result\u003c()\u003e {\n match cmd {\n EventCmd::Add(a) =\u003e todo!(\"event add {:?}\", a),\n EventCmd::Timeline =\u003e todo!(\"event timeline\"),\n }\n}\n","traces":[{"line":19,"address":[2433520],"length":1,"stats":{"Line":0}},{"line":20,"address":[2433559],"length":1,"stats":{"Line":0}}],"covered":0,"coverable":2},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","src","cli","link.rs"],"content":"//! src/cli/link.rs – manage typed relationships between files\n\nuse clap::{Subcommand, Args};\nuse rusqlite::Connection;\n\nuse crate::cli::Format; // output selector\nuse libmarlin::db; // ← switched from `crate::db`\n\n#[derive(Subcommand, Debug)]\npub enum LinkCmd {\n Add(LinkArgs),\n Rm (LinkArgs),\n List(ListArgs),\n Backlinks(BacklinksArgs),\n}\n\n#[derive(Args, Debug)]\npub struct LinkArgs {\n pub from: String,\n pub to: String,\n #[arg(long)]\n pub r#type: Option\u003cString\u003e,\n}\n\n#[derive(Args, Debug)]\npub struct ListArgs {\n pub pattern: String,\n #[arg(long)]\n pub direction: Option\u003cString\u003e,\n #[arg(long)]\n pub r#type: Option\u003cString\u003e,\n}\n\n#[derive(Args, Debug)]\npub struct BacklinksArgs {\n pub pattern: String,\n}\n\npub fn run(cmd: \u0026LinkCmd, conn: \u0026mut Connection, format: Format) -\u003e anyhow::Result\u003c()\u003e {\n match cmd {\n LinkCmd::Add(args) =\u003e {\n let src_id = db::file_id(conn, \u0026args.from)?;\n let dst_id = db::file_id(conn, \u0026args.to)?;\n db::add_link(conn, src_id, dst_id, args.r#type.as_deref())?;\n match format {\n Format::Text =\u003e {\n if let Some(t) = \u0026args.r#type {\n println!(\"Linked '{}' → '{}' [type='{}']\", args.from, args.to, t);\n } else {\n println!(\"Linked '{}' → '{}'\", args.from, args.to);\n }\n }\n Format::Json =\u003e {\n let typ = args\n .r#type\n .as_ref()\n .map(|s| format!(\"\\\"{}\\\"\", s))\n .unwrap_or_else(|| \"null\".into());\n println!(\n \"{{\\\"from\\\":\\\"{}\\\",\\\"to\\\":\\\"{}\\\",\\\"type\\\":{}}}\",\n args.from, args.to, typ\n );\n }\n }\n }\n LinkCmd::Rm(args) =\u003e {\n let src_id = db::file_id(conn, \u0026args.from)?;\n let dst_id = db::file_id(conn, \u0026args.to)?;\n db::remove_link(conn, src_id, dst_id, args.r#type.as_deref())?;\n match format {\n Format::Text =\u003e {\n if let Some(t) = \u0026args.r#type {\n println!(\"Removed link '{}' → '{}' [type='{}']\", args.from, args.to, t);\n } else {\n println!(\"Removed link '{}' → '{}'\", args.from, args.to);\n }\n }\n Format::Json =\u003e {\n let typ = args\n .r#type\n .as_ref()\n .map(|s| format!(\"\\\"{}\\\"\", s))\n .unwrap_or_else(|| \"null\".into());\n println!(\n \"{{\\\"from\\\":\\\"{}\\\",\\\"to\\\":\\\"{}\\\",\\\"type\\\":{}}}\",\n args.from, args.to, typ\n );\n }\n }\n }\n LinkCmd::List(args) =\u003e {\n let results = db::list_links(\n conn,\n \u0026args.pattern,\n args.direction.as_deref(),\n args.r#type.as_deref(),\n )?;\n match format {\n Format::Json =\u003e {\n let items: Vec\u003cString\u003e = results\n .into_iter()\n .map(|(src, dst, t)| {\n let typ = t\n .as_ref()\n .map(|s| format!(\"\\\"{}\\\"\", s))\n .unwrap_or_else(|| \"null\".into());\n format!(\n \"{{\\\"from\\\":\\\"{}\\\",\\\"to\\\":\\\"{}\\\",\\\"type\\\":{}}}\",\n src, dst, typ\n )\n })\n .collect();\n println!(\"[{}]\", items.join(\",\"));\n }\n Format::Text =\u003e {\n for (src, dst, t) in results {\n if let Some(t) = t {\n println!(\"{} → {} [type='{}']\", src, dst, t);\n } else {\n println!(\"{} → {}\", src, dst);\n }\n }\n }\n }\n }\n LinkCmd::Backlinks(args) =\u003e {\n let results = db::find_backlinks(conn, \u0026args.pattern)?;\n match format {\n Format::Json =\u003e {\n let items: Vec\u003cString\u003e = results\n .into_iter()\n .map(|(src, t)| {\n let typ = t\n .as_ref()\n .map(|s| format!(\"\\\"{}\\\"\", s))\n .unwrap_or_else(|| \"null\".into());\n format!(\"{{\\\"from\\\":\\\"{}\\\",\\\"type\\\":{}}}\", src, typ)\n })\n .collect();\n println!(\"[{}]\", items.join(\",\"));\n }\n Format::Text =\u003e {\n for (src, t) in results {\n if let Some(t) = t {\n println!(\"{} [type='{}']\", src, t);\n } else {\n println!(\"{}\", src);\n }\n }\n }\n }\n }\n }\n\n Ok(())\n}\n","traces":[{"line":39,"address":[2065785,2063952,2065779],"length":1,"stats":{"Line":0}},{"line":40,"address":[2064009],"length":1,"stats":{"Line":0}},{"line":41,"address":[2064086],"length":1,"stats":{"Line":0}},{"line":42,"address":[2064644,2064106],"length":1,"stats":{"Line":0}},{"line":43,"address":[2064715],"length":1,"stats":{"Line":0}},{"line":44,"address":[2064854],"length":1,"stats":{"Line":0}},{"line":45,"address":[2064991],"length":1,"stats":{"Line":0}},{"line":47,"address":[2065109],"length":1,"stats":{"Line":0}},{"line":48,"address":[2065179],"length":1,"stats":{"Line":0}},{"line":50,"address":[2065393],"length":1,"stats":{"Line":0}},{"line":54,"address":[2065012],"length":1,"stats":{"Line":0}},{"line":57,"address":[2422784,2422805],"length":1,"stats":{"Line":0}},{"line":58,"address":[2422924,2422912],"length":1,"stats":{"Line":0}},{"line":59,"address":[2065079,2065609],"length":1,"stats":{"Line":0}},{"line":66,"address":[2064187],"length":1,"stats":{"Line":0}},{"line":67,"address":[2064207,2065814],"length":1,"stats":{"Line":0}},{"line":68,"address":[2065882],"length":1,"stats":{"Line":0}},{"line":69,"address":[2066021],"length":1,"stats":{"Line":0}},{"line":70,"address":[2066158],"length":1,"stats":{"Line":0}},{"line":72,"address":[2066276],"length":1,"stats":{"Line":0}},{"line":73,"address":[2066346],"length":1,"stats":{"Line":0}},{"line":75,"address":[2066560],"length":1,"stats":{"Line":0}},{"line":79,"address":[2066179],"length":1,"stats":{"Line":0}},{"line":82,"address":[2422981,2422960],"length":1,"stats":{"Line":0}},{"line":83,"address":[2423100,2423088],"length":1,"stats":{"Line":0}},{"line":84,"address":[2066764,2066246],"length":1,"stats":{"Line":0}},{"line":91,"address":[2064288],"length":1,"stats":{"Line":0}},{"line":94,"address":[2064308],"length":1,"stats":{"Line":0}},{"line":95,"address":[2064337],"length":1,"stats":{"Line":0}},{"line":96,"address":[2064371],"length":1,"stats":{"Line":0}},{"line":98,"address":[2067095],"length":1,"stats":{"Line":0}},{"line":100,"address":[2068413,2067108],"length":1,"stats":{"Line":0}},{"line":102,"address":[2423161,2423710,2423136],"length":1,"stats":{"Line":0}},{"line":103,"address":[2423214,2423289],"length":1,"stats":{"Line":0}},{"line":105,"address":[2423765,2423744],"length":1,"stats":{"Line":0}},{"line":106,"address":[2423884,2423872],"length":1,"stats":{"Line":0}},{"line":107,"address":[2423323,2423385],"length":1,"stats":{"Line":0}},{"line":113,"address":[2068526,2068443],"length":1,"stats":{"Line":0}},{"line":116,"address":[2067174,2067278,2067405],"length":1,"stats":{"Line":0}},{"line":117,"address":[2067665,2067598],"length":1,"stats":{"Line":0}},{"line":118,"address":[2067806,2067705],"length":1,"stats":{"Line":0}},{"line":120,"address":[2068085,2067732],"length":1,"stats":{"Line":0}},{"line":126,"address":[2064525],"length":1,"stats":{"Line":0}},{"line":127,"address":[2068818,2064537,2068761],"length":1,"stats":{"Line":0}},{"line":128,"address":[2068922],"length":1,"stats":{"Line":0}},{"line":130,"address":[2068935,2070060],"length":1,"stats":{"Line":0}},{"line":132,"address":[2423920,2423945,2424376],"length":1,"stats":{"Line":0}},{"line":133,"address":[2424052,2423980],"length":1,"stats":{"Line":0}},{"line":135,"address":[2424400,2424421],"length":1,"stats":{"Line":0}},{"line":136,"address":[2424540,2424528],"length":1,"stats":{"Line":0}},{"line":137,"address":[2424083,2424145],"length":1,"stats":{"Line":0}},{"line":140,"address":[2070090,2070173],"length":1,"stats":{"Line":0}},{"line":143,"address":[2069248,2069113,2069005],"length":1,"stats":{"Line":0}},{"line":144,"address":[2069464,2069393],"length":1,"stats":{"Line":0}},{"line":145,"address":[2069504,2069605],"length":1,"stats":{"Line":0}},{"line":147,"address":[2069531,2069841],"length":1,"stats":{"Line":0}},{"line":155,"address":[2065540],"length":1,"stats":{"Line":0}}],"covered":0,"coverable":57},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","src","cli","remind.rs"],"content":"// src/cli/remind.rs\nuse clap::{Subcommand, Args};\nuse rusqlite::Connection;\nuse crate::cli::Format;\n\n#[derive(Subcommand, Debug)]\npub enum RemindCmd {\n Set(ArgsSet),\n}\n\n#[derive(Args, Debug)]\npub struct ArgsSet {\n pub file_pattern: String,\n pub timestamp: String,\n pub message: String,\n}\n\npub fn run(cmd: \u0026RemindCmd, _conn: \u0026mut Connection, _format: Format) -\u003e anyhow::Result\u003c()\u003e {\n match cmd {\n RemindCmd::Set(a) =\u003e todo!(\"remind set {:?}\", a),\n }\n}\n","traces":[{"line":18,"address":[2142944],"length":1,"stats":{"Line":0}}],"covered":0,"coverable":1},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","src","cli","state.rs"],"content":"// src/cli/state.rs\nuse clap::{Subcommand, Args};\nuse rusqlite::Connection;\nuse crate::cli::Format;\n\n#[derive(Subcommand, Debug)]\npub enum StateCmd {\n Set(ArgsSet),\n TransitionsAdd(ArgsTrans),\n Log(ArgsLog),\n}\n\n#[derive(Args, Debug)]\npub struct ArgsSet { pub file_pattern: String, pub new_state: String }\n#[derive(Args, Debug)]\npub struct ArgsTrans { pub from_state: String, pub to_state: String }\n#[derive(Args, Debug)]\npub struct ArgsLog { pub file_pattern: String }\n\npub fn run(cmd: \u0026StateCmd, _conn: \u0026mut Connection, _format: Format) -\u003e anyhow::Result\u003c()\u003e {\n match cmd {\n StateCmd::Set(a) =\u003e todo!(\"state set {:?}\", a),\n StateCmd::TransitionsAdd(a)=\u003e todo!(\"state transitions-add {:?}\", a),\n StateCmd::Log(a) =\u003e todo!(\"state log {:?}\", a),\n }\n}\n","traces":[{"line":20,"address":[2474816],"length":1,"stats":{"Line":0}},{"line":21,"address":[2474855],"length":1,"stats":{"Line":0}}],"covered":0,"coverable":2},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","src","cli","task.rs"],"content":"// src/cli/task.rs\nuse clap::{Subcommand, Args};\nuse rusqlite::Connection;\nuse crate::cli::Format;\n\n#[derive(Subcommand, Debug)]\npub enum TaskCmd {\n Scan(ArgsScan),\n List(ArgsList),\n}\n\n#[derive(Args, Debug)]\npub struct ArgsScan { pub directory: String }\n#[derive(Args, Debug)]\npub struct ArgsList { #[arg(long)] pub due_today: bool }\n\npub fn run(cmd: \u0026TaskCmd, _conn: \u0026mut Connection, _format: Format) -\u003e anyhow::Result\u003c()\u003e {\n match cmd {\n TaskCmd::Scan(a) =\u003e todo!(\"task scan {:?}\", a),\n TaskCmd::List(a) =\u003e todo!(\"task list {:?}\", a),\n }\n}\n","traces":[{"line":17,"address":[2403248],"length":1,"stats":{"Line":0}},{"line":18,"address":[2403287],"length":1,"stats":{"Line":0}}],"covered":0,"coverable":2},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","src","cli","version.rs"],"content":"// src/cli/version.rs\nuse clap::{Subcommand, Args};\nuse rusqlite::Connection;\nuse crate::cli::Format;\n\n#[derive(Subcommand, Debug)]\npub enum VersionCmd {\n Diff(ArgsDiff),\n}\n\n#[derive(Args, Debug)]\npub struct ArgsDiff { pub file: String }\n\npub fn run(cmd: \u0026VersionCmd, _conn: \u0026mut Connection, _format: Format) -\u003e anyhow::Result\u003c()\u003e {\n match cmd {\n VersionCmd::Diff(a) =\u003e todo!(\"version diff {:?}\", a),\n }\n}\n","traces":[{"line":14,"address":[2314960],"length":1,"stats":{"Line":0}}],"covered":0,"coverable":1},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","src","cli","view.rs"],"content":"//! `marlin view …` – save \u0026 use “smart folders” (named queries).\n\nuse std::fs;\n\nuse anyhow::Result;\nuse clap::{Args, Subcommand};\nuse rusqlite::Connection;\n\nuse crate::cli::Format; // output selector stays local\nuse libmarlin::db; // ← path switched from `crate::db`\n\n#[derive(Subcommand, Debug)]\npub enum ViewCmd {\n /// Save (or update) a view\n Save(ArgsSave),\n /// List all saved views\n List,\n /// Execute a view (print matching paths)\n Exec(ArgsExec),\n}\n\n#[derive(Args, Debug)]\npub struct ArgsSave {\n pub view_name: String,\n pub query: String,\n}\n\n#[derive(Args, Debug)]\npub struct ArgsExec {\n pub view_name: String,\n}\n\npub fn run(cmd: \u0026ViewCmd, conn: \u0026mut Connection, fmt: Format) -\u003e anyhow::Result\u003c()\u003e {\n match cmd {\n /* ── view save ───────────────────────────────────────────── */\n ViewCmd::Save(a) =\u003e {\n db::save_view(conn, \u0026a.view_name, \u0026a.query)?;\n if matches!(fmt, Format::Text) {\n println!(\"Saved view '{}' = {}\", a.view_name, a.query);\n }\n }\n\n /* ── view list ───────────────────────────────────────────── */\n ViewCmd::List =\u003e {\n let views = db::list_views(conn)?;\n match fmt {\n Format::Text =\u003e {\n for (name, q) in views {\n println!(\"{name}: {q}\");\n }\n }\n Format::Json =\u003e {\n #[cfg(feature = \"json\")]\n {\n println!(\"{}\", serde_json::to_string(\u0026views)?);\n }\n }\n }\n }\n\n /* ── view exec ───────────────────────────────────────────── */\n ViewCmd::Exec(a) =\u003e {\n let raw = db::view_query(conn, \u0026a.view_name)?;\n\n // Re-use the tiny parser from marlin search\n let fts_expr = build_fts_match(\u0026raw);\n\n let mut stmt = conn.prepare(\n r#\"\n SELECT f.path\n FROM files_fts\n JOIN files f ON f.rowid = files_fts.rowid\n WHERE files_fts MATCH ?1\n ORDER BY rank\n \"#,\n )?;\n let mut paths: Vec\u003cString\u003e = stmt\n .query_map([fts_expr], |r| r.get::\u003c_, String\u003e(0))?\n .collect::\u003cResult\u003c_, _\u003e\u003e()?;\n\n /* ── NEW: graceful fallback when FTS finds nothing ───── */\n if paths.is_empty() \u0026\u0026 !raw.contains(':') {\n paths = naive_search(conn, \u0026raw)?;\n }\n\n if paths.is_empty() \u0026\u0026 matches!(fmt, Format::Text) {\n eprintln!(\"(view '{}' has no matches)\", a.view_name);\n } else {\n for p in paths {\n println!(\"{p}\");\n }\n }\n }\n }\n Ok(())\n}\n\n/* ─── naive substring path/content search (≤ 64 kB files) ───────── */\n\nfn naive_search(conn: \u0026Connection, term: \u0026str) -\u003e Result\u003cVec\u003cString\u003e\u003e {\n let term_lc = term.to_lowercase();\n let mut stmt = conn.prepare(\"SELECT path FROM files\")?;\n let rows = stmt.query_map([], |r| r.get::\u003c_, String\u003e(0))?;\n\n let mut hits = Vec::new();\n for p in rows {\n let p = p?;\n /* path match */\n if p.to_lowercase().contains(\u0026term_lc) {\n hits.push(p);\n continue;\n }\n /* small-file content match */\n if let Ok(meta) = fs::metadata(\u0026p) {\n if meta.len() \u003e 64_000 {\n continue;\n }\n }\n if let Ok(content) = fs::read_to_string(\u0026p) {\n if content.to_lowercase().contains(\u0026term_lc) {\n hits.push(p);\n }\n }\n }\n Ok(hits)\n}\n\n/* ─── minimal copy of search-string → FTS5 translator ───────────── */\n\nfn build_fts_match(raw_query: \u0026str) -\u003e String {\n use shlex;\n let mut parts = Vec::new();\n let toks = shlex::split(raw_query).unwrap_or_else(|| vec![raw_query.to_string()]);\n for tok in toks {\n if [\"AND\", \"OR\", \"NOT\"].contains(\u0026tok.as_str()) {\n parts.push(tok);\n } else if let Some(tag) = tok.strip_prefix(\"tag:\") {\n for (i, seg) in tag.split('/').filter(|s| !s.is_empty()).enumerate() {\n if i \u003e 0 {\n parts.push(\"AND\".into());\n }\n parts.push(format!(\"tags_text:{}\", escape(seg)));\n }\n } else if let Some(attr) = tok.strip_prefix(\"attr:\") {\n let mut kv = attr.splitn(2, '=');\n let key = kv.next().unwrap();\n if let Some(val) = kv.next() {\n parts.push(format!(\"attrs_text:{}\", escape(key)));\n parts.push(\"AND\".into());\n parts.push(format!(\"attrs_text:{}\", escape(val)));\n } else {\n parts.push(format!(\"attrs_text:{}\", escape(key)));\n }\n } else {\n parts.push(escape(\u0026tok));\n }\n }\n parts.join(\" \")\n}\n\nfn escape(term: \u0026str) -\u003e String {\n if term.contains(|c: char| c.is_whitespace() || \"-:()\\\"\".contains(c))\n || [\"AND\", \"OR\", \"NOT\", \"NEAR\"].contains(\u0026term.to_uppercase().as_str())\n {\n format!(\"\\\"{}\\\"\", term.replace('\"', \"\\\"\\\"\"))\n } else {\n term.to_string()\n }\n}\n","traces":[{"line":33,"address":[2363246,2361648,2363240],"length":1,"stats":{"Line":0}},{"line":34,"address":[2361705],"length":1,"stats":{"Line":0}},{"line":36,"address":[2361821],"length":1,"stats":{"Line":0}},{"line":37,"address":[2362169,2361829],"length":1,"stats":{"Line":0}},{"line":38,"address":[2362218],"length":1,"stats":{"Line":0}},{"line":39,"address":[2362241],"length":1,"stats":{"Line":0}},{"line":45,"address":[2361956,2362416,2362470],"length":1,"stats":{"Line":0}},{"line":46,"address":[2362574],"length":1,"stats":{"Line":0}},{"line":48,"address":[2362849,2362714,2362606],"length":1,"stats":{"Line":0}},{"line":49,"address":[2363043,2362950],"length":1,"stats":{"Line":0}},{"line":62,"address":[2362045],"length":1,"stats":{"Line":0}},{"line":63,"address":[2362062,2363307],"length":1,"stats":{"Line":0}},{"line":66,"address":[2363500,2363421],"length":1,"stats":{"Line":0}},{"line":68,"address":[2363523,2363588,2365825,2363710],"length":1,"stats":{"Line":0}},{"line":77,"address":[2364175,2364067,2365777,2364384,2364000],"length":1,"stats":{"Line":0}},{"line":78,"address":[2096051,2096016],"length":1,"stats":{"Line":0}},{"line":82,"address":[2364598,2364490,2364558,2365008],"length":1,"stats":{"Line":0}},{"line":83,"address":[2364869,2364663,2365017],"length":1,"stats":{"Line":0}},{"line":86,"address":[2365078,2365161,2364564],"length":1,"stats":{"Line":0}},{"line":87,"address":[2365177],"length":1,"stats":{"Line":0}},{"line":89,"address":[2365084,2365421,2365286],"length":1,"stats":{"Line":0}},{"line":90,"address":[2365490,2365659],"length":1,"stats":{"Line":0}},{"line":95,"address":[2362388],"length":1,"stats":{"Line":0}},{"line":100,"address":[2367885,2365904,2368916],"length":1,"stats":{"Line":0}},{"line":101,"address":[2365985],"length":1,"stats":{"Line":0}},{"line":102,"address":[2368914,2366105,2366034],"length":1,"stats":{"Line":0}},{"line":103,"address":[2096115,2096080],"length":1,"stats":{"Line":0}},{"line":105,"address":[2366708],"length":1,"stats":{"Line":0}},{"line":106,"address":[2366768,2366936,2366860,2368590],"length":1,"stats":{"Line":0}},{"line":107,"address":[2367037,2367202],"length":1,"stats":{"Line":0}},{"line":109,"address":[2367488,2367420],"length":1,"stats":{"Line":0}},{"line":110,"address":[2368673],"length":1,"stats":{"Line":0}},{"line":114,"address":[2367657,2367767],"length":1,"stats":{"Line":0}},{"line":115,"address":[2367774,2367835],"length":1,"stats":{"Line":0}},{"line":119,"address":[2367982,2367891],"length":1,"stats":{"Line":0}},{"line":120,"address":[2368030,2368101],"length":1,"stats":{"Line":0}},{"line":121,"address":[2368289],"length":1,"stats":{"Line":0}},{"line":125,"address":[2367090],"length":1,"stats":{"Line":0}},{"line":130,"address":[2368944,2370638,2372260],"length":1,"stats":{"Line":0}},{"line":132,"address":[2368999],"length":1,"stats":{"Line":0}},{"line":133,"address":[2096144,2096178],"length":1,"stats":{"Line":0}},{"line":134,"address":[2369311,2369149,2372214],"length":1,"stats":{"Line":0}},{"line":135,"address":[2369396,2369611],"length":1,"stats":{"Line":0}},{"line":136,"address":[2372201,2369709],"length":1,"stats":{"Line":0}},{"line":137,"address":[2369802,2369686],"length":1,"stats":{"Line":0}},{"line":138,"address":[2096416,2096430],"length":1,"stats":{"Line":0}},{"line":139,"address":[2370204],"length":1,"stats":{"Line":0}},{"line":140,"address":[2370259],"length":1,"stats":{"Line":0}},{"line":142,"address":[2370252,2370333],"length":1,"stats":{"Line":0}},{"line":144,"address":[2370660,2369968],"length":1,"stats":{"Line":0}},{"line":145,"address":[2370782],"length":1,"stats":{"Line":0}},{"line":146,"address":[2370840],"length":1,"stats":{"Line":0}},{"line":147,"address":[2370949],"length":1,"stats":{"Line":0}},{"line":148,"address":[2371130,2371095],"length":1,"stats":{"Line":0}},{"line":149,"address":[2371430],"length":1,"stats":{"Line":0}},{"line":150,"address":[2371520],"length":1,"stats":{"Line":0}},{"line":152,"address":[2371832,2371120],"length":1,"stats":{"Line":0}},{"line":155,"address":[2370820,2372155],"length":1,"stats":{"Line":0}},{"line":158,"address":[2369441],"length":1,"stats":{"Line":0}},{"line":161,"address":[2372288,2372656,2372650],"length":1,"stats":{"Line":0}},{"line":162,"address":[2096492,2096464],"length":1,"stats":{"Line":0}},{"line":163,"address":[2372621,2372484,2372366],"length":1,"stats":{"Line":0}},{"line":165,"address":[2372417,2372669],"length":1,"stats":{"Line":0}},{"line":167,"address":[2372613],"length":1,"stats":{"Line":0}}],"covered":0,"coverable":64},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","src","cli","watch.rs"],"content":"// src/cli/watch.rs\n\nuse anyhow::Result;\nuse clap::Subcommand;\nuse libmarlin::watcher::{WatcherConfig, WatcherState};\nuse rusqlite::Connection;\nuse std::path::PathBuf;\nuse std::sync::Arc;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::thread;\nuse std::time::{Duration, Instant};\nuse tracing::info;\n\n/// Commands related to file watching functionality\n#[derive(Subcommand, Debug)]\npub enum WatchCmd {\n /// Start watching a directory for changes\n Start {\n /// Directory to watch (defaults to current directory)\n #[arg(default_value = \".\")]\n path: PathBuf,\n \n /// Debounce window in milliseconds (default: 100ms)\n #[arg(long, default_value = \"100\")]\n debounce_ms: u64,\n },\n \n /// Show status of currently active watcher\n Status,\n \n /// Stop the currently running watcher\n Stop,\n}\n\n/// Run a watch command\npub fn run(cmd: \u0026WatchCmd, _conn: \u0026mut Connection, _format: super::Format) -\u003e Result\u003c()\u003e {\n match cmd {\n WatchCmd::Start { path, debounce_ms } =\u003e {\n let mut marlin = libmarlin::Marlin::open_default()?;\n let config = WatcherConfig {\n debounce_ms: *debounce_ms,\n ..Default::default()\n };\n let canon_path = path.canonicalize().unwrap_or_else(|_| path.clone());\n info!(\"Starting watcher for directory: {}\", canon_path.display());\n\n let mut watcher = marlin.watch(\u0026canon_path, Some(config))?;\n \n let status = watcher.status();\n info!(\"Watcher started. Press Ctrl+C to stop watching.\");\n info!(\"Watching {} paths\", status.watched_paths.len());\n \n let start_time = Instant::now();\n let mut last_status_time = Instant::now();\n let running = Arc::new(AtomicBool::new(true));\n let r_clone = running.clone();\n\n ctrlc::set_handler(move || {\n info!(\"Ctrl+C received. Signaling watcher to stop...\");\n r_clone.store(false, Ordering::SeqCst);\n })?;\n\n info!(\"Watcher run loop started. Waiting for Ctrl+C or stop signal...\");\n while running.load(Ordering::SeqCst) {\n let current_status = watcher.status();\n if current_status.state == WatcherState::Stopped {\n info!(\"Watcher has stopped (detected by state). Exiting loop.\");\n break;\n }\n\n // Corrected line: removed the extra closing parenthesis\n if last_status_time.elapsed() \u003e Duration::from_secs(10) { \n let uptime = start_time.elapsed();\n info!(\n \"Watcher running for {}s, processed {} events, queue: {}, state: {:?}\",\n uptime.as_secs(),\n current_status.events_processed,\n current_status.queue_size,\n current_status.state\n );\n last_status_time = Instant::now();\n }\n thread::sleep(Duration::from_millis(200));\n }\n\n info!(\"Watcher run loop ended. Explicitly stopping watcher instance...\");\n watcher.stop()?; \n info!(\"Watcher instance fully stopped.\");\n Ok(())\n }\n WatchCmd::Status =\u003e {\n info!(\"Status command: No active watcher process to query in this CLI invocation model.\");\n info!(\"To see live status, run 'marlin watch start' which prints periodic updates.\");\n Ok(())\n }\n WatchCmd::Stop =\u003e {\n info!(\"Stop command: No active watcher process to stop in this CLI invocation model.\");\n info!(\"Please use Ctrl+C in the terminal where 'marlin watch start' is running.\");\n Ok(())\n }\n }\n}","traces":[{"line":36,"address":[2338224,2347011,2346893],"length":1,"stats":{"Line":0}},{"line":37,"address":[2338281],"length":1,"stats":{"Line":0}},{"line":38,"address":[2338369],"length":1,"stats":{"Line":0}},{"line":39,"address":[2338397,2338535],"length":1,"stats":{"Line":0}},{"line":41,"address":[2338841],"length":1,"stats":{"Line":0}},{"line":44,"address":[2056512,2056533],"length":1,"stats":{"Line":0}},{"line":45,"address":[2339509,2339079,2339158],"length":1,"stats":{"Line":0}},{"line":47,"address":[2339423,2346982,2340089],"length":1,"stats":{"Line":0}},{"line":49,"address":[2340273],"length":1,"stats":{"Line":0}},{"line":50,"address":[2340412,2340336,2340708],"length":1,"stats":{"Line":0}},{"line":51,"address":[2341420,2340677,2341126],"length":1,"stats":{"Line":0}},{"line":53,"address":[2341936,2341391],"length":1,"stats":{"Line":0}},{"line":54,"address":[2341951],"length":1,"stats":{"Line":0}},{"line":55,"address":[2342012],"length":1,"stats":{"Line":0}},{"line":56,"address":[2342111,2342190],"length":1,"stats":{"Line":0}},{"line":58,"address":[2056624],"length":1,"stats":{"Line":0}},{"line":59,"address":[2056644,2056834],"length":1,"stats":{"Line":0}},{"line":60,"address":[2056794],"length":1,"stats":{"Line":0}},{"line":63,"address":[2342642,2342312],"length":1,"stats":{"Line":0}},{"line":64,"address":[2342612,2343061],"length":1,"stats":{"Line":0}},{"line":65,"address":[2343135],"length":1,"stats":{"Line":0}},{"line":66,"address":[2343162,2343242],"length":1,"stats":{"Line":0}},{"line":67,"address":[2343282,2344873,2344586],"length":1,"stats":{"Line":0}},{"line":72,"address":[2343248,2343328,2344526],"length":1,"stats":{"Line":0}},{"line":73,"address":[2343473],"length":1,"stats":{"Line":0}},{"line":74,"address":[2344056],"length":1,"stats":{"Line":0}},{"line":81,"address":[2344511,2343837],"length":1,"stats":{"Line":0}},{"line":83,"address":[2343448,2344546],"length":1,"stats":{"Line":0}},{"line":86,"address":[2343104,2345586,2345291],"length":1,"stats":{"Line":0}},{"line":87,"address":[2345556,2345999,2346866],"length":1,"stats":{"Line":0}},{"line":88,"address":[2346101,2346387],"length":1,"stats":{"Line":0}},{"line":89,"address":[2346353],"length":1,"stats":{"Line":0}},{"line":92,"address":[2347205,2338471,2347024],"length":1,"stats":{"Line":0}},{"line":93,"address":[2347740,2347574,2347173],"length":1,"stats":{"Line":0}},{"line":94,"address":[2347723],"length":1,"stats":{"Line":0}},{"line":97,"address":[2348109,2338503,2348290],"length":1,"stats":{"Line":0}},{"line":98,"address":[2348258,2348659,2348825],"length":1,"stats":{"Line":0}},{"line":99,"address":[2348808],"length":1,"stats":{"Line":0}}],"covered":0,"coverable":38},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","src","cli.rs"],"content":"// src/cli.rs\n\npub mod link;\npub mod coll;\npub mod view;\npub mod state;\npub mod task;\npub mod remind;\npub mod annotate;\npub mod version;\npub mod event;\npub mod watch;\n\nuse clap::{Parser, Subcommand, ValueEnum};\nuse clap_complete::Shell;\n\n/// Output format for commands.\n#[derive(ValueEnum, Clone, Copy, Debug)]\npub enum Format {\n Text,\n Json,\n}\n\n/// Marlin – metadata-driven file explorer (CLI utilities)\n#[derive(Parser, Debug)]\n#[command(author, version, about, propagate_version = true)]\npub struct Cli {\n /// Enable debug logging and extra output\n #[arg(long)]\n pub verbose: bool,\n\n /// Output format (text or JSON)\n #[arg(long, default_value = \"text\", value_enum, global = true)]\n pub format: Format,\n\n #[command(subcommand)]\n pub command: Commands,\n}\n\n#[derive(Subcommand, Debug)]\npub enum Commands {\n /// Initialise the database (idempotent)\n Init,\n\n /// Scan one or more directories and populate the file index\n Scan {\n /// Only re-index files marked dirty by `marlin watch`\n #[arg(long)]\n dirty: bool,\n\n /// Directories to scan (defaults to cwd)\n paths: Vec\u003cstd::path::PathBuf\u003e,\n },\n\n /// Tag files matching a glob pattern (hierarchical tags use `/`)\n Tag {\n /// Glob or path pattern\n pattern: String,\n /// Hierarchical tag name (`foo/bar`)\n tag_path: String,\n },\n\n /// Manage custom attributes\n Attr {\n #[command(subcommand)]\n action: AttrCmd,\n },\n\n /// Full-text search; `--exec CMD` runs CMD on each hit (`{}` placeholder)\n Search {\n query: String,\n #[arg(long)]\n exec: Option\u003cString\u003e,\n },\n\n /// Create a timestamped backup of the database\n Backup,\n\n /// Restore from a backup file (overwrites current DB)\n Restore {\n backup_path: std::path::PathBuf,\n },\n\n /// Generate shell completions (hidden)\n #[command(hide = true)]\n Completions {\n /// Which shell to generate for\n #[arg(value_enum)]\n shell: Shell,\n },\n\n /// File-to-file links\n #[command(subcommand)]\n Link(link::LinkCmd),\n\n /// Collections (groups) of files\n #[command(subcommand)]\n Coll(coll::CollCmd),\n\n /// Smart views (saved queries)\n #[command(subcommand)]\n View(view::ViewCmd),\n\n /// Workflow states on files\n #[command(subcommand)]\n State(state::StateCmd),\n\n /// TODO/tasks management\n #[command(subcommand)]\n Task(task::TaskCmd),\n\n /// Reminders on files\n #[command(subcommand)]\n Remind(remind::RemindCmd),\n\n /// File annotations and highlights\n #[command(subcommand)]\n Annotate(annotate::AnnotateCmd),\n\n /// Version diffs\n #[command(subcommand)]\n Version(version::VersionCmd),\n\n /// Calendar events \u0026 timelines\n #[command(subcommand)]\n Event(event::EventCmd),\n\n /// Watch directories for changes\n #[command(subcommand)]\n Watch(watch::WatchCmd),\n}\n\n#[derive(Subcommand, Debug)]\npub enum AttrCmd {\n Set { pattern: String, key: String, value: String },\n Ls { path: std::path::PathBuf },\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","src","main.rs"],"content":"//! Marlin CLI entry-point (post crate-split)\n//!\n//! All heavy lifting now lives in the `libmarlin` crate; this file\n//! handles argument parsing, logging, orchestration and the few\n//! helpers that remain CLI-specific.\n\n#![deny(warnings)]\n\nmod cli; // sub-command definitions and argument structs\n\n/* ── shared modules re-exported from libmarlin ─────────────────── */\nuse libmarlin::{\n config,\n db,\n logging,\n scan,\n utils::determine_scan_root,\n};\nuse libmarlin::db::take_dirty;\n\nuse anyhow::{Context, Result};\nuse clap::{CommandFactory, Parser};\nuse clap_complete::generate;\nuse glob::Pattern;\nuse shellexpand;\nuse shlex;\nuse std::{\n env,\n fs,\n io,\n path::Path,\n process::Command,\n};\nuse tracing::{debug, error, info};\nuse walkdir::WalkDir;\n\nuse cli::{Cli, Commands};\n\nfn main() -\u003e Result\u003c()\u003e {\n /* ── CLI parsing \u0026 logging ────────────────────────────────── */\n let args = Cli::parse();\n if args.verbose {\n env::set_var(\"RUST_LOG\", \"debug\");\n }\n logging::init();\n\n /* ── shell-completion shortcut ────────────────────────────── */\n if let Commands::Completions { shell } = \u0026args.command {\n let mut cmd = Cli::command();\n generate(*shell, \u0026mut cmd, \"marlin\", \u0026mut io::stdout());\n return Ok(());\n }\n\n /* ── config \u0026 automatic backup ───────────────────────────── */\n let cfg = config::Config::load()?; // resolves DB path\n\n match \u0026args.command {\n Commands::Init | Commands::Backup | Commands::Restore { .. } =\u003e {}\n _ =\u003e match db::backup(\u0026cfg.db_path) {\n Ok(p) =\u003e info!(\"Pre-command auto-backup created at {}\", p.display()),\n Err(e) =\u003e error!(\"Failed to create pre-command auto-backup: {e}\"),\n },\n }\n\n /* ── open DB (runs migrations) ───────────────────────────── */\n let mut conn = db::open(\u0026cfg.db_path)?;\n\n /* ── command dispatch ────────────────────────────────────── */\n match args.command {\n Commands::Completions { .. } =\u003e {} // handled above\n\n /* ---- init ------------------------------------------------ */\n Commands::Init =\u003e {\n info!(\"Database initialised at {}\", cfg.db_path.display());\n let cwd = env::current_dir().context(\"getting current directory\")?;\n let count = scan::scan_directory(\u0026mut conn, \u0026cwd)\n .context(\"initial scan failed\")?;\n info!(\"Initial scan complete – indexed/updated {count} files\");\n }\n\n /* ---- scan ------------------------------------------------ */\n Commands::Scan { dirty, paths } =\u003e {\n let scan_paths: Vec\u003cstd::path::PathBuf\u003e = if paths.is_empty() {\n vec![env::current_dir()?]\n } else {\n paths.into_iter().collect()\n };\n\n if dirty {\n let dirty_ids = take_dirty(\u0026conn)?;\n for id in dirty_ids {\n let path: String = conn.query_row(\n \"SELECT path FROM files WHERE id = ?1\",\n [id],\n |r| r.get(0),\n )?;\n scan::scan_directory(\u0026mut conn, Path::new(\u0026path))?;\n }\n } else {\n for p in scan_paths {\n scan::scan_directory(\u0026mut conn, \u0026p)?;\n }\n }\n }\n\n /* ---- tag / attribute / search --------------------------- */\n Commands::Tag { pattern, tag_path } =\u003e\n apply_tag(\u0026conn, \u0026pattern, \u0026tag_path)?,\n\n Commands::Attr { action } =\u003e match action {\n cli::AttrCmd::Set { pattern, key, value } =\u003e\n attr_set(\u0026conn, \u0026pattern, \u0026key, \u0026value)?,\n cli::AttrCmd::Ls { path } =\u003e\n attr_ls(\u0026conn, \u0026path)?,\n },\n\n Commands::Search { query, exec } =\u003e\n run_search(\u0026conn, \u0026query, exec)?,\n\n /* ---- maintenance ---------------------------------------- */\n Commands::Backup =\u003e {\n let p = db::backup(\u0026cfg.db_path)?;\n println!(\"Backup created: {}\", p.display());\n }\n\n Commands::Restore { backup_path } =\u003e {\n drop(conn);\n db::restore(\u0026backup_path, \u0026cfg.db_path).with_context(|| {\n format!(\"Failed to restore DB from {}\", backup_path.display())\n })?;\n println!(\"Restored DB from {}\", backup_path.display());\n db::open(\u0026cfg.db_path).with_context(|| {\n format!(\"Could not open restored DB at {}\", cfg.db_path.display())\n })?;\n info!(\"Successfully opened restored database.\");\n }\n\n /* ---- passthrough sub-modules (some still stubs) ---------- */\n Commands::Link(link_cmd) =\u003e cli::link::run(\u0026link_cmd, \u0026mut conn, args.format)?,\n Commands::Coll(coll_cmd) =\u003e cli::coll::run(\u0026coll_cmd, \u0026mut conn, args.format)?,\n Commands::View(view_cmd) =\u003e cli::view::run(\u0026view_cmd, \u0026mut conn, args.format)?,\n Commands::State(state_cmd) =\u003e cli::state::run(\u0026state_cmd, \u0026mut conn, args.format)?,\n Commands::Task(task_cmd) =\u003e cli::task::run(\u0026task_cmd, \u0026mut conn, args.format)?,\n Commands::Remind(rm_cmd) =\u003e cli::remind::run(\u0026rm_cmd, \u0026mut conn, args.format)?,\n Commands::Annotate(a_cmd) =\u003e cli::annotate::run(\u0026a_cmd, \u0026mut conn, args.format)?,\n Commands::Version(v_cmd) =\u003e cli::version::run(\u0026v_cmd, \u0026mut conn, args.format)?,\n Commands::Event(e_cmd) =\u003e cli::event::run(\u0026e_cmd, \u0026mut conn, args.format)?,\n Commands::Watch(watch_cmd) =\u003e cli::watch::run(\u0026watch_cmd, \u0026mut conn, args.format)?,\n }\n\n Ok(())\n}\n\n/* ─────────────────── helpers \u0026 sub-routines ─────────────────── */\n\n/* ---------- TAGS ---------- */\nfn apply_tag(conn: \u0026rusqlite::Connection, pattern: \u0026str, tag_path: \u0026str) -\u003e Result\u003c()\u003e {\n let leaf_tag_id = db::ensure_tag_path(conn, tag_path)?;\n let mut tag_ids = Vec::new();\n let mut current = Some(leaf_tag_id);\n while let Some(id) = current {\n tag_ids.push(id);\n current = conn.query_row(\n \"SELECT parent_id FROM tags WHERE id=?1\",\n [id],\n |r| r.get::\u003c_, Option\u003ci64\u003e\u003e(0),\n )?;\n }\n\n let expanded = shellexpand::tilde(pattern).into_owned();\n let pat = Pattern::new(\u0026expanded)\n .with_context(|| format!(\"Invalid glob pattern `{expanded}`\"))?;\n let root = determine_scan_root(\u0026expanded);\n\n let mut stmt_file = conn.prepare(\"SELECT id FROM files WHERE path=?1\")?;\n let mut stmt_insert = conn.prepare(\n \"INSERT OR IGNORE INTO file_tags(file_id, tag_id) VALUES (?1, ?2)\",\n )?;\n\n let mut count = 0usize;\n for entry in WalkDir::new(\u0026root)\n .into_iter()\n .filter_map(Result::ok)\n .filter(|e| e.file_type().is_file())\n {\n let p = entry.path().to_string_lossy();\n if !pat.matches(\u0026p) { continue; }\n\n match stmt_file.query_row([p.as_ref()], |r| r.get::\u003c_, i64\u003e(0)) {\n Ok(fid) =\u003e {\n let mut newly = false;\n for \u0026tid in \u0026tag_ids {\n if stmt_insert.execute([fid, tid])? \u003e 0 {\n newly = true;\n }\n }\n if newly {\n info!(file=%p, tag=tag_path, \"tagged\");\n count += 1;\n }\n }\n Err(rusqlite::Error::QueryReturnedNoRows) =\u003e\n error!(file=%p, \"not indexed – run `marlin scan` first\"),\n Err(e) =\u003e\n error!(file=%p, error=%e, \"could not lookup file ID\"),\n }\n }\n\n info!(\"Applied tag '{}' to {} file(s).\", tag_path, count);\n Ok(())\n}\n\n/* ---------- ATTRIBUTES ---------- */\nfn attr_set(conn: \u0026rusqlite::Connection, pattern: \u0026str, key: \u0026str, value: \u0026str) -\u003e Result\u003c()\u003e {\n let expanded = shellexpand::tilde(pattern).into_owned();\n let pat = Pattern::new(\u0026expanded)\n .with_context(|| format!(\"Invalid glob pattern `{expanded}`\"))?;\n let root = determine_scan_root(\u0026expanded);\n\n let mut stmt_file = conn.prepare(\"SELECT id FROM files WHERE path=?1\")?;\n let mut count = 0usize;\n\n for entry in WalkDir::new(\u0026root)\n .into_iter()\n .filter_map(Result::ok)\n .filter(|e| e.file_type().is_file())\n {\n let p = entry.path().to_string_lossy();\n if !pat.matches(\u0026p) { continue; }\n\n match stmt_file.query_row([p.as_ref()], |r| r.get::\u003c_, i64\u003e(0)) {\n Ok(fid) =\u003e {\n db::upsert_attr(conn, fid, key, value)?;\n info!(file=%p, key, value, \"attr set\");\n count += 1;\n }\n Err(rusqlite::Error::QueryReturnedNoRows) =\u003e\n error!(file=%p, \"not indexed – run `marlin scan` first\"),\n Err(e) =\u003e\n error!(file=%p, error=%e, \"could not lookup file ID\"),\n }\n }\n\n info!(\"Attribute '{}={}' set on {} file(s).\", key, value, count);\n Ok(())\n}\n\nfn attr_ls(conn: \u0026rusqlite::Connection, path: \u0026Path) -\u003e Result\u003c()\u003e {\n let fid = db::file_id(conn, \u0026path.to_string_lossy())?;\n let mut stmt = conn.prepare(\n \"SELECT key, value FROM attributes WHERE file_id=?1 ORDER BY key\"\n )?;\n for row in stmt\n .query_map([fid], |r| Ok((r.get::\u003c_, String\u003e(0)?, r.get::\u003c_, String\u003e(1)?)))?\n {\n let (k, v) = row?;\n println!(\"{k} = {v}\");\n }\n Ok(())\n}\n\n/* ---------- SEARCH ---------- */\nfn run_search(conn: \u0026rusqlite::Connection, raw_query: \u0026str, exec: Option\u003cString\u003e) -\u003e Result\u003c()\u003e {\n let mut parts = Vec::new();\n let toks = shlex::split(raw_query).unwrap_or_else(|| vec![raw_query.to_string()]);\n for tok in toks {\n if [\"AND\", \"OR\", \"NOT\"].contains(\u0026tok.as_str()) {\n parts.push(tok);\n } else if let Some(tag) = tok.strip_prefix(\"tag:\") {\n for (i, seg) in tag.split('/').filter(|s| !s.is_empty()).enumerate() {\n if i \u003e 0 { parts.push(\"AND\".into()); }\n parts.push(format!(\"tags_text:{}\", escape_fts(seg)));\n }\n } else if let Some(attr) = tok.strip_prefix(\"attr:\") {\n let mut kv = attr.splitn(2, '=');\n let key = kv.next().unwrap();\n if let Some(val) = kv.next() {\n parts.push(format!(\"attrs_text:{}\", escape_fts(key)));\n parts.push(\"AND\".into());\n parts.push(format!(\"attrs_text:{}\", escape_fts(val)));\n } else {\n parts.push(format!(\"attrs_text:{}\", escape_fts(key)));\n }\n } else {\n parts.push(escape_fts(\u0026tok));\n }\n }\n let fts_expr = parts.join(\" \");\n debug!(\"FTS MATCH expression: {fts_expr}\");\n\n let mut stmt = conn.prepare(\n r#\"\n SELECT f.path\n FROM files_fts\n JOIN files f ON f.rowid = files_fts.rowid\n WHERE files_fts MATCH ?1\n ORDER BY rank\n \"#,\n )?;\n let mut hits: Vec\u003cString\u003e = stmt\n .query_map([\u0026fts_expr], |r| r.get::\u003c_, String\u003e(0))?\n .filter_map(Result::ok)\n .collect();\n\n if hits.is_empty() \u0026\u0026 !raw_query.contains(':') {\n hits = naive_substring_search(conn, raw_query)?;\n }\n\n if let Some(cmd_tpl) = exec {\n run_exec(\u0026hits, \u0026cmd_tpl)?;\n } else {\n if hits.is_empty() {\n eprintln!(\n \"No matches for query: `{raw_query}` (FTS expr: `{fts_expr}`)\"\n );\n } else {\n for p in hits { println!(\"{p}\"); }\n }\n }\n Ok(())\n}\n\nfn naive_substring_search(conn: \u0026rusqlite::Connection, term: \u0026str) -\u003e Result\u003cVec\u003cString\u003e\u003e {\n let needle = term.to_lowercase();\n let mut stmt = conn.prepare(\"SELECT path FROM files\")?;\n let rows = stmt.query_map([], |r| r.get::\u003c_, String\u003e(0))?;\n\n let mut out = Vec::new();\n for p in rows {\n let p = p?;\n if p.to_lowercase().contains(\u0026needle) {\n out.push(p.clone());\n continue;\n }\n if let Ok(meta) = fs::metadata(\u0026p) {\n if meta.len() \u003e 65_536 { continue; }\n }\n if let Ok(body) = fs::read_to_string(\u0026p) {\n if body.to_lowercase().contains(\u0026needle) {\n out.push(p);\n }\n }\n }\n Ok(out)\n}\n\nfn run_exec(paths: \u0026[String], cmd_tpl: \u0026str) -\u003e Result\u003c()\u003e {\n let mut ran_without_placeholder = false;\n\n if paths.is_empty() \u0026\u0026 !cmd_tpl.contains(\"{}\") {\n if let Some(mut parts) = shlex::split(cmd_tpl) {\n if !parts.is_empty() {\n let prog = parts.remove(0);\n let status = Command::new(\u0026prog).args(parts).status()?;\n if !status.success() {\n error!(command=%cmd_tpl, code=?status.code(), \"command failed\");\n }\n }\n }\n ran_without_placeholder = true;\n }\n\n if !ran_without_placeholder {\n for p in paths {\n let quoted = shlex::try_quote(p).unwrap_or_else(|_| p.into());\n let final_cmd = if cmd_tpl.contains(\"{}\") {\n cmd_tpl.replace(\"{}\", \u0026quoted)\n } else {\n format!(\"{cmd_tpl} {quoted}\")\n };\n if let Some(mut parts) = shlex::split(\u0026final_cmd) {\n if parts.is_empty() { continue; }\n let prog = parts.remove(0);\n let status = Command::new(\u0026prog).args(parts).status()?;\n if !status.success() {\n error!(file=%p, command=%final_cmd, code=?status.code(), \"command failed\");\n }\n }\n }\n }\n Ok(())\n}\n\nfn escape_fts(term: \u0026str) -\u003e String {\n if term.contains(|c: char| c.is_whitespace() || \"-:()\\\"\".contains(c))\n || [\"AND\", \"OR\", \"NOT\", \"NEAR\"].contains(\u0026term.to_uppercase().as_str())\n {\n format!(\"\\\"{}\\\"\", term.replace('\"', \"\\\"\\\"\"))\n } else {\n term.to_string()\n }\n}\n\n#[cfg(test)]\nmod tests {\n use assert_cmd::Command;\n use tempfile::tempdir;\n\n #[test]\n fn test_help_command() {\n let mut cmd = Command::cargo_bin(\"marlin\").unwrap();\n cmd.arg(\"--help\");\n cmd.assert()\n .success()\n .stdout(predicates::str::contains(\"Usage: marlin\"));\n }\n\n #[test]\n fn test_version_command() {\n let mut cmd = Command::cargo_bin(\"marlin\").unwrap();\n cmd.arg(\"--version\");\n cmd.assert()\n .success()\n .stdout(predicates::str::contains(\"marlin-cli 0.1.0\"));\n }\n\n #[test]\n fn test_verbose_logging() {\n let tmp = tempdir().unwrap();\n let mut cmd = Command::cargo_bin(\"marlin\").unwrap();\n cmd.env(\"MARLIN_DB_PATH\", tmp.path().join(\"index.db\"));\n cmd.arg(\"--verbose\").arg(\"init\");\n let output = cmd.output().unwrap();\n assert!(output.status.success());\n let stderr = String::from_utf8_lossy(\u0026output.stderr);\n assert!(\n stderr.contains(\"DEBUG\"),\n \"Expected debug logs in stderr, got: {}\",\n stderr\n );\n }\n\n #[test]\n fn test_shell_completions() {\n let mut cmd = Command::cargo_bin(\"marlin\").unwrap();\n cmd.arg(\"completions\").arg(\"bash\");\n cmd.assert()\n .success()\n .stdout(predicates::str::contains(\"_marlin()\"))\n .stdout(predicates::str::contains(\"init\"))\n .stdout(predicates::str::contains(\"scan\"));\n }\n\n #[test]\n fn test_invalid_subcommand() {\n let mut cmd = Command::cargo_bin(\"marlin\").unwrap();\n cmd.arg(\"invalid_cmd\");\n cmd.assert()\n .failure()\n .stderr(predicates::str::contains(\"error: unrecognized subcommand\"));\n }\n\n #[test]\n fn test_init_command() {\n let tmp = tempdir().unwrap();\n let db_path = tmp.path().join(\"index.db\");\n let mut cmd = Command::cargo_bin(\"marlin\").unwrap();\n cmd.env(\"MARLIN_DB_PATH\", \u0026db_path);\n cmd.arg(\"init\");\n cmd.assert().success();\n assert!(db_path.exists(), \"Database file should exist after init\");\n }\n\n #[test]\n fn test_automatic_backup() {\n let tmp = tempdir().unwrap();\n let db_path = tmp.path().join(\"index.db\");\n let backups_dir = tmp.path().join(\"backups\");\n\n // Init: no backup\n let mut cmd_init = Command::cargo_bin(\"marlin\").unwrap();\n cmd_init.env(\"MARLIN_DB_PATH\", \u0026db_path);\n cmd_init.arg(\"init\");\n cmd_init.assert().success();\n assert!(\n !backups_dir.exists() || backups_dir.read_dir().unwrap().next().is_none(),\n \"No backup should be created for init\"\n );\n\n // Scan: backup created\n let mut cmd_scan = Command::cargo_bin(\"marlin\").unwrap();\n cmd_scan.env(\"MARLIN_DB_PATH\", \u0026db_path);\n cmd_scan.arg(\"scan\");\n cmd_scan.assert().success();\n assert!(backups_dir.exists(), \"Backups directory should exist after scan\");\n let backups: Vec\u003c_\u003e = backups_dir.read_dir().unwrap().collect();\n assert_eq!(backups.len(), 1, \"One backup should be created for scan\");\n }\n\n #[test]\n fn test_annotate_stub() {\n let tmp = tempdir().unwrap();\n let mut cmd = Command::cargo_bin(\"marlin\").unwrap();\n cmd.env(\"MARLIN_DB_PATH\", tmp.path().join(\"index.db\"));\n cmd.arg(\"annotate\").arg(\"add\").arg(\"file.txt\").arg(\"note\");\n cmd.assert()\n .failure()\n .stderr(predicates::str::contains(\"not yet implemented\"));\n }\n\n #[test]\n fn test_event_stub() {\n let tmp = tempdir().unwrap();\n let mut cmd = Command::cargo_bin(\"marlin\").unwrap();\n cmd.env(\"MARLIN_DB_PATH\", tmp.path().join(\"index.db\"));\n cmd.arg(\"event\").arg(\"add\").arg(\"file.txt\").arg(\"2025-05-20\").arg(\"desc\");\n cmd.assert()\n .failure()\n .stderr(predicates::str::contains(\"not yet implemented\"));\n }\n}\n","traces":[{"line":39,"address":[2202655,2218682,2201872],"length":1,"stats":{"Line":0}},{"line":41,"address":[2201894],"length":1,"stats":{"Line":0}},{"line":42,"address":[2202219],"length":1,"stats":{"Line":0}},{"line":43,"address":[2202240,2202346],"length":1,"stats":{"Line":0}},{"line":45,"address":[2202229],"length":1,"stats":{"Line":0}},{"line":48,"address":[2202348],"length":1,"stats":{"Line":0}},{"line":49,"address":[2202394],"length":1,"stats":{"Line":0}},{"line":50,"address":[2202431,2202518],"length":1,"stats":{"Line":0}},{"line":51,"address":[2202571],"length":1,"stats":{"Line":0}},{"line":55,"address":[2218129,2202661,2202401],"length":1,"stats":{"Line":0}},{"line":57,"address":[2202834],"length":1,"stats":{"Line":0}},{"line":59,"address":[2202893,2202974],"length":1,"stats":{"Line":0}},{"line":60,"address":[2203058],"length":1,"stats":{"Line":0}},{"line":61,"address":[2203011,2204020],"length":1,"stats":{"Line":0}},{"line":66,"address":[2218111,2202900,2204816],"length":1,"stats":{"Line":0}},{"line":69,"address":[2205102],"length":1,"stats":{"Line":0}},{"line":74,"address":[2206936,2205144,2207223],"length":1,"stats":{"Line":0}},{"line":75,"address":[2207201,2207787,2209168],"length":1,"stats":{"Line":0}},{"line":76,"address":[2207999,2208269,2208082,2209114],"length":1,"stats":{"Line":0}},{"line":78,"address":[2208644,2208322],"length":1,"stats":{"Line":0}},{"line":82,"address":[2205175],"length":1,"stats":{"Line":0}},{"line":83,"address":[2209226,2209760,2205244],"length":1,"stats":{"Line":0}},{"line":84,"address":[2211871,2209309,2209383,2211968],"length":1,"stats":{"Line":0}},{"line":86,"address":[2209232,2209340],"length":1,"stats":{"Line":0}},{"line":89,"address":[2209362,2211077],"length":1,"stats":{"Line":0}},{"line":90,"address":[2210525,2209832,2211788],"length":1,"stats":{"Line":0}},{"line":91,"address":[2210790,2210686,2210941],"length":1,"stats":{"Line":0}},{"line":92,"address":[2211153,2211735,2210995,2211281],"length":1,"stats":{"Line":0}},{"line":94,"address":[2210987],"length":1,"stats":{"Line":0}},{"line":95,"address":[2298240,2298256],"length":1,"stats":{"Line":0}},{"line":97,"address":[2211685,2211382,2211477],"length":1,"stats":{"Line":0}},{"line":100,"address":[2209903,2209765,2210038],"length":1,"stats":{"Line":0}},{"line":101,"address":[2210115,2210236,2210444],"length":1,"stats":{"Line":0}},{"line":107,"address":[2205273],"length":1,"stats":{"Line":0}},{"line":108,"address":[2212000,2205361],"length":1,"stats":{"Line":0}},{"line":110,"address":[2205387],"length":1,"stats":{"Line":0}},{"line":111,"address":[2212486],"length":1,"stats":{"Line":0}},{"line":112,"address":[2212590],"length":1,"stats":{"Line":0}},{"line":113,"address":[2212420],"length":1,"stats":{"Line":0}},{"line":114,"address":[2212460,2213200],"length":1,"stats":{"Line":0}},{"line":117,"address":[2205471],"length":1,"stats":{"Line":0}},{"line":118,"address":[2205567,2213459],"length":1,"stats":{"Line":0}},{"line":122,"address":[2213836,2205609,2214261],"length":1,"stats":{"Line":0}},{"line":123,"address":[2214080,2213997],"length":1,"stats":{"Line":0}},{"line":126,"address":[2205619],"length":1,"stats":{"Line":0}},{"line":127,"address":[2205659],"length":1,"stats":{"Line":0}},{"line":128,"address":[2214470,2214334,2215831],"length":1,"stats":{"Line":0}},{"line":129,"address":[2298310],"length":1,"stats":{"Line":0}},{"line":131,"address":[2214507],"length":1,"stats":{"Line":0}},{"line":132,"address":[2214812,2214697,2215793],"length":1,"stats":{"Line":0}},{"line":133,"address":[2298486],"length":1,"stats":{"Line":0}},{"line":135,"address":[2215052,2215374],"length":1,"stats":{"Line":0}},{"line":139,"address":[2215849,2205812],"length":1,"stats":{"Line":0}},{"line":140,"address":[2216057,2205942],"length":1,"stats":{"Line":0}},{"line":141,"address":[2206040,2216265],"length":1,"stats":{"Line":0}},{"line":142,"address":[2216467,2206138],"length":1,"stats":{"Line":0}},{"line":143,"address":[2206252,2216663],"length":1,"stats":{"Line":0}},{"line":144,"address":[2206334,2216859],"length":1,"stats":{"Line":0}},{"line":145,"address":[2206464,2217055],"length":1,"stats":{"Line":0}},{"line":146,"address":[2206594,2217251],"length":1,"stats":{"Line":0}},{"line":147,"address":[2206676,2217447],"length":1,"stats":{"Line":0}},{"line":148,"address":[2217643,2218006,2206806],"length":1,"stats":{"Line":0}},{"line":151,"address":[2205781],"length":1,"stats":{"Line":0}},{"line":157,"address":[2219744,2227697,2227856],"length":1,"stats":{"Line":0}},{"line":158,"address":[2219830],"length":1,"stats":{"Line":0}},{"line":159,"address":[2219968],"length":1,"stats":{"Line":0}},{"line":160,"address":[2219990],"length":1,"stats":{"Line":0}},{"line":161,"address":[2220010,2220441],"length":1,"stats":{"Line":0}},{"line":162,"address":[2220048],"length":1,"stats":{"Line":0}},{"line":163,"address":[2220454,2220180,2220359],"length":1,"stats":{"Line":0}},{"line":165,"address":[2220172],"length":1,"stats":{"Line":0}},{"line":166,"address":[2298656,2298640],"length":1,"stats":{"Line":0}},{"line":170,"address":[2220090,2220481],"length":1,"stats":{"Line":0}},{"line":171,"address":[2220729,2220599,2227846,2220516],"length":1,"stats":{"Line":0}},{"line":172,"address":[2220713],"length":1,"stats":{"Line":0}},{"line":173,"address":[2220977,2220894],"length":1,"stats":{"Line":0}},{"line":175,"address":[2227787,2221004,2221075],"length":1,"stats":{"Line":0}},{"line":176,"address":[2227747,2221402,2221595,2221473],"length":1,"stats":{"Line":0}},{"line":180,"address":[2221792],"length":1,"stats":{"Line":0}},{"line":181,"address":[2221871,2221820,2222060],"length":1,"stats":{"Line":0}},{"line":184,"address":[2298843,2298816],"length":1,"stats":{"Line":0}},{"line":186,"address":[2222145,2223211],"length":1,"stats":{"Line":0}},{"line":187,"address":[2223218,2223313],"length":1,"stats":{"Line":0}},{"line":189,"address":[2223444,2223371],"length":1,"stats":{"Line":0}},{"line":190,"address":[2223585],"length":1,"stats":{"Line":0}},{"line":191,"address":[2223609],"length":1,"stats":{"Line":0}},{"line":192,"address":[2223617,2223712],"length":1,"stats":{"Line":0}},{"line":193,"address":[2223829,2225104,2225300],"length":1,"stats":{"Line":0}},{"line":194,"address":[2225292],"length":1,"stats":{"Line":0}},{"line":197,"address":[2225079,2223883],"length":1,"stats":{"Line":0}},{"line":198,"address":[2223915,2224249],"length":1,"stats":{"Line":0}},{"line":199,"address":[2224215,2225084,2225071],"length":1,"stats":{"Line":0}},{"line":203,"address":[2225489,2225627],"length":1,"stats":{"Line":0}},{"line":204,"address":[2225517],"length":1,"stats":{"Line":0}},{"line":205,"address":[2225589,2226490],"length":1,"stats":{"Line":0}},{"line":209,"address":[2222198,2222532],"length":1,"stats":{"Line":0}},{"line":210,"address":[2222498],"length":1,"stats":{"Line":0}},{"line":214,"address":[2234849,2227872,2234730],"length":1,"stats":{"Line":0}},{"line":215,"address":[2227966],"length":1,"stats":{"Line":0}},{"line":216,"address":[2228040,2234839,2228253,2228123],"length":1,"stats":{"Line":0}},{"line":217,"address":[2228237],"length":1,"stats":{"Line":0}},{"line":218,"address":[2228418,2228501],"length":1,"stats":{"Line":0}},{"line":220,"address":[2228528,2228599,2234780],"length":1,"stats":{"Line":0}},{"line":221,"address":[2228918],"length":1,"stats":{"Line":0}},{"line":223,"address":[2229186,2228997,2228946],"length":1,"stats":{"Line":0}},{"line":226,"address":[2299056,2299083],"length":1,"stats":{"Line":0}},{"line":228,"address":[2230342,2229271],"length":1,"stats":{"Line":0}},{"line":229,"address":[2230349,2230444],"length":1,"stats":{"Line":0}},{"line":231,"address":[2230575,2230502],"length":1,"stats":{"Line":0}},{"line":232,"address":[2230724],"length":1,"stats":{"Line":0}},{"line":233,"address":[2230740,2230843,2232346],"length":1,"stats":{"Line":0}},{"line":234,"address":[2230954,2231288],"length":1,"stats":{"Line":0}},{"line":235,"address":[2232296,2231254,2232286],"length":1,"stats":{"Line":0}},{"line":238,"address":[2232660,2232522],"length":1,"stats":{"Line":0}},{"line":239,"address":[2232550],"length":1,"stats":{"Line":0}},{"line":240,"address":[2232622,2233523],"length":1,"stats":{"Line":0}},{"line":244,"address":[2229658,2229324],"length":1,"stats":{"Line":0}},{"line":245,"address":[2229624],"length":1,"stats":{"Line":0}},{"line":248,"address":[2236563,2234864,2236624],"length":1,"stats":{"Line":0}},{"line":249,"address":[2236642,2234913],"length":1,"stats":{"Line":0}},{"line":250,"address":[2235279,2235171],"length":1,"stats":{"Line":0}},{"line":253,"address":[2235483,2235590,2235698,2235870,2236617],"length":1,"stats":{"Line":0}},{"line":254,"address":[2235475,2235634],"length":1,"stats":{"Line":0}},{"line":256,"address":[2236573,2235971,2236049],"length":1,"stats":{"Line":0}},{"line":257,"address":[2236295,2236366],"length":1,"stats":{"Line":0}},{"line":259,"address":[2236013],"length":1,"stats":{"Line":0}},{"line":263,"address":[2236672,2243587,2239914],"length":1,"stats":{"Line":0}},{"line":264,"address":[2236719],"length":1,"stats":{"Line":0}},{"line":265,"address":[2236915,2236839],"length":1,"stats":{"Line":0}},{"line":266,"address":[2236961,2237123,2243531],"length":1,"stats":{"Line":0}},{"line":267,"address":[2240934,2237208],"length":1,"stats":{"Line":0}},{"line":268,"address":[2241032,2243518],"length":1,"stats":{"Line":0}},{"line":269,"address":[2241009,2241125],"length":1,"stats":{"Line":0}},{"line":270,"address":[2241259,2241333],"length":1,"stats":{"Line":0}},{"line":271,"address":[2241582,2241527],"length":1,"stats":{"Line":0}},{"line":272,"address":[2241575,2241656],"length":1,"stats":{"Line":0}},{"line":274,"address":[2241977,2241291],"length":1,"stats":{"Line":0}},{"line":275,"address":[2242099],"length":1,"stats":{"Line":0}},{"line":276,"address":[2242157],"length":1,"stats":{"Line":0}},{"line":277,"address":[2242266],"length":1,"stats":{"Line":0}},{"line":278,"address":[2242447,2242412],"length":1,"stats":{"Line":0}},{"line":279,"address":[2242747],"length":1,"stats":{"Line":0}},{"line":280,"address":[2242837],"length":1,"stats":{"Line":0}},{"line":282,"address":[2242437,2243149],"length":1,"stats":{"Line":0}},{"line":285,"address":[2243472,2242137],"length":1,"stats":{"Line":0}},{"line":288,"address":[2237253],"length":1,"stats":{"Line":0}},{"line":289,"address":[2237718,2237415,2237336],"length":1,"stats":{"Line":0}},{"line":291,"address":[2240771,2237688,2238180,2238302],"length":1,"stats":{"Line":0}},{"line":300,"address":[2238622,2238515,2238730,2240731],"length":1,"stats":{"Line":0}},{"line":301,"address":[2238666,2238507],"length":1,"stats":{"Line":0}},{"line":305,"address":[2238861,2238990,2238935,2239380],"length":1,"stats":{"Line":0}},{"line":306,"address":[2239393,2239050],"length":1,"stats":{"Line":0}},{"line":309,"address":[2239431,2238949],"length":1,"stats":{"Line":0}},{"line":310,"address":[2239854,2239610,2239462],"length":1,"stats":{"Line":0}},{"line":312,"address":[2239497,2239927],"length":1,"stats":{"Line":0}},{"line":313,"address":[2240418,2240000],"length":1,"stats":{"Line":0}},{"line":317,"address":[2239933,2240030],"length":1,"stats":{"Line":0}},{"line":320,"address":[2239815],"length":1,"stats":{"Line":0}},{"line":323,"address":[2245619,2243632,2246596],"length":1,"stats":{"Line":0}},{"line":324,"address":[2243713],"length":1,"stats":{"Line":0}},{"line":325,"address":[2246594,2243833,2243762],"length":1,"stats":{"Line":0}},{"line":326,"address":[2244168,2244235,2246573],"length":1,"stats":{"Line":0}},{"line":328,"address":[2244436],"length":1,"stats":{"Line":0}},{"line":329,"address":[2244496,2244588,2246324,2244664],"length":1,"stats":{"Line":0}},{"line":330,"address":[2244930,2244765],"length":1,"stats":{"Line":0}},{"line":331,"address":[2245219,2245148],"length":1,"stats":{"Line":0}},{"line":332,"address":[2246407],"length":1,"stats":{"Line":0}},{"line":335,"address":[2245388,2245498],"length":1,"stats":{"Line":0}},{"line":336,"address":[2245505,2245566],"length":1,"stats":{"Line":0}},{"line":338,"address":[2245625,2245716],"length":1,"stats":{"Line":0}},{"line":339,"address":[2245835,2245764],"length":1,"stats":{"Line":0}},{"line":340,"address":[2246023],"length":1,"stats":{"Line":0}},{"line":344,"address":[2244818],"length":1,"stats":{"Line":0}},{"line":347,"address":[2246624,2248773,2248808],"length":1,"stats":{"Line":0}},{"line":348,"address":[2246679],"length":1,"stats":{"Line":0}},{"line":350,"address":[2246719,2246748,2248866],"length":1,"stats":{"Line":0}},{"line":351,"address":[2248787,2246786],"length":1,"stats":{"Line":0}},{"line":352,"address":[2247029,2246909],"length":1,"stats":{"Line":0}},{"line":353,"address":[2247035],"length":1,"stats":{"Line":0}},{"line":354,"address":[2247104,2247155,2248695],"length":1,"stats":{"Line":0}},{"line":355,"address":[2247428],"length":1,"stats":{"Line":0}},{"line":356,"address":[2247512,2247455],"length":1,"stats":{"Line":0}},{"line":360,"address":[2248858],"length":1,"stats":{"Line":0}},{"line":363,"address":[2246729],"length":1,"stats":{"Line":0}},{"line":364,"address":[2248903,2251960,2248944],"length":1,"stats":{"Line":0}},{"line":365,"address":[2249013],"length":1,"stats":{"Line":0}},{"line":366,"address":[2249090,2249187],"length":1,"stats":{"Line":0}},{"line":367,"address":[2249450,2249220],"length":1,"stats":{"Line":0}},{"line":369,"address":[2249193,2249258],"length":1,"stats":{"Line":0}},{"line":371,"address":[2249566,2251542,2249411],"length":1,"stats":{"Line":0}},{"line":372,"address":[2249774,2249678],"length":1,"stats":{"Line":0}},{"line":373,"address":[2249780],"length":1,"stats":{"Line":0}},{"line":374,"address":[2251552,2249852,2249903],"length":1,"stats":{"Line":0}},{"line":375,"address":[2250173],"length":1,"stats":{"Line":0}},{"line":376,"address":[2250245,2250194],"length":1,"stats":{"Line":0}},{"line":381,"address":[2248927],"length":1,"stats":{"Line":0}},{"line":384,"address":[2252400,2252394,2252032],"length":1,"stats":{"Line":0}},{"line":385,"address":[2252091],"length":1,"stats":{"Line":0}},{"line":386,"address":[2252365,2252110,2252228],"length":1,"stats":{"Line":0}},{"line":388,"address":[2252413,2252161],"length":1,"stats":{"Line":0}},{"line":390,"address":[2252357],"length":1,"stats":{"Line":0}}],"covered":0,"coverable":201},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","tests","e2e.rs"],"content":"//! tests e2e.rs\n//! End-to-end “happy path” smoke-tests for the `marlin` binary.\n//!\n//! Run with `cargo test --test e2e` (CI does) or `cargo test`.\n\nuse assert_cmd::prelude::*;\nuse predicates::prelude::*;\nuse std::{fs, path::PathBuf, process::Command};\nuse tempfile::tempdir;\n\n/// Absolute path to the freshly-built `marlin` binary.\nfn marlin_bin() -\u003e PathBuf {\n PathBuf::from(env!(\"CARGO_BIN_EXE_marlin\"))\n}\n\n/// Create the demo directory structure and seed files.\nfn spawn_demo_tree(root: \u0026PathBuf) {\n fs::create_dir_all(root.join(\"Projects/Alpha\")).unwrap();\n fs::create_dir_all(root.join(\"Projects/Beta\")).unwrap();\n fs::create_dir_all(root.join(\"Projects/Gamma\")).unwrap();\n fs::create_dir_all(root.join(\"Logs\")).unwrap();\n fs::create_dir_all(root.join(\"Reports\")).unwrap();\n\n fs::write(root.join(\"Projects/Alpha/draft1.md\"), \"- [ ] TODO foo\\n\").unwrap();\n fs::write(root.join(\"Projects/Alpha/draft2.md\"), \"- [x] TODO foo\\n\").unwrap();\n fs::write(root.join(\"Projects/Beta/final.md\"), \"done\\n\").unwrap();\n fs::write(root.join(\"Projects/Gamma/TODO.txt\"), \"TODO bar\\n\").unwrap();\n fs::write(root.join(\"Logs/app.log\"), \"ERROR omg\\n\").unwrap();\n fs::write(root.join(\"Reports/Q1.pdf\"), \"PDF\\n\").unwrap();\n}\n\n/// Shorthand for “run and must succeed”.\nfn ok(cmd: \u0026mut Command) -\u003e assert_cmd::assert::Assert {\n cmd.assert().success()\n}\n\n#[test]\nfn full_cli_flow() -\u003e Result\u003c(), Box\u003cdyn std::error::Error\u003e\u003e {\n /* ── 1 ░ sandbox ───────────────────────────────────────────── */\n\n let tmp = tempdir()?; // wiped on drop\n let demo_dir = tmp.path().join(\"marlin_demo\");\n spawn_demo_tree(\u0026demo_dir);\n\n let db_path = demo_dir.join(\"index.db\");\n\n // Helper to spawn a fresh `marlin` Command with the DB env-var set\n let marlin = || {\n let mut c = Command::new(marlin_bin());\n c.env(\"MARLIN_DB_PATH\", \u0026db_path);\n c\n };\n\n /* ── 2 ░ init ( auto-scan cwd ) ───────────────────────────── */\n\n ok(marlin()\n .current_dir(\u0026demo_dir)\n .arg(\"init\"));\n\n /* ── 3 ░ tag \u0026 attr demos ─────────────────────────────────── */\n\n ok(marlin()\n .arg(\"tag\")\n .arg(format!(\"{}/Projects/**/*.md\", demo_dir.display()))\n .arg(\"project/md\"));\n\n ok(marlin()\n .arg(\"attr\")\n .arg(\"set\")\n .arg(format!(\"{}/Reports/*.pdf\", demo_dir.display()))\n .arg(\"reviewed\")\n .arg(\"yes\"));\n\n /* ── 4 ░ quick search sanity checks ───────────────────────── */\n\n marlin()\n .arg(\"search\").arg(\"TODO\")\n .assert()\n .stdout(predicate::str::contains(\"TODO.txt\"));\n\n marlin()\n .arg(\"search\").arg(\"attr:reviewed=yes\")\n .assert()\n .stdout(predicate::str::contains(\"Q1.pdf\"));\n\n /* ── 5 ░ link flow \u0026 backlinks ────────────────────────────── */\n\n let foo = demo_dir.join(\"foo.txt\");\n let bar = demo_dir.join(\"bar.txt\");\n fs::write(\u0026foo, \"\")?;\n fs::write(\u0026bar, \"\")?;\n\n ok(marlin().arg(\"scan\").arg(\u0026demo_dir));\n\n ok(marlin()\n .arg(\"link\").arg(\"add\")\n .arg(\u0026foo).arg(\u0026bar));\n\n marlin()\n .arg(\"link\").arg(\"backlinks\").arg(\u0026bar)\n .assert()\n .stdout(predicate::str::contains(\"foo.txt\"));\n\n /* ── 6 ░ backup → delete DB → restore ────────────────────── */\n\n let backup_path = String::from_utf8(\n marlin().arg(\"backup\").output()?.stdout\n )?;\n let backup_file = backup_path.split_whitespace().last().unwrap();\n\n fs::remove_file(\u0026db_path)?; // simulate corruption\n ok(marlin().arg(\"restore\").arg(backup_file)); // restore\n\n // Search must still work afterwards\n marlin()\n .arg(\"search\").arg(\"TODO\")\n .assert()\n .stdout(predicate::str::contains(\"TODO.txt\"));\n\n Ok(())\n}\n\n","traces":[{"line":12,"address":[789200],"length":1,"stats":{"Line":1}},{"line":13,"address":[789208],"length":1,"stats":{"Line":1}},{"line":17,"address":[789232],"length":1,"stats":{"Line":1}},{"line":18,"address":[789251],"length":1,"stats":{"Line":1}},{"line":19,"address":[789312],"length":1,"stats":{"Line":1}},{"line":20,"address":[789373],"length":1,"stats":{"Line":1}},{"line":21,"address":[789434],"length":1,"stats":{"Line":1}},{"line":22,"address":[789495],"length":1,"stats":{"Line":1}},{"line":24,"address":[789556],"length":1,"stats":{"Line":1}},{"line":25,"address":[789635],"length":1,"stats":{"Line":1}},{"line":26,"address":[789714],"length":1,"stats":{"Line":1}},{"line":27,"address":[789793],"length":1,"stats":{"Line":1}},{"line":28,"address":[789872],"length":1,"stats":{"Line":1}},{"line":29,"address":[789951],"length":1,"stats":{"Line":1}},{"line":33,"address":[790048],"length":1,"stats":{"Line":1}},{"line":34,"address":[790066],"length":1,"stats":{"Line":1}}],"covered":16,"coverable":16},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","tests","integration","watcher","watcher_test.rs"],"content":"//! Integration test for the file watcher functionality\n//! \n//! Tests various aspects of the file system watcher including:\n//! - Basic event handling (create, modify, delete files)\n//! - Debouncing of events\n//! - Hierarchical event coalescing\n//! - Graceful shutdown and event draining\n\nuse marlin::watcher::{FileWatcher, WatcherConfig, WatcherState};\nuse std::path::{Path, PathBuf};\nuse std::fs::{self, File};\nuse std::io::Write;\nuse std::thread;\nuse std::time::{Duration, Instant};\nuse tempfile::tempdir;\n\n// Mock filesystem event simulator inspired by inotify-sim\nstruct MockEventSimulator {\n temp_dir: PathBuf,\n files_created: Vec\u003cPathBuf\u003e,\n}\n\nimpl MockEventSimulator {\n fn new(temp_dir: PathBuf) -\u003e Self {\n Self {\n temp_dir,\n files_created: Vec::new(),\n }\n }\n\n fn create_file(\u0026mut self, relative_path: \u0026str, content: \u0026str) -\u003e PathBuf {\n let path = self.temp_dir.join(relative_path);\n if let Some(parent) = path.parent() {\n fs::create_dir_all(parent).expect(\"Failed to create parent directory\");\n }\n \n let mut file = File::create(\u0026path).expect(\"Failed to create file\");\n file.write_all(content.as_bytes()).expect(\"Failed to write content\");\n \n self.files_created.push(path.clone());\n path\n }\n \n fn modify_file(\u0026self, relative_path: \u0026str, new_content: \u0026str) -\u003e PathBuf {\n let path = self.temp_dir.join(relative_path);\n let mut file = File::create(\u0026path).expect(\"Failed to update file\");\n file.write_all(new_content.as_bytes()).expect(\"Failed to write content\");\n path\n }\n \n fn delete_file(\u0026mut self, relative_path: \u0026str) {\n let path = self.temp_dir.join(relative_path);\n fs::remove_file(\u0026path).expect(\"Failed to delete file\");\n \n self.files_created.retain(|p| p != \u0026path);\n }\n \n fn create_burst(\u0026mut self, count: usize, prefix: \u0026str) -\u003e Vec\u003cPathBuf\u003e {\n let mut paths = Vec::with_capacity(count);\n \n for i in 0..count {\n let file_path = format!(\"{}/burst_file_{}.txt\", prefix, i);\n let path = self.create_file(\u0026file_path, \u0026format!(\"Content {}\", i));\n paths.push(path);\n \n // Small delay to simulate rapid but not instantaneous file creation\n thread::sleep(Duration::from_micros(10));\n }\n \n paths\n }\n \n fn cleanup(\u0026self) {\n // No need to do anything as tempdir will clean itself\n }\n}\n\n#[test]\nfn test_basic_watch_functionality() {\n let temp_dir = tempdir().expect(\"Failed to create temp directory\");\n let temp_path = temp_dir.path().to_path_buf();\n \n let mut simulator = MockEventSimulator::new(temp_path.clone());\n \n // Create a test file before starting the watcher\n let initial_file = simulator.create_file(\"initial.txt\", \"Initial content\");\n \n // Configure and start the watcher\n let config = WatcherConfig {\n debounce_ms: 100,\n batch_size: 100,\n max_queue_size: 1000,\n drain_timeout_ms: 1000,\n };\n \n let mut watcher = FileWatcher::new(vec![temp_path.clone()], config)\n .expect(\"Failed to create file watcher\");\n \n // Start the watcher in a separate thread\n let watcher_thread = thread::spawn(move || {\n watcher.start().expect(\"Failed to start watcher\");\n \n // Let it run for a short time\n thread::sleep(Duration::from_secs(5));\n \n // Stop the watcher\n watcher.stop().expect(\"Failed to stop watcher\");\n \n // Return the watcher for inspection\n watcher\n });\n \n // Wait for watcher to initialize\n thread::sleep(Duration::from_millis(500));\n \n // Generate events\n let file1 = simulator.create_file(\"test1.txt\", \"Hello, world!\");\n thread::sleep(Duration::from_millis(200));\n \n let file2 = simulator.create_file(\"dir1/test2.txt\", \"Hello from subdirectory!\");\n thread::sleep(Duration::from_millis(200));\n \n simulator.modify_file(\"test1.txt\", \"Updated content\");\n thread::sleep(Duration::from_millis(200));\n \n simulator.delete_file(\"test1.txt\");\n \n // Wait for watcher thread to complete\n let finished_watcher = watcher_thread.join().expect(\"Watcher thread panicked\");\n \n // Check status after processing events\n let status = finished_watcher.status();\n \n // Assertions\n assert_eq!(status.state, WatcherState::Stopped);\n assert!(status.events_processed \u003e 0, \"Expected events to be processed\");\n assert_eq!(status.queue_size, 0, \"Expected empty queue after stopping\");\n \n // Clean up\n simulator.cleanup();\n}\n\n#[test]\nfn test_debouncing() {\n let temp_dir = tempdir().expect(\"Failed to create temp directory\");\n let temp_path = temp_dir.path().to_path_buf();\n \n let mut simulator = MockEventSimulator::new(temp_path.clone());\n \n // Configure watcher with larger debounce window for this test\n let config = WatcherConfig {\n debounce_ms: 200, // 200ms debounce window\n batch_size: 100,\n max_queue_size: 1000,\n drain_timeout_ms: 1000,\n };\n \n let mut watcher = FileWatcher::new(vec![temp_path.clone()], config)\n .expect(\"Failed to create file watcher\");\n \n // Start the watcher in a separate thread\n let watcher_thread = thread::spawn(move || {\n watcher.start().expect(\"Failed to start watcher\");\n \n // Let it run for enough time to observe debouncing\n thread::sleep(Duration::from_secs(3));\n \n // Stop the watcher\n watcher.stop().expect(\"Failed to stop watcher\");\n \n // Return the watcher for inspection\n watcher\n });\n \n // Wait for watcher to initialize\n thread::sleep(Duration::from_millis(500));\n \n // Rapidly update the same file multiple times within the debounce window\n let test_file = \"test_debounce.txt\";\n simulator.create_file(test_file, \"Initial content\");\n \n // Update the same file multiple times within debounce window\n for i in 1..10 {\n simulator.modify_file(test_file, \u0026format!(\"Update {}\", i));\n thread::sleep(Duration::from_millis(10)); // Short delay between updates\n }\n \n // Wait for debounce window and processing\n thread::sleep(Duration::from_millis(500));\n \n // Complete the test\n let finished_watcher = watcher_thread.join().expect(\"Watcher thread panicked\");\n let status = finished_watcher.status();\n \n // We should have processed fewer events than modifications made\n // due to debouncing (exact count depends on implementation details)\n assert!(status.events_processed \u003c 10, \n \"Expected fewer events processed than modifications due to debouncing\");\n \n // Clean up\n simulator.cleanup();\n}\n\n#[test]\nfn test_event_flood() {\n let temp_dir = tempdir().expect(\"Failed to create temp directory\");\n let temp_path = temp_dir.path().to_path_buf();\n \n let mut simulator = MockEventSimulator::new(temp_path.clone());\n \n // Configure with settings tuned for burst handling\n let config = WatcherConfig {\n debounce_ms: 100,\n batch_size: 500, // Handle larger batches\n max_queue_size: 10000, // Large queue for burst\n drain_timeout_ms: 5000, // Longer drain time for cleanup\n };\n \n let mut watcher = FileWatcher::new(vec![temp_path.clone()], config)\n .expect(\"Failed to create file watcher\");\n \n // Start the watcher\n let watcher_thread = thread::spawn(move || {\n watcher.start().expect(\"Failed to start watcher\");\n \n // Let it run for enough time to process a large burst\n thread::sleep(Duration::from_secs(10));\n \n // Stop the watcher\n watcher.stop().expect(\"Failed to stop watcher\");\n \n // Return the watcher for inspection\n watcher\n });\n \n // Wait for watcher to initialize\n thread::sleep(Duration::from_millis(500));\n \n // Create 1000 files in rapid succession (smaller scale for test)\n let start_time = Instant::now();\n let created_files = simulator.create_burst(1000, \"flood\");\n let creation_time = start_time.elapsed();\n \n println!(\"Created 1000 files in {:?}\", creation_time);\n \n // Wait for processing to complete\n thread::sleep(Duration::from_secs(5));\n \n // Complete the test\n let finished_watcher = watcher_thread.join().expect(\"Watcher thread panicked\");\n let status = finished_watcher.status();\n \n // Verify processing occurred\n assert!(status.events_processed \u003e 0, \"Expected events to be processed\");\n assert_eq!(status.queue_size, 0, \"Expected empty queue after stopping\");\n \n // Clean up\n simulator.cleanup();\n}\n\n#[test]\nfn test_hierarchical_debouncing() {\n let temp_dir = tempdir().expect(\"Failed to create temp directory\");\n let temp_path = temp_dir.path().to_path_buf();\n \n let mut simulator = MockEventSimulator::new(temp_path.clone());\n \n // Configure watcher\n let config = WatcherConfig {\n debounce_ms: 200,\n batch_size: 100,\n max_queue_size: 1000,\n drain_timeout_ms: 1000,\n };\n \n let mut watcher = FileWatcher::new(vec![temp_path.clone()], config)\n .expect(\"Failed to create file watcher\");\n \n // Start the watcher\n let watcher_thread = thread::spawn(move || {\n watcher.start().expect(\"Failed to start watcher\");\n \n // Let it run\n thread::sleep(Duration::from_secs(5));\n \n // Stop the watcher\n watcher.stop().expect(\"Failed to stop watcher\");\n \n // Return the watcher\n watcher\n });\n \n // Wait for watcher to initialize\n thread::sleep(Duration::from_millis(500));\n \n // Create directory structure\n let nested_dir = \"parent/child/grandchild\";\n fs::create_dir_all(temp_path.join(nested_dir)).expect(\"Failed to create nested directories\");\n \n // Create files in the hierarchy\n simulator.create_file(\"parent/file1.txt\", \"Content 1\");\n simulator.create_file(\"parent/child/file2.txt\", \"Content 2\");\n simulator.create_file(\"parent/child/grandchild/file3.txt\", \"Content 3\");\n \n // Wait a bit\n thread::sleep(Duration::from_millis(300));\n \n // Complete the test\n let finished_watcher = watcher_thread.join().expect(\"Watcher thread panicked\");\n \n // Clean up\n simulator.cleanup();\n}\n\n#[test]\nfn test_graceful_shutdown() {\n let temp_dir = tempdir().expect(\"Failed to create temp directory\");\n let temp_path = temp_dir.path().to_path_buf();\n \n let mut simulator = MockEventSimulator::new(temp_path.clone());\n \n // Configure watcher with specific drain timeout\n let config = WatcherConfig {\n debounce_ms: 100,\n batch_size: 100,\n max_queue_size: 1000,\n drain_timeout_ms: 2000, // 2 second drain timeout\n };\n \n let mut watcher = FileWatcher::new(vec![temp_path.clone()], config)\n .expect(\"Failed to create file watcher\");\n \n // Start the watcher\n watcher.start().expect(\"Failed to start watcher\");\n \n // Wait for initialization\n thread::sleep(Duration::from_millis(500));\n \n // Create files\n for i in 0..10 {\n simulator.create_file(\u0026format!(\"shutdown_test_{}.txt\", i), \"Shutdown test\");\n thread::sleep(Duration::from_millis(10));\n }\n \n // Immediately request shutdown while events are being processed\n let shutdown_start = Instant::now();\n watcher.stop().expect(\"Failed to stop watcher\");\n let shutdown_duration = shutdown_start.elapsed();\n \n // Shutdown should take close to the drain timeout but not excessively longer\n println!(\"Shutdown took {:?}\", shutdown_duration);\n assert!(shutdown_duration \u003e= Duration::from_millis(100), \n \"Shutdown was too quick, may not have drained properly\");\n assert!(shutdown_duration \u003c= Duration::from_millis(3000), \n \"Shutdown took too long\");\n \n // Verify final state\n let status = watcher.status();\n assert_eq!(status.state, WatcherState::Stopped);\n assert_eq!(status.queue_size, 0, \"Queue should be empty after shutdown\");\n \n // Clean up\n simulator.cleanup();\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","tests","neg.rs"],"content":"//! tests neg.rs\n//! Negative-path integration tests (“should fail / warn”).\n\nuse predicates::str;\nuse tempfile::tempdir;\n\nmod util;\nuse util::marlin;\n\n/* ───────────────────────── LINKS ─────────────────────────────── */\n\n#[test]\nfn link_non_indexed_should_fail() {\n let tmp = tempdir().unwrap();\n\n marlin(\u0026tmp).current_dir(tmp.path()).arg(\"init\").assert().success();\n\n std::fs::write(tmp.path().join(\"foo.txt\"), \"\").unwrap();\n std::fs::write(tmp.path().join(\"bar.txt\"), \"\").unwrap();\n\n marlin(\u0026tmp)\n .current_dir(tmp.path())\n .args([\n \"link\", \"add\",\n \u0026tmp.path().join(\"foo.txt\").to_string_lossy(),\n \u0026tmp.path().join(\"bar.txt\").to_string_lossy()\n ])\n .assert()\n .failure()\n .stderr(str::contains(\"file not indexed\"));\n}\n\n/* ───────────────────────── ATTR ─────────────────────────────── */\n\n#[test]\nfn attr_set_on_non_indexed_file_should_warn() {\n let tmp = tempdir().unwrap();\n marlin(\u0026tmp).current_dir(tmp.path()).arg(\"init\").assert().success();\n\n let ghost = tmp.path().join(\"ghost.txt\");\n std::fs::write(\u0026ghost, \"\").unwrap();\n\n marlin(\u0026tmp)\n .args([\"attr\",\"set\",\n \u0026ghost.to_string_lossy(),\"foo\",\"bar\"])\n .assert()\n .success() // exits 0\n .stderr(str::contains(\"not indexed\"));\n}\n\n/* ───────────────────── COLLECTIONS ───────────────────────────── */\n\n#[test]\nfn coll_add_unknown_collection_should_fail() {\n let tmp = tempdir().unwrap();\n let file = tmp.path().join(\"doc.txt\");\n std::fs::write(\u0026file, \"\").unwrap();\n\n marlin(\u0026tmp).current_dir(tmp.path()).arg(\"init\").assert().success();\n\n marlin(\u0026tmp)\n .args([\"coll\",\"add\",\"nope\",\u0026file.to_string_lossy()])\n .assert()\n .failure();\n}\n\n/* ───────────────────── RESTORE (bad file) ───────────────────── */\n\n#[test]\nfn restore_with_nonexistent_backup_should_fail() {\n let tmp = tempdir().unwrap();\n\n // create an empty DB first\n marlin(\u0026tmp).arg(\"init\").assert().success();\n\n marlin(\u0026tmp)\n .args([\"restore\", \"/definitely/not/here.db\"])\n .assert()\n .failure()\n .stderr(str::contains(\"Failed to restore\"));\n}\n\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","tests","pos.rs"],"content":"//! tests pos.rs\n//! Positive-path integration checks for every sub-command\n//! that already has real logic behind it.\n\nmod util;\nuse util::marlin;\n\nuse predicates::{prelude::*, str}; // brings `PredicateBooleanExt::and`\nuse std::fs;\nuse tempfile::tempdir;\n\n/* ─────────────────────────── TAG ─────────────────────────────── */\n\n#[test]\nfn tag_should_add_hierarchical_tag_and_search_finds_it() {\n let tmp = tempdir().unwrap();\n let file = tmp.path().join(\"foo.md\");\n fs::write(\u0026file, \"# test\\n\").unwrap();\n\n marlin(\u0026tmp).current_dir(tmp.path()).arg(\"init\").assert().success();\n\n marlin(\u0026tmp)\n .args([\"tag\", file.to_str().unwrap(), \"project/md\"])\n .assert().success();\n\n marlin(\u0026tmp)\n .args([\"search\", \"tag:project/md\"])\n .assert()\n .success()\n .stdout(str::contains(\"foo.md\"));\n}\n\n/* ─────────────────────────── ATTR ────────────────────────────── */\n\n#[test]\nfn attr_set_then_ls_roundtrip() {\n let tmp = tempdir().unwrap();\n let file = tmp.path().join(\"report.pdf\");\n fs::write(\u0026file, \"%PDF-1.4\\n\").unwrap();\n\n marlin(\u0026tmp).current_dir(tmp.path()).arg(\"init\").assert().success();\n\n marlin(\u0026tmp)\n .args([\"attr\", \"set\", file.to_str().unwrap(), \"reviewed\", \"yes\"])\n .assert().success();\n\n marlin(\u0026tmp)\n .args([\"attr\", \"ls\", file.to_str().unwrap()])\n .assert()\n .success()\n .stdout(str::contains(\"reviewed = yes\"));\n}\n\n/* ─────────────────────── COLLECTIONS ────────────────────────── */\n\n#[test]\nfn coll_create_add_and_list() {\n let tmp = tempdir().unwrap();\n\n let a = tmp.path().join(\"a.txt\");\n let b = tmp.path().join(\"b.txt\");\n fs::write(\u0026a, \"\").unwrap();\n fs::write(\u0026b, \"\").unwrap();\n\n marlin(\u0026tmp).current_dir(tmp.path()).arg(\"init\").assert().success();\n\n marlin(\u0026tmp).args([\"coll\", \"create\", \"Set\"]).assert().success();\n for f in [\u0026a, \u0026b] {\n marlin(\u0026tmp).args([\"coll\", \"add\", \"Set\", f.to_str().unwrap()]).assert().success();\n }\n\n marlin(\u0026tmp)\n .args([\"coll\", \"list\", \"Set\"])\n .assert()\n .success()\n .stdout(str::contains(\"a.txt\").and(str::contains(\"b.txt\")));\n}\n\n/* ─────────────────────────── VIEWS ───────────────────────────── */\n\n#[test]\nfn view_save_list_and_exec() {\n let tmp = tempdir().unwrap();\n\n let todo = tmp.path().join(\"TODO.txt\");\n fs::write(\u0026todo, \"remember the milk\\n\").unwrap();\n\n marlin(\u0026tmp).current_dir(tmp.path()).arg(\"init\").assert().success();\n\n // save \u0026 list\n marlin(\u0026tmp).args([\"view\", \"save\", \"tasks\", \"milk\"]).assert().success();\n marlin(\u0026tmp)\n .args([\"view\", \"list\"])\n .assert()\n .success()\n .stdout(str::contains(\"tasks: milk\"));\n\n // exec\n marlin(\u0026tmp)\n .args([\"view\", \"exec\", \"tasks\"])\n .assert()\n .success()\n .stdout(str::contains(\"TODO.txt\"));\n}\n\n/* ─────────────────────────── LINKS ───────────────────────────── */\n\n#[test]\nfn link_add_rm_and_list() {\n let tmp = tempdir().unwrap();\n\n let foo = tmp.path().join(\"foo.txt\");\n let bar = tmp.path().join(\"bar.txt\");\n fs::write(\u0026foo, \"\").unwrap();\n fs::write(\u0026bar, \"\").unwrap();\n\n // handy closure\n let mc = || marlin(\u0026tmp);\n\n mc().current_dir(tmp.path()).arg(\"init\").assert().success();\n mc().args([\"scan\", tmp.path().to_str().unwrap()]).assert().success();\n\n // add\n mc().args([\"link\", \"add\", foo.to_str().unwrap(), bar.to_str().unwrap()])\n .assert().success();\n\n // list (outgoing default)\n mc().args([\"link\", \"list\", foo.to_str().unwrap()])\n .assert().success()\n .stdout(str::contains(\"foo.txt\").and(str::contains(\"bar.txt\")));\n\n // remove\n mc().args([\"link\", \"rm\", foo.to_str().unwrap(), bar.to_str().unwrap()])\n .assert().success();\n\n // list now empty\n mc().args([\"link\", \"list\", foo.to_str().unwrap()])\n .assert().success()\n .stdout(str::is_empty());\n}\n\n/* ─────────────────────── SCAN (multi-path) ───────────────────── */\n\n#[test]\nfn scan_with_multiple_paths_indexes_all() {\n let tmp = tempdir().unwrap();\n\n let dir_a = tmp.path().join(\"A\");\n let dir_b = tmp.path().join(\"B\");\n std::fs::create_dir_all(\u0026dir_a).unwrap();\n std::fs::create_dir_all(\u0026dir_b).unwrap();\n let f1 = dir_a.join(\"one.txt\");\n let f2 = dir_b.join(\"two.txt\");\n fs::write(\u0026f1, \"\").unwrap();\n fs::write(\u0026f2, \"\").unwrap();\n\n marlin(\u0026tmp).current_dir(tmp.path()).arg(\"init\").assert().success();\n\n // multi-path scan\n marlin(\u0026tmp)\n .args([\"scan\", dir_a.to_str().unwrap(), dir_b.to_str().unwrap()])\n .assert().success();\n\n // both files findable\n for term in [\"one.txt\", \"two.txt\"] {\n marlin(\u0026tmp).args([\"search\", term])\n .assert()\n .success()\n .stdout(str::contains(term));\n }\n}\n\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","tests","util.rs"],"content":"//! tests/util.rs\n//! Small helpers shared across integration tests.\n\nuse std::path::{Path, PathBuf};\nuse tempfile::TempDir;\nuse assert_cmd::Command; \n/// Absolute path to the freshly-built `marlin` binary.\npub fn bin() -\u003e PathBuf {\n PathBuf::from(env!(\"CARGO_BIN_EXE_marlin\"))\n}\n\n/// Build a `Command` for `marlin` whose `MARLIN_DB_PATH` is\n/// `\u003ctmp\u003e/index.db`.\n///\n/// Each call yields a brand-new `Command`, so callers can freely add\n/// arguments, change the working directory, etc., without affecting\n/// other invocations.\npub fn marlin(tmp: \u0026TempDir) -\u003e Command {\n let db_path: \u0026Path = \u0026tmp.path().join(\"index.db\");\n let mut cmd = Command::new(bin());\n cmd.env(\"MARLIN_DB_PATH\", db_path);\n cmd\n}\n","traces":[{"line":8,"address":[776352],"length":1,"stats":{"Line":10}},{"line":9,"address":[776360],"length":1,"stats":{"Line":10}},{"line":18,"address":[776384,776727,776721],"length":1,"stats":{"Line":10}},{"line":19,"address":[776535,776422],"length":1,"stats":{"Line":20}},{"line":20,"address":[776575],"length":1,"stats":{"Line":10}},{"line":21,"address":[776612],"length":1,"stats":{"Line":10}},{"line":22,"address":[776682],"length":1,"stats":{"Line":10}}],"covered":7,"coverable":7},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","backup.rs"],"content":"// libmarlin/src/backup.rs\n\nuse anyhow::{anyhow, Context, Result};\nuse chrono::{DateTime, Local, NaiveDateTime, Utc, TimeZone};\nuse rusqlite;\nuse std::fs;\nuse std::path::{Path, PathBuf};\nuse std::time::Duration;\n\nuse crate::error as marlin_error;\n\n#[derive(Debug, Clone)]\npub struct BackupInfo {\n pub id: String,\n pub timestamp: DateTime\u003cUtc\u003e,\n pub size_bytes: u64,\n pub hash: Option\u003cString\u003e,\n}\n\n#[derive(Debug)]\npub struct PruneResult {\n pub kept: Vec\u003cBackupInfo\u003e,\n pub removed: Vec\u003cBackupInfo\u003e,\n}\n\n// FIX 2: Add derive(Debug) here\n#[derive(Debug)]\npub struct BackupManager {\n live_db_path: PathBuf,\n backups_dir: PathBuf,\n}\n\nimpl BackupManager {\n pub fn new\u003cP1: AsRef\u003cPath\u003e, P2: AsRef\u003cPath\u003e\u003e(live_db_path: P1, backups_dir: P2) -\u003e Result\u003cSelf\u003e {\n let backups_dir_path = backups_dir.as_ref().to_path_buf();\n if !backups_dir_path.exists() {\n fs::create_dir_all(\u0026backups_dir_path).with_context(|| {\n format!(\n \"Failed to create backup directory at {}\",\n backups_dir_path.display()\n )\n })?;\n } else if !backups_dir_path.is_dir() {\n return Err(anyhow!(\"Backups path exists but is not a directory: {}\", backups_dir_path.display()));\n }\n Ok(Self {\n live_db_path: live_db_path.as_ref().to_path_buf(),\n backups_dir: backups_dir_path,\n })\n }\n\n pub fn create_backup(\u0026self) -\u003e Result\u003cBackupInfo\u003e {\n let stamp = Local::now().format(\"%Y-%m-%d_%H-%M-%S_%f\");\n let backup_file_name = format!(\"backup_{stamp}.db\");\n let backup_file_path = self.backups_dir.join(\u0026backup_file_name);\n\n if !self.live_db_path.exists() {\n return Err(anyhow::Error::new(std::io::Error::new(\n std::io::ErrorKind::NotFound,\n format!(\"Live DB path does not exist: {}\", self.live_db_path.display()),\n )).context(\"Cannot create backup from non-existent live DB\"));\n }\n\n let src_conn = rusqlite::Connection::open_with_flags(\n \u0026self.live_db_path,\n rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,\n )\n .with_context(|| {\n format!(\n \"Failed to open source DB ('{}') for backup\",\n self.live_db_path.display()\n )\n })?;\n\n let mut dst_conn = rusqlite::Connection::open(\u0026backup_file_path).with_context(|| {\n format!(\n \"Failed to open destination backup file: {}\",\n backup_file_path.display()\n )\n })?;\n\n let backup_op =\n rusqlite::backup::Backup::new(\u0026src_conn, \u0026mut dst_conn).with_context(|| {\n format!(\n \"Failed to initialize backup from {} to {}\",\n self.live_db_path.display(),\n backup_file_path.display()\n )\n })?;\n\n backup_op\n .run_to_completion(100, Duration::from_millis(250), None)\n .map_err(|e| anyhow::Error::new(e).context(\"SQLite backup operation failed\"))?;\n\n let metadata = fs::metadata(\u0026backup_file_path).with_context(|| {\n format!(\n \"Failed to get metadata for backup file: {}\",\n backup_file_path.display()\n )\n })?;\n\n Ok(BackupInfo {\n id: backup_file_name,\n timestamp: DateTime::from(metadata.modified()?),\n size_bytes: metadata.len(),\n hash: None,\n })\n }\n\n pub fn list_backups(\u0026self) -\u003e Result\u003cVec\u003cBackupInfo\u003e\u003e {\n let mut backup_infos = Vec::new();\n \n if !self.backups_dir.exists() { \n return Ok(backup_infos);\n }\n\n for entry_result in fs::read_dir(\u0026self.backups_dir).with_context(|| {\n format!(\n \"Failed to read backup directory: {}\",\n self.backups_dir.display()\n )\n })? {\n let entry = entry_result?;\n let path = entry.path();\n\n if path.is_file() {\n if let Some(filename_osstr) = path.file_name() {\n if let Some(filename) = filename_osstr.to_str() {\n if filename.starts_with(\"backup_\") \u0026\u0026 filename.ends_with(\".db\") {\n let ts_str = filename\n .trim_start_matches(\"backup_\")\n .trim_end_matches(\".db\");\n \n let naive_dt = match NaiveDateTime::parse_from_str(ts_str, \"%Y-%m-%d_%H-%M-%S_%f\") {\n Ok(dt) =\u003e dt,\n Err(_) =\u003e match NaiveDateTime::parse_from_str(ts_str, \"%Y-%m-%d_%H-%M-%S\") {\n Ok(dt) =\u003e dt,\n Err(_) =\u003e { \n let metadata = fs::metadata(\u0026path).with_context(|| format!(\"Failed to get metadata for {}\", path.display()))?;\n DateTime::\u003cUtc\u003e::from(metadata.modified()?).naive_utc()\n }\n }\n };\n \n let local_dt_result = Local.from_local_datetime(\u0026naive_dt);\n let local_dt = match local_dt_result {\n chrono::LocalResult::Single(dt) =\u003e dt,\n chrono::LocalResult::Ambiguous(dt1, _dt2) =\u003e {\n eprintln!(\"Warning: Ambiguous local time for backup {}, taking first interpretation.\", filename);\n dt1\n },\n chrono::LocalResult::None =\u003e {\n eprintln!(\"Warning: Invalid local time for backup {}, skipping.\", filename);\n continue; \n }\n };\n let timestamp_utc = DateTime::\u003cUtc\u003e::from(local_dt);\n\n let metadata = fs::metadata(\u0026path)?;\n backup_infos.push(BackupInfo {\n id: filename.to_string(),\n timestamp: timestamp_utc,\n size_bytes: metadata.len(),\n hash: None,\n });\n }\n }\n }\n }\n }\n backup_infos.sort_by_key(|b| std::cmp::Reverse(b.timestamp));\n Ok(backup_infos)\n }\n\n pub fn prune(\u0026self, keep_count: usize) -\u003e Result\u003cPruneResult\u003e {\n let all_backups = self.list_backups()?; \n\n let mut kept = Vec::new();\n let mut removed = Vec::new();\n\n if keep_count \u003e= all_backups.len() { \n kept = all_backups;\n } else {\n for (index, backup_info) in all_backups.into_iter().enumerate() {\n if index \u003c keep_count {\n kept.push(backup_info);\n } else {\n let backup_file_path = self.backups_dir.join(\u0026backup_info.id);\n if backup_file_path.exists() { \n fs::remove_file(\u0026backup_file_path).with_context(|| {\n format!(\n \"Failed to remove old backup file: {}\",\n backup_file_path.display()\n )\n })?;\n }\n removed.push(backup_info);\n }\n }\n }\n Ok(PruneResult { kept, removed })\n }\n\n pub fn restore_from_backup(\u0026self, backup_id: \u0026str) -\u003e Result\u003c()\u003e {\n let backup_file_path = self.backups_dir.join(backup_id);\n if !backup_file_path.exists() || !backup_file_path.is_file() {\n return Err(anyhow::Error::new(marlin_error::Error::NotFound(format!(\n \"Backup file not found or is not a file: {}\",\n backup_file_path.display()\n ))));\n }\n\n fs::copy(\u0026backup_file_path, \u0026self.live_db_path).with_context(|| {\n format!(\n \"Failed to copy backup {} to live DB {}\",\n backup_file_path.display(),\n self.live_db_path.display()\n )\n })?;\n Ok(())\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n use tempfile::tempdir;\n use crate::db::open as open_marlin_db;\n // FIX 1: Remove unused import std::io::ErrorKind\n // use std::io::ErrorKind; \n\n fn create_valid_live_db(path: \u0026Path) -\u003e rusqlite::Connection {\n let conn = open_marlin_db(path)\n .unwrap_or_else(|e| panic!(\"Failed to open/create test DB at {}: {:?}\", path.display(), e));\n conn.execute_batch(\n \"CREATE TABLE IF NOT EXISTS test_table (id INTEGER PRIMARY KEY, data TEXT);\n INSERT INTO test_table (data) VALUES ('initial_data');\"\n ).expect(\"Failed to initialize test table\");\n conn\n }\n\n #[test]\n fn test_backup_manager_new_creates_dir() {\n let base_tmp = tempdir().unwrap();\n let live_db_path = base_tmp.path().join(\"live_new_creates.db\");\n let _conn = create_valid_live_db(\u0026live_db_path);\n\n let backups_dir = base_tmp.path().join(\"my_backups_new_creates_test\");\n\n assert!(!backups_dir.exists());\n let manager = BackupManager::new(\u0026live_db_path, \u0026backups_dir).unwrap();\n assert!(manager.backups_dir.exists()); \n assert!(backups_dir.exists());\n }\n\n #[test]\n fn test_backup_manager_new_with_existing_dir() {\n let base_tmp = tempdir().unwrap();\n let live_db_path = base_tmp.path().join(\"live_existing_dir.db\");\n let _conn = create_valid_live_db(\u0026live_db_path);\n\n let backups_dir = base_tmp.path().join(\"my_backups_existing_test\");\n std::fs::create_dir_all(\u0026backups_dir).unwrap(); \n\n assert!(backups_dir.exists());\n let manager_res = BackupManager::new(\u0026live_db_path, \u0026backups_dir);\n assert!(manager_res.is_ok());\n let manager = manager_res.unwrap();\n assert_eq!(manager.backups_dir, backups_dir);\n }\n \n #[test]\n fn test_backup_manager_new_fails_if_backup_path_is_file() {\n let base_tmp = tempdir().unwrap();\n let live_db_path = base_tmp.path().join(\"live_backup_path_is_file.db\");\n let _conn = create_valid_live_db(\u0026live_db_path);\n let file_as_backups_dir = base_tmp.path().join(\"file_as_backups_dir\");\n std::fs::write(\u0026file_as_backups_dir, \"i am a file\").unwrap();\n\n let manager_res = BackupManager::new(\u0026live_db_path, \u0026file_as_backups_dir);\n assert!(manager_res.is_err());\n assert!(manager_res.unwrap_err().to_string().contains(\"Backups path exists but is not a directory\"));\n }\n\n #[test]\n fn test_create_backup_failure_non_existent_live_db() {\n let base_tmp = tempdir().unwrap();\n let live_db_path = base_tmp.path().join(\"non_existent_live.db\"); \n let backups_dir = base_tmp.path().join(\"backups_fail_test\");\n\n let manager = BackupManager::new(\u0026live_db_path, \u0026backups_dir).unwrap();\n let backup_result = manager.create_backup();\n assert!(backup_result.is_err());\n let err_str = backup_result.unwrap_err().to_string();\n assert!(err_str.contains(\"Cannot create backup from non-existent live DB\") || err_str.contains(\"Failed to open source DB\"));\n }\n\n #[test]\n fn test_create_list_prune_backups() {\n let tmp = tempdir().unwrap();\n let live_db_file = tmp.path().join(\"live_for_clp_test.db\");\n let _conn_live = create_valid_live_db(\u0026live_db_file);\n\n let backups_storage_dir = tmp.path().join(\"backups_clp_storage_test\");\n \n let manager = BackupManager::new(\u0026live_db_file, \u0026backups_storage_dir).unwrap();\n\n let initial_list = manager.list_backups().unwrap();\n assert!(initial_list.is_empty(), \"Backup list should be empty initially\");\n\n let prune_empty_result = manager.prune(2).unwrap();\n assert!(prune_empty_result.kept.is_empty());\n assert!(prune_empty_result.removed.is_empty());\n\n let mut created_backup_ids = Vec::new();\n for i in 0..5 {\n let info = manager\n .create_backup()\n .unwrap_or_else(|e| panic!(\"Failed to create backup {}: {:?}\", i, e));\n created_backup_ids.push(info.id.clone()); \n std::thread::sleep(std::time::Duration::from_millis(30));\n }\n\n let listed_backups = manager.list_backups().unwrap();\n assert_eq!(listed_backups.len(), 5);\n for id in \u0026created_backup_ids {\n assert!(\n listed_backups.iter().any(|b| \u0026b.id == id),\n \"Backup ID {} not found in list\", id\n );\n }\n if listed_backups.len() \u003e= 2 {\n assert!(listed_backups[0].timestamp \u003e= listed_backups[1].timestamp);\n }\n\n let prune_to_zero_result = manager.prune(0).unwrap();\n assert_eq!(prune_to_zero_result.kept.len(), 0);\n assert_eq!(prune_to_zero_result.removed.len(), 5);\n let listed_after_prune_zero = manager.list_backups().unwrap();\n assert!(listed_after_prune_zero.is_empty());\n\n created_backup_ids.clear();\n for i in 0..5 { \n let info = manager\n .create_backup()\n .unwrap_or_else(|e| panic!(\"Failed to create backup {}: {:?}\", i, e));\n created_backup_ids.push(info.id.clone());\n std::thread::sleep(std::time::Duration::from_millis(30));\n }\n\n let prune_keep_more_result = manager.prune(10).unwrap();\n assert_eq!(prune_keep_more_result.kept.len(), 5);\n assert_eq!(prune_keep_more_result.removed.len(), 0);\n let listed_after_prune_more = manager.list_backups().unwrap();\n assert_eq!(listed_after_prune_more.len(), 5);\n\n let prune_result = manager.prune(2).unwrap();\n assert_eq!(prune_result.kept.len(), 2);\n assert_eq!(prune_result.removed.len(), 3);\n\n let listed_after_prune = manager.list_backups().unwrap();\n assert_eq!(listed_after_prune.len(), 2);\n\n assert_eq!(listed_after_prune[0].id, created_backup_ids[4]);\n assert_eq!(listed_after_prune[1].id, created_backup_ids[3]);\n \n for removed_info in prune_result.removed {\n assert!(\n !backups_storage_dir.join(\u0026removed_info.id).exists(),\n \"Removed backup file {} should not exist\", removed_info.id\n );\n }\n for kept_info in prune_result.kept {\n assert!(\n backups_storage_dir.join(\u0026kept_info.id).exists(),\n \"Kept backup file {} should exist\", kept_info.id\n );\n }\n }\n\n #[test]\n fn test_restore_backup() {\n let tmp = tempdir().unwrap();\n let live_db_path = tmp.path().join(\"live_for_restore_test.db\");\n \n let initial_value = \"initial_data_for_restore\";\n {\n // FIX 3: Remove `mut` from conn here\n let conn = create_valid_live_db(\u0026live_db_path);\n conn.execute(\"DELETE FROM test_table\", []).unwrap(); \n conn.execute(\"INSERT INTO test_table (data) VALUES (?1)\", [initial_value]).unwrap();\n }\n\n let backups_dir = tmp.path().join(\"backups_for_restore_test_dir\");\n let manager = BackupManager::new(\u0026live_db_path, \u0026backups_dir).unwrap();\n\n let backup_info = manager.create_backup().unwrap();\n\n let modified_value = \"modified_data_for_restore\";\n {\n // FIX 3: Remove `mut` from conn here\n let conn = rusqlite::Connection::open(\u0026live_db_path) \n .expect(\"Failed to open live DB for modification\");\n conn.execute(\"UPDATE test_table SET data = ?1\", [modified_value])\n .expect(\"Failed to update data\");\n let modified_check: String = conn\n .query_row(\"SELECT data FROM test_table\", [], |row| row.get(0))\n .unwrap();\n assert_eq!(modified_check, modified_value);\n }\n \n manager.restore_from_backup(\u0026backup_info.id).unwrap();\n\n {\n let conn_after_restore = rusqlite::Connection::open(\u0026live_db_path)\n .expect(\"Failed to open live DB after restore\");\n let restored_data: String = conn_after_restore\n .query_row(\"SELECT data FROM test_table\", [], |row| row.get(0))\n .unwrap();\n assert_eq!(restored_data, initial_value);\n }\n }\n\n #[test]\n fn test_restore_non_existent_backup() {\n let tmp = tempdir().unwrap();\n let live_db_path = tmp.path().join(\"live_for_restore_fail_test.db\");\n let _conn = create_valid_live_db(\u0026live_db_path);\n\n let backups_dir = tmp.path().join(\"backups_for_restore_fail_test\");\n let manager = BackupManager::new(\u0026live_db_path, \u0026backups_dir).unwrap();\n\n let result = manager.restore_from_backup(\"non_existent_backup.db\");\n assert!(result.is_err());\n let err_string = result.unwrap_err().to_string();\n assert!(err_string.contains(\"Backup file not found\"), \"Error string was: {}\", err_string);\n }\n\n #[test]\n fn list_backups_with_non_backup_files() {\n let tmp = tempdir().unwrap();\n let live_db_file = tmp.path().join(\"live_for_list_test.db\");\n let _conn = create_valid_live_db(\u0026live_db_file);\n let backups_dir = tmp.path().join(\"backups_list_mixed_files_test\");\n \n let manager = BackupManager::new(\u0026live_db_file, \u0026backups_dir).unwrap();\n\n manager.create_backup().unwrap(); \n \n std::fs::write(backups_dir.join(\"not_a_backup.txt\"), \"hello\").unwrap();\n std::fs::write(\n backups_dir.join(\"backup_malformed.db.tmp\"),\n \"temp data\",\n )\n .unwrap();\n std::fs::create_dir(backups_dir.join(\"a_subdir\")).unwrap();\n\n let listed_backups = manager.list_backups().unwrap();\n assert_eq!(\n listed_backups.len(),\n 1,\n \"Should only list the valid backup file\"\n );\n assert!(listed_backups[0].id.starts_with(\"backup_\"));\n assert!(listed_backups[0].id.ends_with(\".db\"));\n }\n\n #[test]\n fn list_backups_handles_io_error_on_read_dir() {\n let tmp = tempdir().unwrap();\n let live_db_file = tmp.path().join(\"live_for_list_io_error.db\");\n let _conn = create_valid_live_db(\u0026live_db_file);\n \n let backups_dir_for_deletion = tmp.path().join(\"backups_dir_to_delete_test\");\n let manager_for_deletion = BackupManager::new(\u0026live_db_file, \u0026backups_dir_for_deletion).unwrap();\n std::fs::remove_dir_all(\u0026backups_dir_for_deletion).unwrap(); \n\n let list_res = manager_for_deletion.list_backups().unwrap();\n assert!(list_res.is_empty());\n }\n}","traces":[{"line":34,"address":[1940524,1940518,1939536],"length":1,"stats":{"Line":1}},{"line":35,"address":[],"length":0,"stats":{"Line":2}},{"line":36,"address":[],"length":0,"stats":{"Line":2}},{"line":37,"address":[1939830,1939973,1940544,1939777,1939927],"length":1,"stats":{"Line":2}},{"line":38,"address":[],"length":0,"stats":{"Line":0}},{"line":39,"address":[],"length":0,"stats":{"Line":0}},{"line":40,"address":[],"length":0,"stats":{"Line":0}},{"line":43,"address":[],"length":0,"stats":{"Line":4}},{"line":44,"address":[],"length":0,"stats":{"Line":1}},{"line":46,"address":[1940399],"length":1,"stats":{"Line":1}},{"line":47,"address":[1940324,1939941],"length":1,"stats":{"Line":2}},{"line":48,"address":[],"length":0,"stats":{"Line":1}},{"line":52,"address":[1715154,1711872,1715148],"length":1,"stats":{"Line":1}},{"line":53,"address":[1711911],"length":1,"stats":{"Line":1}},{"line":54,"address":[1712001,1712048],"length":1,"stats":{"Line":2}},{"line":55,"address":[1712164,1712248],"length":1,"stats":{"Line":2}},{"line":57,"address":[5530103,5530186],"length":1,"stats":{"Line":2}},{"line":58,"address":[5530494],"length":1,"stats":{"Line":1}},{"line":59,"address":[1712401],"length":1,"stats":{"Line":1}},{"line":60,"address":[1712500,1712409],"length":1,"stats":{"Line":2}},{"line":68,"address":[1940720],"length":1,"stats":{"Line":0}},{"line":69,"address":[1940779],"length":1,"stats":{"Line":0}},{"line":71,"address":[5443926],"length":1,"stats":{"Line":0}},{"line":75,"address":[5530945,5531116,5531016,5533143],"length":1,"stats":{"Line":2}},{"line":76,"address":[1940955],"length":1,"stats":{"Line":0}},{"line":78,"address":[1940918],"length":1,"stats":{"Line":0}},{"line":82,"address":[5444256],"length":1,"stats":{"Line":3}},{"line":84,"address":[1941194,1941135],"length":1,"stats":{"Line":0}},{"line":86,"address":[1941098],"length":1,"stats":{"Line":0}},{"line":87,"address":[5444341],"length":1,"stats":{"Line":0}},{"line":91,"address":[5531685,5531838,5533098],"length":1,"stats":{"Line":1}},{"line":92,"address":[5531673,5531603],"length":1,"stats":{"Line":2}},{"line":93,"address":[1713942],"length":1,"stats":{"Line":0}},{"line":95,"address":[1713975,1715167,1714108],"length":1,"stats":{"Line":1}},{"line":96,"address":[5444619],"length":1,"stats":{"Line":0}},{"line":98,"address":[5444582],"length":1,"stats":{"Line":0}},{"line":102,"address":[1714546],"length":1,"stats":{"Line":2}},{"line":103,"address":[1714205],"length":1,"stats":{"Line":1}},{"line":104,"address":[1714245,1714324],"length":1,"stats":{"Line":3}},{"line":105,"address":[5532407],"length":1,"stats":{"Line":1}},{"line":106,"address":[1714538],"length":1,"stats":{"Line":1}},{"line":110,"address":[1718643,1715312,1718888],"length":1,"stats":{"Line":1}},{"line":111,"address":[1715351],"length":1,"stats":{"Line":2}},{"line":113,"address":[1715388,1715471],"length":1,"stats":{"Line":4}},{"line":114,"address":[1715506],"length":1,"stats":{"Line":1}},{"line":117,"address":[5444736],"length":1,"stats":{"Line":3}},{"line":118,"address":[1941615],"length":1,"stats":{"Line":0}},{"line":120,"address":[1941574],"length":1,"stats":{"Line":0}},{"line":123,"address":[1718853,1716004,1716210],"length":1,"stats":{"Line":2}},{"line":124,"address":[1716378],"length":1,"stats":{"Line":1}},{"line":126,"address":[1716537,1716457],"length":1,"stats":{"Line":2}},{"line":127,"address":[1716598],"length":1,"stats":{"Line":1}},{"line":128,"address":[1716763],"length":1,"stats":{"Line":1}},{"line":129,"address":[1716885],"length":1,"stats":{"Line":1}},{"line":130,"address":[1717009],"length":1,"stats":{"Line":1}},{"line":134,"address":[5535107],"length":1,"stats":{"Line":1}},{"line":135,"address":[5535208],"length":1,"stats":{"Line":1}},{"line":136,"address":[1717208,1717335],"length":1,"stats":{"Line":0}},{"line":137,"address":[1717389],"length":1,"stats":{"Line":0}},{"line":139,"address":[5444912,5444934],"length":1,"stats":{"Line":0}},{"line":140,"address":[1717657,1718780],"length":1,"stats":{"Line":0}},{"line":145,"address":[5535268],"length":1,"stats":{"Line":1}},{"line":146,"address":[5535839],"length":1,"stats":{"Line":1}},{"line":147,"address":[1717887],"length":1,"stats":{"Line":1}},{"line":148,"address":[1717953],"length":1,"stats":{"Line":0}},{"line":149,"address":[5536073,5535973],"length":1,"stats":{"Line":0}},{"line":150,"address":[1718154],"length":1,"stats":{"Line":0}},{"line":153,"address":[1718670,1718012],"length":1,"stats":{"Line":0}},{"line":157,"address":[5536030],"length":1,"stats":{"Line":1}},{"line":159,"address":[1718649,1718191],"length":1,"stats":{"Line":1}},{"line":160,"address":[1718506],"length":1,"stats":{"Line":1}},{"line":161,"address":[1718384],"length":1,"stats":{"Line":1}},{"line":163,"address":[1718427],"length":1,"stats":{"Line":1}},{"line":164,"address":[1718498],"length":1,"stats":{"Line":1}},{"line":171,"address":[1716041],"length":1,"stats":{"Line":3}},{"line":172,"address":[1716095],"length":1,"stats":{"Line":1}},{"line":175,"address":[1720660,1720827,1718912],"length":1,"stats":{"Line":1}},{"line":176,"address":[1718955],"length":1,"stats":{"Line":1}},{"line":178,"address":[1719157],"length":1,"stats":{"Line":1}},{"line":179,"address":[5537212],"length":1,"stats":{"Line":1}},{"line":181,"address":[5537344,5537272,5538907],"length":1,"stats":{"Line":3}},{"line":182,"address":[1719386,1720686],"length":1,"stats":{"Line":1}},{"line":184,"address":[1720555,1719326,1719457,1719576],"length":1,"stats":{"Line":4}},{"line":185,"address":[1719682],"length":1,"stats":{"Line":1}},{"line":186,"address":[1719968,1720666],"length":1,"stats":{"Line":2}},{"line":188,"address":[1720140,1719944],"length":1,"stats":{"Line":2}},{"line":189,"address":[1720175,1720243],"length":1,"stats":{"Line":2}},{"line":190,"address":[1941952],"length":1,"stats":{"Line":1}},{"line":191,"address":[1942011],"length":1,"stats":{"Line":0}},{"line":193,"address":[1941974],"length":1,"stats":{"Line":0}},{"line":197,"address":[5538319],"length":1,"stats":{"Line":1}},{"line":201,"address":[1719716],"length":1,"stats":{"Line":1}},{"line":204,"address":[1721712,1720864,1721706],"length":1,"stats":{"Line":1}},{"line":205,"address":[1720919],"length":1,"stats":{"Line":1}},{"line":206,"address":[1720980,1721117,1721063],"length":1,"stats":{"Line":3}},{"line":207,"address":[1721258],"length":1,"stats":{"Line":1}},{"line":209,"address":[5539315,5539188],"length":1,"stats":{"Line":2}},{"line":213,"address":[1721513,1721613,1721693,1721185],"length":1,"stats":{"Line":2}},{"line":214,"address":[1942250,1942191],"length":1,"stats":{"Line":0}},{"line":216,"address":[1942154],"length":1,"stats":{"Line":0}},{"line":217,"address":[1942213],"length":1,"stats":{"Line":0}},{"line":220,"address":[1721647],"length":1,"stats":{"Line":1}}],"covered":73,"coverable":102},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","config.rs"],"content":"use anyhow::Result;\nuse directories::ProjectDirs;\nuse std::{\n collections::hash_map::DefaultHasher,\n hash::{Hash, Hasher},\n path::{Path, PathBuf},\n};\n\n/// Runtime configuration (currently just the DB path).\n#[derive(Debug, Clone)]\npub struct Config {\n pub db_path: PathBuf,\n}\n\nimpl Config {\n /// Resolve configuration from environment or derive one per-workspace.\n ///\n /// Priority:\n /// 1. `MARLIN_DB_PATH` env-var (explicit override)\n /// 2. *Workspace-local* file under XDG data dir\n /// (`~/.local/share/marlin/index_\u003chash\u003e.db`)\n /// 3. Fallback to `./index.db` when we cannot locate an XDG dir\n pub fn load() -\u003e Result\u003cSelf\u003e {\n // 1) explicit override\n if let Some(val) = std::env::var_os(\"MARLIN_DB_PATH\") {\n let p = PathBuf::from(val);\n std::fs::create_dir_all(p.parent().expect(\"has parent\"))?;\n return Ok(Self { db_path: p });\n }\n\n // 2) derive per-workspace DB name from CWD hash\n let cwd = std::env::current_dir()?;\n let mut h = DefaultHasher::new();\n cwd.hash(\u0026mut h);\n let digest = h.finish(); // 64-bit\n let file_name = format!(\"index_{digest:016x}.db\");\n\n if let Some(dirs) = ProjectDirs::from(\"io\", \"Marlin\", \"marlin\") {\n let dir = dirs.data_dir();\n std::fs::create_dir_all(dir)?;\n return Ok(Self {\n db_path: dir.join(file_name),\n });\n }\n\n // 3) very last resort – workspace-relative DB\n Ok(Self {\n db_path: Path::new(\u0026file_name).to_path_buf(),\n })\n }\n}\n","traces":[{"line":23,"address":[5516629,5516623,5515840],"length":1,"stats":{"Line":1}},{"line":25,"address":[2078215],"length":1,"stats":{"Line":1}},{"line":26,"address":[2078350],"length":1,"stats":{"Line":1}},{"line":27,"address":[2078919,2078371,2078553],"length":1,"stats":{"Line":3}},{"line":28,"address":[2078768],"length":1,"stats":{"Line":2}},{"line":32,"address":[2078411,2078990],"length":1,"stats":{"Line":1}},{"line":33,"address":[2079092],"length":1,"stats":{"Line":1}},{"line":34,"address":[2079143],"length":1,"stats":{"Line":1}},{"line":35,"address":[2079178],"length":1,"stats":{"Line":1}},{"line":36,"address":[2079203],"length":1,"stats":{"Line":1}},{"line":38,"address":[2079590,2079488],"length":1,"stats":{"Line":2}},{"line":39,"address":[2079667,2079760],"length":1,"stats":{"Line":2}},{"line":40,"address":[2079792,2080093],"length":1,"stats":{"Line":1}},{"line":41,"address":[2079991],"length":1,"stats":{"Line":1}},{"line":42,"address":[2079920],"length":1,"stats":{"Line":1}},{"line":47,"address":[2080213],"length":1,"stats":{"Line":0}},{"line":48,"address":[2080159],"length":1,"stats":{"Line":0}}],"covered":15,"coverable":17},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","config_tests.rs"],"content":"// libmarlin/src/config_tests.rs\n\nuse super::config::Config;\nuse std::env;\nuse tempfile::tempdir;\n\n#[test]\nfn load_env_override() {\n let tmp = tempdir().unwrap();\n let db = tmp.path().join(\"custom.db\");\n env::set_var(\"MARLIN_DB_PATH\", \u0026db);\n let cfg = Config::load().unwrap();\n assert_eq!(cfg.db_path, db);\n env::remove_var(\"MARLIN_DB_PATH\");\n}\n\n#[test]\nfn load_xdg_or_fallback() {\n // since XDG_DATA_HOME will normally be present, just test it doesn't error\n let cfg = Config::load().unwrap();\n assert!(cfg.db_path.to_string_lossy().ends_with(\".db\"));\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","db","database.rs"],"content":"//! Database abstraction for Marlin\n//! \n//! This module provides a database abstraction layer that wraps the SQLite connection\n//! and provides methods for common database operations.\n\nuse rusqlite::Connection;\nuse std::path::PathBuf;\nuse anyhow::Result;\n\n/// Options for indexing files\n#[derive(Debug, Clone)]\npub struct IndexOptions {\n /// Only update files marked as dirty\n pub dirty_only: bool,\n \n /// Index file contents (not just metadata)\n pub index_contents: bool,\n \n /// Maximum file size to index (in bytes)\n pub max_size: Option\u003cu64\u003e,\n}\n\nimpl Default for IndexOptions {\n fn default() -\u003e Self {\n Self {\n dirty_only: false,\n index_contents: true,\n max_size: Some(1_000_000), // 1MB default limit\n }\n }\n}\n\n/// Database wrapper for Marlin\npub struct Database {\n /// The SQLite connection\n conn: Connection,\n}\n\nimpl Database {\n /// Create a new database wrapper around an existing connection\n pub fn new(conn: Connection) -\u003e Self {\n Self { conn }\n }\n \n /// Get a reference to the underlying connection\n pub fn conn(\u0026self) -\u003e \u0026Connection {\n \u0026self.conn\n }\n \n /// Get a mutable reference to the underlying connection\n pub fn conn_mut(\u0026mut self) -\u003e \u0026mut Connection {\n \u0026mut self.conn\n }\n \n /// Index one or more files\n pub fn index_files(\u0026mut self, paths: \u0026[PathBuf], _options: \u0026IndexOptions) -\u003e Result\u003cusize\u003e {\n // In a real implementation, this would index the files\n // For now, we just return the number of files \"indexed\"\n if paths.is_empty() { // Add a branch for coverage\n return Ok(0);\n }\n Ok(paths.len())\n }\n \n /// Remove files from the index\n pub fn remove_files(\u0026mut self, paths: \u0026[PathBuf]) -\u003e Result\u003cusize\u003e {\n // In a real implementation, this would remove the files\n // For now, we just return the number of files \"removed\"\n if paths.is_empty() { // Add a branch for coverage\n return Ok(0);\n }\n Ok(paths.len())\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n use crate::db::open as open_marlin_db; // Use your project's DB open function\n use tempfile::tempdir;\n use std::fs::File;\n\n fn setup_db() -\u003e Database {\n let conn = open_marlin_db(\":memory:\").expect(\"Failed to open in-memory DB\");\n Database::new(conn)\n }\n\n #[test]\n fn test_database_new_conn_conn_mut() {\n let mut db = setup_db();\n let _conn_ref = db.conn();\n let _conn_mut_ref = db.conn_mut();\n // Just checking they don't panic and can be called.\n }\n\n #[test]\n fn test_index_files_stub() {\n let mut db = setup_db();\n let tmp = tempdir().unwrap();\n let file1 = tmp.path().join(\"file1.txt\");\n File::create(\u0026file1).unwrap();\n\n let paths = vec![file1.to_path_buf()];\n let options = IndexOptions::default();\n \n assert_eq!(db.index_files(\u0026paths, \u0026options).unwrap(), 1);\n assert_eq!(db.index_files(\u0026[], \u0026options).unwrap(), 0); // Test empty case\n }\n\n #[test]\n fn test_remove_files_stub() {\n let mut db = setup_db();\n let tmp = tempdir().unwrap();\n let file1 = tmp.path().join(\"file1.txt\");\n File::create(\u0026file1).unwrap(); // File doesn't need to be in DB for this stub\n\n let paths = vec![file1.to_path_buf()];\n \n assert_eq!(db.remove_files(\u0026paths).unwrap(), 1);\n assert_eq!(db.remove_files(\u0026[]).unwrap(), 0); // Test empty case\n }\n\n #[test]\n fn test_index_options_default() {\n let options = IndexOptions::default();\n assert!(!options.dirty_only);\n assert!(options.index_contents);\n assert_eq!(options.max_size, Some(1_000_000));\n }\n}\n","traces":[{"line":24,"address":[5509984],"length":1,"stats":{"Line":1}},{"line":28,"address":[2083427],"length":1,"stats":{"Line":1}},{"line":41,"address":[2083472],"length":1,"stats":{"Line":3}},{"line":46,"address":[2083504],"length":1,"stats":{"Line":1}},{"line":51,"address":[2083520],"length":1,"stats":{"Line":1}},{"line":56,"address":[2083536],"length":1,"stats":{"Line":1}},{"line":59,"address":[2083589],"length":1,"stats":{"Line":1}},{"line":60,"address":[2083619],"length":1,"stats":{"Line":1}},{"line":62,"address":[2083603],"length":1,"stats":{"Line":1}},{"line":66,"address":[2083664],"length":1,"stats":{"Line":1}},{"line":69,"address":[2083714],"length":1,"stats":{"Line":1}},{"line":70,"address":[2083744],"length":1,"stats":{"Line":1}},{"line":72,"address":[2083728],"length":1,"stats":{"Line":1}}],"covered":13,"coverable":13},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","db","mod.rs"],"content":"//! Central DB helper – connection bootstrap, migrations **and** most\n//! data-access helpers (tags, links, collections, saved views, …).\n\nmod database;\npub use database::{Database, IndexOptions};\n\nuse std::{\n fs,\n path::{Path, PathBuf},\n};\n\nuse std::result::Result as StdResult;\nuse anyhow::{Context, Result};\nuse chrono::Local;\nuse rusqlite::{\n backup::{Backup, StepResult},\n params,\n Connection,\n OpenFlags,\n OptionalExtension,\n TransactionBehavior,\n};\nuse tracing::{debug, info, warn};\n\n/* ─── embedded migrations ─────────────────────────────────────────── */\n\nconst MIGRATIONS: \u0026[(\u0026str, \u0026str)] = \u0026[\n (\"0001_initial_schema.sql\", include_str!(\"migrations/0001_initial_schema.sql\")),\n (\"0002_update_fts_and_triggers.sql\", include_str!(\"migrations/0002_update_fts_and_triggers.sql\")),\n (\"0003_create_links_collections_views.sql\", include_str!(\"migrations/0003_create_links_collections_views.sql\")),\n (\"0004_fix_hierarchical_tags_fts.sql\", include_str!(\"migrations/0004_fix_hierarchical_tags_fts.sql\")),\n (\"0005_add_dirty_table.sql\", include_str!(\"migrations/0005_add_dirty_table.sql\")),\n];\n\n/* ─── connection bootstrap ────────────────────────────────────────── */\n\npub fn open\u003cP: AsRef\u003cPath\u003e\u003e(db_path: P) -\u003e Result\u003cConnection\u003e {\n let db_path_ref = db_path.as_ref();\n let mut conn = Connection::open(db_path_ref)\n .with_context(|| format!(\"failed to open DB at {}\", db_path_ref.display()))?;\n\n conn.pragma_update(None, \"journal_mode\", \"WAL\")?;\n conn.pragma_update(None, \"foreign_keys\", \"ON\")?;\n\n // Wait up to 30 s for a competing writer before giving up\n conn.busy_timeout(std::time::Duration::from_secs(30))?;\n\n apply_migrations(\u0026mut conn)?;\n Ok(conn)\n}\n\n/* ─── migration runner ────────────────────────────────────────────── */\n\npub(crate) fn apply_migrations(conn: \u0026mut Connection) -\u003e Result\u003c()\u003e {\n // Ensure schema_version bookkeeping table exists\n conn.execute_batch(\n \"CREATE TABLE IF NOT EXISTS schema_version (\n version INTEGER PRIMARY KEY,\n applied_on TEXT NOT NULL\n );\",\n )?;\n\n // Legacy patch – ignore errors if column already exists\n let _ = conn.execute(\"ALTER TABLE schema_version ADD COLUMN applied_on TEXT\", []);\n\n // Grab the write-lock up-front so migrations can run uninterrupted\n let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;\n\n for (fname, sql) in MIGRATIONS {\n let version: i64 = fname\n .split('_')\n .next()\n .and_then(|s| s.parse().ok())\n .expect(\"migration filenames start with number\");\n\n let already: Option\u003ci64\u003e = tx\n .query_row(\n \"SELECT version FROM schema_version WHERE version = ?1\",\n [version],\n |r| r.get(0),\n )\n .optional()?;\n\n if already.is_some() {\n debug!(\"migration {} already applied\", fname);\n continue;\n }\n\n info!(\"applying migration {}\", fname);\n tx.execute_batch(sql)\n .with_context(|| format!(\"could not apply migration {}\", fname))?;\n\n tx.execute(\n \"INSERT INTO schema_version (version, applied_on) VALUES (?1, ?2)\",\n params![version, Local::now().to_rfc3339()],\n )?;\n }\n\n tx.commit()?;\n\n // sanity – warn if any embedded migration got skipped\n let mut missing = Vec::new();\n for (fname, _) in MIGRATIONS {\n let v: i64 = fname.split('_').next().unwrap().parse().unwrap();\n let ok: bool = conn\n .query_row(\n \"SELECT 1 FROM schema_version WHERE version = ?1\",\n [v],\n |_| Ok(true),\n )\n .optional()?\n .unwrap_or(false);\n if !ok {\n missing.push(v);\n }\n }\n if !missing.is_empty() {\n warn!(\"migrations not applied: {:?}\", missing);\n }\n\n Ok(())\n}\n\n/* ─── tag helpers ─────────────────────────────────────────────────── */\n\npub fn ensure_tag_path(conn: \u0026Connection, path: \u0026str) -\u003e Result\u003ci64\u003e {\n let mut parent: Option\u003ci64\u003e = None;\n for segment in path.split('/').filter(|s| !s.is_empty()) {\n conn.execute(\n \"INSERT OR IGNORE INTO tags(name, parent_id) VALUES (?1, ?2)\",\n params![segment, parent],\n )?;\n let id: i64 = conn.query_row(\n \"SELECT id FROM tags WHERE name = ?1 AND (parent_id IS ?2 OR parent_id = ?2)\",\n params![segment, parent],\n |r| r.get(0),\n )?;\n parent = Some(id);\n }\n parent.ok_or_else(|| anyhow::anyhow!(\"empty tag path\"))\n}\n\npub fn file_id(conn: \u0026Connection, path: \u0026str) -\u003e Result\u003ci64\u003e {\n conn.query_row(\"SELECT id FROM files WHERE path = ?1\", [path], |r| r.get(0))\n .map_err(|_| anyhow::anyhow!(\"file not indexed: {}\", path))\n}\n\n/* ─── attributes ──────────────────────────────────────────────────── */\n\npub fn upsert_attr(conn: \u0026Connection, file_id: i64, key: \u0026str, value: \u0026str) -\u003e Result\u003c()\u003e {\n conn.execute(\n r#\"\n INSERT INTO attributes(file_id, key, value)\n VALUES (?1, ?2, ?3)\n ON CONFLICT(file_id, key) DO UPDATE SET value = excluded.value\n \"#,\n params![file_id, key, value],\n )?;\n Ok(())\n}\n\n/* ─── links ───────────────────────────────────────────────────────── */\n\npub fn add_link(\n conn: \u0026Connection,\n src_file_id: i64,\n dst_file_id: i64,\n link_type: Option\u003c\u0026str\u003e,\n) -\u003e Result\u003c()\u003e {\n conn.execute(\n \"INSERT INTO links(src_file_id, dst_file_id, type)\n VALUES (?1, ?2, ?3)\n ON CONFLICT(src_file_id, dst_file_id, type) DO NOTHING\",\n params![src_file_id, dst_file_id, link_type],\n )?;\n Ok(())\n}\n\npub fn remove_link(\n conn: \u0026Connection,\n src_file_id: i64,\n dst_file_id: i64,\n link_type: Option\u003c\u0026str\u003e,\n) -\u003e Result\u003c()\u003e {\n conn.execute(\n \"DELETE FROM links\n WHERE src_file_id = ?1\n AND dst_file_id = ?2\n AND (type IS ?3 OR type = ?3)\",\n params![src_file_id, dst_file_id, link_type],\n )?;\n Ok(())\n}\n\npub fn list_links(\n conn: \u0026Connection,\n pattern: \u0026str,\n direction: Option\u003c\u0026str\u003e,\n link_type: Option\u003c\u0026str\u003e,\n) -\u003e Result\u003cVec\u003c(String, String, Option\u003cString\u003e)\u003e\u003e {\n let like_pattern = pattern.replace('*', \"%\");\n\n // Files matching pattern\n let mut stmt = conn.prepare(\"SELECT id, path FROM files WHERE path LIKE ?1\")?;\n let rows = stmt\n .query_map(params![like_pattern], |r| {\n Ok((r.get::\u003c_, i64\u003e(0)?, r.get::\u003c_, String\u003e(1)?))\n })?\n .collect::\u003cStdResult\u003cVec\u003c_\u003e, _\u003e\u003e()?;\n\n let mut out = Vec::new();\n for (fid, fpath) in rows {\n let (src_col, dst_col) = match direction {\n Some(\"in\") =\u003e (\"dst_file_id\", \"src_file_id\"),\n _ =\u003e (\"src_file_id\", \"dst_file_id\"),\n };\n\n let sql = format!(\n \"SELECT f2.path, l.type\n FROM links l\n JOIN files f2 ON f2.id = l.{dst_col}\n WHERE l.{src_col} = ?1\n AND (?2 IS NULL OR l.type = ?2)\",\n );\n\n let mut stmt2 = conn.prepare(\u0026sql)?;\n let links = stmt2\n .query_map(params![fid, link_type], |r| {\n Ok((r.get::\u003c_, String\u003e(0)?, r.get::\u003c_, Option\u003cString\u003e\u003e(1)?))\n })?\n .collect::\u003cStdResult\u003cVec\u003c_\u003e, _\u003e\u003e()?;\n\n for (other, typ) in links {\n out.push((fpath.clone(), other, typ));\n }\n }\n Ok(out)\n}\n\npub fn find_backlinks(\n conn: \u0026Connection,\n pattern: \u0026str,\n) -\u003e Result\u003cVec\u003c(String, Option\u003cString\u003e)\u003e\u003e {\n let like = pattern.replace('*', \"%\");\n\n let mut stmt = conn.prepare(\n \"SELECT f1.path, l.type\n FROM links l\n JOIN files f1 ON f1.id = l.src_file_id\n JOIN files f2 ON f2.id = l.dst_file_id\n WHERE f2.path LIKE ?1\",\n )?;\n\n let rows = stmt.query_map([like], |r| {\n Ok((r.get::\u003c_, String\u003e(0)?, r.get::\u003c_, Option\u003cString\u003e\u003e(1)?))\n })?;\n\n let out = rows.collect::\u003cStdResult\u003cVec\u003c_\u003e, _\u003e\u003e()?;\n Ok(out)\n}\n\n/* ─── collections helpers ────────────────────────────────────────── */\n\npub fn ensure_collection(conn: \u0026Connection, name: \u0026str) -\u003e Result\u003ci64\u003e {\n conn.execute(\n \"INSERT OR IGNORE INTO collections(name) VALUES (?1)\",\n params![name],\n )?;\n conn.query_row(\n \"SELECT id FROM collections WHERE name = ?1\",\n params![name],\n |r| r.get(0),\n )\n .context(\"collection lookup failed\")\n}\n\npub fn add_file_to_collection(conn: \u0026Connection, coll_id: i64, file_id: i64) -\u003e Result\u003c()\u003e {\n conn.execute(\n \"INSERT OR IGNORE INTO collection_files(collection_id, file_id)\n VALUES (?1, ?2)\",\n params![coll_id, file_id],\n )?;\n Ok(())\n}\n\npub fn list_collection(conn: \u0026Connection, name: \u0026str) -\u003e Result\u003cVec\u003cString\u003e\u003e {\n let mut stmt = conn.prepare(\n r#\"SELECT f.path\n FROM collections c\n JOIN collection_files cf ON cf.collection_id = c.id\n JOIN files f ON f.id = cf.file_id\n WHERE c.name = ?1\n ORDER BY f.path\"#,\n )?;\n\n let rows = stmt.query_map([name], |r| r.get::\u003c_, String\u003e(0))?;\n let list = rows.collect::\u003cStdResult\u003cVec\u003c_\u003e, _\u003e\u003e()?;\n Ok(list)\n}\n\n/* ─── saved views (smart folders) ───────────────────────────────── */\n\npub fn save_view(conn: \u0026Connection, name: \u0026str, query: \u0026str) -\u003e Result\u003c()\u003e {\n conn.execute(\n \"INSERT INTO views(name, query)\n VALUES (?1, ?2)\n ON CONFLICT(name) DO UPDATE SET query = excluded.query\",\n params![name, query],\n )?;\n Ok(())\n}\n\npub fn list_views(conn: \u0026Connection) -\u003e Result\u003cVec\u003c(String, String)\u003e\u003e {\n let mut stmt = conn.prepare(\"SELECT name, query FROM views ORDER BY name\")?;\n let rows = stmt.query_map([], |r| Ok((r.get::\u003c_, String\u003e(0)?, r.get::\u003c_, String\u003e(1)?)))?;\n let list = rows.collect::\u003cStdResult\u003cVec\u003c_\u003e, _\u003e\u003e()?;\n Ok(list)\n}\n\npub fn view_query(conn: \u0026Connection, name: \u0026str) -\u003e Result\u003cString\u003e {\n conn.query_row(\n \"SELECT query FROM views WHERE name = ?1\",\n [name],\n |r| r.get::\u003c_, String\u003e(0),\n )\n .context(format!(\"no view called '{}'\", name))\n}\n\n/* ─── dirty‐scan helpers ─────────────────────────────────────────── */\n\n/// Mark a file as “dirty” so it’ll be picked up by `scan_dirty`.\npub fn mark_dirty(conn: \u0026Connection, file_id: i64) -\u003e Result\u003c()\u003e {\n conn.execute(\n \"INSERT OR IGNORE INTO file_changes(file_id, marked_at)\n VALUES (?1, strftime('%s','now'))\",\n params![file_id],\n )?;\n Ok(())\n}\n\n/// Take and clear all dirty file IDs for incremental re-scan.\npub fn take_dirty(conn: \u0026Connection) -\u003e Result\u003cVec\u003ci64\u003e\u003e {\n let mut ids = Vec::new();\n {\n let mut stmt = conn.prepare(\"SELECT file_id FROM file_changes\")?;\n for row in stmt.query_map([], |r| r.get(0))? {\n ids.push(row?);\n }\n }\n conn.execute(\"DELETE FROM file_changes\", [])?;\n Ok(ids)\n}\n\n/* ─── backup / restore helpers ────────────────────────────────────── */\n\npub fn backup\u003cP: AsRef\u003cPath\u003e\u003e(db_path: P) -\u003e Result\u003cPathBuf\u003e {\n let src = db_path.as_ref();\n let dir = src\n .parent()\n .ok_or_else(|| anyhow::anyhow!(\"invalid DB path: {}\", src.display()))?\n .join(\"backups\");\n fs::create_dir_all(\u0026dir)?;\n\n let stamp = Local::now().format(\"%Y-%m-%d_%H-%M-%S\");\n let dst = dir.join(format!(\"backup_{stamp}.db\"));\n\n let src_conn = Connection::open_with_flags(src, OpenFlags::SQLITE_OPEN_READ_ONLY)?;\n let mut dst_conn = Connection::open(\u0026dst)?;\n\n let bk = Backup::new(\u0026src_conn, \u0026mut dst_conn)?;\n while let StepResult::More = bk.step(100)? {}\n Ok(dst)\n}\n\npub fn restore\u003cP: AsRef\u003cPath\u003e\u003e(backup_path: P, live_db_path: P) -\u003e Result\u003c()\u003e {\n fs::copy(\u0026backup_path, \u0026live_db_path)?;\n Ok(())\n}\n\n/* ─── tests ───────────────────────────────────────────────────────── */\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn migrations_apply_in_memory() {\n open(\":memory:\").expect(\"all migrations apply\");\n }\n}\n","traces":[{"line":37,"address":[5479632,5480989,5479584],"length":1,"stats":{"Line":10}},{"line":38,"address":[],"length":0,"stats":{"Line":20}},{"line":39,"address":[5479827,5480995,5479709],"length":1,"stats":{"Line":10}},{"line":40,"address":[],"length":0,"stats":{"Line":0}},{"line":42,"address":[1760529,1762721,1761802,1761913,1761337,1759969,1760418,1759050,1759161],"length":1,"stats":{"Line":16}},{"line":43,"address":[1760687,1761335,1759319,1762071,1762719,1759967],"length":1,"stats":{"Line":4}},{"line":46,"address":[1762304,1759965,1762717,1759552,1761333,1760920],"length":1,"stats":{"Line":7}},{"line":48,"address":[],"length":0,"stats":{"Line":7}},{"line":49,"address":[5480888],"length":1,"stats":{"Line":3}},{"line":54,"address":[5373047,5375769,5370432],"length":1,"stats":{"Line":1}},{"line":56,"address":[5370577,5370458],"length":1,"stats":{"Line":1}},{"line":64,"address":[5370619],"length":1,"stats":{"Line":1}},{"line":67,"address":[2110984],"length":1,"stats":{"Line":1}},{"line":69,"address":[2111275,2111183],"length":1,"stats":{"Line":2}},{"line":70,"address":[2111405,2113314],"length":1,"stats":{"Line":2}},{"line":73,"address":[5481184,5481204],"length":1,"stats":{"Line":2}},{"line":76,"address":[5375727,5373476,5373266,5373227],"length":1,"stats":{"Line":2}},{"line":79,"address":[5373250],"length":1,"stats":{"Line":1}},{"line":80,"address":[5481248,5481264],"length":1,"stats":{"Line":2}},{"line":84,"address":[5373561],"length":1,"stats":{"Line":6}},{"line":85,"address":[5373628,5375061],"length":1,"stats":{"Line":2}},{"line":89,"address":[5373600,5373666,5373957],"length":1,"stats":{"Line":5}},{"line":90,"address":[2114759,2114628,2115228,2114154],"length":1,"stats":{"Line":6}},{"line":91,"address":[2114743],"length":1,"stats":{"Line":0}},{"line":93,"address":[2114789,2114995,2115123,2115193,2114910],"length":1,"stats":{"Line":5}},{"line":95,"address":[2114801],"length":1,"stats":{"Line":3}},{"line":99,"address":[5371140,5373061],"length":1,"stats":{"Line":7}},{"line":102,"address":[5371343],"length":1,"stats":{"Line":6}},{"line":103,"address":[2111661,2111752],"length":1,"stats":{"Line":7}},{"line":104,"address":[2112731,2111854],"length":1,"stats":{"Line":7}},{"line":105,"address":[2112913,2113111,2113248],"length":1,"stats":{"Line":1}},{"line":108,"address":[2112905],"length":1,"stats":{"Line":6}},{"line":109,"address":[1763504,1763512],"length":1,"stats":{"Line":7}},{"line":113,"address":[2113197],"length":1,"stats":{"Line":6}},{"line":114,"address":[5372962],"length":1,"stats":{"Line":0}},{"line":117,"address":[5371601],"length":1,"stats":{"Line":2}},{"line":118,"address":[2111923,2111988],"length":1,"stats":{"Line":0}},{"line":121,"address":[2111951],"length":1,"stats":{"Line":6}},{"line":126,"address":[5375808],"length":1,"stats":{"Line":1}},{"line":127,"address":[5375844],"length":1,"stats":{"Line":1}},{"line":128,"address":[2116766,2116029],"length":1,"stats":{"Line":4}},{"line":129,"address":[2116264,2116433],"length":1,"stats":{"Line":1}},{"line":131,"address":[5376032],"length":1,"stats":{"Line":1}},{"line":133,"address":[2116545,2116669],"length":1,"stats":{"Line":1}},{"line":135,"address":[5376316],"length":1,"stats":{"Line":1}},{"line":136,"address":[1763600,1763584],"length":1,"stats":{"Line":2}},{"line":138,"address":[2116720],"length":1,"stats":{"Line":1}},{"line":140,"address":[2116364],"length":1,"stats":{"Line":1}},{"line":143,"address":[2116784],"length":1,"stats":{"Line":0}},{"line":144,"address":[2116809],"length":1,"stats":{"Line":0}},{"line":145,"address":[1763744,1763761],"length":1,"stats":{"Line":0}},{"line":150,"address":[2116896],"length":1,"stats":{"Line":1}},{"line":151,"address":[2117135,2117026],"length":1,"stats":{"Line":1}},{"line":157,"address":[2116942],"length":1,"stats":{"Line":1}},{"line":159,"address":[2117175],"length":1,"stats":{"Line":1}},{"line":164,"address":[5377024],"length":1,"stats":{"Line":1}},{"line":170,"address":[2117431,2117322],"length":1,"stats":{"Line":1}},{"line":174,"address":[5377064],"length":1,"stats":{"Line":1}},{"line":176,"address":[2117471],"length":1,"stats":{"Line":1}},{"line":179,"address":[2117504],"length":1,"stats":{"Line":1}},{"line":185,"address":[2117626,2117735],"length":1,"stats":{"Line":1}},{"line":190,"address":[2117544],"length":1,"stats":{"Line":1}},{"line":192,"address":[2117775],"length":1,"stats":{"Line":1}},{"line":195,"address":[2121829,2117808,2121615],"length":1,"stats":{"Line":1}},{"line":201,"address":[2117916],"length":1,"stats":{"Line":1}},{"line":204,"address":[2117985,2118053,2121827],"length":1,"stats":{"Line":2}},{"line":205,"address":[2118422,2121807,2118803,2118602,2118489],"length":1,"stats":{"Line":2}},{"line":206,"address":[2118370],"length":1,"stats":{"Line":2}},{"line":207,"address":[1764030],"length":1,"stats":{"Line":1}},{"line":211,"address":[2118908],"length":1,"stats":{"Line":1}},{"line":212,"address":[2118956,2119056,2119183],"length":1,"stats":{"Line":3}},{"line":213,"address":[2119644,2119284],"length":1,"stats":{"Line":2}},{"line":214,"address":[5379460,5379323],"length":1,"stats":{"Line":0}},{"line":215,"address":[2119480],"length":1,"stats":{"Line":1}},{"line":218,"address":[5379584],"length":1,"stats":{"Line":1}},{"line":226,"address":[2119970,2121711,2119886],"length":1,"stats":{"Line":2}},{"line":227,"address":[2120377,2120758,2120557,2121661,2120444,2121690],"length":1,"stats":{"Line":2}},{"line":228,"address":[2120297],"length":1,"stats":{"Line":2}},{"line":229,"address":[5482462,5483060],"length":1,"stats":{"Line":1}},{"line":233,"address":[2121576,2120855,2120959,2121086],"length":1,"stats":{"Line":4}},{"line":234,"address":[5381277,5381119],"length":1,"stats":{"Line":2}},{"line":237,"address":[5379217],"length":1,"stats":{"Line":1}},{"line":240,"address":[2123125,2123080,2121856],"length":1,"stats":{"Line":1}},{"line":244,"address":[2121926],"length":1,"stats":{"Line":1}},{"line":246,"address":[5382118,5383068,5381923,5381991],"length":1,"stats":{"Line":2}},{"line":254,"address":[5383054,5382295,5382434,5382567],"length":1,"stats":{"Line":3}},{"line":255,"address":[1765246,1765829],"length":1,"stats":{"Line":1}},{"line":258,"address":[2122700,2123036,2122796],"length":1,"stats":{"Line":2}},{"line":259,"address":[5382932],"length":1,"stats":{"Line":1}},{"line":264,"address":[2123168],"length":1,"stats":{"Line":1}},{"line":265,"address":[5383309,5383199],"length":1,"stats":{"Line":1}},{"line":267,"address":[2123203],"length":1,"stats":{"Line":1}},{"line":269,"address":[5383387],"length":1,"stats":{"Line":1}},{"line":271,"address":[5383359],"length":1,"stats":{"Line":1}},{"line":272,"address":[1765920,1765904],"length":1,"stats":{"Line":2}},{"line":277,"address":[2123520],"length":1,"stats":{"Line":1}},{"line":278,"address":[2123604,2123713],"length":1,"stats":{"Line":1}},{"line":281,"address":[5383518],"length":1,"stats":{"Line":1}},{"line":283,"address":[2123753],"length":1,"stats":{"Line":1}},{"line":286,"address":[2123776,2124801,2124809],"length":1,"stats":{"Line":1}},{"line":287,"address":[2123938,2123826],"length":1,"stats":{"Line":1}},{"line":296,"address":[1765952,1765987],"length":1,"stats":{"Line":4}},{"line":297,"address":[2124409,2124760,2124505],"length":1,"stats":{"Line":2}},{"line":298,"address":[5384702],"length":1,"stats":{"Line":1}},{"line":303,"address":[2124832],"length":1,"stats":{"Line":1}},{"line":304,"address":[2125035,2124926],"length":1,"stats":{"Line":1}},{"line":308,"address":[2124872],"length":1,"stats":{"Line":1}},{"line":310,"address":[2125075],"length":1,"stats":{"Line":1}},{"line":313,"address":[2126071,2125104,2126063],"length":1,"stats":{"Line":1}},{"line":314,"address":[2125128],"length":1,"stats":{"Line":1}},{"line":315,"address":[5483968,5484014],"length":1,"stats":{"Line":4}},{"line":316,"address":[2126022,2125769,2125673],"length":1,"stats":{"Line":2}},{"line":317,"address":[2125955],"length":1,"stats":{"Line":1}},{"line":320,"address":[2126096,2126405,2126430],"length":1,"stats":{"Line":1}},{"line":321,"address":[2126130,2126359,2126158],"length":1,"stats":{"Line":3}},{"line":323,"address":[2126138],"length":1,"stats":{"Line":1}},{"line":324,"address":[1766720,1766736],"length":1,"stats":{"Line":2}},{"line":326,"address":[2126193,2126389,2126423,2126217],"length":1,"stats":{"Line":2}},{"line":332,"address":[2126448],"length":1,"stats":{"Line":0}},{"line":333,"address":[2126502,2126611],"length":1,"stats":{"Line":0}},{"line":336,"address":[2126474],"length":1,"stats":{"Line":0}},{"line":338,"address":[2126651],"length":1,"stats":{"Line":0}},{"line":342,"address":[2128121,2128134,2126688],"length":1,"stats":{"Line":0}},{"line":343,"address":[2126723],"length":1,"stats":{"Line":0}},{"line":345,"address":[2126797,2128129,2126733],"length":1,"stats":{"Line":0}},{"line":346,"address":[2127110,2127174,2127448,2128127],"length":1,"stats":{"Line":0}},{"line":347,"address":[2127549,2127919],"length":1,"stats":{"Line":0}},{"line":350,"address":[2127905,2127615],"length":1,"stats":{"Line":0}},{"line":351,"address":[2127818],"length":1,"stats":{"Line":0}},{"line":356,"address":[],"length":0,"stats":{"Line":1}},{"line":357,"address":[],"length":0,"stats":{"Line":2}},{"line":358,"address":[1767096,1766965,1769333],"length":1,"stats":{"Line":1}},{"line":360,"address":[],"length":0,"stats":{"Line":0}},{"line":362,"address":[],"length":0,"stats":{"Line":2}},{"line":364,"address":[],"length":0,"stats":{"Line":1}},{"line":365,"address":[],"length":0,"stats":{"Line":2}},{"line":367,"address":[2498760,2497145,2497253],"length":1,"stats":{"Line":2}},{"line":368,"address":[1768151,1768084,1769239],"length":1,"stats":{"Line":2}},{"line":370,"address":[1768549,1769218,1768482],"length":1,"stats":{"Line":2}},{"line":371,"address":[1768807,1768756],"length":1,"stats":{"Line":2}},{"line":372,"address":[],"length":0,"stats":{"Line":1}},{"line":375,"address":[],"length":0,"stats":{"Line":1}},{"line":376,"address":[],"length":0,"stats":{"Line":2}},{"line":377,"address":[],"length":0,"stats":{"Line":1}}],"covered":124,"coverable":144},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","db_tests.rs"],"content":"// libmarlin/src/db_tests.rs\n\nuse super::db;\nuse rusqlite::Connection;\nuse tempfile::tempdir;\n\nfn open_mem() -\u003e Connection {\n // helper to open an in-memory DB with migrations applied\n db::open(\":memory:\").expect(\"open in-memory DB\")\n}\n\n#[test]\nfn ensure_tag_path_creates_hierarchy() {\n let conn = open_mem();\n // create foo/bar/baz\n let leaf = db::ensure_tag_path(\u0026conn, \"foo/bar/baz\").unwrap();\n // foo should exist as a root tag\n let foo: i64 = conn\n .query_row(\n \"SELECT id FROM tags WHERE name='foo' AND parent_id IS NULL\",\n [],\n |r| r.get(0),\n )\n .unwrap();\n // bar should be child of foo\n let bar: i64 = conn\n .query_row(\n \"SELECT id FROM tags WHERE name='bar' AND parent_id = ?1\",\n [foo],\n |r| r.get(0),\n )\n .unwrap();\n // baz should be child of bar, and its ID is what we got back\n let baz: i64 = conn\n .query_row(\n \"SELECT id FROM tags WHERE name='baz' AND parent_id = ?1\",\n [bar],\n |r| r.get(0),\n )\n .unwrap();\n assert_eq!(leaf, baz);\n}\n\n#[test]\nfn upsert_attr_inserts_and_updates() {\n let conn = open_mem();\n // insert a dummy file\n conn.execute(\n \"INSERT INTO files(path, size, mtime) VALUES (?1, 0, 0)\",\n [\"a.txt\"],\n )\n .unwrap();\n let fid: i64 = conn\n .query_row(\"SELECT id FROM files WHERE path='a.txt'\", [], |r| r.get(0))\n .unwrap();\n\n // insert\n db::upsert_attr(\u0026conn, fid, \"k\", \"v\").unwrap();\n let v1: String = conn\n .query_row(\n \"SELECT value FROM attributes WHERE file_id=?1 AND key='k'\",\n [fid],\n |r| r.get(0),\n )\n .unwrap();\n assert_eq!(v1, \"v\");\n\n // update\n db::upsert_attr(\u0026conn, fid, \"k\", \"v2\").unwrap();\n let v2: String = conn\n .query_row(\n \"SELECT value FROM attributes WHERE file_id=?1 AND key='k'\",\n [fid],\n |r| r.get(0),\n )\n .unwrap();\n assert_eq!(v2, \"v2\");\n}\n\n#[test]\nfn add_and_remove_links_and_backlinks() {\n let conn = open_mem();\n // create two files\n conn.execute(\n \"INSERT INTO files(path, size, mtime) VALUES (?1, 0, 0)\",\n [\"one.txt\"],\n )\n .unwrap();\n conn.execute(\n \"INSERT INTO files(path, size, mtime) VALUES (?1, 0, 0)\",\n [\"two.txt\"],\n )\n .unwrap();\n let src: i64 = conn\n .query_row(\"SELECT id FROM files WHERE path='one.txt'\", [], |r| r.get(0))\n .unwrap();\n let dst: i64 = conn\n .query_row(\"SELECT id FROM files WHERE path='two.txt'\", [], |r| r.get(0))\n .unwrap();\n\n // add a link of type \"ref\"\n db::add_link(\u0026conn, src, dst, Some(\"ref\")).unwrap();\n let out = db::list_links(\u0026conn, \"one%\", None, None).unwrap();\n assert_eq!(out.len(), 1);\n assert_eq!(out[0].2.as_deref(), Some(\"ref\"));\n\n // backlinks should mirror\n let back = db::find_backlinks(\u0026conn, \"two%\").unwrap();\n assert_eq!(back.len(), 1);\n assert_eq!(back[0].1.as_deref(), Some(\"ref\"));\n\n // remove it\n db::remove_link(\u0026conn, src, dst, Some(\"ref\")).unwrap();\n let empty = db::list_links(\u0026conn, \"one%\", None, None).unwrap();\n assert!(empty.is_empty());\n}\n\n#[test]\nfn collections_roundtrip() {\n let conn = open_mem();\n // create collection \"C\"\n let cid = db::ensure_collection(\u0026conn, \"C\").unwrap();\n\n // add a file\n conn.execute(\n \"INSERT INTO files(path, size, mtime) VALUES (?1, 0, 0)\",\n [\"f.txt\"],\n )\n .unwrap();\n let fid: i64 = conn\n .query_row(\"SELECT id FROM files WHERE path='f.txt'\", [], |r| r.get(0))\n .unwrap();\n\n db::add_file_to_collection(\u0026conn, cid, fid).unwrap();\n let files = db::list_collection(\u0026conn, \"C\").unwrap();\n assert_eq!(files, vec![\"f.txt\".to_string()]);\n}\n\n#[test]\nfn views_save_and_query() {\n let conn = open_mem();\n db::save_view(\u0026conn, \"v1\", \"some_query\").unwrap();\n let all = db::list_views(\u0026conn).unwrap();\n assert_eq!(all, vec![(\"v1\".to_string(), \"some_query\".to_string())]);\n\n let q = db::view_query(\u0026conn, \"v1\").unwrap();\n assert_eq!(q, \"some_query\");\n}\n\n#[test]\nfn backup_and_restore_cycle() {\n let tmp = tempdir().unwrap();\n let db_path = tmp.path().join(\"data.db\");\n let live = db::open(\u0026db_path).unwrap();\n\n // insert a file\n live.execute(\n \"INSERT INTO files(path, size, mtime) VALUES (?1, 0, 0)\",\n [\"x.bin\"],\n )\n .unwrap();\n\n // backup\n let backup = db::backup(\u0026db_path).unwrap();\n // remove original\n std::fs::remove_file(\u0026db_path).unwrap();\n // restore\n db::restore(\u0026backup, \u0026db_path).unwrap();\n\n // reopen and check that x.bin survived\n let conn2 = db::open(\u0026db_path).unwrap();\n let cnt: i64 =\n conn2.query_row(\"SELECT COUNT(*) FROM files WHERE path='x.bin'\", [], |r| r.get(0)).unwrap();\n assert_eq!(cnt, 1);\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","error.rs"],"content":"// libmarlin/src/error.rs\n\nuse std::io;\nuse std::fmt;\n// Ensure these are present if Error enum variants use them directly\n// use rusqlite;\n// use notify;\n\npub type Result\u003cT\u003e = std::result::Result\u003cT, Error\u003e;\n\n#[derive(Debug)]\npub enum Error {\n Io(io::Error),\n Database(rusqlite::Error), \n Watch(notify::Error), \n InvalidState(String),\n NotFound(String),\n Config(String),\n Other(String),\n}\n\nimpl fmt::Display for Error {\n fn fmt(\u0026self, f: \u0026mut fmt::Formatter\u003c'_\u003e) -\u003e fmt::Result {\n match self {\n Self::Io(err) =\u003e write!(f, \"IO error: {}\", err),\n Self::Database(err) =\u003e write!(f, \"Database error: {}\", err),\n Self::Watch(err) =\u003e write!(f, \"Watch error: {}\", err),\n Self::InvalidState(msg) =\u003e write!(f, \"Invalid state: {}\", msg),\n Self::NotFound(path) =\u003e write!(f, \"Not found: {}\", path),\n Self::Config(msg) =\u003e write!(f, \"Configuration error: {}\", msg),\n Self::Other(msg) =\u003e write!(f, \"Error: {}\", msg),\n }\n }\n}\n\nimpl std::error::Error for Error {\n fn source(\u0026self) -\u003e Option\u003c\u0026(dyn std::error::Error + 'static)\u003e {\n match self {\n Self::Io(err) =\u003e Some(err),\n Self::Database(err) =\u003e Some(err),\n Self::Watch(err) =\u003e Some(err),\n Self::InvalidState(_) | Self::NotFound(_) | Self::Config(_) | Self::Other(_) =\u003e None,\n }\n }\n}\n\nimpl From\u003cio::Error\u003e for Error {\n fn from(err: io::Error) -\u003e Self {\n Self::Io(err)\n }\n}\n\nimpl From\u003crusqlite::Error\u003e for Error {\n fn from(err: rusqlite::Error) -\u003e Self {\n Self::Database(err)\n }\n}\n\nimpl From\u003cnotify::Error\u003e for Error {\n fn from(err: notify::Error) -\u003e Self {\n Self::Watch(err)\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n use std::error::Error as StdError; \n\n #[test]\n fn test_error_display_and_from() {\n // Test Io variant\n let io_err_inner_for_source_check = io::Error::new(io::ErrorKind::NotFound, \"test io error\");\n let io_err_marlin = Error::from(io::Error::new(io::ErrorKind::NotFound, \"test io error\"));\n assert_eq!(io_err_marlin.to_string(), \"IO error: test io error\");\n let source = io_err_marlin.source();\n assert!(source.is_some(), \"Io error should have a source\");\n if let Some(s) = source {\n // Compare details of the source if necessary, or just its string representation\n assert_eq!(s.to_string(), io_err_inner_for_source_check.to_string());\n }\n\n // Test Database variant\n let rusqlite_err_inner_for_source_check = rusqlite::Error::SqliteFailure(\n rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_ERROR), \n Some(\"test db error\".to_string()),\n );\n // We need to create the error again for the From conversion if we want to compare the source\n let db_err_marlin = Error::from(rusqlite::Error::SqliteFailure(\n rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_ERROR), \n Some(\"test db error\".to_string()),\n ));\n assert!(db_err_marlin.to_string().contains(\"Database error: test db error\"));\n let source = db_err_marlin.source();\n assert!(source.is_some(), \"Database error should have a source\");\n if let Some(s) = source {\n assert_eq!(s.to_string(), rusqlite_err_inner_for_source_check.to_string());\n }\n\n\n // Test Watch variant\n let notify_raw_err_inner_for_source_check = notify::Error::new(notify::ErrorKind::Generic(\"test watch error\".to_string()));\n let watch_err_marlin = Error::from(notify::Error::new(notify::ErrorKind::Generic(\"test watch error\".to_string())));\n assert!(watch_err_marlin.to_string().contains(\"Watch error: test watch error\"));\n let source = watch_err_marlin.source();\n assert!(source.is_some(), \"Watch error should have a source\");\n if let Some(s) = source {\n assert_eq!(s.to_string(), notify_raw_err_inner_for_source_check.to_string());\n }\n\n\n let invalid_state_err = Error::InvalidState(\"bad state\".to_string());\n assert_eq!(invalid_state_err.to_string(), \"Invalid state: bad state\");\n assert!(invalid_state_err.source().is_none());\n\n let not_found_err = Error::NotFound(\"missing_file.txt\".to_string());\n assert_eq!(not_found_err.to_string(), \"Not found: missing_file.txt\");\n assert!(not_found_err.source().is_none());\n\n let config_err = Error::Config(\"bad config\".to_string());\n assert_eq!(config_err.to_string(), \"Configuration error: bad config\");\n assert!(config_err.source().is_none());\n\n let other_err = Error::Other(\"some other issue\".to_string());\n assert_eq!(other_err.to_string(), \"Error: some other issue\");\n assert!(other_err.source().is_none());\n }\n\n #[test]\n fn test_rusqlite_error_without_message() {\n let sqlite_busy_error = rusqlite::Error::SqliteFailure(\n rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_BUSY),\n None,\n );\n let db_err_no_msg = Error::from(sqlite_busy_error);\n \n let expected_rusqlite_msg = rusqlite::Error::SqliteFailure(\n rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_BUSY),\n None,\n ).to_string(); \n \n let expected_marlin_msg = format!(\"Database error: {}\", expected_rusqlite_msg);\n \n // Verify the string matches the expected format\n assert_eq!(db_err_no_msg.to_string(), expected_marlin_msg);\n \n // Check the error code directly instead of the string\n if let Error::Database(rusqlite::Error::SqliteFailure(err, _)) = \u0026db_err_no_msg {\n assert_eq!(err.code, rusqlite::ffi::ErrorCode::DatabaseBusy);\n } else {\n panic!(\"Expected Error::Database variant\");\n }\n \n // Verify the source exists\n assert!(db_err_no_msg.source().is_some());\n }\n}","traces":[{"line":23,"address":[5592736],"length":1,"stats":{"Line":1}},{"line":24,"address":[2070560],"length":1,"stats":{"Line":1}},{"line":25,"address":[2070624],"length":1,"stats":{"Line":1}},{"line":26,"address":[2070722],"length":1,"stats":{"Line":1}},{"line":27,"address":[2070839],"length":1,"stats":{"Line":1}},{"line":28,"address":[2070966],"length":1,"stats":{"Line":1}},{"line":29,"address":[2071094],"length":1,"stats":{"Line":1}},{"line":30,"address":[2071222],"length":1,"stats":{"Line":1}},{"line":31,"address":[2071347],"length":1,"stats":{"Line":1}},{"line":37,"address":[2071488],"length":1,"stats":{"Line":1}},{"line":38,"address":[2071498],"length":1,"stats":{"Line":1}},{"line":39,"address":[2071563],"length":1,"stats":{"Line":1}},{"line":40,"address":[5593804],"length":1,"stats":{"Line":2}},{"line":41,"address":[2071625],"length":1,"stats":{"Line":1}},{"line":42,"address":[2071653],"length":1,"stats":{"Line":1}},{"line":48,"address":[2071680],"length":1,"stats":{"Line":1}},{"line":49,"address":[2071688],"length":1,"stats":{"Line":1}},{"line":54,"address":[2071712],"length":1,"stats":{"Line":1}},{"line":55,"address":[2071720],"length":1,"stats":{"Line":1}},{"line":60,"address":[2071744],"length":1,"stats":{"Line":1}},{"line":61,"address":[2071761],"length":1,"stats":{"Line":1}}],"covered":21,"coverable":21},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","facade_tests.rs"],"content":"// libmarlin/src/facade_tests.rs\n\nuse super::*; // brings Marlin, config, etc.\nuse std::{env, fs};\nuse tempfile::tempdir;\n\n#[test]\nfn open_at_and_scan_and_search() {\n // 1) Prepare a temp workspace with one file\n let tmp = tempdir().unwrap();\n let file = tmp.path().join(\"hello.txt\");\n fs::write(\u0026file, \"hello FAÇT\").unwrap();\n\n // 2) Use open_at to create a fresh DB\n let db_path = tmp.path().join(\"explicit.db\");\n let mut m = Marlin::open_at(\u0026db_path).expect(\"open_at should succeed\");\n assert!(db_path.exists(), \"DB file should be created\");\n\n // 3) Scan the directory\n let count = m.scan(\u0026[tmp.path()]).expect(\"scan should succeed\");\n assert_eq!(count, 1, \"we created exactly one file\");\n\n // 4) Search using an FTS hit\n let hits = m.search(\"hello\").expect(\"search must not error\");\n assert_eq!(hits.len(), 1);\n assert!(hits[0].ends_with(\"hello.txt\"));\n\n // 5) Search a substring that isn't a valid token (fires fallback)\n let fallback_hits = m.search(\"FAÇT\").expect(\"fallback search works\");\n assert_eq!(fallback_hits.len(), 1);\n assert!(fallback_hits[0].ends_with(\"hello.txt\"));\n}\n\n#[test]\nfn tag_and_search_by_tag() {\n let tmp = tempdir().unwrap();\n let a = tmp.path().join(\"a.md\");\n let b = tmp.path().join(\"b.md\");\n fs::write(\u0026a, \"# a\").unwrap();\n fs::write(\u0026b, \"# b\").unwrap();\n\n let db_path = tmp.path().join(\"my.db\");\n env::set_var(\"MARLIN_DB_PATH\", \u0026db_path);\n\n let mut m = Marlin::open_default().unwrap();\n m.scan(\u0026[tmp.path()]).unwrap();\n\n let changed = m.tag(\"*.md\", \"foo/bar\").unwrap();\n assert_eq!(changed, 2);\n\n let tagged = m.search(\"tags_text:\\\"foo/bar\\\"\").unwrap();\n assert_eq!(tagged.len(), 2);\n\n env::remove_var(\"MARLIN_DB_PATH\");\n}\n\n#[test]\nfn open_default_fallback_config() {\n // Unset all overrides\n env::remove_var(\"MARLIN_DB_PATH\");\n env::remove_var(\"XDG_DATA_HOME\");\n\n // Simulate no XDG: temporarily point HOME to a read-only dir\n let fake_home = tempdir().unwrap();\n env::set_var(\"HOME\", fake_home.path());\n // This should fall back to \"./index_\u003chash\u003e.db\"\n let cfg = config::Config::load().unwrap();\n let fname = cfg.db_path.file_name().unwrap().to_string_lossy();\n assert!(fname.starts_with(\"index_\") \u0026\u0026 fname.ends_with(\".db\"));\n\n // Clean up\n env::remove_var(\"HOME\");\n}\n\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","lib.rs"],"content":"//! libmarlin – public API surface for the Marlin core.\n//!\n//! Down-stream crates (`cli-bin`, `tui-bin`, tests, plugins) should depend\n//! *only* on the helpers re-exported here, never on internal modules\n//! directly. That gives us room to refactor internals without breaking\n//! callers.\n\n#![deny(warnings)]\n\npub mod backup;\npub mod config;\npub mod db;\npub mod error;\npub mod logging;\npub mod scan;\npub mod utils;\npub mod watcher;\n\n#[cfg(test)]\nmod utils_tests;\n#[cfg(test)]\nmod config_tests;\n#[cfg(test)]\nmod scan_tests;\n#[cfg(test)]\nmod logging_tests;\n#[cfg(test)]\nmod db_tests;\n#[cfg(test)]\nmod facade_tests;\n#[cfg(test)]\nmod watcher_tests;\n\nuse anyhow::{Context, Result};\nuse rusqlite::Connection;\nuse std::{fs, path::Path, sync::{Arc, Mutex}};\n\n/// Main handle for interacting with a Marlin database.\npub struct Marlin {\n #[allow(dead_code)]\n cfg: config::Config,\n conn: Connection,\n}\n\nimpl Marlin {\n /// Open using the default config (env override or XDG/CWD fallback),\n /// ensuring parent directories exist and applying migrations.\n pub fn open_default() -\u003e Result\u003cSelf\u003e {\n // 1) Load configuration\n let cfg = config::Config::load()?;\n // 2) Ensure the DB's parent directory exists\n if let Some(parent) = cfg.db_path.parent() {\n fs::create_dir_all(parent)?;\n }\n // 3) Open the database and run migrations\n let conn = db::open(\u0026cfg.db_path)\n .context(format!(\"opening database at {}\", cfg.db_path.display()))?;\n Ok(Marlin { cfg, conn })\n }\n\n /// Open a Marlin instance at the specified database path,\n /// creating parent directories and applying migrations.\n pub fn open_at\u003cP: AsRef\u003cPath\u003e\u003e(db_path: P) -\u003e Result\u003cSelf\u003e {\n let db_path = db_path.as_ref();\n // Ensure the specified DB directory exists\n if let Some(parent) = db_path.parent() {\n fs::create_dir_all(parent)?;\n }\n // Build a minimal Config so callers can still inspect cfg.db_path\n let cfg = config::Config { db_path: db_path.to_path_buf() };\n // Open the database and run migrations\n let conn = db::open(db_path)\n .context(format!(\"opening database at {}\", db_path.display()))?;\n Ok(Marlin { cfg, conn })\n }\n\n /// Recursively index one or more directories.\n pub fn scan\u003cP: AsRef\u003cPath\u003e\u003e(\u0026mut self, paths: \u0026[P]) -\u003e Result\u003cusize\u003e {\n let mut total = 0;\n for p in paths {\n total += scan::scan_directory(\u0026mut self.conn, p.as_ref())?;\n }\n Ok(total)\n }\n\n /// Attach a hierarchical tag (`foo/bar`) to every _indexed_ file\n /// matching the glob. Returns the number of files actually updated.\n pub fn tag(\u0026mut self, pattern: \u0026str, tag_path: \u0026str) -\u003e Result\u003cusize\u003e {\n use glob::Pattern;\n\n // 1) ensure tag hierarchy\n let leaf = db::ensure_tag_path(\u0026self.conn, tag_path)?;\n\n // 2) collect leaf + ancestors\n let mut tag_ids = Vec::new();\n let mut cur = Some(leaf);\n while let Some(id) = cur {\n tag_ids.push(id);\n cur = self.conn.query_row(\n \"SELECT parent_id FROM tags WHERE id = ?1\",\n [id],\n |r| r.get::\u003c_, Option\u003ci64\u003e\u003e(0),\n )?;\n }\n\n // 3) match files by glob against stored paths\n let expanded = shellexpand::tilde(pattern).into_owned();\n let pat = Pattern::new(\u0026expanded)\n .with_context(|| format!(\"Invalid glob pattern `{}`\", expanded))?;\n\n let mut stmt_all = self.conn.prepare(\"SELECT id, path FROM files\")?;\n let rows = stmt_all.query_map([], |r| Ok((r.get(0)?, r.get(1)?)))?;\n\n let mut stmt_ins = self.conn.prepare(\n \"INSERT OR IGNORE INTO file_tags(file_id, tag_id) VALUES (?1, ?2)\",\n )?;\n\n let mut changed = 0;\n for row in rows {\n let (fid, path_str): (i64, String) = row?;\n let is_match = if expanded.contains(std::path::MAIN_SEPARATOR) {\n pat.matches(\u0026path_str)\n } else {\n Path::new(\u0026path_str)\n .file_name()\n .and_then(|n| n.to_str())\n .map(|n| pat.matches(n))\n .unwrap_or(false)\n };\n if !is_match {\n continue;\n }\n\n let mut newly = false;\n for \u0026tid in \u0026tag_ids {\n if stmt_ins.execute([fid, tid])? \u003e 0 {\n newly = true;\n }\n }\n if newly {\n changed += 1;\n }\n }\n Ok(changed)\n }\n\n /// Full-text search over path, tags, and attrs, with substring fallback.\n pub fn search(\u0026self, query: \u0026str) -\u003e Result\u003cVec\u003cString\u003e\u003e {\n let mut stmt = self.conn.prepare(\n \"SELECT f.path FROM files_fts JOIN files f ON f.rowid = files_fts.rowid WHERE files_fts MATCH ?1 ORDER BY rank\",\n )?;\n let mut hits = stmt.query_map([query], |r| r.get(0))?\n .collect::\u003cstd::result::Result\u003cVec\u003c_\u003e, rusqlite::Error\u003e\u003e()?;\n\n if hits.is_empty() \u0026\u0026 !query.contains(':') {\n hits = self.fallback_search(query)?;\n }\n Ok(hits)\n }\n\n fn fallback_search(\u0026self, term: \u0026str) -\u003e Result\u003cVec\u003cString\u003e\u003e {\n let needle = term.to_lowercase();\n let mut stmt = self.conn.prepare(\"SELECT path FROM files\")?;\n let rows = stmt.query_map([], |r| r.get(0))?;\n let mut out = Vec::new();\n for res in rows {\n let p: String = res?;\n if p.to_lowercase().contains(\u0026needle) {\n out.push(p.clone());\n continue;\n }\n if let Ok(meta) = fs::metadata(\u0026p) {\n if meta.len() \u003c= 65_536 { \n if let Ok(body) = fs::read_to_string(\u0026p) {\n if body.to_lowercase().contains(\u0026needle) {\n out.push(p.clone());\n }\n }\n }\n }\n }\n Ok(out)\n }\n\n /// Borrow the raw SQLite connection.\n pub fn conn(\u0026self) -\u003e \u0026Connection {\n \u0026self.conn\n }\n\n /// Spawn a file-watcher that indexes changes in real time.\n pub fn watch\u003cP: AsRef\u003cPath\u003e\u003e(\n \u0026mut self,\n path: P,\n config: Option\u003cwatcher::WatcherConfig\u003e,\n ) -\u003e Result\u003cwatcher::FileWatcher\u003e {\n let cfg = config.unwrap_or_default();\n let p = path.as_ref().to_path_buf();\n let new_conn = db::open(\u0026self.cfg.db_path)\n .context(\"opening database for watcher\")?;\n let watcher_db = Arc::new(Mutex::new(db::Database::new(new_conn)));\n \n let mut owned_w = watcher::FileWatcher::new(vec![p], cfg)?;\n owned_w.with_database(watcher_db); // Modifies owned_w in place\n owned_w.start()?; // Start the watcher after it has been fully configured\n \n Ok(owned_w) // Return the owned FileWatcher\n }\n}","traces":[{"line":48,"address":[5390771,5390765,5389632],"length":1,"stats":{"Line":1}},{"line":50,"address":[2099265],"length":1,"stats":{"Line":1}},{"line":52,"address":[2099445,2099509],"length":1,"stats":{"Line":2}},{"line":53,"address":[2099656,2099619],"length":1,"stats":{"Line":2}},{"line":56,"address":[2099768,2100017,2100122,2100080,2099644,2100322],"length":1,"stats":{"Line":4}},{"line":57,"address":[2099798,2099781,2100106,2100335,2100056],"length":1,"stats":{"Line":2}},{"line":58,"address":[2100169],"length":1,"stats":{"Line":1}},{"line":63,"address":[],"length":0,"stats":{"Line":1}},{"line":64,"address":[1683494,1683573],"length":1,"stats":{"Line":2}},{"line":66,"address":[],"length":0,"stats":{"Line":1}},{"line":67,"address":[],"length":0,"stats":{"Line":2}},{"line":70,"address":[1683849,1683722],"length":1,"stats":{"Line":2}},{"line":72,"address":[],"length":0,"stats":{"Line":4}},{"line":73,"address":[1683970,1684249,1683954,1684500,1684199],"length":1,"stats":{"Line":2}},{"line":74,"address":[],"length":0,"stats":{"Line":1}},{"line":78,"address":[],"length":0,"stats":{"Line":1}},{"line":79,"address":[],"length":0,"stats":{"Line":1}},{"line":80,"address":[],"length":0,"stats":{"Line":2}},{"line":81,"address":[1684765,1684843,1684666],"length":1,"stats":{"Line":1}},{"line":83,"address":[],"length":0,"stats":{"Line":1}},{"line":88,"address":[5395118,5390784,5394802],"length":1,"stats":{"Line":1}},{"line":92,"address":[2100477],"length":1,"stats":{"Line":1}},{"line":95,"address":[5391027],"length":1,"stats":{"Line":1}},{"line":96,"address":[2100629],"length":1,"stats":{"Line":1}},{"line":97,"address":[5391516,5391069],"length":1,"stats":{"Line":2}},{"line":98,"address":[5391107],"length":1,"stats":{"Line":1}},{"line":99,"address":[2100799,2100811,2100986,2101093],"length":1,"stats":{"Line":2}},{"line":101,"address":[2100803],"length":1,"stats":{"Line":1}},{"line":102,"address":[1684864,1684880],"length":1,"stats":{"Line":2}},{"line":107,"address":[5391572,5391149],"length":1,"stats":{"Line":2}},{"line":108,"address":[5391690,5391607,5391824,5395100],"length":1,"stats":{"Line":2}},{"line":109,"address":[5391808],"length":1,"stats":{"Line":0}},{"line":111,"address":[2104506,2101615,2101540],"length":1,"stats":{"Line":2}},{"line":112,"address":[5600174,5600128],"length":1,"stats":{"Line":4}},{"line":114,"address":[5392707,5392901,5392779,5394889],"length":1,"stats":{"Line":2}},{"line":118,"address":[5393106],"length":1,"stats":{"Line":1}},{"line":119,"address":[2102748,2102641,2102824],"length":1,"stats":{"Line":3}},{"line":120,"address":[2102941,2104294,2103115],"length":1,"stats":{"Line":2}},{"line":121,"address":[2103353,2103436],"length":1,"stats":{"Line":2}},{"line":122,"address":[5394005,5394179],"length":1,"stats":{"Line":0}},{"line":124,"address":[2103474,2103529],"length":1,"stats":{"Line":2}},{"line":126,"address":[5600654,5600640],"length":1,"stats":{"Line":2}},{"line":127,"address":[5600672,5600690],"length":1,"stats":{"Line":2}},{"line":130,"address":[2103643],"length":1,"stats":{"Line":1}},{"line":134,"address":[5394239],"length":1,"stats":{"Line":1}},{"line":135,"address":[2103741],"length":1,"stats":{"Line":1}},{"line":136,"address":[2103879,2104015,2104208],"length":1,"stats":{"Line":3}},{"line":137,"address":[5394726],"length":1,"stats":{"Line":1}},{"line":140,"address":[5394448,5394515],"length":1,"stats":{"Line":2}},{"line":141,"address":[2103995,2103958],"length":1,"stats":{"Line":1}},{"line":144,"address":[2102966],"length":1,"stats":{"Line":1}},{"line":148,"address":[5396601,5396611,5395136],"length":1,"stats":{"Line":1}},{"line":149,"address":[2104781,2104664],"length":1,"stats":{"Line":1}},{"line":152,"address":[1685632,1685667],"length":1,"stats":{"Line":4}},{"line":155,"address":[2105992,2105465,2105656,2105536],"length":1,"stats":{"Line":4}},{"line":156,"address":[5396430,5396266],"length":1,"stats":{"Line":1}},{"line":158,"address":[2105547],"length":1,"stats":{"Line":1}},{"line":161,"address":[5396624,5399133,5399481],"length":1,"stats":{"Line":1}},{"line":162,"address":[2106145],"length":1,"stats":{"Line":1}},{"line":163,"address":[2106261,2106186,2108847],"length":1,"stats":{"Line":2}},{"line":164,"address":[5397211,5397140,5399458],"length":1,"stats":{"Line":4}},{"line":165,"address":[2106856],"length":1,"stats":{"Line":1}},{"line":166,"address":[2107084,2107008,2106916],"length":1,"stats":{"Line":3}},{"line":167,"address":[2107346,2107201],"length":1,"stats":{"Line":2}},{"line":168,"address":[2107623,2107552],"length":1,"stats":{"Line":2}},{"line":169,"address":[2108664],"length":1,"stats":{"Line":0}},{"line":172,"address":[2107804,2107894],"length":1,"stats":{"Line":2}},{"line":173,"address":[2107962,2107901,2108574],"length":1,"stats":{"Line":3}},{"line":174,"address":[2108088,2107990],"length":1,"stats":{"Line":2}},{"line":175,"address":[2108136,2108207],"length":1,"stats":{"Line":2}},{"line":176,"address":[2108391],"length":1,"stats":{"Line":1}},{"line":182,"address":[2107234],"length":1,"stats":{"Line":1}},{"line":186,"address":[2108864],"length":1,"stats":{"Line":0}},{"line":187,"address":[2108872],"length":1,"stats":{"Line":0}},{"line":191,"address":[2308112,2308196,2309672],"length":1,"stats":{"Line":0}},{"line":196,"address":[],"length":0,"stats":{"Line":0}},{"line":197,"address":[],"length":0,"stats":{"Line":0}},{"line":198,"address":[],"length":0,"stats":{"Line":0}},{"line":200,"address":[],"length":0,"stats":{"Line":0}},{"line":202,"address":[],"length":0,"stats":{"Line":0}},{"line":203,"address":[2309358],"length":1,"stats":{"Line":0}},{"line":204,"address":[2309429],"length":1,"stats":{"Line":0}},{"line":206,"address":[2309555],"length":1,"stats":{"Line":0}}],"covered":69,"coverable":83},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","logging.rs"],"content":"use tracing_subscriber::{fmt, EnvFilter};\n\n/// Initialise global tracing subscriber.\n///\n/// Reads `RUST_LOG` for filtering, falls back to `info`.\npub fn init() {\n let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(\"info\"));\n\n // All tracing output (INFO, WARN, ERROR …) now goes to *stderr* so the\n // integration tests can assert on warnings / errors reliably.\n fmt()\n .with_target(false) // hide module targets\n .with_level(true) // include log level\n .with_env_filter(filter) // respect RUST_LOG\n .with_writer(std::io::stderr) // \u003c-- NEW: send to stderr\n .init();\n}\n","traces":[{"line":6,"address":[2108944,2109307,2109278],"length":1,"stats":{"Line":1}},{"line":7,"address":[2108981],"length":1,"stats":{"Line":1}},{"line":11,"address":[2109041,2109119,2109197],"length":1,"stats":{"Line":3}},{"line":14,"address":[2109154],"length":1,"stats":{"Line":1}}],"covered":4,"coverable":4},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","logging_tests.rs"],"content":"// libmarlin/src/logging_tests.rs\n\nuse super::logging;\nuse tracing::Level;\n\n#[test]\nfn init_sets_up_subscriber() {\n // set RUST_LOG to something to test the EnvFilter path\n std::env::set_var(\"RUST_LOG\", \"debug\");\n logging::init();\n tracing::event!(Level::INFO, \"this is a test log\");\n // if we made it here without panic, we’re good\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","scan.rs"],"content":"// src/scan.rs\n\nuse std::fs;\nuse std::path::Path;\n\nuse anyhow::Result;\nuse rusqlite::{params, Connection};\nuse tracing::{debug, info};\nuse walkdir::WalkDir;\n\n/// Recursively walk `root` and upsert file metadata.\n/// Triggers keep the FTS table in sync.\npub fn scan_directory(conn: \u0026mut Connection, root: \u0026Path) -\u003e Result\u003cusize\u003e {\n // Begin a transaction so we batch many inserts/updates together\n let tx = conn.transaction()?;\n\n // Prepare the upsert statement once\n let mut stmt = tx.prepare(\n r#\"\n INSERT INTO files(path, size, mtime)\n VALUES (?1, ?2, ?3)\n ON CONFLICT(path) DO UPDATE\n SET size = excluded.size,\n mtime = excluded.mtime\n \"#,\n )?;\n\n let mut count = 0usize;\n\n // Walk the directory recursively\n for entry in WalkDir::new(root)\n .into_iter()\n .filter_map(Result::ok)\n .filter(|e| e.file_type().is_file())\n {\n let path = entry.path();\n\n // Skip the database file and its WAL/SHM siblings\n if let Some(name) = path.file_name().and_then(|n| n.to_str()) {\n if name.ends_with(\".db\") || name.ends_with(\"-wal\") || name.ends_with(\"-shm\") {\n continue;\n }\n }\n\n // Gather file metadata\n let meta = fs::metadata(path)?;\n let size = meta.len() as i64;\n let mtime = meta\n .modified()?\n .duration_since(std::time::UNIX_EPOCH)?\n .as_secs() as i64;\n\n // Execute the upsert\n let path_str = path.to_string_lossy();\n stmt.execute(params![path_str, size, mtime])?;\n count += 1;\n\n debug!(file = %path_str, \"indexed\");\n }\n\n // Finalize and commit\n drop(stmt);\n tx.commit()?;\n\n info!(indexed = count, \"scan complete\");\n Ok(count)\n}\n","traces":[{"line":13,"address":[5442107,5436832,5441856],"length":1,"stats":{"Line":1}},{"line":15,"address":[1954756],"length":1,"stats":{"Line":1}},{"line":18,"address":[1954971,1955035,1955184,1959810],"length":1,"stats":{"Line":2}},{"line":28,"address":[1955409],"length":1,"stats":{"Line":1}},{"line":31,"address":[1955662,1955429,1955477],"length":1,"stats":{"Line":3}},{"line":34,"address":[2057008,2057035],"length":1,"stats":{"Line":2}},{"line":36,"address":[1957173,1955747],"length":1,"stats":{"Line":2}},{"line":39,"address":[1957211],"length":1,"stats":{"Line":3}},{"line":40,"address":[1957544,1957463,1957388],"length":1,"stats":{"Line":3}},{"line":46,"address":[1957434,1959697,1957612],"length":1,"stats":{"Line":2}},{"line":47,"address":[5440026],"length":1,"stats":{"Line":1}},{"line":48,"address":[1957947,1957851,1959629,1958123],"length":1,"stats":{"Line":1}},{"line":49,"address":[5440141],"length":1,"stats":{"Line":0}},{"line":50,"address":[5440311],"length":1,"stats":{"Line":0}},{"line":54,"address":[1958255],"length":1,"stats":{"Line":1}},{"line":55,"address":[5440492,5440654,5441802],"length":1,"stats":{"Line":2}},{"line":56,"address":[1958667,1958607],"length":1,"stats":{"Line":1}},{"line":58,"address":[1958642,1958936,1958691],"length":1,"stats":{"Line":3}},{"line":62,"address":[1955800],"length":1,"stats":{"Line":1}},{"line":63,"address":[1955903,1957078],"length":1,"stats":{"Line":1}},{"line":65,"address":[1956459,1956110],"length":1,"stats":{"Line":2}},{"line":66,"address":[1956410],"length":1,"stats":{"Line":1}}],"covered":20,"coverable":22},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","scan_tests.rs"],"content":"// libmarlin/src/scan_tests.rs\n\nuse super::scan::scan_directory;\nuse super::db;\nuse tempfile::tempdir;\nuse std::fs::File;\n\n#[test]\nfn scan_directory_counts_files() {\n let tmp = tempdir().unwrap();\n\n // create a couple of files\n File::create(tmp.path().join(\"a.txt\")).unwrap();\n File::create(tmp.path().join(\"b.log\")).unwrap();\n\n // open an in-memory DB (runs migrations)\n let mut conn = db::open(\":memory:\").unwrap();\n\n let count = scan_directory(\u0026mut conn, tmp.path()).unwrap();\n assert_eq!(count, 2);\n\n // ensure the paths were inserted\n let mut stmt = conn.prepare(\"SELECT COUNT(*) FROM files\").unwrap();\n let total: i64 = stmt.query_row([], |r| r.get(0)).unwrap();\n assert_eq!(total, 2);\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","utils.rs"],"content":"//! Misc shared helpers.\n\nuse std::path::PathBuf;\n\n/// Determine a filesystem root to limit recursive walking on glob scans.\n///\n/// If the pattern contains any of `*?[`, we take everything up to the\n/// first such character, and then (if that still contains metacharacters)\n/// walk up until there aren’t any left. If there are *no* metachars at\n/// all, we treat the entire string as a path and return its parent\n/// directory (or `.` if it has no parent).\npub fn determine_scan_root(pattern: \u0026str) -\u003e PathBuf {\n // find first wildcard char\n let first_wild = pattern\n .find(|c| matches!(c, '*' | '?' | '['))\n .unwrap_or(pattern.len());\n\n // everything up to the wildcard (or the whole string if none)\n let prefix = \u0026pattern[..first_wild];\n let mut root = PathBuf::from(prefix);\n\n // If there were NO wildcards at all, just return the parent directory\n if first_wild == pattern.len() {\n return root.parent().map(|p| p.to_path_buf()).unwrap_or_else(|| PathBuf::from(\".\"));\n }\n\n // Otherwise, if the prefix still has any wildcards (e.g. \"foo*/bar\"),\n // walk back up until it doesn’t\n while root\n .as_os_str()\n .to_string_lossy()\n .chars()\n .any(|c| matches!(c, '*' | '?' | '['))\n {\n root = root.parent().map(|p| p.to_path_buf()).unwrap_or_default();\n }\n\n if root.as_os_str().is_empty() {\n PathBuf::from(\".\")\n } else {\n root\n }\n}\n","traces":[{"line":12,"address":[5518304,5519490,5519621],"length":1,"stats":{"Line":1}},{"line":14,"address":[1785170,1785101],"length":1,"stats":{"Line":2}},{"line":15,"address":[2163328,2163341],"length":1,"stats":{"Line":2}},{"line":16,"address":[1785146],"length":1,"stats":{"Line":1}},{"line":19,"address":[1785217],"length":1,"stats":{"Line":1}},{"line":20,"address":[1785252],"length":1,"stats":{"Line":1}},{"line":23,"address":[1785368,1785293],"length":1,"stats":{"Line":2}},{"line":24,"address":[5629772,5629734,5629760,5629712],"length":1,"stats":{"Line":4}},{"line":29,"address":[1785381,1785454,1786189,1785591],"length":1,"stats":{"Line":3}},{"line":33,"address":[1785663,1785537],"length":1,"stats":{"Line":4}},{"line":35,"address":[1786050,1785935],"length":1,"stats":{"Line":0}},{"line":38,"address":[1785838,1785712],"length":1,"stats":{"Line":3}},{"line":39,"address":[1785883,1785848],"length":1,"stats":{"Line":2}},{"line":41,"address":[1785795],"length":1,"stats":{"Line":1}}],"covered":13,"coverable":14},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","utils_tests.rs"],"content":"// libmarlin/src/utils_tests.rs\n\nuse super::utils::determine_scan_root;\nuse std::path::PathBuf;\n\n#[test]\nfn determine_scan_root_plain_path() {\n let root = determine_scan_root(\"foo/bar/baz.txt\");\n assert_eq!(root, PathBuf::from(\"foo/bar\"));\n}\n\n#[test]\nfn determine_scan_root_glob() {\n let root = determine_scan_root(\"foo/*/baz.rs\");\n assert_eq!(root, PathBuf::from(\"foo\"));\n}\n\n#[test]\nfn determine_scan_root_only_wildcards() {\n let root = determine_scan_root(\"**/*.txt\");\n assert_eq!(root, PathBuf::from(\".\"));\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","watcher.rs"],"content":"// libmarlin/src/watcher.rs\n\n//! File system watcher implementation for Marlin\n//!\n//! This module provides real-time index updates by monitoring file system events\n//! (create, modify, delete) using the `notify` crate. It implements event debouncing,\n//! batch processing, and a state machine for robust lifecycle management.\n\nuse anyhow::{Result, Context};\nuse crate::db::Database;\nuse crossbeam_channel::{bounded, Receiver};\nuse notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcherTrait};\nuse std::collections::HashMap;\nuse std::path::PathBuf;\nuse std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};\nuse std::sync::{Arc, Mutex};\nuse std::thread::{self, JoinHandle};\nuse std::time::{Duration, Instant};\n// REMOVED: use std::fs; // \u003c\u003c\u003c\u003c\u003c\u003c\u003c\u003c\u003c\u003c\u003c\u003c THIS LINE WAS REMOVED\n\n/// Configuration for the file watcher\n#[derive(Debug, Clone)]\npub struct WatcherConfig {\n /// Time in milliseconds to debounce file events\n pub debounce_ms: u64,\n\n /// Maximum number of events to process in a single batch\n pub batch_size: usize,\n\n /// Maximum size of the event queue before applying backpressure\n pub max_queue_size: usize,\n\n /// Time in milliseconds to wait for events to drain during shutdown\n pub drain_timeout_ms: u64,\n}\n\nimpl Default for WatcherConfig {\n fn default() -\u003e Self {\n Self {\n debounce_ms: 100,\n batch_size: 1000,\n max_queue_size: 100_000,\n drain_timeout_ms: 5000,\n }\n }\n}\n\n/// State of the file watcher\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum WatcherState {\n Initializing,\n Watching,\n Paused,\n ShuttingDown,\n Stopped,\n}\n\n/// Status information about the file watcher\n#[derive(Debug, Clone)]\npub struct WatcherStatus {\n pub state: WatcherState,\n pub events_processed: usize,\n pub queue_size: usize,\n pub start_time: Option\u003cInstant\u003e,\n pub watched_paths: Vec\u003cPathBuf\u003e,\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]\nenum EventPriority {\n Create = 0,\n Delete = 1,\n Modify = 2,\n Access = 3,\n}\n\n#[derive(Debug, Clone)]\nstruct ProcessedEvent {\n path: PathBuf,\n kind: EventKind,\n priority: EventPriority,\n timestamp: Instant,\n}\n\nstruct EventDebouncer {\n events: HashMap\u003cPathBuf, ProcessedEvent\u003e,\n debounce_window_ms: u64,\n last_flush: Instant,\n}\n\nimpl EventDebouncer {\n fn new(debounce_window_ms: u64) -\u003e Self {\n Self {\n events: HashMap::new(),\n debounce_window_ms,\n last_flush: Instant::now(),\n }\n }\n\n fn add_event(\u0026mut self, event: ProcessedEvent) {\n let path = event.path.clone();\n if path.is_dir() { // This relies on the PathBuf itself knowing if it's a directory\n // or on the underlying FS. For unit tests, ensure paths are created.\n self.events.retain(|file_path, _| !file_path.starts_with(\u0026path) || file_path == \u0026path );\n }\n match self.events.get_mut(\u0026path) {\n Some(existing) =\u003e {\n if event.priority \u003c existing.priority {\n existing.priority = event.priority;\n }\n existing.timestamp = event.timestamp;\n existing.kind = event.kind;\n }\n None =\u003e {\n self.events.insert(path, event);\n }\n }\n }\n\n fn is_ready_to_flush(\u0026self) -\u003e bool {\n self.last_flush.elapsed() \u003e= Duration::from_millis(self.debounce_window_ms)\n }\n\n fn flush(\u0026mut self) -\u003e Vec\u003cProcessedEvent\u003e {\n let mut events: Vec\u003cProcessedEvent\u003e = self.events.drain().map(|(_, e)| e).collect();\n events.sort_by_key(|e| e.priority);\n self.last_flush = Instant::now();\n events\n }\n\n #[allow(dead_code)]\n fn len(\u0026self) -\u003e usize {\n self.events.len()\n }\n}\n\n#[cfg(test)]\nmod event_debouncer_tests {\n use super::*;\n use notify::event::{CreateKind, DataChange, ModifyKind, RemoveKind, RenameMode};\n use std::fs; // fs is needed for these tests to create dirs/files\n use tempfile; \n\n #[test]\n fn debouncer_add_and_flush() {\n let mut debouncer = EventDebouncer::new(100);\n std::thread::sleep(Duration::from_millis(110)); \n assert!(debouncer.is_ready_to_flush());\n assert_eq!(debouncer.len(), 0);\n\n let path1 = PathBuf::from(\"file1.txt\");\n debouncer.add_event(ProcessedEvent {\n path: path1.clone(),\n kind: EventKind::Create(CreateKind::File),\n priority: EventPriority::Create,\n timestamp: Instant::now(),\n });\n assert_eq!(debouncer.len(), 1);\n \n debouncer.last_flush = Instant::now(); \n assert!(!debouncer.is_ready_to_flush());\n\n std::thread::sleep(Duration::from_millis(110));\n assert!(debouncer.is_ready_to_flush());\n\n let flushed = debouncer.flush();\n assert_eq!(flushed.len(), 1);\n assert_eq!(flushed[0].path, path1);\n assert_eq!(debouncer.len(), 0);\n assert!(!debouncer.is_ready_to_flush()); \n }\n\n #[test]\n fn debouncer_coalesce_events() {\n let mut debouncer = EventDebouncer::new(100);\n let path1 = PathBuf::from(\"file1.txt\");\n\n let t1 = Instant::now();\n debouncer.add_event(ProcessedEvent {\n path: path1.clone(),\n kind: EventKind::Create(CreateKind::File),\n priority: EventPriority::Create,\n timestamp: t1,\n });\n std::thread::sleep(Duration::from_millis(10));\n let t2 = Instant::now();\n debouncer.add_event(ProcessedEvent {\n path: path1.clone(),\n kind: EventKind::Modify(ModifyKind::Data(DataChange::Any)),\n priority: EventPriority::Modify,\n timestamp: t2,\n });\n \n assert_eq!(debouncer.len(), 1);\n \n std::thread::sleep(Duration::from_millis(110));\n let flushed = debouncer.flush();\n assert_eq!(flushed.len(), 1);\n assert_eq!(flushed[0].path, path1);\n assert_eq!(flushed[0].priority, EventPriority::Create); \n assert_eq!( \n flushed[0].kind,\n EventKind::Modify(ModifyKind::Data(DataChange::Any))\n );\n assert_eq!(flushed[0].timestamp, t2);\n }\n\n #[test]\n fn debouncer_hierarchical() {\n let mut debouncer_h = EventDebouncer::new(100);\n let temp_dir_obj = tempfile::tempdir().expect(\"Failed to create temp dir\");\n let p_dir = temp_dir_obj.path().to_path_buf(); \n let p_file = p_dir.join(\"file.txt\");\n \n fs::File::create(\u0026p_file).expect(\"Failed to create test file for hierarchical debounce\");\n\n debouncer_h.add_event(ProcessedEvent {\n path: p_file.clone(),\n kind: EventKind::Create(CreateKind::File),\n priority: EventPriority::Create,\n timestamp: Instant::now(),\n });\n assert_eq!(debouncer_h.len(), 1);\n \n debouncer_h.add_event(ProcessedEvent {\n path: p_dir.clone(), \n kind: EventKind::Remove(RemoveKind::Folder), \n priority: EventPriority::Delete,\n timestamp: Instant::now(),\n });\n assert_eq!(debouncer_h.len(), 1, \"Hierarchical debounce should remove child event, leaving only parent dir event\");\n \n std::thread::sleep(Duration::from_millis(110));\n let flushed = debouncer_h.flush();\n assert_eq!(flushed.len(), 1);\n assert_eq!(flushed[0].path, p_dir);\n }\n\n #[test]\n fn debouncer_different_files() {\n let mut debouncer = EventDebouncer::new(100);\n let path1 = PathBuf::from(\"file1.txt\");\n let path2 = PathBuf::from(\"file2.txt\");\n\n debouncer.add_event(ProcessedEvent {\n path: path1.clone(),\n kind: EventKind::Create(CreateKind::File),\n priority: EventPriority::Create,\n timestamp: Instant::now(),\n });\n debouncer.add_event(ProcessedEvent {\n path: path2.clone(),\n kind: EventKind::Create(CreateKind::File),\n priority: EventPriority::Create,\n timestamp: Instant::now(),\n });\n assert_eq!(debouncer.len(), 2);\n std::thread::sleep(Duration::from_millis(110));\n let flushed = debouncer.flush();\n assert_eq!(flushed.len(), 2);\n }\n\n #[test]\n fn debouncer_priority_sorting_on_flush() {\n let mut debouncer = EventDebouncer::new(100);\n let path1 = PathBuf::from(\"file1.txt\"); \n let path2 = PathBuf::from(\"file2.txt\"); \n let path3 = PathBuf::from(\"file3.txt\"); \n\n debouncer.add_event(ProcessedEvent { path: path1, kind: EventKind::Modify(ModifyKind::Name(RenameMode::To)), priority: EventPriority::Modify, timestamp: Instant::now() });\n debouncer.add_event(ProcessedEvent { path: path2, kind: EventKind::Create(CreateKind::File), priority: EventPriority::Create, timestamp: Instant::now() });\n debouncer.add_event(ProcessedEvent { path: path3, kind: EventKind::Remove(RemoveKind::File), priority: EventPriority::Delete, timestamp: Instant::now() });\n \n std::thread::sleep(Duration::from_millis(110));\n let flushed = debouncer.flush();\n assert_eq!(flushed.len(), 3);\n assert_eq!(flushed[0].priority, EventPriority::Create); \n assert_eq!(flushed[1].priority, EventPriority::Delete); \n assert_eq!(flushed[2].priority, EventPriority::Modify); \n }\n\n #[test]\n fn debouncer_no_events_flush_empty() {\n let mut debouncer = EventDebouncer::new(100);\n std::thread::sleep(Duration::from_millis(110));\n let flushed = debouncer.flush();\n assert!(flushed.is_empty());\n assert_eq!(debouncer.len(), 0);\n }\n}\n\n\npub struct FileWatcher {\n state: Arc\u003cMutex\u003cWatcherState\u003e\u003e,\n #[allow(dead_code)]\n config: WatcherConfig,\n watched_paths: Vec\u003cPathBuf\u003e,\n #[allow(dead_code)]\n event_receiver: Receiver\u003cstd::result::Result\u003cEvent, notify::Error\u003e\u003e,\n #[allow(dead_code)] \n watcher: RecommendedWatcher,\n processor_thread: Option\u003cJoinHandle\u003c()\u003e\u003e,\n stop_flag: Arc\u003cAtomicBool\u003e,\n events_processed: Arc\u003cAtomicUsize\u003e,\n queue_size: Arc\u003cAtomicUsize\u003e,\n start_time: Instant,\n db_shared: Arc\u003cMutex\u003cOption\u003cArc\u003cMutex\u003cDatabase\u003e\u003e\u003e\u003e\u003e,\n}\n\nimpl FileWatcher {\n pub fn new(paths: Vec\u003cPathBuf\u003e, config: WatcherConfig) -\u003e Result\u003cSelf\u003e {\n let stop_flag = Arc::new(AtomicBool::new(false));\n let events_processed = Arc::new(AtomicUsize::new(0));\n let queue_size = Arc::new(AtomicUsize::new(0));\n let state = Arc::new(Mutex::new(WatcherState::Initializing));\n\n let (tx, rx) = bounded(config.max_queue_size);\n\n let event_tx = tx.clone();\n let mut actual_watcher = RecommendedWatcher::new(\n move |event_res: std::result::Result\u003cEvent, notify::Error\u003e| {\n if event_tx.send(event_res).is_err() {\n // Receiver dropped\n }\n },\n notify::Config::default(),\n )?;\n\n for path_to_watch in \u0026paths {\n actual_watcher\n .watch(path_to_watch, RecursiveMode::Recursive)\n .with_context(|| format!(\"Failed to watch path: {}\", path_to_watch.display()))?;\n }\n\n let config_clone = config.clone();\n let stop_flag_clone = stop_flag.clone();\n let events_processed_clone = events_processed.clone();\n let queue_size_clone = queue_size.clone();\n let state_clone = state.clone();\n let receiver_clone = rx.clone(); \n\n let db_shared_for_thread = Arc::new(Mutex::new(None::\u003cArc\u003cMutex\u003cDatabase\u003e\u003e\u003e));\n let db_captured_for_thread = db_shared_for_thread.clone();\n\n let processor_thread = thread::spawn(move || {\n let mut debouncer = EventDebouncer::new(config_clone.debounce_ms);\n\n while !stop_flag_clone.load(Ordering::Relaxed) { \n let current_state = { state_clone.lock().unwrap().clone() };\n\n if current_state == WatcherState::Paused {\n thread::sleep(Duration::from_millis(100));\n continue;\n }\n if current_state == WatcherState::ShuttingDown || current_state == WatcherState::Stopped {\n break;\n }\n\n let mut received_in_batch = 0;\n while let Ok(evt_res) = receiver_clone.try_recv() {\n received_in_batch +=1;\n match evt_res {\n Ok(event) =\u003e {\n for path in event.paths {\n let prio = match event.kind {\n EventKind::Create(_) =\u003e EventPriority::Create,\n EventKind::Remove(_) =\u003e EventPriority::Delete,\n EventKind::Modify(_) =\u003e EventPriority::Modify,\n EventKind::Access(_) =\u003e EventPriority::Access,\n _ =\u003e EventPriority::Modify,\n };\n debouncer.add_event(ProcessedEvent {\n path,\n kind: event.kind.clone(),\n priority: prio,\n timestamp: Instant::now(),\n });\n }\n }\n Err(e) =\u003e {\n eprintln!(\"Watcher channel error: {:?}\", e);\n }\n }\n if received_in_batch \u003e= config_clone.batch_size {\n break;\n }\n }\n\n queue_size_clone.store(debouncer.len(), Ordering::SeqCst);\n\n if debouncer.is_ready_to_flush() \u0026\u0026 debouncer.len() \u003e 0 {\n let evts_to_process = debouncer.flush();\n let num_evts = evts_to_process.len();\n events_processed_clone.fetch_add(num_evts, Ordering::SeqCst);\n\n let db_guard_option = db_captured_for_thread.lock().unwrap();\n if let Some(db_mutex) = \u0026*db_guard_option {\n let mut _db_instance_guard = db_mutex.lock().unwrap();\n for event_item in \u0026evts_to_process {\n println!( \n \"Processing event (DB available): {:?} for path {:?}\",\n event_item.kind, event_item.path\n );\n }\n } else {\n for event_item in \u0026evts_to_process {\n println!( \n \"Processing event (no DB): {:?} for path {:?}\",\n event_item.kind, event_item.path\n );\n }\n }\n }\n thread::sleep(Duration::from_millis(50));\n }\n\n if debouncer.len() \u003e 0 {\n let final_evts = debouncer.flush();\n events_processed_clone.fetch_add(final_evts.len(), Ordering::SeqCst);\n for processed_event in final_evts {\n println!(\n \"Processing final event: {:?} for path {:?}\",\n processed_event.kind, processed_event.path\n );\n }\n }\n let mut final_state_guard = state_clone.lock().unwrap();\n *final_state_guard = WatcherState::Stopped;\n });\n\n Ok(Self {\n state,\n config,\n watched_paths: paths,\n event_receiver: rx,\n watcher: actual_watcher,\n processor_thread: Some(processor_thread),\n stop_flag,\n events_processed,\n queue_size,\n start_time: Instant::now(),\n db_shared: db_shared_for_thread,\n })\n }\n\n pub fn with_database(\u0026mut self, db_arc: Arc\u003cMutex\u003cDatabase\u003e\u003e) -\u003e \u0026mut Self {\n { \n let mut shared_db_guard = self.db_shared.lock().unwrap();\n *shared_db_guard = Some(db_arc);\n } \n self\n }\n\n pub fn start(\u0026mut self) -\u003e Result\u003c()\u003e {\n let mut state_guard = self.state.lock().unwrap();\n if *state_guard == WatcherState::Watching || self.processor_thread.is_none() {\n if self.processor_thread.is_none() {\n return Err(anyhow::anyhow!(\"Watcher thread not available to start.\"));\n }\n if *state_guard == WatcherState::Initializing {\n *state_guard = WatcherState::Watching;\n }\n return Ok(());\n }\n if *state_guard != WatcherState::Initializing \u0026\u0026 *state_guard != WatcherState::Stopped \u0026\u0026 *state_guard != WatcherState::Paused {\n return Err(anyhow::anyhow!(format!(\"Cannot start watcher from state {:?}\", *state_guard)));\n }\n\n *state_guard = WatcherState::Watching;\n Ok(())\n }\n\n pub fn pause(\u0026mut self) -\u003e Result\u003c()\u003e {\n let mut state_guard = self.state.lock().unwrap();\n match *state_guard {\n WatcherState::Watching =\u003e {\n *state_guard = WatcherState::Paused;\n Ok(())\n }\n WatcherState::Paused =\u003e Ok(()), \n _ =\u003e Err(anyhow::anyhow!(format!(\"Watcher not in watching state to pause (current: {:?})\", *state_guard))),\n }\n }\n\n pub fn resume(\u0026mut self) -\u003e Result\u003c()\u003e {\n let mut state_guard = self.state.lock().unwrap();\n match *state_guard {\n WatcherState::Paused =\u003e {\n *state_guard = WatcherState::Watching;\n Ok(())\n }\n WatcherState::Watching =\u003e Ok(()), \n _ =\u003e Err(anyhow::anyhow!(format!(\"Watcher not in paused state to resume (current: {:?})\", *state_guard))),\n }\n }\n\n pub fn stop(\u0026mut self) -\u003e Result\u003c()\u003e {\n let mut current_state_guard = self.state.lock().unwrap();\n if *current_state_guard == WatcherState::Stopped || *current_state_guard == WatcherState::ShuttingDown {\n return Ok(());\n }\n *current_state_guard = WatcherState::ShuttingDown;\n drop(current_state_guard);\n\n self.stop_flag.store(true, Ordering::SeqCst);\n\n if let Some(handle) = self.processor_thread.take() {\n match handle.join() {\n Ok(_) =\u003e { /* Thread joined cleanly */ }\n Err(join_err) =\u003e {\n eprintln!(\"Watcher processor thread panicked: {:?}\", join_err);\n }\n }\n }\n \n let mut final_state_guard = self.state.lock().unwrap();\n *final_state_guard = WatcherState::Stopped;\n Ok(())\n }\n\n pub fn status(\u0026self) -\u003e WatcherStatus {\n let state_guard = self.state.lock().unwrap().clone();\n WatcherStatus {\n state: state_guard,\n events_processed: self.events_processed.load(Ordering::SeqCst),\n queue_size: self.queue_size.load(Ordering::SeqCst),\n start_time: Some(self.start_time),\n watched_paths: self.watched_paths.clone(),\n }\n }\n}\n\nimpl Drop for FileWatcher {\n fn drop(\u0026mut self) {\n if let Err(e) = self.stop() {\n eprintln!(\"Error stopping watcher in Drop: {:?}\", e);\n }\n }\n}\n\n\n#[cfg(test)]\nmod file_watcher_state_tests { \n use super::*;\n use tempfile::tempdir;\n use std::fs as FsMod; // Alias to avoid conflict with local `fs` module name if any\n\n #[test]\n fn test_watcher_pause_resume_stop() {\n let tmp_dir = tempdir().unwrap();\n let watch_path = tmp_dir.path().to_path_buf();\n FsMod::create_dir_all(\u0026watch_path).expect(\"Failed to create temp dir for watching\");\n\n let config = WatcherConfig::default();\n\n let mut watcher = FileWatcher::new(vec![watch_path], config).expect(\"Failed to create watcher\");\n\n assert_eq!(watcher.status().state, WatcherState::Initializing);\n\n watcher.start().expect(\"Start failed\");\n assert_eq!(watcher.status().state, WatcherState::Watching);\n\n watcher.pause().expect(\"Pause failed\");\n assert_eq!(watcher.status().state, WatcherState::Paused);\n\n watcher.pause().expect(\"Second pause failed\");\n assert_eq!(watcher.status().state, WatcherState::Paused);\n\n watcher.resume().expect(\"Resume failed\");\n assert_eq!(watcher.status().state, WatcherState::Watching);\n \n watcher.resume().expect(\"Second resume failed\");\n assert_eq!(watcher.status().state, WatcherState::Watching);\n\n watcher.stop().expect(\"Stop failed\");\n assert_eq!(watcher.status().state, WatcherState::Stopped);\n\n watcher.stop().expect(\"Second stop failed\");\n assert_eq!(watcher.status().state, WatcherState::Stopped);\n }\n\n #[test]\n fn test_watcher_start_errors() {\n let tmp_dir = tempdir().unwrap();\n FsMod::create_dir_all(tmp_dir.path()).expect(\"Failed to create temp dir for watching\");\n let mut watcher = FileWatcher::new(vec![tmp_dir.path().to_path_buf()], WatcherConfig::default()).unwrap();\n \n {\n let mut state_guard = watcher.state.lock().unwrap();\n *state_guard = WatcherState::Watching; \n }\n assert!(watcher.start().is_ok(), \"Should be able to call start when already Watching (idempotent state change)\");\n assert_eq!(watcher.status().state, WatcherState::Watching);\n \n {\n let mut state_guard = watcher.state.lock().unwrap();\n *state_guard = WatcherState::ShuttingDown;\n }\n assert!(watcher.start().is_err(), \"Should not be able to start from ShuttingDown\");\n }\n\n #[test]\n fn test_new_watcher_with_nonexistent_path() {\n let non_existent_path = PathBuf::from(\"/path/that/REALLY/does/not/exist/for/sure/and/cannot/be/created\");\n let config = WatcherConfig::default();\n let watcher_result = FileWatcher::new(vec![non_existent_path], config);\n assert!(watcher_result.is_err());\n if let Err(e) = watcher_result {\n let err_string = e.to_string();\n assert!(err_string.contains(\"Failed to watch path\") || err_string.contains(\"os error 2\"), \"Error was: {}\", err_string);\n }\n }\n\n #[test]\n fn test_watcher_default_config() {\n let config = WatcherConfig::default(); \n assert_eq!(config.debounce_ms, 100);\n assert_eq!(config.batch_size, 1000);\n assert_eq!(config.max_queue_size, 100_000);\n assert_eq!(config.drain_timeout_ms, 5000);\n }\n}","traces":[{"line":38,"address":[5542400],"length":1,"stats":{"Line":1}},{"line":91,"address":[2059006,2058864,2059012],"length":1,"stats":{"Line":1}},{"line":93,"address":[2058893],"length":1,"stats":{"Line":1}},{"line":95,"address":[2058898],"length":1,"stats":{"Line":1}},{"line":99,"address":[2059653,2059024],"length":1,"stats":{"Line":1}},{"line":100,"address":[2059049,2059126],"length":1,"stats":{"Line":2}},{"line":101,"address":[2059139,2059204],"length":1,"stats":{"Line":2}},{"line":103,"address":[2059261],"length":1,"stats":{"Line":3}},{"line":105,"address":[2059275,2059239],"length":1,"stats":{"Line":2}},{"line":106,"address":[2059313],"length":1,"stats":{"Line":1}},{"line":107,"address":[2059576,2059496,2059331],"length":1,"stats":{"Line":2}},{"line":108,"address":[2059570],"length":1,"stats":{"Line":0}},{"line":110,"address":[2059512],"length":1,"stats":{"Line":1}},{"line":111,"address":[5543146],"length":1,"stats":{"Line":1}},{"line":114,"address":[2059363],"length":1,"stats":{"Line":1}},{"line":119,"address":[2059696],"length":1,"stats":{"Line":1}},{"line":120,"address":[2059710],"length":1,"stats":{"Line":1}},{"line":123,"address":[2060025,2059776,2060031],"length":1,"stats":{"Line":1}},{"line":124,"address":[2059819],"length":1,"stats":{"Line":5}},{"line":125,"address":[2004170,2004160],"length":1,"stats":{"Line":5}},{"line":126,"address":[2059940],"length":1,"stats":{"Line":1}},{"line":127,"address":[2059991],"length":1,"stats":{"Line":1}},{"line":131,"address":[2060048],"length":1,"stats":{"Line":1}},{"line":132,"address":[5543701],"length":1,"stats":{"Line":2}},{"line":310,"address":[2060064,2063925,2063163],"length":1,"stats":{"Line":1}},{"line":311,"address":[5543751,5543927],"length":1,"stats":{"Line":2}},{"line":312,"address":[2060369,2060433],"length":1,"stats":{"Line":2}},{"line":313,"address":[2060524,2060588],"length":1,"stats":{"Line":2}},{"line":314,"address":[2060675,2060755],"length":1,"stats":{"Line":2}},{"line":316,"address":[2060850,2060910],"length":1,"stats":{"Line":2}},{"line":318,"address":[2060990,2061070],"length":1,"stats":{"Line":2}},{"line":320,"address":[2004176,2004268,2004274],"length":1,"stats":{"Line":2}},{"line":321,"address":[2004191],"length":1,"stats":{"Line":1}},{"line":325,"address":[2061110],"length":1,"stats":{"Line":1}},{"line":328,"address":[5545137,5545221],"length":1,"stats":{"Line":2}},{"line":329,"address":[2063405,2063555],"length":1,"stats":{"Line":2}},{"line":330,"address":[2061644,2063397],"length":1,"stats":{"Line":2}},{"line":331,"address":[2004288,2004310],"length":1,"stats":{"Line":3}},{"line":334,"address":[2061680],"length":1,"stats":{"Line":1}},{"line":335,"address":[2061687],"length":1,"stats":{"Line":1}},{"line":336,"address":[2061740,2061798],"length":1,"stats":{"Line":2}},{"line":337,"address":[5545573,5545506],"length":1,"stats":{"Line":2}},{"line":338,"address":[2061956,2061901],"length":1,"stats":{"Line":2}},{"line":339,"address":[2062045,2061980],"length":1,"stats":{"Line":2}},{"line":341,"address":[2062069,2062150],"length":1,"stats":{"Line":2}},{"line":342,"address":[2062183,2062237],"length":1,"stats":{"Line":2}},{"line":344,"address":[2062245],"length":1,"stats":{"Line":2}},{"line":345,"address":[2004570,2004479],"length":1,"stats":{"Line":2}},{"line":347,"address":[2004648,2004580],"length":1,"stats":{"Line":2}},{"line":348,"address":[2004760,2004699],"length":1,"stats":{"Line":2}},{"line":350,"address":[2004950],"length":1,"stats":{"Line":1}},{"line":351,"address":[2008058,2005026],"length":1,"stats":{"Line":0}},{"line":354,"address":[2005058,2004992],"length":1,"stats":{"Line":2}},{"line":358,"address":[2005114],"length":1,"stats":{"Line":1}},{"line":359,"address":[2005142,2006547,2005222],"length":1,"stats":{"Line":3}},{"line":360,"address":[2005272,2005346],"length":1,"stats":{"Line":1}},{"line":361,"address":[2005313],"length":1,"stats":{"Line":1}},{"line":362,"address":[2005521],"length":1,"stats":{"Line":1}},{"line":363,"address":[2005577,2005815,2005680],"length":1,"stats":{"Line":3}},{"line":364,"address":[2005884],"length":1,"stats":{"Line":1}},{"line":365,"address":[2006022],"length":1,"stats":{"Line":1}},{"line":366,"address":[2006042],"length":1,"stats":{"Line":1}},{"line":367,"address":[2006032],"length":1,"stats":{"Line":1}},{"line":368,"address":[2006012],"length":1,"stats":{"Line":1}},{"line":369,"address":[2006002],"length":1,"stats":{"Line":0}},{"line":371,"address":[2006268],"length":1,"stats":{"Line":1}},{"line":372,"address":[5524074],"length":1,"stats":{"Line":1}},{"line":373,"address":[2006155,2006082],"length":1,"stats":{"Line":2}},{"line":374,"address":[2006206],"length":1,"stats":{"Line":1}},{"line":375,"address":[2006220],"length":1,"stats":{"Line":1}},{"line":379,"address":[2005423],"length":1,"stats":{"Line":0}},{"line":380,"address":[2005511,2006432],"length":1,"stats":{"Line":0}},{"line":383,"address":[2005979],"length":1,"stats":{"Line":1}},{"line":388,"address":[2006663,2006719],"length":1,"stats":{"Line":2}},{"line":390,"address":[2006846,2006781],"length":1,"stats":{"Line":2}},{"line":391,"address":[2006891],"length":1,"stats":{"Line":1}},{"line":392,"address":[2006977,2006906],"length":1,"stats":{"Line":2}},{"line":393,"address":[2006985],"length":1,"stats":{"Line":1}},{"line":395,"address":[5525091],"length":1,"stats":{"Line":1}},{"line":396,"address":[5525214,5525293],"length":1,"stats":{"Line":2}},{"line":397,"address":[5525335,5525397],"length":1,"stats":{"Line":0}},{"line":398,"address":[2007476,2007409],"length":1,"stats":{"Line":0}},{"line":399,"address":[5525734,5525665],"length":1,"stats":{"Line":0}},{"line":405,"address":[5525354,5525856],"length":1,"stats":{"Line":2}},{"line":406,"address":[5526046,5525965],"length":1,"stats":{"Line":2}},{"line":413,"address":[5526025,5524841],"length":1,"stats":{"Line":2}},{"line":416,"address":[2008588,2008082,2004726],"length":1,"stats":{"Line":2}},{"line":417,"address":[5526247],"length":1,"stats":{"Line":0}},{"line":418,"address":[5526331,5526270],"length":1,"stats":{"Line":0}},{"line":419,"address":[5526388,5526590],"length":1,"stats":{"Line":0}},{"line":420,"address":[2008843,2008539],"length":1,"stats":{"Line":0}},{"line":426,"address":[5526208,5526742],"length":1,"stats":{"Line":2}},{"line":427,"address":[2008743,2008670],"length":1,"stats":{"Line":2}},{"line":430,"address":[5546512],"length":1,"stats":{"Line":1}},{"line":431,"address":[5546176],"length":1,"stats":{"Line":1}},{"line":433,"address":[2062476],"length":1,"stats":{"Line":1}},{"line":434,"address":[2062507],"length":1,"stats":{"Line":1}},{"line":435,"address":[5546271],"length":1,"stats":{"Line":1}},{"line":436,"address":[5546311],"length":1,"stats":{"Line":1}},{"line":437,"address":[5546343],"length":1,"stats":{"Line":1}},{"line":438,"address":[2062643],"length":1,"stats":{"Line":1}},{"line":439,"address":[2062667],"length":1,"stats":{"Line":1}},{"line":440,"address":[5546415],"length":1,"stats":{"Line":1}},{"line":441,"address":[2062776],"length":1,"stats":{"Line":1}},{"line":445,"address":[2064322,2063968,2064316],"length":1,"stats":{"Line":0}},{"line":447,"address":[2064005,2064066],"length":1,"stats":{"Line":0}},{"line":448,"address":[5548035,5547941,5548140,5547982],"length":1,"stats":{"Line":0}},{"line":450,"address":[5547798],"length":1,"stats":{"Line":0}},{"line":453,"address":[5549403,5548192,5549178],"length":1,"stats":{"Line":1}},{"line":454,"address":[5548215],"length":1,"stats":{"Line":1}},{"line":455,"address":[5548306,5548385,5548480],"length":1,"stats":{"Line":3}},{"line":456,"address":[2065274,2064587],"length":1,"stats":{"Line":2}},{"line":457,"address":[5549218,5549350],"length":1,"stats":{"Line":0}},{"line":459,"address":[5549249,5549340,5549194],"length":1,"stats":{"Line":2}},{"line":460,"address":[2065376],"length":1,"stats":{"Line":0}},{"line":462,"address":[5549277],"length":1,"stats":{"Line":1}},{"line":464,"address":[5548486,5548582],"length":1,"stats":{"Line":2}},{"line":465,"address":[5548792,5548707,5549156,5548986],"length":1,"stats":{"Line":2}},{"line":468,"address":[5548547,5548736],"length":1,"stats":{"Line":2}},{"line":469,"address":[5548745],"length":1,"stats":{"Line":1}},{"line":472,"address":[5549424,5550079,5550085],"length":1,"stats":{"Line":1}},{"line":473,"address":[2065519],"length":1,"stats":{"Line":1}},{"line":474,"address":[2065642,2065588],"length":1,"stats":{"Line":2}},{"line":476,"address":[5549674,5549632],"length":1,"stats":{"Line":2}},{"line":477,"address":[2065732],"length":1,"stats":{"Line":1}},{"line":479,"address":[2065710],"length":1,"stats":{"Line":1}},{"line":480,"address":[2065949,2065676,2065777],"length":1,"stats":{"Line":0}},{"line":484,"address":[5550769,5550112,5550775],"length":1,"stats":{"Line":1}},{"line":485,"address":[2066159],"length":1,"stats":{"Line":1}},{"line":486,"address":[5550203,5550270],"length":1,"stats":{"Line":2}},{"line":488,"address":[2066339,2066389],"length":1,"stats":{"Line":2}},{"line":489,"address":[2066395],"length":1,"stats":{"Line":1}},{"line":491,"address":[5550320],"length":1,"stats":{"Line":1}},{"line":492,"address":[2066316,2066419,2066591],"length":1,"stats":{"Line":0}},{"line":496,"address":[2067872,2067606,2066784],"length":1,"stats":{"Line":1}},{"line":497,"address":[2066804],"length":1,"stats":{"Line":1}},{"line":498,"address":[5551072,5550918,5550985],"length":1,"stats":{"Line":3}},{"line":499,"address":[2066998],"length":1,"stats":{"Line":1}},{"line":501,"address":[5551100],"length":1,"stats":{"Line":1}},{"line":502,"address":[2067102],"length":1,"stats":{"Line":1}},{"line":504,"address":[2067135],"length":1,"stats":{"Line":1}},{"line":506,"address":[2067193],"length":1,"stats":{"Line":1}},{"line":507,"address":[5551343,5551422],"length":1,"stats":{"Line":2}},{"line":509,"address":[2067416],"length":1,"stats":{"Line":0}},{"line":510,"address":[2067464,2067515],"length":1,"stats":{"Line":0}},{"line":515,"address":[2067683,2067635],"length":1,"stats":{"Line":2}},{"line":516,"address":[2067747,2067818],"length":1,"stats":{"Line":2}},{"line":517,"address":[2067827],"length":1,"stats":{"Line":1}},{"line":520,"address":[5552406,5552412,5551984],"length":1,"stats":{"Line":1}},{"line":521,"address":[2067942],"length":1,"stats":{"Line":1}},{"line":524,"address":[5552195],"length":1,"stats":{"Line":1}},{"line":525,"address":[2068130],"length":1,"stats":{"Line":1}},{"line":526,"address":[2068167],"length":1,"stats":{"Line":1}},{"line":527,"address":[2068183],"length":1,"stats":{"Line":1}},{"line":533,"address":[2001554,2001560,2001328],"length":1,"stats":{"Line":1}},{"line":534,"address":[2001340],"length":1,"stats":{"Line":1}},{"line":535,"address":[2001410,2001484],"length":1,"stats":{"Line":0}}],"covered":134,"coverable":157},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","watcher_tests.rs"],"content":"//! Tests for the file system watcher functionality\n\n#[cfg(test)]\nmod tests {\n // Updated import for BackupManager from the new backup module\n use crate::backup::BackupManager;\n // These are still from the watcher module\n use crate::watcher::{FileWatcher, WatcherConfig, WatcherState};\n use crate::db::open as open_marlin_db; // Use your project's DB open function\n\n\n use std::fs::{self, File};\n use std::io::Write;\n // No longer need: use std::path::PathBuf;\n use std::thread;\n use std::time::Duration;\n use tempfile::tempdir;\n\n #[test]\n fn test_watcher_lifecycle() {\n // Create a temp directory for testing\n let temp_dir = tempdir().expect(\"Failed to create temp directory\");\n let temp_path = temp_dir.path();\n\n // Create a test file\n let test_file_path = temp_path.join(\"test.txt\");\n let mut file = File::create(\u0026test_file_path).expect(\"Failed to create test file\");\n writeln!(file, \"Test content\").expect(\"Failed to write to test file\");\n drop(file);\n\n // Configure and start the watcher\n let config = WatcherConfig {\n debounce_ms: 100,\n batch_size: 10,\n max_queue_size: 100,\n drain_timeout_ms: 1000,\n };\n\n let mut watcher = FileWatcher::new(vec![temp_path.to_path_buf()], config)\n .expect(\"Failed to create watcher\");\n\n watcher.start().expect(\"Failed to start watcher\");\n assert_eq!(watcher.status().state, WatcherState::Watching);\n\n thread::sleep(Duration::from_millis(200));\n let new_file_path = temp_path.join(\"new_file.txt\");\n let mut new_file_handle = File::create(\u0026new_file_path).expect(\"Failed to create new file\");\n writeln!(new_file_handle, \"New file content\").expect(\"Failed to write to new file\");\n drop(new_file_handle);\n\n thread::sleep(Duration::from_millis(200));\n let mut existing_file_handle = fs::OpenOptions::new()\n .write(true)\n .append(true)\n .open(\u0026test_file_path)\n .expect(\"Failed to open test file for modification\");\n writeln!(existing_file_handle, \"Additional content\").expect(\"Failed to append to test file\");\n drop(existing_file_handle);\n\n thread::sleep(Duration::from_millis(200));\n fs::remove_file(\u0026new_file_path).expect(\"Failed to remove file\");\n\n thread::sleep(Duration::from_millis(500));\n watcher.stop().expect(\"Failed to stop watcher\");\n\n assert_eq!(watcher.status().state, WatcherState::Stopped);\n assert!(watcher.status().events_processed \u003e 0, \"Expected some file events to be processed\");\n }\n\n #[test]\n fn test_backup_manager_related_functionality() {\n let live_db_tmp_dir = tempdir().expect(\"Failed to create temp directory for live DB\");\n let backups_storage_tmp_dir = tempdir().expect(\"Failed to create temp directory for backups storage\");\n \n let live_db_path = live_db_tmp_dir.path().join(\"test_live_watcher.db\"); // Unique name\n let backups_actual_dir = backups_storage_tmp_dir.path().join(\"my_backups_watcher\"); // Unique name\n\n // Initialize a proper SQLite DB for the \"live\" database\n let _conn = open_marlin_db(\u0026live_db_path).expect(\"Failed to open test_live_watcher.db for backup test\");\n \n let backup_manager = BackupManager::new(\u0026live_db_path, \u0026backups_actual_dir)\n .expect(\"Failed to create BackupManager instance\");\n \n let backup_info = backup_manager.create_backup().expect(\"Failed to create first backup\");\n \n assert!(backups_actual_dir.join(\u0026backup_info.id).exists(), \"Backup file should exist\");\n assert!(backup_info.size_bytes \u003e 0, \"Backup size should be greater than 0\");\n \n for i in 0..3 {\n std::thread::sleep(std::time::Duration::from_millis(30)); // Ensure timestamp difference\n backup_manager.create_backup().unwrap_or_else(|e| panic!(\"Failed to create additional backup {}: {:?}\", i, e));\n }\n \n let backups = backup_manager.list_backups().expect(\"Failed to list backups\");\n assert_eq!(backups.len(), 4, \"Should have 4 backups listed\");\n \n let prune_result = backup_manager.prune(2).expect(\"Failed to prune backups\");\n \n assert_eq!(prune_result.kept.len(), 2, \"Should have kept 2 backups\");\n assert_eq!(prune_result.removed.len(), 2, \"Should have removed 2 backups (4 initial - 2 kept)\");\n \n let remaining_backups = backup_manager.list_backups().expect(\"Failed to list backups after prune\");\n assert_eq!(remaining_backups.len(), 2, \"Should have 2 backups remaining after prune\");\n\n for removed_info in prune_result.removed {\n assert!(!backups_actual_dir.join(\u0026removed_info.id).exists(), \"Removed backup file {} should not exist\", removed_info.id);\n }\n for kept_info in prune_result.kept {\n assert!(backups_actual_dir.join(\u0026kept_info.id).exists(), \"Kept backup file {} should exist\", kept_info.id);\n }\n }\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","user","Documents","GitHub","Marlin","tui-bin","src","main.rs"],"content":"// tui-bin/src/main.rs\n\nfn main() {\n eprintln!(\"marlin-tui is not yet implemented. Stay tuned!\");\n}\n","traces":[{"line":3,"address":[127488],"length":1,"stats":{"Line":0}},{"line":4,"address":[127492],"length":1,"stats":{"Line":0}}],"covered":0,"coverable":2}]};
|
||
var previousData = {"files":[{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","build.rs"],"content":"// cli-bin/build.rs\n//\n// The CLI currently needs no build-time code-generation, but Cargo\n// insists on rerunning any build-script each compile. Tell it to\n// rebuild only if this file itself changes.\n\nfn main() {\n // If you later add code-gen (e.g. embed completions or YAML), add\n // further `cargo:rerun-if-changed=\u003cpath\u003e` lines here.\n println!(\"cargo:rerun-if-changed=build.rs\");\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","src","cli","annotate.rs"],"content":"// src/cli/annotate.rs\nuse clap::{Subcommand, Args};\nuse rusqlite::Connection;\nuse crate::cli::Format;\n\n#[derive(Subcommand, Debug)]\npub enum AnnotateCmd {\n Add (ArgsAdd),\n List(ArgsList),\n}\n\n#[derive(Args, Debug)]\npub struct ArgsAdd {\n pub file: String,\n pub note: String,\n #[arg(long)] pub range: Option\u003cString\u003e,\n #[arg(long)] pub highlight: bool,\n}\n\n#[derive(Args, Debug)]\npub struct ArgsList { pub file_pattern: String }\n\npub fn run(cmd: \u0026AnnotateCmd, _conn: \u0026mut Connection, _format: Format) -\u003e anyhow::Result\u003c()\u003e {\n match cmd {\n AnnotateCmd::Add(a) =\u003e todo!(\"annotate add {:?}\", a),\n AnnotateCmd::List(a) =\u003e todo!(\"annotate list {:?}\", a),\n }\n}\n","traces":[{"line":23,"address":[2450848],"length":1,"stats":{"Line":0}},{"line":24,"address":[2450887],"length":1,"stats":{"Line":0}}],"covered":0,"coverable":2},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","src","cli","coll.rs"],"content":"//! `marlin coll …` – named collections of files (simple “playlists”).\n\nuse clap::{Args, Subcommand};\nuse rusqlite::Connection;\n\nuse crate::cli::Format; // local enum for text / json output\nuse libmarlin::db; // core DB helpers from the library crate\n\n#[derive(Subcommand, Debug)]\npub enum CollCmd {\n /// Create an empty collection\n Create(CreateArgs),\n /// Add files (glob) to a collection\n Add(AddArgs),\n /// List files inside a collection\n List(ListArgs),\n}\n\n#[derive(Args, Debug)]\npub struct CreateArgs {\n pub name: String,\n}\n\n#[derive(Args, Debug)]\npub struct AddArgs {\n pub name: String,\n pub file_pattern: String,\n}\n\n#[derive(Args, Debug)]\npub struct ListArgs {\n pub name: String,\n}\n\n/// Look-up an existing collection **without** implicitly creating it.\n///\n/// Returns the collection ID or an error if it doesn’t exist.\nfn lookup_collection_id(conn: \u0026Connection, name: \u0026str) -\u003e anyhow::Result\u003ci64\u003e {\n conn.query_row(\n \"SELECT id FROM collections WHERE name = ?1\",\n [name],\n |r| r.get(0),\n )\n .map_err(|_| anyhow::anyhow!(\"collection not found: {}\", name))\n}\n\npub fn run(cmd: \u0026CollCmd, conn: \u0026mut Connection, fmt: Format) -\u003e anyhow::Result\u003c()\u003e {\n match cmd {\n /* ── coll create ──────────────────────────────────────────── */\n CollCmd::Create(a) =\u003e {\n db::ensure_collection(conn, \u0026a.name)?;\n if matches!(fmt, Format::Text) {\n println!(\"Created collection '{}'\", a.name);\n }\n }\n\n /* ── coll add ─────────────────────────────────────────────── */\n CollCmd::Add(a) =\u003e {\n // Fail if the target collection does not yet exist\n let coll_id = lookup_collection_id(conn, \u0026a.name)?;\n\n let like = a.file_pattern.replace('*', \"%\");\n let mut stmt = conn.prepare(\"SELECT id FROM files WHERE path LIKE ?1\")?;\n let ids: Vec\u003ci64\u003e = stmt\n .query_map([\u0026like], |r| r.get::\u003c_, i64\u003e(0))?\n .collect::\u003cResult\u003c_, _\u003e\u003e()?;\n\n for fid in \u0026ids {\n db::add_file_to_collection(conn, coll_id, *fid)?;\n }\n\n match fmt {\n Format::Text =\u003e println!(\"Added {} file(s) → '{}'\", ids.len(), a.name),\n Format::Json =\u003e {\n #[cfg(feature = \"json\")]\n {\n println!(\n \"{{\\\"collection\\\":\\\"{}\\\",\\\"added\\\":{}}}\",\n a.name,\n ids.len()\n );\n }\n }\n }\n }\n\n /* ── coll list ────────────────────────────────────────────── */\n CollCmd::List(a) =\u003e {\n let files = db::list_collection(conn, \u0026a.name)?;\n match fmt {\n Format::Text =\u003e {\n for f in files {\n println!(\"{f}\");\n }\n }\n Format::Json =\u003e {\n #[cfg(feature = \"json\")]\n {\n println!(\"{}\", serde_json::to_string(\u0026files)?);\n }\n }\n }\n }\n }\n Ok(())\n}\n","traces":[{"line":38,"address":[2169520],"length":1,"stats":{"Line":0}},{"line":39,"address":[2169565],"length":1,"stats":{"Line":0}},{"line":41,"address":[2169545],"length":1,"stats":{"Line":0}},{"line":42,"address":[2296656,2296672],"length":1,"stats":{"Line":0}},{"line":44,"address":[2296721,2296704],"length":1,"stats":{"Line":0}},{"line":47,"address":[2169632,2171921,2171991],"length":1,"stats":{"Line":0}},{"line":48,"address":[2169683],"length":1,"stats":{"Line":0}},{"line":50,"address":[2169766],"length":1,"stats":{"Line":0}},{"line":51,"address":[2170057,2169783],"length":1,"stats":{"Line":0}},{"line":52,"address":[2170122],"length":1,"stats":{"Line":0}},{"line":53,"address":[2170138],"length":1,"stats":{"Line":0}},{"line":58,"address":[2169858],"length":1,"stats":{"Line":0}},{"line":60,"address":[2170255,2169866],"length":1,"stats":{"Line":0}},{"line":62,"address":[2170320],"length":1,"stats":{"Line":0}},{"line":63,"address":[2171981,2170443,2170372],"length":1,"stats":{"Line":0}},{"line":64,"address":[2171932,2171195,2170775,2170990,2170882],"length":1,"stats":{"Line":0}},{"line":65,"address":[2170767,2170926],"length":1,"stats":{"Line":0}},{"line":68,"address":[2171376,2171293],"length":1,"stats":{"Line":0}},{"line":69,"address":[2171767,2171486],"length":1,"stats":{"Line":0}},{"line":72,"address":[2171515],"length":1,"stats":{"Line":0}},{"line":73,"address":[2171558],"length":1,"stats":{"Line":0}},{"line":88,"address":[2169940],"length":1,"stats":{"Line":0}},{"line":89,"address":[2169952,2172004,2172061],"length":1,"stats":{"Line":0}},{"line":90,"address":[2172165],"length":1,"stats":{"Line":0}},{"line":92,"address":[2172197,2172305,2172440],"length":1,"stats":{"Line":0}},{"line":93,"address":[2172509,2172602],"length":1,"stats":{"Line":0}},{"line":105,"address":[2170227],"length":1,"stats":{"Line":0}}],"covered":0,"coverable":27},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","src","cli","event.rs"],"content":"// src/cli/event.rs\nuse clap::{Subcommand, Args};\nuse rusqlite::Connection;\nuse crate::cli::Format;\n\n#[derive(Subcommand, Debug)]\npub enum EventCmd {\n Add (ArgsAdd),\n Timeline,\n}\n\n#[derive(Args, Debug)]\npub struct ArgsAdd {\n pub file: String,\n pub date: String,\n pub description: String,\n}\n\npub fn run(cmd: \u0026EventCmd, _conn: \u0026mut Connection, _format: Format) -\u003e anyhow::Result\u003c()\u003e {\n match cmd {\n EventCmd::Add(a) =\u003e todo!(\"event add {:?}\", a),\n EventCmd::Timeline =\u003e todo!(\"event timeline\"),\n }\n}\n","traces":[{"line":19,"address":[2433520],"length":1,"stats":{"Line":0}},{"line":20,"address":[2433559],"length":1,"stats":{"Line":0}}],"covered":0,"coverable":2},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","src","cli","link.rs"],"content":"//! src/cli/link.rs – manage typed relationships between files\n\nuse clap::{Subcommand, Args};\nuse rusqlite::Connection;\n\nuse crate::cli::Format; // output selector\nuse libmarlin::db; // ← switched from `crate::db`\n\n#[derive(Subcommand, Debug)]\npub enum LinkCmd {\n Add(LinkArgs),\n Rm (LinkArgs),\n List(ListArgs),\n Backlinks(BacklinksArgs),\n}\n\n#[derive(Args, Debug)]\npub struct LinkArgs {\n pub from: String,\n pub to: String,\n #[arg(long)]\n pub r#type: Option\u003cString\u003e,\n}\n\n#[derive(Args, Debug)]\npub struct ListArgs {\n pub pattern: String,\n #[arg(long)]\n pub direction: Option\u003cString\u003e,\n #[arg(long)]\n pub r#type: Option\u003cString\u003e,\n}\n\n#[derive(Args, Debug)]\npub struct BacklinksArgs {\n pub pattern: String,\n}\n\npub fn run(cmd: \u0026LinkCmd, conn: \u0026mut Connection, format: Format) -\u003e anyhow::Result\u003c()\u003e {\n match cmd {\n LinkCmd::Add(args) =\u003e {\n let src_id = db::file_id(conn, \u0026args.from)?;\n let dst_id = db::file_id(conn, \u0026args.to)?;\n db::add_link(conn, src_id, dst_id, args.r#type.as_deref())?;\n match format {\n Format::Text =\u003e {\n if let Some(t) = \u0026args.r#type {\n println!(\"Linked '{}' → '{}' [type='{}']\", args.from, args.to, t);\n } else {\n println!(\"Linked '{}' → '{}'\", args.from, args.to);\n }\n }\n Format::Json =\u003e {\n let typ = args\n .r#type\n .as_ref()\n .map(|s| format!(\"\\\"{}\\\"\", s))\n .unwrap_or_else(|| \"null\".into());\n println!(\n \"{{\\\"from\\\":\\\"{}\\\",\\\"to\\\":\\\"{}\\\",\\\"type\\\":{}}}\",\n args.from, args.to, typ\n );\n }\n }\n }\n LinkCmd::Rm(args) =\u003e {\n let src_id = db::file_id(conn, \u0026args.from)?;\n let dst_id = db::file_id(conn, \u0026args.to)?;\n db::remove_link(conn, src_id, dst_id, args.r#type.as_deref())?;\n match format {\n Format::Text =\u003e {\n if let Some(t) = \u0026args.r#type {\n println!(\"Removed link '{}' → '{}' [type='{}']\", args.from, args.to, t);\n } else {\n println!(\"Removed link '{}' → '{}'\", args.from, args.to);\n }\n }\n Format::Json =\u003e {\n let typ = args\n .r#type\n .as_ref()\n .map(|s| format!(\"\\\"{}\\\"\", s))\n .unwrap_or_else(|| \"null\".into());\n println!(\n \"{{\\\"from\\\":\\\"{}\\\",\\\"to\\\":\\\"{}\\\",\\\"type\\\":{}}}\",\n args.from, args.to, typ\n );\n }\n }\n }\n LinkCmd::List(args) =\u003e {\n let results = db::list_links(\n conn,\n \u0026args.pattern,\n args.direction.as_deref(),\n args.r#type.as_deref(),\n )?;\n match format {\n Format::Json =\u003e {\n let items: Vec\u003cString\u003e = results\n .into_iter()\n .map(|(src, dst, t)| {\n let typ = t\n .as_ref()\n .map(|s| format!(\"\\\"{}\\\"\", s))\n .unwrap_or_else(|| \"null\".into());\n format!(\n \"{{\\\"from\\\":\\\"{}\\\",\\\"to\\\":\\\"{}\\\",\\\"type\\\":{}}}\",\n src, dst, typ\n )\n })\n .collect();\n println!(\"[{}]\", items.join(\",\"));\n }\n Format::Text =\u003e {\n for (src, dst, t) in results {\n if let Some(t) = t {\n println!(\"{} → {} [type='{}']\", src, dst, t);\n } else {\n println!(\"{} → {}\", src, dst);\n }\n }\n }\n }\n }\n LinkCmd::Backlinks(args) =\u003e {\n let results = db::find_backlinks(conn, \u0026args.pattern)?;\n match format {\n Format::Json =\u003e {\n let items: Vec\u003cString\u003e = results\n .into_iter()\n .map(|(src, t)| {\n let typ = t\n .as_ref()\n .map(|s| format!(\"\\\"{}\\\"\", s))\n .unwrap_or_else(|| \"null\".into());\n format!(\"{{\\\"from\\\":\\\"{}\\\",\\\"type\\\":{}}}\", src, typ)\n })\n .collect();\n println!(\"[{}]\", items.join(\",\"));\n }\n Format::Text =\u003e {\n for (src, t) in results {\n if let Some(t) = t {\n println!(\"{} [type='{}']\", src, t);\n } else {\n println!(\"{}\", src);\n }\n }\n }\n }\n }\n }\n\n Ok(())\n}\n","traces":[{"line":39,"address":[2065779,2063952,2065785],"length":1,"stats":{"Line":0}},{"line":40,"address":[2064009],"length":1,"stats":{"Line":0}},{"line":41,"address":[2064086],"length":1,"stats":{"Line":0}},{"line":42,"address":[2064106,2064644],"length":1,"stats":{"Line":0}},{"line":43,"address":[2064715],"length":1,"stats":{"Line":0}},{"line":44,"address":[2064854],"length":1,"stats":{"Line":0}},{"line":45,"address":[2064991],"length":1,"stats":{"Line":0}},{"line":47,"address":[2065109],"length":1,"stats":{"Line":0}},{"line":48,"address":[2065179],"length":1,"stats":{"Line":0}},{"line":50,"address":[2065393],"length":1,"stats":{"Line":0}},{"line":54,"address":[2065012],"length":1,"stats":{"Line":0}},{"line":57,"address":[2422805,2422784],"length":1,"stats":{"Line":0}},{"line":58,"address":[2422924,2422912],"length":1,"stats":{"Line":0}},{"line":59,"address":[2065079,2065609],"length":1,"stats":{"Line":0}},{"line":66,"address":[2064187],"length":1,"stats":{"Line":0}},{"line":67,"address":[2065814,2064207],"length":1,"stats":{"Line":0}},{"line":68,"address":[2065882],"length":1,"stats":{"Line":0}},{"line":69,"address":[2066021],"length":1,"stats":{"Line":0}},{"line":70,"address":[2066158],"length":1,"stats":{"Line":0}},{"line":72,"address":[2066276],"length":1,"stats":{"Line":0}},{"line":73,"address":[2066346],"length":1,"stats":{"Line":0}},{"line":75,"address":[2066560],"length":1,"stats":{"Line":0}},{"line":79,"address":[2066179],"length":1,"stats":{"Line":0}},{"line":82,"address":[2422960,2422981],"length":1,"stats":{"Line":0}},{"line":83,"address":[2423100,2423088],"length":1,"stats":{"Line":0}},{"line":84,"address":[2066246,2066764],"length":1,"stats":{"Line":0}},{"line":91,"address":[2064288],"length":1,"stats":{"Line":0}},{"line":94,"address":[2064308],"length":1,"stats":{"Line":0}},{"line":95,"address":[2064337],"length":1,"stats":{"Line":0}},{"line":96,"address":[2064371],"length":1,"stats":{"Line":0}},{"line":98,"address":[2067095],"length":1,"stats":{"Line":0}},{"line":100,"address":[2067108,2068413],"length":1,"stats":{"Line":0}},{"line":102,"address":[2423710,2423136,2423161],"length":1,"stats":{"Line":0}},{"line":103,"address":[2423289,2423214],"length":1,"stats":{"Line":0}},{"line":105,"address":[2423744,2423765],"length":1,"stats":{"Line":0}},{"line":106,"address":[2423884,2423872],"length":1,"stats":{"Line":0}},{"line":107,"address":[2423385,2423323],"length":1,"stats":{"Line":0}},{"line":113,"address":[2068443,2068526],"length":1,"stats":{"Line":0}},{"line":116,"address":[2067174,2067278,2067405],"length":1,"stats":{"Line":0}},{"line":117,"address":[2067665,2067598],"length":1,"stats":{"Line":0}},{"line":118,"address":[2067705,2067806],"length":1,"stats":{"Line":0}},{"line":120,"address":[2068085,2067732],"length":1,"stats":{"Line":0}},{"line":126,"address":[2064525],"length":1,"stats":{"Line":0}},{"line":127,"address":[2068761,2064537,2068818],"length":1,"stats":{"Line":0}},{"line":128,"address":[2068922],"length":1,"stats":{"Line":0}},{"line":130,"address":[2068935,2070060],"length":1,"stats":{"Line":0}},{"line":132,"address":[2424376,2423945,2423920],"length":1,"stats":{"Line":0}},{"line":133,"address":[2423980,2424052],"length":1,"stats":{"Line":0}},{"line":135,"address":[2424400,2424421],"length":1,"stats":{"Line":0}},{"line":136,"address":[2424528,2424540],"length":1,"stats":{"Line":0}},{"line":137,"address":[2424083,2424145],"length":1,"stats":{"Line":0}},{"line":140,"address":[2070090,2070173],"length":1,"stats":{"Line":0}},{"line":143,"address":[2069113,2069248,2069005],"length":1,"stats":{"Line":0}},{"line":144,"address":[2069464,2069393],"length":1,"stats":{"Line":0}},{"line":145,"address":[2069504,2069605],"length":1,"stats":{"Line":0}},{"line":147,"address":[2069531,2069841],"length":1,"stats":{"Line":0}},{"line":155,"address":[2065540],"length":1,"stats":{"Line":0}}],"covered":0,"coverable":57},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","src","cli","remind.rs"],"content":"// src/cli/remind.rs\nuse clap::{Subcommand, Args};\nuse rusqlite::Connection;\nuse crate::cli::Format;\n\n#[derive(Subcommand, Debug)]\npub enum RemindCmd {\n Set(ArgsSet),\n}\n\n#[derive(Args, Debug)]\npub struct ArgsSet {\n pub file_pattern: String,\n pub timestamp: String,\n pub message: String,\n}\n\npub fn run(cmd: \u0026RemindCmd, _conn: \u0026mut Connection, _format: Format) -\u003e anyhow::Result\u003c()\u003e {\n match cmd {\n RemindCmd::Set(a) =\u003e todo!(\"remind set {:?}\", a),\n }\n}\n","traces":[{"line":18,"address":[2142944],"length":1,"stats":{"Line":0}}],"covered":0,"coverable":1},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","src","cli","state.rs"],"content":"// src/cli/state.rs\nuse clap::{Subcommand, Args};\nuse rusqlite::Connection;\nuse crate::cli::Format;\n\n#[derive(Subcommand, Debug)]\npub enum StateCmd {\n Set(ArgsSet),\n TransitionsAdd(ArgsTrans),\n Log(ArgsLog),\n}\n\n#[derive(Args, Debug)]\npub struct ArgsSet { pub file_pattern: String, pub new_state: String }\n#[derive(Args, Debug)]\npub struct ArgsTrans { pub from_state: String, pub to_state: String }\n#[derive(Args, Debug)]\npub struct ArgsLog { pub file_pattern: String }\n\npub fn run(cmd: \u0026StateCmd, _conn: \u0026mut Connection, _format: Format) -\u003e anyhow::Result\u003c()\u003e {\n match cmd {\n StateCmd::Set(a) =\u003e todo!(\"state set {:?}\", a),\n StateCmd::TransitionsAdd(a)=\u003e todo!(\"state transitions-add {:?}\", a),\n StateCmd::Log(a) =\u003e todo!(\"state log {:?}\", a),\n }\n}\n","traces":[{"line":20,"address":[2474816],"length":1,"stats":{"Line":0}},{"line":21,"address":[2474855],"length":1,"stats":{"Line":0}}],"covered":0,"coverable":2},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","src","cli","task.rs"],"content":"// src/cli/task.rs\nuse clap::{Subcommand, Args};\nuse rusqlite::Connection;\nuse crate::cli::Format;\n\n#[derive(Subcommand, Debug)]\npub enum TaskCmd {\n Scan(ArgsScan),\n List(ArgsList),\n}\n\n#[derive(Args, Debug)]\npub struct ArgsScan { pub directory: String }\n#[derive(Args, Debug)]\npub struct ArgsList { #[arg(long)] pub due_today: bool }\n\npub fn run(cmd: \u0026TaskCmd, _conn: \u0026mut Connection, _format: Format) -\u003e anyhow::Result\u003c()\u003e {\n match cmd {\n TaskCmd::Scan(a) =\u003e todo!(\"task scan {:?}\", a),\n TaskCmd::List(a) =\u003e todo!(\"task list {:?}\", a),\n }\n}\n","traces":[{"line":17,"address":[2403248],"length":1,"stats":{"Line":0}},{"line":18,"address":[2403287],"length":1,"stats":{"Line":0}}],"covered":0,"coverable":2},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","src","cli","version.rs"],"content":"// src/cli/version.rs\nuse clap::{Subcommand, Args};\nuse rusqlite::Connection;\nuse crate::cli::Format;\n\n#[derive(Subcommand, Debug)]\npub enum VersionCmd {\n Diff(ArgsDiff),\n}\n\n#[derive(Args, Debug)]\npub struct ArgsDiff { pub file: String }\n\npub fn run(cmd: \u0026VersionCmd, _conn: \u0026mut Connection, _format: Format) -\u003e anyhow::Result\u003c()\u003e {\n match cmd {\n VersionCmd::Diff(a) =\u003e todo!(\"version diff {:?}\", a),\n }\n}\n","traces":[{"line":14,"address":[2314960],"length":1,"stats":{"Line":0}}],"covered":0,"coverable":1},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","src","cli","view.rs"],"content":"//! `marlin view …` – save \u0026 use “smart folders” (named queries).\n\nuse std::fs;\n\nuse anyhow::Result;\nuse clap::{Args, Subcommand};\nuse rusqlite::Connection;\n\nuse crate::cli::Format; // output selector stays local\nuse libmarlin::db; // ← path switched from `crate::db`\n\n#[derive(Subcommand, Debug)]\npub enum ViewCmd {\n /// Save (or update) a view\n Save(ArgsSave),\n /// List all saved views\n List,\n /// Execute a view (print matching paths)\n Exec(ArgsExec),\n}\n\n#[derive(Args, Debug)]\npub struct ArgsSave {\n pub view_name: String,\n pub query: String,\n}\n\n#[derive(Args, Debug)]\npub struct ArgsExec {\n pub view_name: String,\n}\n\npub fn run(cmd: \u0026ViewCmd, conn: \u0026mut Connection, fmt: Format) -\u003e anyhow::Result\u003c()\u003e {\n match cmd {\n /* ── view save ───────────────────────────────────────────── */\n ViewCmd::Save(a) =\u003e {\n db::save_view(conn, \u0026a.view_name, \u0026a.query)?;\n if matches!(fmt, Format::Text) {\n println!(\"Saved view '{}' = {}\", a.view_name, a.query);\n }\n }\n\n /* ── view list ───────────────────────────────────────────── */\n ViewCmd::List =\u003e {\n let views = db::list_views(conn)?;\n match fmt {\n Format::Text =\u003e {\n for (name, q) in views {\n println!(\"{name}: {q}\");\n }\n }\n Format::Json =\u003e {\n #[cfg(feature = \"json\")]\n {\n println!(\"{}\", serde_json::to_string(\u0026views)?);\n }\n }\n }\n }\n\n /* ── view exec ───────────────────────────────────────────── */\n ViewCmd::Exec(a) =\u003e {\n let raw = db::view_query(conn, \u0026a.view_name)?;\n\n // Re-use the tiny parser from marlin search\n let fts_expr = build_fts_match(\u0026raw);\n\n let mut stmt = conn.prepare(\n r#\"\n SELECT f.path\n FROM files_fts\n JOIN files f ON f.rowid = files_fts.rowid\n WHERE files_fts MATCH ?1\n ORDER BY rank\n \"#,\n )?;\n let mut paths: Vec\u003cString\u003e = stmt\n .query_map([fts_expr], |r| r.get::\u003c_, String\u003e(0))?\n .collect::\u003cResult\u003c_, _\u003e\u003e()?;\n\n /* ── NEW: graceful fallback when FTS finds nothing ───── */\n if paths.is_empty() \u0026\u0026 !raw.contains(':') {\n paths = naive_search(conn, \u0026raw)?;\n }\n\n if paths.is_empty() \u0026\u0026 matches!(fmt, Format::Text) {\n eprintln!(\"(view '{}' has no matches)\", a.view_name);\n } else {\n for p in paths {\n println!(\"{p}\");\n }\n }\n }\n }\n Ok(())\n}\n\n/* ─── naive substring path/content search (≤ 64 kB files) ───────── */\n\nfn naive_search(conn: \u0026Connection, term: \u0026str) -\u003e Result\u003cVec\u003cString\u003e\u003e {\n let term_lc = term.to_lowercase();\n let mut stmt = conn.prepare(\"SELECT path FROM files\")?;\n let rows = stmt.query_map([], |r| r.get::\u003c_, String\u003e(0))?;\n\n let mut hits = Vec::new();\n for p in rows {\n let p = p?;\n /* path match */\n if p.to_lowercase().contains(\u0026term_lc) {\n hits.push(p);\n continue;\n }\n /* small-file content match */\n if let Ok(meta) = fs::metadata(\u0026p) {\n if meta.len() \u003e 64_000 {\n continue;\n }\n }\n if let Ok(content) = fs::read_to_string(\u0026p) {\n if content.to_lowercase().contains(\u0026term_lc) {\n hits.push(p);\n }\n }\n }\n Ok(hits)\n}\n\n/* ─── minimal copy of search-string → FTS5 translator ───────────── */\n\nfn build_fts_match(raw_query: \u0026str) -\u003e String {\n use shlex;\n let mut parts = Vec::new();\n let toks = shlex::split(raw_query).unwrap_or_else(|| vec![raw_query.to_string()]);\n for tok in toks {\n if [\"AND\", \"OR\", \"NOT\"].contains(\u0026tok.as_str()) {\n parts.push(tok);\n } else if let Some(tag) = tok.strip_prefix(\"tag:\") {\n for (i, seg) in tag.split('/').filter(|s| !s.is_empty()).enumerate() {\n if i \u003e 0 {\n parts.push(\"AND\".into());\n }\n parts.push(format!(\"tags_text:{}\", escape(seg)));\n }\n } else if let Some(attr) = tok.strip_prefix(\"attr:\") {\n let mut kv = attr.splitn(2, '=');\n let key = kv.next().unwrap();\n if let Some(val) = kv.next() {\n parts.push(format!(\"attrs_text:{}\", escape(key)));\n parts.push(\"AND\".into());\n parts.push(format!(\"attrs_text:{}\", escape(val)));\n } else {\n parts.push(format!(\"attrs_text:{}\", escape(key)));\n }\n } else {\n parts.push(escape(\u0026tok));\n }\n }\n parts.join(\" \")\n}\n\nfn escape(term: \u0026str) -\u003e String {\n if term.contains(|c: char| c.is_whitespace() || \"-:()\\\"\".contains(c))\n || [\"AND\", \"OR\", \"NOT\", \"NEAR\"].contains(\u0026term.to_uppercase().as_str())\n {\n format!(\"\\\"{}\\\"\", term.replace('\"', \"\\\"\\\"\"))\n } else {\n term.to_string()\n }\n}\n","traces":[{"line":33,"address":[2361648,2363246,2363240],"length":1,"stats":{"Line":0}},{"line":34,"address":[2361705],"length":1,"stats":{"Line":0}},{"line":36,"address":[2361821],"length":1,"stats":{"Line":0}},{"line":37,"address":[2362169,2361829],"length":1,"stats":{"Line":0}},{"line":38,"address":[2362218],"length":1,"stats":{"Line":0}},{"line":39,"address":[2362241],"length":1,"stats":{"Line":0}},{"line":45,"address":[2362416,2362470,2361956],"length":1,"stats":{"Line":0}},{"line":46,"address":[2362574],"length":1,"stats":{"Line":0}},{"line":48,"address":[2362849,2362714,2362606],"length":1,"stats":{"Line":0}},{"line":49,"address":[2363043,2362950],"length":1,"stats":{"Line":0}},{"line":62,"address":[2362045],"length":1,"stats":{"Line":0}},{"line":63,"address":[2362062,2363307],"length":1,"stats":{"Line":0}},{"line":66,"address":[2363421,2363500],"length":1,"stats":{"Line":0}},{"line":68,"address":[2365825,2363588,2363710,2363523],"length":1,"stats":{"Line":0}},{"line":77,"address":[2364067,2364000,2364175,2364384,2365777],"length":1,"stats":{"Line":0}},{"line":78,"address":[2096051,2096016],"length":1,"stats":{"Line":0}},{"line":82,"address":[2364490,2365008,2364598,2364558],"length":1,"stats":{"Line":0}},{"line":83,"address":[2364869,2364663,2365017],"length":1,"stats":{"Line":0}},{"line":86,"address":[2365078,2365161,2364564],"length":1,"stats":{"Line":0}},{"line":87,"address":[2365177],"length":1,"stats":{"Line":0}},{"line":89,"address":[2365286,2365084,2365421],"length":1,"stats":{"Line":0}},{"line":90,"address":[2365659,2365490],"length":1,"stats":{"Line":0}},{"line":95,"address":[2362388],"length":1,"stats":{"Line":0}},{"line":100,"address":[2367885,2365904,2368916],"length":1,"stats":{"Line":0}},{"line":101,"address":[2365985],"length":1,"stats":{"Line":0}},{"line":102,"address":[2366105,2368914,2366034],"length":1,"stats":{"Line":0}},{"line":103,"address":[2096115,2096080],"length":1,"stats":{"Line":0}},{"line":105,"address":[2366708],"length":1,"stats":{"Line":0}},{"line":106,"address":[2368590,2366768,2366860,2366936],"length":1,"stats":{"Line":0}},{"line":107,"address":[2367037,2367202],"length":1,"stats":{"Line":0}},{"line":109,"address":[2367420,2367488],"length":1,"stats":{"Line":0}},{"line":110,"address":[2368673],"length":1,"stats":{"Line":0}},{"line":114,"address":[2367657,2367767],"length":1,"stats":{"Line":0}},{"line":115,"address":[2367774,2367835],"length":1,"stats":{"Line":0}},{"line":119,"address":[2367982,2367891],"length":1,"stats":{"Line":0}},{"line":120,"address":[2368030,2368101],"length":1,"stats":{"Line":0}},{"line":121,"address":[2368289],"length":1,"stats":{"Line":0}},{"line":125,"address":[2367090],"length":1,"stats":{"Line":0}},{"line":130,"address":[2372260,2370638,2368944],"length":1,"stats":{"Line":0}},{"line":132,"address":[2368999],"length":1,"stats":{"Line":0}},{"line":133,"address":[2096178,2096144],"length":1,"stats":{"Line":0}},{"line":134,"address":[2369149,2372214,2369311],"length":1,"stats":{"Line":0}},{"line":135,"address":[2369611,2369396],"length":1,"stats":{"Line":0}},{"line":136,"address":[2369709,2372201],"length":1,"stats":{"Line":0}},{"line":137,"address":[2369802,2369686],"length":1,"stats":{"Line":0}},{"line":138,"address":[2096416,2096430],"length":1,"stats":{"Line":0}},{"line":139,"address":[2370204],"length":1,"stats":{"Line":0}},{"line":140,"address":[2370259],"length":1,"stats":{"Line":0}},{"line":142,"address":[2370333,2370252],"length":1,"stats":{"Line":0}},{"line":144,"address":[2369968,2370660],"length":1,"stats":{"Line":0}},{"line":145,"address":[2370782],"length":1,"stats":{"Line":0}},{"line":146,"address":[2370840],"length":1,"stats":{"Line":0}},{"line":147,"address":[2370949],"length":1,"stats":{"Line":0}},{"line":148,"address":[2371095,2371130],"length":1,"stats":{"Line":0}},{"line":149,"address":[2371430],"length":1,"stats":{"Line":0}},{"line":150,"address":[2371520],"length":1,"stats":{"Line":0}},{"line":152,"address":[2371832,2371120],"length":1,"stats":{"Line":0}},{"line":155,"address":[2372155,2370820],"length":1,"stats":{"Line":0}},{"line":158,"address":[2369441],"length":1,"stats":{"Line":0}},{"line":161,"address":[2372650,2372656,2372288],"length":1,"stats":{"Line":0}},{"line":162,"address":[2096492,2096464],"length":1,"stats":{"Line":0}},{"line":163,"address":[2372484,2372366,2372621],"length":1,"stats":{"Line":0}},{"line":165,"address":[2372417,2372669],"length":1,"stats":{"Line":0}},{"line":167,"address":[2372613],"length":1,"stats":{"Line":0}}],"covered":0,"coverable":64},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","src","cli","watch.rs"],"content":"// src/cli/watch.rs\n\nuse anyhow::Result;\nuse clap::Subcommand;\nuse libmarlin::watcher::{WatcherConfig, WatcherState};\nuse rusqlite::Connection;\nuse std::path::PathBuf;\nuse std::sync::Arc;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::thread;\nuse std::time::{Duration, Instant};\nuse tracing::info;\n\n/// Commands related to file watching functionality\n#[derive(Subcommand, Debug)]\npub enum WatchCmd {\n /// Start watching a directory for changes\n Start {\n /// Directory to watch (defaults to current directory)\n #[arg(default_value = \".\")]\n path: PathBuf,\n \n /// Debounce window in milliseconds (default: 100ms)\n #[arg(long, default_value = \"100\")]\n debounce_ms: u64,\n },\n \n /// Show status of currently active watcher\n Status,\n \n /// Stop the currently running watcher\n Stop,\n}\n\n/// Run a watch command\npub fn run(cmd: \u0026WatchCmd, _conn: \u0026mut Connection, _format: super::Format) -\u003e Result\u003c()\u003e {\n match cmd {\n WatchCmd::Start { path, debounce_ms } =\u003e {\n let mut marlin = libmarlin::Marlin::open_default()?;\n let config = WatcherConfig {\n debounce_ms: *debounce_ms,\n ..Default::default()\n };\n let canon_path = path.canonicalize().unwrap_or_else(|_| path.clone());\n info!(\"Starting watcher for directory: {}\", canon_path.display());\n\n let mut watcher = marlin.watch(\u0026canon_path, Some(config))?;\n \n let status = watcher.status();\n info!(\"Watcher started. Press Ctrl+C to stop watching.\");\n info!(\"Watching {} paths\", status.watched_paths.len());\n \n let start_time = Instant::now();\n let mut last_status_time = Instant::now();\n let running = Arc::new(AtomicBool::new(true));\n let r_clone = running.clone();\n\n ctrlc::set_handler(move || {\n info!(\"Ctrl+C received. Signaling watcher to stop...\");\n r_clone.store(false, Ordering::SeqCst);\n })?;\n\n info!(\"Watcher run loop started. Waiting for Ctrl+C or stop signal...\");\n while running.load(Ordering::SeqCst) {\n let current_status = watcher.status();\n if current_status.state == WatcherState::Stopped {\n info!(\"Watcher has stopped (detected by state). Exiting loop.\");\n break;\n }\n\n // Corrected line: removed the extra closing parenthesis\n if last_status_time.elapsed() \u003e Duration::from_secs(10) { \n let uptime = start_time.elapsed();\n info!(\n \"Watcher running for {}s, processed {} events, queue: {}, state: {:?}\",\n uptime.as_secs(),\n current_status.events_processed,\n current_status.queue_size,\n current_status.state\n );\n last_status_time = Instant::now();\n }\n thread::sleep(Duration::from_millis(200));\n }\n\n info!(\"Watcher run loop ended. Explicitly stopping watcher instance...\");\n watcher.stop()?; \n info!(\"Watcher instance fully stopped.\");\n Ok(())\n }\n WatchCmd::Status =\u003e {\n info!(\"Status command: No active watcher process to query in this CLI invocation model.\");\n info!(\"To see live status, run 'marlin watch start' which prints periodic updates.\");\n Ok(())\n }\n WatchCmd::Stop =\u003e {\n info!(\"Stop command: No active watcher process to stop in this CLI invocation model.\");\n info!(\"Please use Ctrl+C in the terminal where 'marlin watch start' is running.\");\n Ok(())\n }\n }\n}","traces":[{"line":36,"address":[2347011,2346893,2338224],"length":1,"stats":{"Line":0}},{"line":37,"address":[2338281],"length":1,"stats":{"Line":0}},{"line":38,"address":[2338369],"length":1,"stats":{"Line":0}},{"line":39,"address":[2338397,2338535],"length":1,"stats":{"Line":0}},{"line":41,"address":[2338841],"length":1,"stats":{"Line":0}},{"line":44,"address":[2056512,2056533],"length":1,"stats":{"Line":0}},{"line":45,"address":[2339509,2339158,2339079],"length":1,"stats":{"Line":0}},{"line":47,"address":[2346982,2340089,2339423],"length":1,"stats":{"Line":0}},{"line":49,"address":[2340273],"length":1,"stats":{"Line":0}},{"line":50,"address":[2340336,2340708,2340412],"length":1,"stats":{"Line":0}},{"line":51,"address":[2340677,2341126,2341420],"length":1,"stats":{"Line":0}},{"line":53,"address":[2341391,2341936],"length":1,"stats":{"Line":0}},{"line":54,"address":[2341951],"length":1,"stats":{"Line":0}},{"line":55,"address":[2342012],"length":1,"stats":{"Line":0}},{"line":56,"address":[2342111,2342190],"length":1,"stats":{"Line":0}},{"line":58,"address":[2056624],"length":1,"stats":{"Line":0}},{"line":59,"address":[2056834,2056644],"length":1,"stats":{"Line":0}},{"line":60,"address":[2056794],"length":1,"stats":{"Line":0}},{"line":63,"address":[2342312,2342642],"length":1,"stats":{"Line":0}},{"line":64,"address":[2343061,2342612],"length":1,"stats":{"Line":0}},{"line":65,"address":[2343135],"length":1,"stats":{"Line":0}},{"line":66,"address":[2343162,2343242],"length":1,"stats":{"Line":0}},{"line":67,"address":[2344586,2343282,2344873],"length":1,"stats":{"Line":0}},{"line":72,"address":[2344526,2343328,2343248],"length":1,"stats":{"Line":0}},{"line":73,"address":[2343473],"length":1,"stats":{"Line":0}},{"line":74,"address":[2343537,2344087,2343866],"length":1,"stats":{"Line":0}},{"line":81,"address":[2343837,2344511],"length":1,"stats":{"Line":0}},{"line":83,"address":[2344546,2343448],"length":1,"stats":{"Line":0}},{"line":86,"address":[2343104,2345291,2345586],"length":1,"stats":{"Line":0}},{"line":87,"address":[2345999,2345556,2346866],"length":1,"stats":{"Line":0}},{"line":88,"address":[2346387,2346101],"length":1,"stats":{"Line":0}},{"line":89,"address":[2346353],"length":1,"stats":{"Line":0}},{"line":92,"address":[2347205,2338471,2347024],"length":1,"stats":{"Line":0}},{"line":93,"address":[2347173,2347740,2347574],"length":1,"stats":{"Line":0}},{"line":94,"address":[2347723],"length":1,"stats":{"Line":0}},{"line":97,"address":[2338503,2348109,2348290],"length":1,"stats":{"Line":0}},{"line":98,"address":[2348659,2348825,2348258],"length":1,"stats":{"Line":0}},{"line":99,"address":[2348808],"length":1,"stats":{"Line":0}}],"covered":0,"coverable":38},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","src","cli.rs"],"content":"// src/cli.rs\n\npub mod link;\npub mod coll;\npub mod view;\npub mod state;\npub mod task;\npub mod remind;\npub mod annotate;\npub mod version;\npub mod event;\npub mod watch;\n\nuse clap::{Parser, Subcommand, ValueEnum};\nuse clap_complete::Shell;\n\n/// Output format for commands.\n#[derive(ValueEnum, Clone, Copy, Debug)]\npub enum Format {\n Text,\n Json,\n}\n\n/// Marlin – metadata-driven file explorer (CLI utilities)\n#[derive(Parser, Debug)]\n#[command(author, version, about, propagate_version = true)]\npub struct Cli {\n /// Enable debug logging and extra output\n #[arg(long)]\n pub verbose: bool,\n\n /// Output format (text or JSON)\n #[arg(long, default_value = \"text\", value_enum, global = true)]\n pub format: Format,\n\n #[command(subcommand)]\n pub command: Commands,\n}\n\n#[derive(Subcommand, Debug)]\npub enum Commands {\n /// Initialise the database (idempotent)\n Init,\n\n /// Scan one or more directories and populate the file index\n Scan {\n /// Only re-index files marked dirty by `marlin watch`\n #[arg(long)]\n dirty: bool,\n\n /// Directories to scan (defaults to cwd)\n paths: Vec\u003cstd::path::PathBuf\u003e,\n },\n\n /// Tag files matching a glob pattern (hierarchical tags use `/`)\n Tag {\n /// Glob or path pattern\n pattern: String,\n /// Hierarchical tag name (`foo/bar`)\n tag_path: String,\n },\n\n /// Manage custom attributes\n Attr {\n #[command(subcommand)]\n action: AttrCmd,\n },\n\n /// Full-text search; `--exec CMD` runs CMD on each hit (`{}` placeholder)\n Search {\n query: String,\n #[arg(long)]\n exec: Option\u003cString\u003e,\n },\n\n /// Create a timestamped backup of the database\n Backup,\n\n /// Restore from a backup file (overwrites current DB)\n Restore {\n backup_path: std::path::PathBuf,\n },\n\n /// Generate shell completions (hidden)\n #[command(hide = true)]\n Completions {\n /// Which shell to generate for\n #[arg(value_enum)]\n shell: Shell,\n },\n\n /// File-to-file links\n #[command(subcommand)]\n Link(link::LinkCmd),\n\n /// Collections (groups) of files\n #[command(subcommand)]\n Coll(coll::CollCmd),\n\n /// Smart views (saved queries)\n #[command(subcommand)]\n View(view::ViewCmd),\n\n /// Workflow states on files\n #[command(subcommand)]\n State(state::StateCmd),\n\n /// TODO/tasks management\n #[command(subcommand)]\n Task(task::TaskCmd),\n\n /// Reminders on files\n #[command(subcommand)]\n Remind(remind::RemindCmd),\n\n /// File annotations and highlights\n #[command(subcommand)]\n Annotate(annotate::AnnotateCmd),\n\n /// Version diffs\n #[command(subcommand)]\n Version(version::VersionCmd),\n\n /// Calendar events \u0026 timelines\n #[command(subcommand)]\n Event(event::EventCmd),\n\n /// Watch directories for changes\n #[command(subcommand)]\n Watch(watch::WatchCmd),\n}\n\n#[derive(Subcommand, Debug)]\npub enum AttrCmd {\n Set { pattern: String, key: String, value: String },\n Ls { path: std::path::PathBuf },\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","src","main.rs"],"content":"//! Marlin CLI entry-point (post crate-split)\n//!\n//! All heavy lifting now lives in the `libmarlin` crate; this file\n//! handles argument parsing, logging, orchestration and the few\n//! helpers that remain CLI-specific.\n\n#![deny(warnings)]\n\nmod cli; // sub-command definitions and argument structs\n\n/* ── shared modules re-exported from libmarlin ─────────────────── */\nuse libmarlin::{\n config,\n db,\n logging,\n scan,\n utils::determine_scan_root,\n};\nuse libmarlin::db::take_dirty;\n\nuse anyhow::{Context, Result};\nuse clap::{CommandFactory, Parser};\nuse clap_complete::generate;\nuse glob::Pattern;\nuse shellexpand;\nuse shlex;\nuse std::{\n env,\n fs,\n io,\n path::Path,\n process::Command,\n};\nuse tracing::{debug, error, info};\nuse walkdir::WalkDir;\n\nuse cli::{Cli, Commands};\n\nfn main() -\u003e Result\u003c()\u003e {\n /* ── CLI parsing \u0026 logging ────────────────────────────────── */\n let args = Cli::parse();\n if args.verbose {\n env::set_var(\"RUST_LOG\", \"debug\");\n }\n logging::init();\n\n /* ── shell-completion shortcut ────────────────────────────── */\n if let Commands::Completions { shell } = \u0026args.command {\n let mut cmd = Cli::command();\n generate(*shell, \u0026mut cmd, \"marlin\", \u0026mut io::stdout());\n return Ok(());\n }\n\n /* ── config \u0026 automatic backup ───────────────────────────── */\n let cfg = config::Config::load()?; // resolves DB path\n\n match \u0026args.command {\n Commands::Init | Commands::Backup | Commands::Restore { .. } =\u003e {}\n _ =\u003e match db::backup(\u0026cfg.db_path) {\n Ok(p) =\u003e info!(\"Pre-command auto-backup created at {}\", p.display()),\n Err(e) =\u003e error!(\"Failed to create pre-command auto-backup: {e}\"),\n },\n }\n\n /* ── open DB (runs migrations) ───────────────────────────── */\n let mut conn = db::open(\u0026cfg.db_path)?;\n\n /* ── command dispatch ────────────────────────────────────── */\n match args.command {\n Commands::Completions { .. } =\u003e {} // handled above\n\n /* ---- init ------------------------------------------------ */\n Commands::Init =\u003e {\n info!(\"Database initialised at {}\", cfg.db_path.display());\n let cwd = env::current_dir().context(\"getting current directory\")?;\n let count = scan::scan_directory(\u0026mut conn, \u0026cwd)\n .context(\"initial scan failed\")?;\n info!(\"Initial scan complete – indexed/updated {count} files\");\n }\n\n /* ---- scan ------------------------------------------------ */\n Commands::Scan { dirty, paths } =\u003e {\n let scan_paths: Vec\u003cstd::path::PathBuf\u003e = if paths.is_empty() {\n vec![env::current_dir()?]\n } else {\n paths.into_iter().collect()\n };\n\n if dirty {\n let dirty_ids = take_dirty(\u0026conn)?;\n for id in dirty_ids {\n let path: String = conn.query_row(\n \"SELECT path FROM files WHERE id = ?1\",\n [id],\n |r| r.get(0),\n )?;\n scan::scan_directory(\u0026mut conn, Path::new(\u0026path))?;\n }\n } else {\n for p in scan_paths {\n scan::scan_directory(\u0026mut conn, \u0026p)?;\n }\n }\n }\n\n /* ---- tag / attribute / search --------------------------- */\n Commands::Tag { pattern, tag_path } =\u003e\n apply_tag(\u0026conn, \u0026pattern, \u0026tag_path)?,\n\n Commands::Attr { action } =\u003e match action {\n cli::AttrCmd::Set { pattern, key, value } =\u003e\n attr_set(\u0026conn, \u0026pattern, \u0026key, \u0026value)?,\n cli::AttrCmd::Ls { path } =\u003e\n attr_ls(\u0026conn, \u0026path)?,\n },\n\n Commands::Search { query, exec } =\u003e\n run_search(\u0026conn, \u0026query, exec)?,\n\n /* ---- maintenance ---------------------------------------- */\n Commands::Backup =\u003e {\n let p = db::backup(\u0026cfg.db_path)?;\n println!(\"Backup created: {}\", p.display());\n }\n\n Commands::Restore { backup_path } =\u003e {\n drop(conn);\n db::restore(\u0026backup_path, \u0026cfg.db_path).with_context(|| {\n format!(\"Failed to restore DB from {}\", backup_path.display())\n })?;\n println!(\"Restored DB from {}\", backup_path.display());\n db::open(\u0026cfg.db_path).with_context(|| {\n format!(\"Could not open restored DB at {}\", cfg.db_path.display())\n })?;\n info!(\"Successfully opened restored database.\");\n }\n\n /* ---- passthrough sub-modules (some still stubs) ---------- */\n Commands::Link(link_cmd) =\u003e cli::link::run(\u0026link_cmd, \u0026mut conn, args.format)?,\n Commands::Coll(coll_cmd) =\u003e cli::coll::run(\u0026coll_cmd, \u0026mut conn, args.format)?,\n Commands::View(view_cmd) =\u003e cli::view::run(\u0026view_cmd, \u0026mut conn, args.format)?,\n Commands::State(state_cmd) =\u003e cli::state::run(\u0026state_cmd, \u0026mut conn, args.format)?,\n Commands::Task(task_cmd) =\u003e cli::task::run(\u0026task_cmd, \u0026mut conn, args.format)?,\n Commands::Remind(rm_cmd) =\u003e cli::remind::run(\u0026rm_cmd, \u0026mut conn, args.format)?,\n Commands::Annotate(a_cmd) =\u003e cli::annotate::run(\u0026a_cmd, \u0026mut conn, args.format)?,\n Commands::Version(v_cmd) =\u003e cli::version::run(\u0026v_cmd, \u0026mut conn, args.format)?,\n Commands::Event(e_cmd) =\u003e cli::event::run(\u0026e_cmd, \u0026mut conn, args.format)?,\n Commands::Watch(watch_cmd) =\u003e cli::watch::run(\u0026watch_cmd, \u0026mut conn, args.format)?,\n }\n\n Ok(())\n}\n\n/* ─────────────────── helpers \u0026 sub-routines ─────────────────── */\n\n/* ---------- TAGS ---------- */\nfn apply_tag(conn: \u0026rusqlite::Connection, pattern: \u0026str, tag_path: \u0026str) -\u003e Result\u003c()\u003e {\n let leaf_tag_id = db::ensure_tag_path(conn, tag_path)?;\n let mut tag_ids = Vec::new();\n let mut current = Some(leaf_tag_id);\n while let Some(id) = current {\n tag_ids.push(id);\n current = conn.query_row(\n \"SELECT parent_id FROM tags WHERE id=?1\",\n [id],\n |r| r.get::\u003c_, Option\u003ci64\u003e\u003e(0),\n )?;\n }\n\n let expanded = shellexpand::tilde(pattern).into_owned();\n let pat = Pattern::new(\u0026expanded)\n .with_context(|| format!(\"Invalid glob pattern `{expanded}`\"))?;\n let root = determine_scan_root(\u0026expanded);\n\n let mut stmt_file = conn.prepare(\"SELECT id FROM files WHERE path=?1\")?;\n let mut stmt_insert = conn.prepare(\n \"INSERT OR IGNORE INTO file_tags(file_id, tag_id) VALUES (?1, ?2)\",\n )?;\n\n let mut count = 0usize;\n for entry in WalkDir::new(\u0026root)\n .into_iter()\n .filter_map(Result::ok)\n .filter(|e| e.file_type().is_file())\n {\n let p = entry.path().to_string_lossy();\n if !pat.matches(\u0026p) { continue; }\n\n match stmt_file.query_row([p.as_ref()], |r| r.get::\u003c_, i64\u003e(0)) {\n Ok(fid) =\u003e {\n let mut newly = false;\n for \u0026tid in \u0026tag_ids {\n if stmt_insert.execute([fid, tid])? \u003e 0 {\n newly = true;\n }\n }\n if newly {\n info!(file=%p, tag=tag_path, \"tagged\");\n count += 1;\n }\n }\n Err(rusqlite::Error::QueryReturnedNoRows) =\u003e\n error!(file=%p, \"not indexed – run `marlin scan` first\"),\n Err(e) =\u003e\n error!(file=%p, error=%e, \"could not lookup file ID\"),\n }\n }\n\n info!(\"Applied tag '{}' to {} file(s).\", tag_path, count);\n Ok(())\n}\n\n/* ---------- ATTRIBUTES ---------- */\nfn attr_set(conn: \u0026rusqlite::Connection, pattern: \u0026str, key: \u0026str, value: \u0026str) -\u003e Result\u003c()\u003e {\n let expanded = shellexpand::tilde(pattern).into_owned();\n let pat = Pattern::new(\u0026expanded)\n .with_context(|| format!(\"Invalid glob pattern `{expanded}`\"))?;\n let root = determine_scan_root(\u0026expanded);\n\n let mut stmt_file = conn.prepare(\"SELECT id FROM files WHERE path=?1\")?;\n let mut count = 0usize;\n\n for entry in WalkDir::new(\u0026root)\n .into_iter()\n .filter_map(Result::ok)\n .filter(|e| e.file_type().is_file())\n {\n let p = entry.path().to_string_lossy();\n if !pat.matches(\u0026p) { continue; }\n\n match stmt_file.query_row([p.as_ref()], |r| r.get::\u003c_, i64\u003e(0)) {\n Ok(fid) =\u003e {\n db::upsert_attr(conn, fid, key, value)?;\n info!(file=%p, key, value, \"attr set\");\n count += 1;\n }\n Err(rusqlite::Error::QueryReturnedNoRows) =\u003e\n error!(file=%p, \"not indexed – run `marlin scan` first\"),\n Err(e) =\u003e\n error!(file=%p, error=%e, \"could not lookup file ID\"),\n }\n }\n\n info!(\"Attribute '{}={}' set on {} file(s).\", key, value, count);\n Ok(())\n}\n\nfn attr_ls(conn: \u0026rusqlite::Connection, path: \u0026Path) -\u003e Result\u003c()\u003e {\n let fid = db::file_id(conn, \u0026path.to_string_lossy())?;\n let mut stmt = conn.prepare(\n \"SELECT key, value FROM attributes WHERE file_id=?1 ORDER BY key\"\n )?;\n for row in stmt\n .query_map([fid], |r| Ok((r.get::\u003c_, String\u003e(0)?, r.get::\u003c_, String\u003e(1)?)))?\n {\n let (k, v) = row?;\n println!(\"{k} = {v}\");\n }\n Ok(())\n}\n\n/* ---------- SEARCH ---------- */\nfn run_search(conn: \u0026rusqlite::Connection, raw_query: \u0026str, exec: Option\u003cString\u003e) -\u003e Result\u003c()\u003e {\n let mut parts = Vec::new();\n let toks = shlex::split(raw_query).unwrap_or_else(|| vec![raw_query.to_string()]);\n for tok in toks {\n if [\"AND\", \"OR\", \"NOT\"].contains(\u0026tok.as_str()) {\n parts.push(tok);\n } else if let Some(tag) = tok.strip_prefix(\"tag:\") {\n for (i, seg) in tag.split('/').filter(|s| !s.is_empty()).enumerate() {\n if i \u003e 0 { parts.push(\"AND\".into()); }\n parts.push(format!(\"tags_text:{}\", escape_fts(seg)));\n }\n } else if let Some(attr) = tok.strip_prefix(\"attr:\") {\n let mut kv = attr.splitn(2, '=');\n let key = kv.next().unwrap();\n if let Some(val) = kv.next() {\n parts.push(format!(\"attrs_text:{}\", escape_fts(key)));\n parts.push(\"AND\".into());\n parts.push(format!(\"attrs_text:{}\", escape_fts(val)));\n } else {\n parts.push(format!(\"attrs_text:{}\", escape_fts(key)));\n }\n } else {\n parts.push(escape_fts(\u0026tok));\n }\n }\n let fts_expr = parts.join(\" \");\n debug!(\"FTS MATCH expression: {fts_expr}\");\n\n let mut stmt = conn.prepare(\n r#\"\n SELECT f.path\n FROM files_fts\n JOIN files f ON f.rowid = files_fts.rowid\n WHERE files_fts MATCH ?1\n ORDER BY rank\n \"#,\n )?;\n let mut hits: Vec\u003cString\u003e = stmt\n .query_map([\u0026fts_expr], |r| r.get::\u003c_, String\u003e(0))?\n .filter_map(Result::ok)\n .collect();\n\n if hits.is_empty() \u0026\u0026 !raw_query.contains(':') {\n hits = naive_substring_search(conn, raw_query)?;\n }\n\n if let Some(cmd_tpl) = exec {\n run_exec(\u0026hits, \u0026cmd_tpl)?;\n } else {\n if hits.is_empty() {\n eprintln!(\n \"No matches for query: `{raw_query}` (FTS expr: `{fts_expr}`)\"\n );\n } else {\n for p in hits { println!(\"{p}\"); }\n }\n }\n Ok(())\n}\n\nfn naive_substring_search(conn: \u0026rusqlite::Connection, term: \u0026str) -\u003e Result\u003cVec\u003cString\u003e\u003e {\n let needle = term.to_lowercase();\n let mut stmt = conn.prepare(\"SELECT path FROM files\")?;\n let rows = stmt.query_map([], |r| r.get::\u003c_, String\u003e(0))?;\n\n let mut out = Vec::new();\n for p in rows {\n let p = p?;\n if p.to_lowercase().contains(\u0026needle) {\n out.push(p.clone());\n continue;\n }\n if let Ok(meta) = fs::metadata(\u0026p) {\n if meta.len() \u003e 65_536 { continue; }\n }\n if let Ok(body) = fs::read_to_string(\u0026p) {\n if body.to_lowercase().contains(\u0026needle) {\n out.push(p);\n }\n }\n }\n Ok(out)\n}\n\nfn run_exec(paths: \u0026[String], cmd_tpl: \u0026str) -\u003e Result\u003c()\u003e {\n let mut ran_without_placeholder = false;\n\n if paths.is_empty() \u0026\u0026 !cmd_tpl.contains(\"{}\") {\n if let Some(mut parts) = shlex::split(cmd_tpl) {\n if !parts.is_empty() {\n let prog = parts.remove(0);\n let status = Command::new(\u0026prog).args(parts).status()?;\n if !status.success() {\n error!(command=%cmd_tpl, code=?status.code(), \"command failed\");\n }\n }\n }\n ran_without_placeholder = true;\n }\n\n if !ran_without_placeholder {\n for p in paths {\n let quoted = shlex::try_quote(p).unwrap_or_else(|_| p.into());\n let final_cmd = if cmd_tpl.contains(\"{}\") {\n cmd_tpl.replace(\"{}\", \u0026quoted)\n } else {\n format!(\"{cmd_tpl} {quoted}\")\n };\n if let Some(mut parts) = shlex::split(\u0026final_cmd) {\n if parts.is_empty() { continue; }\n let prog = parts.remove(0);\n let status = Command::new(\u0026prog).args(parts).status()?;\n if !status.success() {\n error!(file=%p, command=%final_cmd, code=?status.code(), \"command failed\");\n }\n }\n }\n }\n Ok(())\n}\n\nfn escape_fts(term: \u0026str) -\u003e String {\n if term.contains(|c: char| c.is_whitespace() || \"-:()\\\"\".contains(c))\n || [\"AND\", \"OR\", \"NOT\", \"NEAR\"].contains(\u0026term.to_uppercase().as_str())\n {\n format!(\"\\\"{}\\\"\", term.replace('\"', \"\\\"\\\"\"))\n } else {\n term.to_string()\n }\n}\n\n#[cfg(test)]\nmod tests {\n use assert_cmd::Command;\n use tempfile::tempdir;\n\n #[test]\n fn test_help_command() {\n let mut cmd = Command::cargo_bin(\"marlin\").unwrap();\n cmd.arg(\"--help\");\n cmd.assert()\n .success()\n .stdout(predicates::str::contains(\"Usage: marlin\"));\n }\n\n #[test]\n fn test_version_command() {\n let mut cmd = Command::cargo_bin(\"marlin\").unwrap();\n cmd.arg(\"--version\");\n cmd.assert()\n .success()\n .stdout(predicates::str::contains(\"marlin-cli 0.1.0\"));\n }\n\n #[test]\n fn test_verbose_logging() {\n let tmp = tempdir().unwrap();\n let mut cmd = Command::cargo_bin(\"marlin\").unwrap();\n cmd.env(\"MARLIN_DB_PATH\", tmp.path().join(\"index.db\"));\n cmd.arg(\"--verbose\").arg(\"init\");\n let output = cmd.output().unwrap();\n assert!(output.status.success());\n let stderr = String::from_utf8_lossy(\u0026output.stderr);\n assert!(\n stderr.contains(\"DEBUG\"),\n \"Expected debug logs in stderr, got: {}\",\n stderr\n );\n }\n\n #[test]\n fn test_shell_completions() {\n let mut cmd = Command::cargo_bin(\"marlin\").unwrap();\n cmd.arg(\"completions\").arg(\"bash\");\n cmd.assert()\n .success()\n .stdout(predicates::str::contains(\"_marlin()\"))\n .stdout(predicates::str::contains(\"init\"))\n .stdout(predicates::str::contains(\"scan\"));\n }\n\n #[test]\n fn test_invalid_subcommand() {\n let mut cmd = Command::cargo_bin(\"marlin\").unwrap();\n cmd.arg(\"invalid_cmd\");\n cmd.assert()\n .failure()\n .stderr(predicates::str::contains(\"error: unrecognized subcommand\"));\n }\n\n #[test]\n fn test_init_command() {\n let tmp = tempdir().unwrap();\n let db_path = tmp.path().join(\"index.db\");\n let mut cmd = Command::cargo_bin(\"marlin\").unwrap();\n cmd.env(\"MARLIN_DB_PATH\", \u0026db_path);\n cmd.arg(\"init\");\n cmd.assert().success();\n assert!(db_path.exists(), \"Database file should exist after init\");\n }\n\n #[test]\n fn test_automatic_backup() {\n let tmp = tempdir().unwrap();\n let db_path = tmp.path().join(\"index.db\");\n let backups_dir = tmp.path().join(\"backups\");\n\n // Init: no backup\n let mut cmd_init = Command::cargo_bin(\"marlin\").unwrap();\n cmd_init.env(\"MARLIN_DB_PATH\", \u0026db_path);\n cmd_init.arg(\"init\");\n cmd_init.assert().success();\n assert!(\n !backups_dir.exists() || backups_dir.read_dir().unwrap().next().is_none(),\n \"No backup should be created for init\"\n );\n\n // Scan: backup created\n let mut cmd_scan = Command::cargo_bin(\"marlin\").unwrap();\n cmd_scan.env(\"MARLIN_DB_PATH\", \u0026db_path);\n cmd_scan.arg(\"scan\");\n cmd_scan.assert().success();\n assert!(backups_dir.exists(), \"Backups directory should exist after scan\");\n let backups: Vec\u003c_\u003e = backups_dir.read_dir().unwrap().collect();\n assert_eq!(backups.len(), 1, \"One backup should be created for scan\");\n }\n\n #[test]\n fn test_annotate_stub() {\n let tmp = tempdir().unwrap();\n let mut cmd = Command::cargo_bin(\"marlin\").unwrap();\n cmd.env(\"MARLIN_DB_PATH\", tmp.path().join(\"index.db\"));\n cmd.arg(\"annotate\").arg(\"add\").arg(\"file.txt\").arg(\"note\");\n cmd.assert()\n .failure()\n .stderr(predicates::str::contains(\"not yet implemented\"));\n }\n\n #[test]\n fn test_event_stub() {\n let tmp = tempdir().unwrap();\n let mut cmd = Command::cargo_bin(\"marlin\").unwrap();\n cmd.env(\"MARLIN_DB_PATH\", tmp.path().join(\"index.db\"));\n cmd.arg(\"event\").arg(\"add\").arg(\"file.txt\").arg(\"2025-05-20\").arg(\"desc\");\n cmd.assert()\n .failure()\n .stderr(predicates::str::contains(\"not yet implemented\"));\n }\n}\n","traces":[{"line":39,"address":[2202655,2218682,2201872],"length":1,"stats":{"Line":0}},{"line":41,"address":[2201894],"length":1,"stats":{"Line":0}},{"line":42,"address":[2202219],"length":1,"stats":{"Line":0}},{"line":43,"address":[2202240,2202346],"length":1,"stats":{"Line":0}},{"line":45,"address":[2202229],"length":1,"stats":{"Line":0}},{"line":48,"address":[2202348],"length":1,"stats":{"Line":0}},{"line":49,"address":[2202394],"length":1,"stats":{"Line":0}},{"line":50,"address":[2202518,2202431],"length":1,"stats":{"Line":0}},{"line":51,"address":[2202571],"length":1,"stats":{"Line":0}},{"line":55,"address":[2202661,2218129,2202401],"length":1,"stats":{"Line":0}},{"line":57,"address":[2202834],"length":1,"stats":{"Line":0}},{"line":59,"address":[2202974,2202893],"length":1,"stats":{"Line":0}},{"line":60,"address":[2203058],"length":1,"stats":{"Line":0}},{"line":61,"address":[2203011,2204020],"length":1,"stats":{"Line":0}},{"line":66,"address":[2218111,2204816,2202900],"length":1,"stats":{"Line":0}},{"line":69,"address":[2205102],"length":1,"stats":{"Line":0}},{"line":74,"address":[2206936,2207223,2205144],"length":1,"stats":{"Line":0}},{"line":75,"address":[2209168,2207787,2207201],"length":1,"stats":{"Line":0}},{"line":76,"address":[2209114,2208269,2207999,2208082],"length":1,"stats":{"Line":0}},{"line":78,"address":[2208644,2208322],"length":1,"stats":{"Line":0}},{"line":82,"address":[2205175],"length":1,"stats":{"Line":0}},{"line":83,"address":[2209760,2209226,2205244],"length":1,"stats":{"Line":0}},{"line":84,"address":[2209309,2209383,2211968,2211871],"length":1,"stats":{"Line":0}},{"line":86,"address":[2209232,2209340],"length":1,"stats":{"Line":0}},{"line":89,"address":[2209362,2211077],"length":1,"stats":{"Line":0}},{"line":90,"address":[2210525,2209832,2211788],"length":1,"stats":{"Line":0}},{"line":91,"address":[2210686,2210941,2210790],"length":1,"stats":{"Line":0}},{"line":92,"address":[2210995,2211153,2211735,2211281],"length":1,"stats":{"Line":0}},{"line":94,"address":[2210987],"length":1,"stats":{"Line":0}},{"line":95,"address":[2298240,2298256],"length":1,"stats":{"Line":0}},{"line":97,"address":[2211382,2211685,2211477],"length":1,"stats":{"Line":0}},{"line":100,"address":[2209765,2209903,2210038],"length":1,"stats":{"Line":0}},{"line":101,"address":[2210444,2210115,2210236],"length":1,"stats":{"Line":0}},{"line":107,"address":[2205273],"length":1,"stats":{"Line":0}},{"line":108,"address":[2212000,2205361],"length":1,"stats":{"Line":0}},{"line":110,"address":[2205387],"length":1,"stats":{"Line":0}},{"line":111,"address":[2212486],"length":1,"stats":{"Line":0}},{"line":112,"address":[2212590],"length":1,"stats":{"Line":0}},{"line":113,"address":[2212420],"length":1,"stats":{"Line":0}},{"line":114,"address":[2212460,2213200],"length":1,"stats":{"Line":0}},{"line":117,"address":[2205471],"length":1,"stats":{"Line":0}},{"line":118,"address":[2213459,2205567],"length":1,"stats":{"Line":0}},{"line":122,"address":[2205609,2213836,2214261],"length":1,"stats":{"Line":0}},{"line":123,"address":[2214080,2213997],"length":1,"stats":{"Line":0}},{"line":126,"address":[2205619],"length":1,"stats":{"Line":0}},{"line":127,"address":[2205659],"length":1,"stats":{"Line":0}},{"line":128,"address":[2214334,2215831,2214470],"length":1,"stats":{"Line":0}},{"line":129,"address":[2298310],"length":1,"stats":{"Line":0}},{"line":131,"address":[2214507],"length":1,"stats":{"Line":0}},{"line":132,"address":[2215793,2214697,2214812],"length":1,"stats":{"Line":0}},{"line":133,"address":[2298486],"length":1,"stats":{"Line":0}},{"line":135,"address":[2215374,2215052],"length":1,"stats":{"Line":0}},{"line":139,"address":[2205812,2215849],"length":1,"stats":{"Line":0}},{"line":140,"address":[2205942,2216057],"length":1,"stats":{"Line":0}},{"line":141,"address":[2216265,2206040],"length":1,"stats":{"Line":0}},{"line":142,"address":[2216467,2206138],"length":1,"stats":{"Line":0}},{"line":143,"address":[2206252,2216663],"length":1,"stats":{"Line":0}},{"line":144,"address":[2206334,2216859],"length":1,"stats":{"Line":0}},{"line":145,"address":[2217055,2206464],"length":1,"stats":{"Line":0}},{"line":146,"address":[2217251,2206594],"length":1,"stats":{"Line":0}},{"line":147,"address":[2206676,2217447],"length":1,"stats":{"Line":0}},{"line":148,"address":[2218006,2206806,2217643],"length":1,"stats":{"Line":0}},{"line":151,"address":[2205781],"length":1,"stats":{"Line":0}},{"line":157,"address":[2227697,2219744,2227856],"length":1,"stats":{"Line":0}},{"line":158,"address":[2219830],"length":1,"stats":{"Line":0}},{"line":159,"address":[2219968],"length":1,"stats":{"Line":0}},{"line":160,"address":[2219990],"length":1,"stats":{"Line":0}},{"line":161,"address":[2220441,2220010],"length":1,"stats":{"Line":0}},{"line":162,"address":[2220048],"length":1,"stats":{"Line":0}},{"line":163,"address":[2220359,2220454,2220180],"length":1,"stats":{"Line":0}},{"line":165,"address":[2220172],"length":1,"stats":{"Line":0}},{"line":166,"address":[2298640,2298656],"length":1,"stats":{"Line":0}},{"line":170,"address":[2220481,2220090],"length":1,"stats":{"Line":0}},{"line":171,"address":[2220729,2220516,2227846,2220599],"length":1,"stats":{"Line":0}},{"line":172,"address":[2220713],"length":1,"stats":{"Line":0}},{"line":173,"address":[2220894,2220977],"length":1,"stats":{"Line":0}},{"line":175,"address":[2221075,2221004,2227787],"length":1,"stats":{"Line":0}},{"line":176,"address":[2221595,2221473,2227747,2221402],"length":1,"stats":{"Line":0}},{"line":180,"address":[2221792],"length":1,"stats":{"Line":0}},{"line":181,"address":[2221871,2221820,2222060],"length":1,"stats":{"Line":0}},{"line":184,"address":[2298816,2298843],"length":1,"stats":{"Line":0}},{"line":186,"address":[2222145,2223211],"length":1,"stats":{"Line":0}},{"line":187,"address":[2223218,2223313],"length":1,"stats":{"Line":0}},{"line":189,"address":[2223444,2223371],"length":1,"stats":{"Line":0}},{"line":190,"address":[2223585],"length":1,"stats":{"Line":0}},{"line":191,"address":[2223609],"length":1,"stats":{"Line":0}},{"line":192,"address":[2223712,2223617],"length":1,"stats":{"Line":0}},{"line":193,"address":[2223829,2225300,2225104],"length":1,"stats":{"Line":0}},{"line":194,"address":[2225292],"length":1,"stats":{"Line":0}},{"line":197,"address":[2223883,2225079],"length":1,"stats":{"Line":0}},{"line":198,"address":[2223915,2224249],"length":1,"stats":{"Line":0}},{"line":199,"address":[2224215,2225084,2225071],"length":1,"stats":{"Line":0}},{"line":203,"address":[2225489,2225627],"length":1,"stats":{"Line":0}},{"line":204,"address":[2225517],"length":1,"stats":{"Line":0}},{"line":205,"address":[2225589,2226490],"length":1,"stats":{"Line":0}},{"line":209,"address":[2222198,2222532],"length":1,"stats":{"Line":0}},{"line":210,"address":[2222498],"length":1,"stats":{"Line":0}},{"line":214,"address":[2227872,2234849,2234730],"length":1,"stats":{"Line":0}},{"line":215,"address":[2227966],"length":1,"stats":{"Line":0}},{"line":216,"address":[2228253,2234839,2228123,2228040],"length":1,"stats":{"Line":0}},{"line":217,"address":[2228237],"length":1,"stats":{"Line":0}},{"line":218,"address":[2228418,2228501],"length":1,"stats":{"Line":0}},{"line":220,"address":[2234780,2228599,2228528],"length":1,"stats":{"Line":0}},{"line":221,"address":[2228918],"length":1,"stats":{"Line":0}},{"line":223,"address":[2228997,2228946,2229186],"length":1,"stats":{"Line":0}},{"line":226,"address":[2299056,2299083],"length":1,"stats":{"Line":0}},{"line":228,"address":[2230342,2229271],"length":1,"stats":{"Line":0}},{"line":229,"address":[2230444,2230349],"length":1,"stats":{"Line":0}},{"line":231,"address":[2230502,2230575],"length":1,"stats":{"Line":0}},{"line":232,"address":[2230724],"length":1,"stats":{"Line":0}},{"line":233,"address":[2232346,2230843,2230740],"length":1,"stats":{"Line":0}},{"line":234,"address":[2231288,2230954],"length":1,"stats":{"Line":0}},{"line":235,"address":[2232286,2232296,2231254],"length":1,"stats":{"Line":0}},{"line":238,"address":[2232660,2232522],"length":1,"stats":{"Line":0}},{"line":239,"address":[2232550],"length":1,"stats":{"Line":0}},{"line":240,"address":[2233523,2232622],"length":1,"stats":{"Line":0}},{"line":244,"address":[2229658,2229324],"length":1,"stats":{"Line":0}},{"line":245,"address":[2229624],"length":1,"stats":{"Line":0}},{"line":248,"address":[2236624,2236563,2234864],"length":1,"stats":{"Line":0}},{"line":249,"address":[2236642,2234913],"length":1,"stats":{"Line":0}},{"line":250,"address":[2235279,2235171],"length":1,"stats":{"Line":0}},{"line":253,"address":[2235698,2236617,2235870,2235483,2235590],"length":1,"stats":{"Line":0}},{"line":254,"address":[2235634,2235475],"length":1,"stats":{"Line":0}},{"line":256,"address":[2236049,2235971,2236573],"length":1,"stats":{"Line":0}},{"line":257,"address":[2236366,2236295],"length":1,"stats":{"Line":0}},{"line":259,"address":[2236013],"length":1,"stats":{"Line":0}},{"line":263,"address":[2243587,2236672,2239914],"length":1,"stats":{"Line":0}},{"line":264,"address":[2236719],"length":1,"stats":{"Line":0}},{"line":265,"address":[2236839,2236915],"length":1,"stats":{"Line":0}},{"line":266,"address":[2236961,2243531,2237123],"length":1,"stats":{"Line":0}},{"line":267,"address":[2237208,2240934],"length":1,"stats":{"Line":0}},{"line":268,"address":[2241032,2243518],"length":1,"stats":{"Line":0}},{"line":269,"address":[2241009,2241125],"length":1,"stats":{"Line":0}},{"line":270,"address":[2241259,2241333],"length":1,"stats":{"Line":0}},{"line":271,"address":[2241527,2241582],"length":1,"stats":{"Line":0}},{"line":272,"address":[2241656,2241575],"length":1,"stats":{"Line":0}},{"line":274,"address":[2241291,2241977],"length":1,"stats":{"Line":0}},{"line":275,"address":[2242099],"length":1,"stats":{"Line":0}},{"line":276,"address":[2242157],"length":1,"stats":{"Line":0}},{"line":277,"address":[2242266],"length":1,"stats":{"Line":0}},{"line":278,"address":[2242447,2242412],"length":1,"stats":{"Line":0}},{"line":279,"address":[2242747],"length":1,"stats":{"Line":0}},{"line":280,"address":[2242837],"length":1,"stats":{"Line":0}},{"line":282,"address":[2243149,2242437],"length":1,"stats":{"Line":0}},{"line":285,"address":[2243472,2242137],"length":1,"stats":{"Line":0}},{"line":288,"address":[2237253],"length":1,"stats":{"Line":0}},{"line":289,"address":[2237718,2237415,2237336],"length":1,"stats":{"Line":0}},{"line":291,"address":[2238180,2237688,2240771,2238302],"length":1,"stats":{"Line":0}},{"line":300,"address":[2238622,2240731,2238730,2238515],"length":1,"stats":{"Line":0}},{"line":301,"address":[2238666,2238507],"length":1,"stats":{"Line":0}},{"line":305,"address":[2238990,2239380,2238861,2238935],"length":1,"stats":{"Line":0}},{"line":306,"address":[2239393,2239050],"length":1,"stats":{"Line":0}},{"line":309,"address":[2238949,2239431],"length":1,"stats":{"Line":0}},{"line":310,"address":[2239610,2239854,2239462],"length":1,"stats":{"Line":0}},{"line":312,"address":[2239497,2239927],"length":1,"stats":{"Line":0}},{"line":313,"address":[2240418,2240000],"length":1,"stats":{"Line":0}},{"line":317,"address":[2240030,2239933],"length":1,"stats":{"Line":0}},{"line":320,"address":[2239815],"length":1,"stats":{"Line":0}},{"line":323,"address":[2243632,2246596,2245619],"length":1,"stats":{"Line":0}},{"line":324,"address":[2243713],"length":1,"stats":{"Line":0}},{"line":325,"address":[2243762,2246594,2243833],"length":1,"stats":{"Line":0}},{"line":326,"address":[2244168,2244235,2246573],"length":1,"stats":{"Line":0}},{"line":328,"address":[2244436],"length":1,"stats":{"Line":0}},{"line":329,"address":[2244496,2244588,2244664,2246324],"length":1,"stats":{"Line":0}},{"line":330,"address":[2244930,2244765],"length":1,"stats":{"Line":0}},{"line":331,"address":[2245148,2245219],"length":1,"stats":{"Line":0}},{"line":332,"address":[2246407],"length":1,"stats":{"Line":0}},{"line":335,"address":[2245498,2245388],"length":1,"stats":{"Line":0}},{"line":336,"address":[2245505,2245566],"length":1,"stats":{"Line":0}},{"line":338,"address":[2245625,2245716],"length":1,"stats":{"Line":0}},{"line":339,"address":[2245764,2245835],"length":1,"stats":{"Line":0}},{"line":340,"address":[2246023],"length":1,"stats":{"Line":0}},{"line":344,"address":[2244818],"length":1,"stats":{"Line":0}},{"line":347,"address":[2246624,2248773,2248808],"length":1,"stats":{"Line":0}},{"line":348,"address":[2246679],"length":1,"stats":{"Line":0}},{"line":350,"address":[2246748,2248866,2246719],"length":1,"stats":{"Line":0}},{"line":351,"address":[2246786,2248787],"length":1,"stats":{"Line":0}},{"line":352,"address":[2247029,2246909],"length":1,"stats":{"Line":0}},{"line":353,"address":[2247035],"length":1,"stats":{"Line":0}},{"line":354,"address":[2247155,2247104,2248695],"length":1,"stats":{"Line":0}},{"line":355,"address":[2247428],"length":1,"stats":{"Line":0}},{"line":356,"address":[2247455,2247512],"length":1,"stats":{"Line":0}},{"line":360,"address":[2248858],"length":1,"stats":{"Line":0}},{"line":363,"address":[2246729],"length":1,"stats":{"Line":0}},{"line":364,"address":[2248944,2251960,2248903],"length":1,"stats":{"Line":0}},{"line":365,"address":[2249013],"length":1,"stats":{"Line":0}},{"line":366,"address":[2249187,2249090],"length":1,"stats":{"Line":0}},{"line":367,"address":[2249220,2249450],"length":1,"stats":{"Line":0}},{"line":369,"address":[2249193,2249258],"length":1,"stats":{"Line":0}},{"line":371,"address":[2249411,2249566,2251542],"length":1,"stats":{"Line":0}},{"line":372,"address":[2249774,2249678],"length":1,"stats":{"Line":0}},{"line":373,"address":[2249780],"length":1,"stats":{"Line":0}},{"line":374,"address":[2249903,2249852,2251552],"length":1,"stats":{"Line":0}},{"line":375,"address":[2250173],"length":1,"stats":{"Line":0}},{"line":376,"address":[2250194,2250245],"length":1,"stats":{"Line":0}},{"line":381,"address":[2248927],"length":1,"stats":{"Line":0}},{"line":384,"address":[2252032,2252400,2252394],"length":1,"stats":{"Line":0}},{"line":385,"address":[2252091],"length":1,"stats":{"Line":0}},{"line":386,"address":[2252110,2252228,2252365],"length":1,"stats":{"Line":0}},{"line":388,"address":[2252413,2252161],"length":1,"stats":{"Line":0}},{"line":390,"address":[2252357],"length":1,"stats":{"Line":0}}],"covered":0,"coverable":201},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","tests","e2e.rs"],"content":"//! tests e2e.rs\n//! End-to-end “happy path” smoke-tests for the `marlin` binary.\n//!\n//! Run with `cargo test --test e2e` (CI does) or `cargo test`.\n\nuse assert_cmd::prelude::*;\nuse predicates::prelude::*;\nuse std::{fs, path::PathBuf, process::Command};\nuse tempfile::tempdir;\n\n/// Absolute path to the freshly-built `marlin` binary.\nfn marlin_bin() -\u003e PathBuf {\n PathBuf::from(env!(\"CARGO_BIN_EXE_marlin\"))\n}\n\n/// Create the demo directory structure and seed files.\nfn spawn_demo_tree(root: \u0026PathBuf) {\n fs::create_dir_all(root.join(\"Projects/Alpha\")).unwrap();\n fs::create_dir_all(root.join(\"Projects/Beta\")).unwrap();\n fs::create_dir_all(root.join(\"Projects/Gamma\")).unwrap();\n fs::create_dir_all(root.join(\"Logs\")).unwrap();\n fs::create_dir_all(root.join(\"Reports\")).unwrap();\n\n fs::write(root.join(\"Projects/Alpha/draft1.md\"), \"- [ ] TODO foo\\n\").unwrap();\n fs::write(root.join(\"Projects/Alpha/draft2.md\"), \"- [x] TODO foo\\n\").unwrap();\n fs::write(root.join(\"Projects/Beta/final.md\"), \"done\\n\").unwrap();\n fs::write(root.join(\"Projects/Gamma/TODO.txt\"), \"TODO bar\\n\").unwrap();\n fs::write(root.join(\"Logs/app.log\"), \"ERROR omg\\n\").unwrap();\n fs::write(root.join(\"Reports/Q1.pdf\"), \"PDF\\n\").unwrap();\n}\n\n/// Shorthand for “run and must succeed”.\nfn ok(cmd: \u0026mut Command) -\u003e assert_cmd::assert::Assert {\n cmd.assert().success()\n}\n\n#[test]\nfn full_cli_flow() -\u003e Result\u003c(), Box\u003cdyn std::error::Error\u003e\u003e {\n /* ── 1 ░ sandbox ───────────────────────────────────────────── */\n\n let tmp = tempdir()?; // wiped on drop\n let demo_dir = tmp.path().join(\"marlin_demo\");\n spawn_demo_tree(\u0026demo_dir);\n\n let db_path = demo_dir.join(\"index.db\");\n\n // Helper to spawn a fresh `marlin` Command with the DB env-var set\n let marlin = || {\n let mut c = Command::new(marlin_bin());\n c.env(\"MARLIN_DB_PATH\", \u0026db_path);\n c\n };\n\n /* ── 2 ░ init ( auto-scan cwd ) ───────────────────────────── */\n\n ok(marlin()\n .current_dir(\u0026demo_dir)\n .arg(\"init\"));\n\n /* ── 3 ░ tag \u0026 attr demos ─────────────────────────────────── */\n\n ok(marlin()\n .arg(\"tag\")\n .arg(format!(\"{}/Projects/**/*.md\", demo_dir.display()))\n .arg(\"project/md\"));\n\n ok(marlin()\n .arg(\"attr\")\n .arg(\"set\")\n .arg(format!(\"{}/Reports/*.pdf\", demo_dir.display()))\n .arg(\"reviewed\")\n .arg(\"yes\"));\n\n /* ── 4 ░ quick search sanity checks ───────────────────────── */\n\n marlin()\n .arg(\"search\").arg(\"TODO\")\n .assert()\n .stdout(predicate::str::contains(\"TODO.txt\"));\n\n marlin()\n .arg(\"search\").arg(\"attr:reviewed=yes\")\n .assert()\n .stdout(predicate::str::contains(\"Q1.pdf\"));\n\n /* ── 5 ░ link flow \u0026 backlinks ────────────────────────────── */\n\n let foo = demo_dir.join(\"foo.txt\");\n let bar = demo_dir.join(\"bar.txt\");\n fs::write(\u0026foo, \"\")?;\n fs::write(\u0026bar, \"\")?;\n\n ok(marlin().arg(\"scan\").arg(\u0026demo_dir));\n\n ok(marlin()\n .arg(\"link\").arg(\"add\")\n .arg(\u0026foo).arg(\u0026bar));\n\n marlin()\n .arg(\"link\").arg(\"backlinks\").arg(\u0026bar)\n .assert()\n .stdout(predicate::str::contains(\"foo.txt\"));\n\n /* ── 6 ░ backup → delete DB → restore ────────────────────── */\n\n let backup_path = String::from_utf8(\n marlin().arg(\"backup\").output()?.stdout\n )?;\n let backup_file = backup_path.split_whitespace().last().unwrap();\n\n fs::remove_file(\u0026db_path)?; // simulate corruption\n ok(marlin().arg(\"restore\").arg(backup_file)); // restore\n\n // Search must still work afterwards\n marlin()\n .arg(\"search\").arg(\"TODO\")\n .assert()\n .stdout(predicate::str::contains(\"TODO.txt\"));\n\n Ok(())\n}\n\n","traces":[{"line":12,"address":[789200],"length":1,"stats":{"Line":1}},{"line":13,"address":[789208],"length":1,"stats":{"Line":1}},{"line":17,"address":[789232],"length":1,"stats":{"Line":1}},{"line":18,"address":[789251],"length":1,"stats":{"Line":1}},{"line":19,"address":[789312],"length":1,"stats":{"Line":1}},{"line":20,"address":[789373],"length":1,"stats":{"Line":1}},{"line":21,"address":[789434],"length":1,"stats":{"Line":1}},{"line":22,"address":[789495],"length":1,"stats":{"Line":1}},{"line":24,"address":[789556],"length":1,"stats":{"Line":1}},{"line":25,"address":[789635],"length":1,"stats":{"Line":1}},{"line":26,"address":[789714],"length":1,"stats":{"Line":1}},{"line":27,"address":[789793],"length":1,"stats":{"Line":1}},{"line":28,"address":[789872],"length":1,"stats":{"Line":1}},{"line":29,"address":[789951],"length":1,"stats":{"Line":1}},{"line":33,"address":[790048],"length":1,"stats":{"Line":1}},{"line":34,"address":[790066],"length":1,"stats":{"Line":1}}],"covered":16,"coverable":16},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","tests","integration","watcher","watcher_test.rs"],"content":"//! Integration test for the file watcher functionality\n//! \n//! Tests various aspects of the file system watcher including:\n//! - Basic event handling (create, modify, delete files)\n//! - Debouncing of events\n//! - Hierarchical event coalescing\n//! - Graceful shutdown and event draining\n\nuse marlin::watcher::{FileWatcher, WatcherConfig, WatcherState};\nuse std::path::{Path, PathBuf};\nuse std::fs::{self, File};\nuse std::io::Write;\nuse std::thread;\nuse std::time::{Duration, Instant};\nuse tempfile::tempdir;\n\n// Mock filesystem event simulator inspired by inotify-sim\nstruct MockEventSimulator {\n temp_dir: PathBuf,\n files_created: Vec\u003cPathBuf\u003e,\n}\n\nimpl MockEventSimulator {\n fn new(temp_dir: PathBuf) -\u003e Self {\n Self {\n temp_dir,\n files_created: Vec::new(),\n }\n }\n\n fn create_file(\u0026mut self, relative_path: \u0026str, content: \u0026str) -\u003e PathBuf {\n let path = self.temp_dir.join(relative_path);\n if let Some(parent) = path.parent() {\n fs::create_dir_all(parent).expect(\"Failed to create parent directory\");\n }\n \n let mut file = File::create(\u0026path).expect(\"Failed to create file\");\n file.write_all(content.as_bytes()).expect(\"Failed to write content\");\n \n self.files_created.push(path.clone());\n path\n }\n \n fn modify_file(\u0026self, relative_path: \u0026str, new_content: \u0026str) -\u003e PathBuf {\n let path = self.temp_dir.join(relative_path);\n let mut file = File::create(\u0026path).expect(\"Failed to update file\");\n file.write_all(new_content.as_bytes()).expect(\"Failed to write content\");\n path\n }\n \n fn delete_file(\u0026mut self, relative_path: \u0026str) {\n let path = self.temp_dir.join(relative_path);\n fs::remove_file(\u0026path).expect(\"Failed to delete file\");\n \n self.files_created.retain(|p| p != \u0026path);\n }\n \n fn create_burst(\u0026mut self, count: usize, prefix: \u0026str) -\u003e Vec\u003cPathBuf\u003e {\n let mut paths = Vec::with_capacity(count);\n \n for i in 0..count {\n let file_path = format!(\"{}/burst_file_{}.txt\", prefix, i);\n let path = self.create_file(\u0026file_path, \u0026format!(\"Content {}\", i));\n paths.push(path);\n \n // Small delay to simulate rapid but not instantaneous file creation\n thread::sleep(Duration::from_micros(10));\n }\n \n paths\n }\n \n fn cleanup(\u0026self) {\n // No need to do anything as tempdir will clean itself\n }\n}\n\n#[test]\nfn test_basic_watch_functionality() {\n let temp_dir = tempdir().expect(\"Failed to create temp directory\");\n let temp_path = temp_dir.path().to_path_buf();\n \n let mut simulator = MockEventSimulator::new(temp_path.clone());\n \n // Create a test file before starting the watcher\n let initial_file = simulator.create_file(\"initial.txt\", \"Initial content\");\n \n // Configure and start the watcher\n let config = WatcherConfig {\n debounce_ms: 100,\n batch_size: 100,\n max_queue_size: 1000,\n drain_timeout_ms: 1000,\n };\n \n let mut watcher = FileWatcher::new(vec![temp_path.clone()], config)\n .expect(\"Failed to create file watcher\");\n \n // Start the watcher in a separate thread\n let watcher_thread = thread::spawn(move || {\n watcher.start().expect(\"Failed to start watcher\");\n \n // Let it run for a short time\n thread::sleep(Duration::from_secs(5));\n \n // Stop the watcher\n watcher.stop().expect(\"Failed to stop watcher\");\n \n // Return the watcher for inspection\n watcher\n });\n \n // Wait for watcher to initialize\n thread::sleep(Duration::from_millis(500));\n \n // Generate events\n let file1 = simulator.create_file(\"test1.txt\", \"Hello, world!\");\n thread::sleep(Duration::from_millis(200));\n \n let file2 = simulator.create_file(\"dir1/test2.txt\", \"Hello from subdirectory!\");\n thread::sleep(Duration::from_millis(200));\n \n simulator.modify_file(\"test1.txt\", \"Updated content\");\n thread::sleep(Duration::from_millis(200));\n \n simulator.delete_file(\"test1.txt\");\n \n // Wait for watcher thread to complete\n let finished_watcher = watcher_thread.join().expect(\"Watcher thread panicked\");\n \n // Check status after processing events\n let status = finished_watcher.status();\n \n // Assertions\n assert_eq!(status.state, WatcherState::Stopped);\n assert!(status.events_processed \u003e 0, \"Expected events to be processed\");\n assert_eq!(status.queue_size, 0, \"Expected empty queue after stopping\");\n \n // Clean up\n simulator.cleanup();\n}\n\n#[test]\nfn test_debouncing() {\n let temp_dir = tempdir().expect(\"Failed to create temp directory\");\n let temp_path = temp_dir.path().to_path_buf();\n \n let mut simulator = MockEventSimulator::new(temp_path.clone());\n \n // Configure watcher with larger debounce window for this test\n let config = WatcherConfig {\n debounce_ms: 200, // 200ms debounce window\n batch_size: 100,\n max_queue_size: 1000,\n drain_timeout_ms: 1000,\n };\n \n let mut watcher = FileWatcher::new(vec![temp_path.clone()], config)\n .expect(\"Failed to create file watcher\");\n \n // Start the watcher in a separate thread\n let watcher_thread = thread::spawn(move || {\n watcher.start().expect(\"Failed to start watcher\");\n \n // Let it run for enough time to observe debouncing\n thread::sleep(Duration::from_secs(3));\n \n // Stop the watcher\n watcher.stop().expect(\"Failed to stop watcher\");\n \n // Return the watcher for inspection\n watcher\n });\n \n // Wait for watcher to initialize\n thread::sleep(Duration::from_millis(500));\n \n // Rapidly update the same file multiple times within the debounce window\n let test_file = \"test_debounce.txt\";\n simulator.create_file(test_file, \"Initial content\");\n \n // Update the same file multiple times within debounce window\n for i in 1..10 {\n simulator.modify_file(test_file, \u0026format!(\"Update {}\", i));\n thread::sleep(Duration::from_millis(10)); // Short delay between updates\n }\n \n // Wait for debounce window and processing\n thread::sleep(Duration::from_millis(500));\n \n // Complete the test\n let finished_watcher = watcher_thread.join().expect(\"Watcher thread panicked\");\n let status = finished_watcher.status();\n \n // We should have processed fewer events than modifications made\n // due to debouncing (exact count depends on implementation details)\n assert!(status.events_processed \u003c 10, \n \"Expected fewer events processed than modifications due to debouncing\");\n \n // Clean up\n simulator.cleanup();\n}\n\n#[test]\nfn test_event_flood() {\n let temp_dir = tempdir().expect(\"Failed to create temp directory\");\n let temp_path = temp_dir.path().to_path_buf();\n \n let mut simulator = MockEventSimulator::new(temp_path.clone());\n \n // Configure with settings tuned for burst handling\n let config = WatcherConfig {\n debounce_ms: 100,\n batch_size: 500, // Handle larger batches\n max_queue_size: 10000, // Large queue for burst\n drain_timeout_ms: 5000, // Longer drain time for cleanup\n };\n \n let mut watcher = FileWatcher::new(vec![temp_path.clone()], config)\n .expect(\"Failed to create file watcher\");\n \n // Start the watcher\n let watcher_thread = thread::spawn(move || {\n watcher.start().expect(\"Failed to start watcher\");\n \n // Let it run for enough time to process a large burst\n thread::sleep(Duration::from_secs(10));\n \n // Stop the watcher\n watcher.stop().expect(\"Failed to stop watcher\");\n \n // Return the watcher for inspection\n watcher\n });\n \n // Wait for watcher to initialize\n thread::sleep(Duration::from_millis(500));\n \n // Create 1000 files in rapid succession (smaller scale for test)\n let start_time = Instant::now();\n let created_files = simulator.create_burst(1000, \"flood\");\n let creation_time = start_time.elapsed();\n \n println!(\"Created 1000 files in {:?}\", creation_time);\n \n // Wait for processing to complete\n thread::sleep(Duration::from_secs(5));\n \n // Complete the test\n let finished_watcher = watcher_thread.join().expect(\"Watcher thread panicked\");\n let status = finished_watcher.status();\n \n // Verify processing occurred\n assert!(status.events_processed \u003e 0, \"Expected events to be processed\");\n assert_eq!(status.queue_size, 0, \"Expected empty queue after stopping\");\n \n // Clean up\n simulator.cleanup();\n}\n\n#[test]\nfn test_hierarchical_debouncing() {\n let temp_dir = tempdir().expect(\"Failed to create temp directory\");\n let temp_path = temp_dir.path().to_path_buf();\n \n let mut simulator = MockEventSimulator::new(temp_path.clone());\n \n // Configure watcher\n let config = WatcherConfig {\n debounce_ms: 200,\n batch_size: 100,\n max_queue_size: 1000,\n drain_timeout_ms: 1000,\n };\n \n let mut watcher = FileWatcher::new(vec![temp_path.clone()], config)\n .expect(\"Failed to create file watcher\");\n \n // Start the watcher\n let watcher_thread = thread::spawn(move || {\n watcher.start().expect(\"Failed to start watcher\");\n \n // Let it run\n thread::sleep(Duration::from_secs(5));\n \n // Stop the watcher\n watcher.stop().expect(\"Failed to stop watcher\");\n \n // Return the watcher\n watcher\n });\n \n // Wait for watcher to initialize\n thread::sleep(Duration::from_millis(500));\n \n // Create directory structure\n let nested_dir = \"parent/child/grandchild\";\n fs::create_dir_all(temp_path.join(nested_dir)).expect(\"Failed to create nested directories\");\n \n // Create files in the hierarchy\n simulator.create_file(\"parent/file1.txt\", \"Content 1\");\n simulator.create_file(\"parent/child/file2.txt\", \"Content 2\");\n simulator.create_file(\"parent/child/grandchild/file3.txt\", \"Content 3\");\n \n // Wait a bit\n thread::sleep(Duration::from_millis(300));\n \n // Complete the test\n let finished_watcher = watcher_thread.join().expect(\"Watcher thread panicked\");\n \n // Clean up\n simulator.cleanup();\n}\n\n#[test]\nfn test_graceful_shutdown() {\n let temp_dir = tempdir().expect(\"Failed to create temp directory\");\n let temp_path = temp_dir.path().to_path_buf();\n \n let mut simulator = MockEventSimulator::new(temp_path.clone());\n \n // Configure watcher with specific drain timeout\n let config = WatcherConfig {\n debounce_ms: 100,\n batch_size: 100,\n max_queue_size: 1000,\n drain_timeout_ms: 2000, // 2 second drain timeout\n };\n \n let mut watcher = FileWatcher::new(vec![temp_path.clone()], config)\n .expect(\"Failed to create file watcher\");\n \n // Start the watcher\n watcher.start().expect(\"Failed to start watcher\");\n \n // Wait for initialization\n thread::sleep(Duration::from_millis(500));\n \n // Create files\n for i in 0..10 {\n simulator.create_file(\u0026format!(\"shutdown_test_{}.txt\", i), \"Shutdown test\");\n thread::sleep(Duration::from_millis(10));\n }\n \n // Immediately request shutdown while events are being processed\n let shutdown_start = Instant::now();\n watcher.stop().expect(\"Failed to stop watcher\");\n let shutdown_duration = shutdown_start.elapsed();\n \n // Shutdown should take close to the drain timeout but not excessively longer\n println!(\"Shutdown took {:?}\", shutdown_duration);\n assert!(shutdown_duration \u003e= Duration::from_millis(100), \n \"Shutdown was too quick, may not have drained properly\");\n assert!(shutdown_duration \u003c= Duration::from_millis(3000), \n \"Shutdown took too long\");\n \n // Verify final state\n let status = watcher.status();\n assert_eq!(status.state, WatcherState::Stopped);\n assert_eq!(status.queue_size, 0, \"Queue should be empty after shutdown\");\n \n // Clean up\n simulator.cleanup();\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","tests","neg.rs"],"content":"//! tests neg.rs\n//! Negative-path integration tests (“should fail / warn”).\n\nuse predicates::str;\nuse tempfile::tempdir;\n\nmod util;\nuse util::marlin;\n\n/* ───────────────────────── LINKS ─────────────────────────────── */\n\n#[test]\nfn link_non_indexed_should_fail() {\n let tmp = tempdir().unwrap();\n\n marlin(\u0026tmp).current_dir(tmp.path()).arg(\"init\").assert().success();\n\n std::fs::write(tmp.path().join(\"foo.txt\"), \"\").unwrap();\n std::fs::write(tmp.path().join(\"bar.txt\"), \"\").unwrap();\n\n marlin(\u0026tmp)\n .current_dir(tmp.path())\n .args([\n \"link\", \"add\",\n \u0026tmp.path().join(\"foo.txt\").to_string_lossy(),\n \u0026tmp.path().join(\"bar.txt\").to_string_lossy()\n ])\n .assert()\n .failure()\n .stderr(str::contains(\"file not indexed\"));\n}\n\n/* ───────────────────────── ATTR ─────────────────────────────── */\n\n#[test]\nfn attr_set_on_non_indexed_file_should_warn() {\n let tmp = tempdir().unwrap();\n marlin(\u0026tmp).current_dir(tmp.path()).arg(\"init\").assert().success();\n\n let ghost = tmp.path().join(\"ghost.txt\");\n std::fs::write(\u0026ghost, \"\").unwrap();\n\n marlin(\u0026tmp)\n .args([\"attr\",\"set\",\n \u0026ghost.to_string_lossy(),\"foo\",\"bar\"])\n .assert()\n .success() // exits 0\n .stderr(str::contains(\"not indexed\"));\n}\n\n/* ───────────────────── COLLECTIONS ───────────────────────────── */\n\n#[test]\nfn coll_add_unknown_collection_should_fail() {\n let tmp = tempdir().unwrap();\n let file = tmp.path().join(\"doc.txt\");\n std::fs::write(\u0026file, \"\").unwrap();\n\n marlin(\u0026tmp).current_dir(tmp.path()).arg(\"init\").assert().success();\n\n marlin(\u0026tmp)\n .args([\"coll\",\"add\",\"nope\",\u0026file.to_string_lossy()])\n .assert()\n .failure();\n}\n\n/* ───────────────────── RESTORE (bad file) ───────────────────── */\n\n#[test]\nfn restore_with_nonexistent_backup_should_fail() {\n let tmp = tempdir().unwrap();\n\n // create an empty DB first\n marlin(\u0026tmp).arg(\"init\").assert().success();\n\n marlin(\u0026tmp)\n .args([\"restore\", \"/definitely/not/here.db\"])\n .assert()\n .failure()\n .stderr(str::contains(\"Failed to restore\"));\n}\n\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","tests","pos.rs"],"content":"//! tests pos.rs\n//! Positive-path integration checks for every sub-command\n//! that already has real logic behind it.\n\nmod util;\nuse util::marlin;\n\nuse predicates::{prelude::*, str}; // brings `PredicateBooleanExt::and`\nuse std::fs;\nuse tempfile::tempdir;\n\n/* ─────────────────────────── TAG ─────────────────────────────── */\n\n#[test]\nfn tag_should_add_hierarchical_tag_and_search_finds_it() {\n let tmp = tempdir().unwrap();\n let file = tmp.path().join(\"foo.md\");\n fs::write(\u0026file, \"# test\\n\").unwrap();\n\n marlin(\u0026tmp).current_dir(tmp.path()).arg(\"init\").assert().success();\n\n marlin(\u0026tmp)\n .args([\"tag\", file.to_str().unwrap(), \"project/md\"])\n .assert().success();\n\n marlin(\u0026tmp)\n .args([\"search\", \"tag:project/md\"])\n .assert()\n .success()\n .stdout(str::contains(\"foo.md\"));\n}\n\n/* ─────────────────────────── ATTR ────────────────────────────── */\n\n#[test]\nfn attr_set_then_ls_roundtrip() {\n let tmp = tempdir().unwrap();\n let file = tmp.path().join(\"report.pdf\");\n fs::write(\u0026file, \"%PDF-1.4\\n\").unwrap();\n\n marlin(\u0026tmp).current_dir(tmp.path()).arg(\"init\").assert().success();\n\n marlin(\u0026tmp)\n .args([\"attr\", \"set\", file.to_str().unwrap(), \"reviewed\", \"yes\"])\n .assert().success();\n\n marlin(\u0026tmp)\n .args([\"attr\", \"ls\", file.to_str().unwrap()])\n .assert()\n .success()\n .stdout(str::contains(\"reviewed = yes\"));\n}\n\n/* ─────────────────────── COLLECTIONS ────────────────────────── */\n\n#[test]\nfn coll_create_add_and_list() {\n let tmp = tempdir().unwrap();\n\n let a = tmp.path().join(\"a.txt\");\n let b = tmp.path().join(\"b.txt\");\n fs::write(\u0026a, \"\").unwrap();\n fs::write(\u0026b, \"\").unwrap();\n\n marlin(\u0026tmp).current_dir(tmp.path()).arg(\"init\").assert().success();\n\n marlin(\u0026tmp).args([\"coll\", \"create\", \"Set\"]).assert().success();\n for f in [\u0026a, \u0026b] {\n marlin(\u0026tmp).args([\"coll\", \"add\", \"Set\", f.to_str().unwrap()]).assert().success();\n }\n\n marlin(\u0026tmp)\n .args([\"coll\", \"list\", \"Set\"])\n .assert()\n .success()\n .stdout(str::contains(\"a.txt\").and(str::contains(\"b.txt\")));\n}\n\n/* ─────────────────────────── VIEWS ───────────────────────────── */\n\n#[test]\nfn view_save_list_and_exec() {\n let tmp = tempdir().unwrap();\n\n let todo = tmp.path().join(\"TODO.txt\");\n fs::write(\u0026todo, \"remember the milk\\n\").unwrap();\n\n marlin(\u0026tmp).current_dir(tmp.path()).arg(\"init\").assert().success();\n\n // save \u0026 list\n marlin(\u0026tmp).args([\"view\", \"save\", \"tasks\", \"milk\"]).assert().success();\n marlin(\u0026tmp)\n .args([\"view\", \"list\"])\n .assert()\n .success()\n .stdout(str::contains(\"tasks: milk\"));\n\n // exec\n marlin(\u0026tmp)\n .args([\"view\", \"exec\", \"tasks\"])\n .assert()\n .success()\n .stdout(str::contains(\"TODO.txt\"));\n}\n\n/* ─────────────────────────── LINKS ───────────────────────────── */\n\n#[test]\nfn link_add_rm_and_list() {\n let tmp = tempdir().unwrap();\n\n let foo = tmp.path().join(\"foo.txt\");\n let bar = tmp.path().join(\"bar.txt\");\n fs::write(\u0026foo, \"\").unwrap();\n fs::write(\u0026bar, \"\").unwrap();\n\n // handy closure\n let mc = || marlin(\u0026tmp);\n\n mc().current_dir(tmp.path()).arg(\"init\").assert().success();\n mc().args([\"scan\", tmp.path().to_str().unwrap()]).assert().success();\n\n // add\n mc().args([\"link\", \"add\", foo.to_str().unwrap(), bar.to_str().unwrap()])\n .assert().success();\n\n // list (outgoing default)\n mc().args([\"link\", \"list\", foo.to_str().unwrap()])\n .assert().success()\n .stdout(str::contains(\"foo.txt\").and(str::contains(\"bar.txt\")));\n\n // remove\n mc().args([\"link\", \"rm\", foo.to_str().unwrap(), bar.to_str().unwrap()])\n .assert().success();\n\n // list now empty\n mc().args([\"link\", \"list\", foo.to_str().unwrap()])\n .assert().success()\n .stdout(str::is_empty());\n}\n\n/* ─────────────────────── SCAN (multi-path) ───────────────────── */\n\n#[test]\nfn scan_with_multiple_paths_indexes_all() {\n let tmp = tempdir().unwrap();\n\n let dir_a = tmp.path().join(\"A\");\n let dir_b = tmp.path().join(\"B\");\n std::fs::create_dir_all(\u0026dir_a).unwrap();\n std::fs::create_dir_all(\u0026dir_b).unwrap();\n let f1 = dir_a.join(\"one.txt\");\n let f2 = dir_b.join(\"two.txt\");\n fs::write(\u0026f1, \"\").unwrap();\n fs::write(\u0026f2, \"\").unwrap();\n\n marlin(\u0026tmp).current_dir(tmp.path()).arg(\"init\").assert().success();\n\n // multi-path scan\n marlin(\u0026tmp)\n .args([\"scan\", dir_a.to_str().unwrap(), dir_b.to_str().unwrap()])\n .assert().success();\n\n // both files findable\n for term in [\"one.txt\", \"two.txt\"] {\n marlin(\u0026tmp).args([\"search\", term])\n .assert()\n .success()\n .stdout(str::contains(term));\n }\n}\n\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","user","Documents","GitHub","Marlin","cli-bin","tests","util.rs"],"content":"//! tests/util.rs\n//! Small helpers shared across integration tests.\n\nuse std::path::{Path, PathBuf};\nuse tempfile::TempDir;\nuse assert_cmd::Command; \n/// Absolute path to the freshly-built `marlin` binary.\npub fn bin() -\u003e PathBuf {\n PathBuf::from(env!(\"CARGO_BIN_EXE_marlin\"))\n}\n\n/// Build a `Command` for `marlin` whose `MARLIN_DB_PATH` is\n/// `\u003ctmp\u003e/index.db`.\n///\n/// Each call yields a brand-new `Command`, so callers can freely add\n/// arguments, change the working directory, etc., without affecting\n/// other invocations.\npub fn marlin(tmp: \u0026TempDir) -\u003e Command {\n let db_path: \u0026Path = \u0026tmp.path().join(\"index.db\");\n let mut cmd = Command::new(bin());\n cmd.env(\"MARLIN_DB_PATH\", db_path);\n cmd\n}\n","traces":[{"line":8,"address":[776352],"length":1,"stats":{"Line":10}},{"line":9,"address":[776360],"length":1,"stats":{"Line":10}},{"line":18,"address":[776727,776384,776721],"length":1,"stats":{"Line":10}},{"line":19,"address":[776422,776535],"length":1,"stats":{"Line":20}},{"line":20,"address":[776575],"length":1,"stats":{"Line":10}},{"line":21,"address":[776612],"length":1,"stats":{"Line":10}},{"line":22,"address":[776682],"length":1,"stats":{"Line":10}}],"covered":7,"coverable":7},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","backup.rs"],"content":"// libmarlin/src/backup.rs\n\nuse anyhow::{anyhow, Context, Result};\nuse chrono::{DateTime, Local, NaiveDateTime, Utc, TimeZone};\nuse rusqlite;\nuse std::fs;\nuse std::path::{Path, PathBuf};\nuse std::time::Duration;\n\nuse crate::error as marlin_error;\n\n#[derive(Debug, Clone)]\npub struct BackupInfo {\n pub id: String,\n pub timestamp: DateTime\u003cUtc\u003e,\n pub size_bytes: u64,\n pub hash: Option\u003cString\u003e,\n}\n\n#[derive(Debug)]\npub struct PruneResult {\n pub kept: Vec\u003cBackupInfo\u003e,\n pub removed: Vec\u003cBackupInfo\u003e,\n}\n\n// FIX 2: Add derive(Debug) here\n#[derive(Debug)]\npub struct BackupManager {\n live_db_path: PathBuf,\n backups_dir: PathBuf,\n}\n\nimpl BackupManager {\n pub fn new\u003cP1: AsRef\u003cPath\u003e, P2: AsRef\u003cPath\u003e\u003e(live_db_path: P1, backups_dir: P2) -\u003e Result\u003cSelf\u003e {\n let backups_dir_path = backups_dir.as_ref().to_path_buf();\n if !backups_dir_path.exists() {\n fs::create_dir_all(\u0026backups_dir_path).with_context(|| {\n format!(\n \"Failed to create backup directory at {}\",\n backups_dir_path.display()\n )\n })?;\n } else if !backups_dir_path.is_dir() {\n return Err(anyhow!(\"Backups path exists but is not a directory: {}\", backups_dir_path.display()));\n }\n Ok(Self {\n live_db_path: live_db_path.as_ref().to_path_buf(),\n backups_dir: backups_dir_path,\n })\n }\n\n pub fn create_backup(\u0026self) -\u003e Result\u003cBackupInfo\u003e {\n let stamp = Local::now().format(\"%Y-%m-%d_%H-%M-%S_%f\");\n let backup_file_name = format!(\"backup_{stamp}.db\");\n let backup_file_path = self.backups_dir.join(\u0026backup_file_name);\n\n if !self.live_db_path.exists() {\n return Err(anyhow::Error::new(std::io::Error::new(\n std::io::ErrorKind::NotFound,\n format!(\"Live DB path does not exist: {}\", self.live_db_path.display()),\n )).context(\"Cannot create backup from non-existent live DB\"));\n }\n\n let src_conn = rusqlite::Connection::open_with_flags(\n \u0026self.live_db_path,\n rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,\n )\n .with_context(|| {\n format!(\n \"Failed to open source DB ('{}') for backup\",\n self.live_db_path.display()\n )\n })?;\n\n let mut dst_conn = rusqlite::Connection::open(\u0026backup_file_path).with_context(|| {\n format!(\n \"Failed to open destination backup file: {}\",\n backup_file_path.display()\n )\n })?;\n\n let backup_op =\n rusqlite::backup::Backup::new(\u0026src_conn, \u0026mut dst_conn).with_context(|| {\n format!(\n \"Failed to initialize backup from {} to {}\",\n self.live_db_path.display(),\n backup_file_path.display()\n )\n })?;\n\n backup_op\n .run_to_completion(100, Duration::from_millis(250), None)\n .map_err(|e| anyhow::Error::new(e).context(\"SQLite backup operation failed\"))?;\n\n let metadata = fs::metadata(\u0026backup_file_path).with_context(|| {\n format!(\n \"Failed to get metadata for backup file: {}\",\n backup_file_path.display()\n )\n })?;\n\n Ok(BackupInfo {\n id: backup_file_name,\n timestamp: DateTime::from(metadata.modified()?),\n size_bytes: metadata.len(),\n hash: None,\n })\n }\n\n pub fn list_backups(\u0026self) -\u003e Result\u003cVec\u003cBackupInfo\u003e\u003e {\n let mut backup_infos = Vec::new();\n \n if !self.backups_dir.exists() { \n return Ok(backup_infos);\n }\n\n for entry_result in fs::read_dir(\u0026self.backups_dir).with_context(|| {\n format!(\n \"Failed to read backup directory: {}\",\n self.backups_dir.display()\n )\n })? {\n let entry = entry_result?;\n let path = entry.path();\n\n if path.is_file() {\n if let Some(filename_osstr) = path.file_name() {\n if let Some(filename) = filename_osstr.to_str() {\n if filename.starts_with(\"backup_\") \u0026\u0026 filename.ends_with(\".db\") {\n let ts_str = filename\n .trim_start_matches(\"backup_\")\n .trim_end_matches(\".db\");\n \n let naive_dt = match NaiveDateTime::parse_from_str(ts_str, \"%Y-%m-%d_%H-%M-%S_%f\") {\n Ok(dt) =\u003e dt,\n Err(_) =\u003e match NaiveDateTime::parse_from_str(ts_str, \"%Y-%m-%d_%H-%M-%S\") {\n Ok(dt) =\u003e dt,\n Err(_) =\u003e { \n let metadata = fs::metadata(\u0026path).with_context(|| format!(\"Failed to get metadata for {}\", path.display()))?;\n DateTime::\u003cUtc\u003e::from(metadata.modified()?).naive_utc()\n }\n }\n };\n \n let local_dt_result = Local.from_local_datetime(\u0026naive_dt);\n let local_dt = match local_dt_result {\n chrono::LocalResult::Single(dt) =\u003e dt,\n chrono::LocalResult::Ambiguous(dt1, _dt2) =\u003e {\n eprintln!(\"Warning: Ambiguous local time for backup {}, taking first interpretation.\", filename);\n dt1\n },\n chrono::LocalResult::None =\u003e {\n eprintln!(\"Warning: Invalid local time for backup {}, skipping.\", filename);\n continue; \n }\n };\n let timestamp_utc = DateTime::\u003cUtc\u003e::from(local_dt);\n\n let metadata = fs::metadata(\u0026path)?;\n backup_infos.push(BackupInfo {\n id: filename.to_string(),\n timestamp: timestamp_utc,\n size_bytes: metadata.len(),\n hash: None,\n });\n }\n }\n }\n }\n }\n backup_infos.sort_by_key(|b| std::cmp::Reverse(b.timestamp));\n Ok(backup_infos)\n }\n\n pub fn prune(\u0026self, keep_count: usize) -\u003e Result\u003cPruneResult\u003e {\n let all_backups = self.list_backups()?; \n\n let mut kept = Vec::new();\n let mut removed = Vec::new();\n\n if keep_count \u003e= all_backups.len() { \n kept = all_backups;\n } else {\n for (index, backup_info) in all_backups.into_iter().enumerate() {\n if index \u003c keep_count {\n kept.push(backup_info);\n } else {\n let backup_file_path = self.backups_dir.join(\u0026backup_info.id);\n if backup_file_path.exists() { \n fs::remove_file(\u0026backup_file_path).with_context(|| {\n format!(\n \"Failed to remove old backup file: {}\",\n backup_file_path.display()\n )\n })?;\n }\n removed.push(backup_info);\n }\n }\n }\n Ok(PruneResult { kept, removed })\n }\n\n pub fn restore_from_backup(\u0026self, backup_id: \u0026str) -\u003e Result\u003c()\u003e {\n let backup_file_path = self.backups_dir.join(backup_id);\n if !backup_file_path.exists() || !backup_file_path.is_file() {\n return Err(anyhow::Error::new(marlin_error::Error::NotFound(format!(\n \"Backup file not found or is not a file: {}\",\n backup_file_path.display()\n ))));\n }\n\n fs::copy(\u0026backup_file_path, \u0026self.live_db_path).with_context(|| {\n format!(\n \"Failed to copy backup {} to live DB {}\",\n backup_file_path.display(),\n self.live_db_path.display()\n )\n })?;\n Ok(())\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n use tempfile::tempdir;\n use crate::db::open as open_marlin_db;\n // FIX 1: Remove unused import std::io::ErrorKind\n // use std::io::ErrorKind; \n\n fn create_valid_live_db(path: \u0026Path) -\u003e rusqlite::Connection {\n let conn = open_marlin_db(path)\n .unwrap_or_else(|e| panic!(\"Failed to open/create test DB at {}: {:?}\", path.display(), e));\n conn.execute_batch(\n \"CREATE TABLE IF NOT EXISTS test_table (id INTEGER PRIMARY KEY, data TEXT);\n INSERT INTO test_table (data) VALUES ('initial_data');\"\n ).expect(\"Failed to initialize test table\");\n conn\n }\n\n #[test]\n fn test_backup_manager_new_creates_dir() {\n let base_tmp = tempdir().unwrap();\n let live_db_path = base_tmp.path().join(\"live_new_creates.db\");\n let _conn = create_valid_live_db(\u0026live_db_path);\n\n let backups_dir = base_tmp.path().join(\"my_backups_new_creates_test\");\n\n assert!(!backups_dir.exists());\n let manager = BackupManager::new(\u0026live_db_path, \u0026backups_dir).unwrap();\n assert!(manager.backups_dir.exists()); \n assert!(backups_dir.exists());\n }\n\n #[test]\n fn test_backup_manager_new_with_existing_dir() {\n let base_tmp = tempdir().unwrap();\n let live_db_path = base_tmp.path().join(\"live_existing_dir.db\");\n let _conn = create_valid_live_db(\u0026live_db_path);\n\n let backups_dir = base_tmp.path().join(\"my_backups_existing_test\");\n std::fs::create_dir_all(\u0026backups_dir).unwrap(); \n\n assert!(backups_dir.exists());\n let manager_res = BackupManager::new(\u0026live_db_path, \u0026backups_dir);\n assert!(manager_res.is_ok());\n let manager = manager_res.unwrap();\n assert_eq!(manager.backups_dir, backups_dir);\n }\n \n #[test]\n fn test_backup_manager_new_fails_if_backup_path_is_file() {\n let base_tmp = tempdir().unwrap();\n let live_db_path = base_tmp.path().join(\"live_backup_path_is_file.db\");\n let _conn = create_valid_live_db(\u0026live_db_path);\n let file_as_backups_dir = base_tmp.path().join(\"file_as_backups_dir\");\n std::fs::write(\u0026file_as_backups_dir, \"i am a file\").unwrap();\n\n let manager_res = BackupManager::new(\u0026live_db_path, \u0026file_as_backups_dir);\n assert!(manager_res.is_err());\n assert!(manager_res.unwrap_err().to_string().contains(\"Backups path exists but is not a directory\"));\n }\n\n #[test]\n fn test_create_backup_failure_non_existent_live_db() {\n let base_tmp = tempdir().unwrap();\n let live_db_path = base_tmp.path().join(\"non_existent_live.db\"); \n let backups_dir = base_tmp.path().join(\"backups_fail_test\");\n\n let manager = BackupManager::new(\u0026live_db_path, \u0026backups_dir).unwrap();\n let backup_result = manager.create_backup();\n assert!(backup_result.is_err());\n let err_str = backup_result.unwrap_err().to_string();\n assert!(err_str.contains(\"Cannot create backup from non-existent live DB\") || err_str.contains(\"Failed to open source DB\"));\n }\n\n #[test]\n fn test_create_list_prune_backups() {\n let tmp = tempdir().unwrap();\n let live_db_file = tmp.path().join(\"live_for_clp_test.db\");\n let _conn_live = create_valid_live_db(\u0026live_db_file);\n\n let backups_storage_dir = tmp.path().join(\"backups_clp_storage_test\");\n \n let manager = BackupManager::new(\u0026live_db_file, \u0026backups_storage_dir).unwrap();\n\n let initial_list = manager.list_backups().unwrap();\n assert!(initial_list.is_empty(), \"Backup list should be empty initially\");\n\n let prune_empty_result = manager.prune(2).unwrap();\n assert!(prune_empty_result.kept.is_empty());\n assert!(prune_empty_result.removed.is_empty());\n\n let mut created_backup_ids = Vec::new();\n for i in 0..5 {\n let info = manager\n .create_backup()\n .unwrap_or_else(|e| panic!(\"Failed to create backup {}: {:?}\", i, e));\n created_backup_ids.push(info.id.clone()); \n std::thread::sleep(std::time::Duration::from_millis(30));\n }\n\n let listed_backups = manager.list_backups().unwrap();\n assert_eq!(listed_backups.len(), 5);\n for id in \u0026created_backup_ids {\n assert!(\n listed_backups.iter().any(|b| \u0026b.id == id),\n \"Backup ID {} not found in list\", id\n );\n }\n if listed_backups.len() \u003e= 2 {\n assert!(listed_backups[0].timestamp \u003e= listed_backups[1].timestamp);\n }\n\n let prune_to_zero_result = manager.prune(0).unwrap();\n assert_eq!(prune_to_zero_result.kept.len(), 0);\n assert_eq!(prune_to_zero_result.removed.len(), 5);\n let listed_after_prune_zero = manager.list_backups().unwrap();\n assert!(listed_after_prune_zero.is_empty());\n\n created_backup_ids.clear();\n for i in 0..5 { \n let info = manager\n .create_backup()\n .unwrap_or_else(|e| panic!(\"Failed to create backup {}: {:?}\", i, e));\n created_backup_ids.push(info.id.clone());\n std::thread::sleep(std::time::Duration::from_millis(30));\n }\n\n let prune_keep_more_result = manager.prune(10).unwrap();\n assert_eq!(prune_keep_more_result.kept.len(), 5);\n assert_eq!(prune_keep_more_result.removed.len(), 0);\n let listed_after_prune_more = manager.list_backups().unwrap();\n assert_eq!(listed_after_prune_more.len(), 5);\n\n let prune_result = manager.prune(2).unwrap();\n assert_eq!(prune_result.kept.len(), 2);\n assert_eq!(prune_result.removed.len(), 3);\n\n let listed_after_prune = manager.list_backups().unwrap();\n assert_eq!(listed_after_prune.len(), 2);\n\n assert_eq!(listed_after_prune[0].id, created_backup_ids[4]);\n assert_eq!(listed_after_prune[1].id, created_backup_ids[3]);\n \n for removed_info in prune_result.removed {\n assert!(\n !backups_storage_dir.join(\u0026removed_info.id).exists(),\n \"Removed backup file {} should not exist\", removed_info.id\n );\n }\n for kept_info in prune_result.kept {\n assert!(\n backups_storage_dir.join(\u0026kept_info.id).exists(),\n \"Kept backup file {} should exist\", kept_info.id\n );\n }\n }\n\n #[test]\n fn test_restore_backup() {\n let tmp = tempdir().unwrap();\n let live_db_path = tmp.path().join(\"live_for_restore_test.db\");\n \n let initial_value = \"initial_data_for_restore\";\n {\n // FIX 3: Remove `mut` from conn here\n let conn = create_valid_live_db(\u0026live_db_path);\n conn.execute(\"DELETE FROM test_table\", []).unwrap(); \n conn.execute(\"INSERT INTO test_table (data) VALUES (?1)\", [initial_value]).unwrap();\n }\n\n let backups_dir = tmp.path().join(\"backups_for_restore_test_dir\");\n let manager = BackupManager::new(\u0026live_db_path, \u0026backups_dir).unwrap();\n\n let backup_info = manager.create_backup().unwrap();\n\n let modified_value = \"modified_data_for_restore\";\n {\n // FIX 3: Remove `mut` from conn here\n let conn = rusqlite::Connection::open(\u0026live_db_path) \n .expect(\"Failed to open live DB for modification\");\n conn.execute(\"UPDATE test_table SET data = ?1\", [modified_value])\n .expect(\"Failed to update data\");\n let modified_check: String = conn\n .query_row(\"SELECT data FROM test_table\", [], |row| row.get(0))\n .unwrap();\n assert_eq!(modified_check, modified_value);\n }\n \n manager.restore_from_backup(\u0026backup_info.id).unwrap();\n\n {\n let conn_after_restore = rusqlite::Connection::open(\u0026live_db_path)\n .expect(\"Failed to open live DB after restore\");\n let restored_data: String = conn_after_restore\n .query_row(\"SELECT data FROM test_table\", [], |row| row.get(0))\n .unwrap();\n assert_eq!(restored_data, initial_value);\n }\n }\n\n #[test]\n fn test_restore_non_existent_backup() {\n let tmp = tempdir().unwrap();\n let live_db_path = tmp.path().join(\"live_for_restore_fail_test.db\");\n let _conn = create_valid_live_db(\u0026live_db_path);\n\n let backups_dir = tmp.path().join(\"backups_for_restore_fail_test\");\n let manager = BackupManager::new(\u0026live_db_path, \u0026backups_dir).unwrap();\n\n let result = manager.restore_from_backup(\"non_existent_backup.db\");\n assert!(result.is_err());\n let err_string = result.unwrap_err().to_string();\n assert!(err_string.contains(\"Backup file not found\"), \"Error string was: {}\", err_string);\n }\n\n #[test]\n fn list_backups_with_non_backup_files() {\n let tmp = tempdir().unwrap();\n let live_db_file = tmp.path().join(\"live_for_list_test.db\");\n let _conn = create_valid_live_db(\u0026live_db_file);\n let backups_dir = tmp.path().join(\"backups_list_mixed_files_test\");\n \n let manager = BackupManager::new(\u0026live_db_file, \u0026backups_dir).unwrap();\n\n manager.create_backup().unwrap(); \n \n std::fs::write(backups_dir.join(\"not_a_backup.txt\"), \"hello\").unwrap();\n std::fs::write(\n backups_dir.join(\"backup_malformed.db.tmp\"),\n \"temp data\",\n )\n .unwrap();\n std::fs::create_dir(backups_dir.join(\"a_subdir\")).unwrap();\n\n let listed_backups = manager.list_backups().unwrap();\n assert_eq!(\n listed_backups.len(),\n 1,\n \"Should only list the valid backup file\"\n );\n assert!(listed_backups[0].id.starts_with(\"backup_\"));\n assert!(listed_backups[0].id.ends_with(\".db\"));\n }\n\n #[test]\n fn list_backups_handles_io_error_on_read_dir() {\n let tmp = tempdir().unwrap();\n let live_db_file = tmp.path().join(\"live_for_list_io_error.db\");\n let _conn = create_valid_live_db(\u0026live_db_file);\n \n let backups_dir_for_deletion = tmp.path().join(\"backups_dir_to_delete_test\");\n let manager_for_deletion = BackupManager::new(\u0026live_db_file, \u0026backups_dir_for_deletion).unwrap();\n std::fs::remove_dir_all(\u0026backups_dir_for_deletion).unwrap(); \n\n let list_res = manager_for_deletion.list_backups().unwrap();\n assert!(list_res.is_empty());\n }\n}","traces":[{"line":34,"address":[1939536,1940524,1940518],"length":1,"stats":{"Line":1}},{"line":35,"address":[],"length":0,"stats":{"Line":2}},{"line":36,"address":[],"length":0,"stats":{"Line":2}},{"line":37,"address":[1939830,1940544,1939927,1939777,1939973],"length":1,"stats":{"Line":2}},{"line":38,"address":[],"length":0,"stats":{"Line":0}},{"line":39,"address":[],"length":0,"stats":{"Line":0}},{"line":40,"address":[],"length":0,"stats":{"Line":0}},{"line":43,"address":[],"length":0,"stats":{"Line":4}},{"line":44,"address":[],"length":0,"stats":{"Line":1}},{"line":46,"address":[1940399],"length":1,"stats":{"Line":1}},{"line":47,"address":[1940324,1939941],"length":1,"stats":{"Line":2}},{"line":48,"address":[],"length":0,"stats":{"Line":1}},{"line":52,"address":[1715148,1715154,1711872],"length":1,"stats":{"Line":1}},{"line":53,"address":[1711911],"length":1,"stats":{"Line":1}},{"line":54,"address":[1712001,1712048],"length":1,"stats":{"Line":2}},{"line":55,"address":[1712248,1712164],"length":1,"stats":{"Line":2}},{"line":57,"address":[5530186,5530103],"length":1,"stats":{"Line":2}},{"line":58,"address":[5530494],"length":1,"stats":{"Line":1}},{"line":59,"address":[1712401],"length":1,"stats":{"Line":1}},{"line":60,"address":[1712409,1712500],"length":1,"stats":{"Line":2}},{"line":68,"address":[1940720],"length":1,"stats":{"Line":0}},{"line":69,"address":[1940779],"length":1,"stats":{"Line":0}},{"line":71,"address":[5443926],"length":1,"stats":{"Line":0}},{"line":75,"address":[5531016,5531116,5530945,5533143],"length":1,"stats":{"Line":2}},{"line":76,"address":[1940955],"length":1,"stats":{"Line":0}},{"line":78,"address":[1940918],"length":1,"stats":{"Line":0}},{"line":82,"address":[5444256],"length":1,"stats":{"Line":2}},{"line":84,"address":[1941194,1941135],"length":1,"stats":{"Line":0}},{"line":86,"address":[1941098],"length":1,"stats":{"Line":0}},{"line":87,"address":[5444341],"length":1,"stats":{"Line":0}},{"line":91,"address":[5531685,5531838,5533098],"length":1,"stats":{"Line":2}},{"line":92,"address":[5531673,5531603],"length":1,"stats":{"Line":4}},{"line":93,"address":[1713942],"length":1,"stats":{"Line":0}},{"line":95,"address":[1713975,1715167,1714108],"length":1,"stats":{"Line":1}},{"line":96,"address":[5444619],"length":1,"stats":{"Line":0}},{"line":98,"address":[5444582],"length":1,"stats":{"Line":0}},{"line":102,"address":[1714546],"length":1,"stats":{"Line":1}},{"line":103,"address":[1714205],"length":1,"stats":{"Line":1}},{"line":104,"address":[1714324,1714245],"length":1,"stats":{"Line":2}},{"line":105,"address":[5532407],"length":1,"stats":{"Line":1}},{"line":106,"address":[1714538],"length":1,"stats":{"Line":1}},{"line":110,"address":[1718888,1715312,1718643],"length":1,"stats":{"Line":2}},{"line":111,"address":[1715351],"length":1,"stats":{"Line":2}},{"line":113,"address":[1715471,1715388],"length":1,"stats":{"Line":3}},{"line":114,"address":[1715506],"length":1,"stats":{"Line":1}},{"line":117,"address":[5444736],"length":1,"stats":{"Line":3}},{"line":118,"address":[1941615],"length":1,"stats":{"Line":0}},{"line":120,"address":[1941574],"length":1,"stats":{"Line":0}},{"line":123,"address":[1718853,1716004,1716210],"length":1,"stats":{"Line":2}},{"line":124,"address":[1716378],"length":1,"stats":{"Line":1}},{"line":126,"address":[1716457,1716537],"length":1,"stats":{"Line":2}},{"line":127,"address":[1716598],"length":1,"stats":{"Line":1}},{"line":128,"address":[1716763],"length":1,"stats":{"Line":1}},{"line":129,"address":[1716885],"length":1,"stats":{"Line":1}},{"line":130,"address":[1717009],"length":1,"stats":{"Line":1}},{"line":134,"address":[5535107],"length":1,"stats":{"Line":1}},{"line":135,"address":[5535208],"length":1,"stats":{"Line":1}},{"line":136,"address":[1717208,1717335],"length":1,"stats":{"Line":0}},{"line":137,"address":[1717389],"length":1,"stats":{"Line":0}},{"line":139,"address":[5444934,5444912],"length":1,"stats":{"Line":0}},{"line":140,"address":[1717657,1718780],"length":1,"stats":{"Line":0}},{"line":145,"address":[5535268],"length":1,"stats":{"Line":1}},{"line":146,"address":[5535839],"length":1,"stats":{"Line":1}},{"line":147,"address":[1717887],"length":1,"stats":{"Line":1}},{"line":148,"address":[1717953],"length":1,"stats":{"Line":0}},{"line":149,"address":[5536073,5535973],"length":1,"stats":{"Line":0}},{"line":150,"address":[1718154],"length":1,"stats":{"Line":0}},{"line":153,"address":[1718670,1718012],"length":1,"stats":{"Line":0}},{"line":157,"address":[5536030],"length":1,"stats":{"Line":1}},{"line":159,"address":[1718191,1718649],"length":1,"stats":{"Line":1}},{"line":160,"address":[1718506],"length":1,"stats":{"Line":1}},{"line":161,"address":[1718384],"length":1,"stats":{"Line":1}},{"line":163,"address":[1718427],"length":1,"stats":{"Line":1}},{"line":164,"address":[1718498],"length":1,"stats":{"Line":1}},{"line":171,"address":[1716041],"length":1,"stats":{"Line":3}},{"line":172,"address":[1716095],"length":1,"stats":{"Line":1}},{"line":175,"address":[1718912,1720660,1720827],"length":1,"stats":{"Line":1}},{"line":176,"address":[1718955],"length":1,"stats":{"Line":1}},{"line":178,"address":[1719157],"length":1,"stats":{"Line":1}},{"line":179,"address":[5537212],"length":1,"stats":{"Line":1}},{"line":181,"address":[5537272,5538907,5537344],"length":1,"stats":{"Line":3}},{"line":182,"address":[1720686,1719386],"length":1,"stats":{"Line":1}},{"line":184,"address":[1720555,1719326,1719576,1719457],"length":1,"stats":{"Line":4}},{"line":185,"address":[1719682],"length":1,"stats":{"Line":1}},{"line":186,"address":[1720666,1719968],"length":1,"stats":{"Line":2}},{"line":188,"address":[1720140,1719944],"length":1,"stats":{"Line":2}},{"line":189,"address":[1720243,1720175],"length":1,"stats":{"Line":2}},{"line":190,"address":[1941952],"length":1,"stats":{"Line":1}},{"line":191,"address":[1942011],"length":1,"stats":{"Line":0}},{"line":193,"address":[1941974],"length":1,"stats":{"Line":0}},{"line":197,"address":[5538319],"length":1,"stats":{"Line":1}},{"line":201,"address":[1719716],"length":1,"stats":{"Line":1}},{"line":204,"address":[1720864,1721712,1721706],"length":1,"stats":{"Line":1}},{"line":205,"address":[1720919],"length":1,"stats":{"Line":1}},{"line":206,"address":[1721063,1721117,1720980],"length":1,"stats":{"Line":3}},{"line":207,"address":[1721258],"length":1,"stats":{"Line":1}},{"line":209,"address":[5539188,5539315],"length":1,"stats":{"Line":2}},{"line":213,"address":[1721185,1721613,1721693,1721513],"length":1,"stats":{"Line":2}},{"line":214,"address":[1942191,1942250],"length":1,"stats":{"Line":0}},{"line":216,"address":[1942154],"length":1,"stats":{"Line":0}},{"line":217,"address":[1942213],"length":1,"stats":{"Line":0}},{"line":220,"address":[1721647],"length":1,"stats":{"Line":1}}],"covered":73,"coverable":102},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","config.rs"],"content":"use anyhow::Result;\nuse directories::ProjectDirs;\nuse std::{\n collections::hash_map::DefaultHasher,\n hash::{Hash, Hasher},\n path::{Path, PathBuf},\n};\n\n/// Runtime configuration (currently just the DB path).\n#[derive(Debug, Clone)]\npub struct Config {\n pub db_path: PathBuf,\n}\n\nimpl Config {\n /// Resolve configuration from environment or derive one per-workspace.\n ///\n /// Priority:\n /// 1. `MARLIN_DB_PATH` env-var (explicit override)\n /// 2. *Workspace-local* file under XDG data dir\n /// (`~/.local/share/marlin/index_\u003chash\u003e.db`)\n /// 3. Fallback to `./index.db` when we cannot locate an XDG dir\n pub fn load() -\u003e Result\u003cSelf\u003e {\n // 1) explicit override\n if let Some(val) = std::env::var_os(\"MARLIN_DB_PATH\") {\n let p = PathBuf::from(val);\n std::fs::create_dir_all(p.parent().expect(\"has parent\"))?;\n return Ok(Self { db_path: p });\n }\n\n // 2) derive per-workspace DB name from CWD hash\n let cwd = std::env::current_dir()?;\n let mut h = DefaultHasher::new();\n cwd.hash(\u0026mut h);\n let digest = h.finish(); // 64-bit\n let file_name = format!(\"index_{digest:016x}.db\");\n\n if let Some(dirs) = ProjectDirs::from(\"io\", \"Marlin\", \"marlin\") {\n let dir = dirs.data_dir();\n std::fs::create_dir_all(dir)?;\n return Ok(Self {\n db_path: dir.join(file_name),\n });\n }\n\n // 3) very last resort – workspace-relative DB\n Ok(Self {\n db_path: Path::new(\u0026file_name).to_path_buf(),\n })\n }\n}\n","traces":[{"line":23,"address":[5516623,5515840,5516629],"length":1,"stats":{"Line":1}},{"line":25,"address":[2078215],"length":1,"stats":{"Line":1}},{"line":26,"address":[2078350],"length":1,"stats":{"Line":2}},{"line":27,"address":[2078919,2078553,2078371],"length":1,"stats":{"Line":4}},{"line":28,"address":[2078768],"length":1,"stats":{"Line":2}},{"line":32,"address":[2078411,2078990],"length":1,"stats":{"Line":1}},{"line":33,"address":[2079092],"length":1,"stats":{"Line":1}},{"line":34,"address":[2079143],"length":1,"stats":{"Line":1}},{"line":35,"address":[2079178],"length":1,"stats":{"Line":1}},{"line":36,"address":[2079203],"length":1,"stats":{"Line":1}},{"line":38,"address":[2079590,2079488],"length":1,"stats":{"Line":2}},{"line":39,"address":[2079667,2079760],"length":1,"stats":{"Line":2}},{"line":40,"address":[2080093,2079792],"length":1,"stats":{"Line":1}},{"line":41,"address":[2079991],"length":1,"stats":{"Line":1}},{"line":42,"address":[2079920],"length":1,"stats":{"Line":1}},{"line":47,"address":[2080213],"length":1,"stats":{"Line":0}},{"line":48,"address":[2080159],"length":1,"stats":{"Line":0}}],"covered":15,"coverable":17},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","config_tests.rs"],"content":"// libmarlin/src/config_tests.rs\n\nuse super::config::Config;\nuse std::env;\nuse tempfile::tempdir;\n\n#[test]\nfn load_env_override() {\n let tmp = tempdir().unwrap();\n let db = tmp.path().join(\"custom.db\");\n env::set_var(\"MARLIN_DB_PATH\", \u0026db);\n let cfg = Config::load().unwrap();\n assert_eq!(cfg.db_path, db);\n env::remove_var(\"MARLIN_DB_PATH\");\n}\n\n#[test]\nfn load_xdg_or_fallback() {\n // since XDG_DATA_HOME will normally be present, just test it doesn't error\n let cfg = Config::load().unwrap();\n assert!(cfg.db_path.to_string_lossy().ends_with(\".db\"));\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","db","database.rs"],"content":"//! Database abstraction for Marlin\n//! \n//! This module provides a database abstraction layer that wraps the SQLite connection\n//! and provides methods for common database operations.\n\nuse rusqlite::Connection;\nuse std::path::PathBuf;\nuse anyhow::Result;\n\n/// Options for indexing files\n#[derive(Debug, Clone)]\npub struct IndexOptions {\n /// Only update files marked as dirty\n pub dirty_only: bool,\n \n /// Index file contents (not just metadata)\n pub index_contents: bool,\n \n /// Maximum file size to index (in bytes)\n pub max_size: Option\u003cu64\u003e,\n}\n\nimpl Default for IndexOptions {\n fn default() -\u003e Self {\n Self {\n dirty_only: false,\n index_contents: true,\n max_size: Some(1_000_000), // 1MB default limit\n }\n }\n}\n\n/// Database wrapper for Marlin\npub struct Database {\n /// The SQLite connection\n conn: Connection,\n}\n\nimpl Database {\n /// Create a new database wrapper around an existing connection\n pub fn new(conn: Connection) -\u003e Self {\n Self { conn }\n }\n \n /// Get a reference to the underlying connection\n pub fn conn(\u0026self) -\u003e \u0026Connection {\n \u0026self.conn\n }\n \n /// Get a mutable reference to the underlying connection\n pub fn conn_mut(\u0026mut self) -\u003e \u0026mut Connection {\n \u0026mut self.conn\n }\n \n /// Index one or more files\n pub fn index_files(\u0026mut self, paths: \u0026[PathBuf], _options: \u0026IndexOptions) -\u003e Result\u003cusize\u003e {\n // In a real implementation, this would index the files\n // For now, we just return the number of files \"indexed\"\n if paths.is_empty() { // Add a branch for coverage\n return Ok(0);\n }\n Ok(paths.len())\n }\n \n /// Remove files from the index\n pub fn remove_files(\u0026mut self, paths: \u0026[PathBuf]) -\u003e Result\u003cusize\u003e {\n // In a real implementation, this would remove the files\n // For now, we just return the number of files \"removed\"\n if paths.is_empty() { // Add a branch for coverage\n return Ok(0);\n }\n Ok(paths.len())\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n use crate::db::open as open_marlin_db; // Use your project's DB open function\n use tempfile::tempdir;\n use std::fs::File;\n\n fn setup_db() -\u003e Database {\n let conn = open_marlin_db(\":memory:\").expect(\"Failed to open in-memory DB\");\n Database::new(conn)\n }\n\n #[test]\n fn test_database_new_conn_conn_mut() {\n let mut db = setup_db();\n let _conn_ref = db.conn();\n let _conn_mut_ref = db.conn_mut();\n // Just checking they don't panic and can be called.\n }\n\n #[test]\n fn test_index_files_stub() {\n let mut db = setup_db();\n let tmp = tempdir().unwrap();\n let file1 = tmp.path().join(\"file1.txt\");\n File::create(\u0026file1).unwrap();\n\n let paths = vec![file1.to_path_buf()];\n let options = IndexOptions::default();\n \n assert_eq!(db.index_files(\u0026paths, \u0026options).unwrap(), 1);\n assert_eq!(db.index_files(\u0026[], \u0026options).unwrap(), 0); // Test empty case\n }\n\n #[test]\n fn test_remove_files_stub() {\n let mut db = setup_db();\n let tmp = tempdir().unwrap();\n let file1 = tmp.path().join(\"file1.txt\");\n File::create(\u0026file1).unwrap(); // File doesn't need to be in DB for this stub\n\n let paths = vec![file1.to_path_buf()];\n \n assert_eq!(db.remove_files(\u0026paths).unwrap(), 1);\n assert_eq!(db.remove_files(\u0026[]).unwrap(), 0); // Test empty case\n }\n\n #[test]\n fn test_index_options_default() {\n let options = IndexOptions::default();\n assert!(!options.dirty_only);\n assert!(options.index_contents);\n assert_eq!(options.max_size, Some(1_000_000));\n }\n}\n","traces":[{"line":24,"address":[5509984],"length":1,"stats":{"Line":1}},{"line":28,"address":[2083427],"length":1,"stats":{"Line":1}},{"line":41,"address":[2083472],"length":1,"stats":{"Line":3}},{"line":46,"address":[2083504],"length":1,"stats":{"Line":1}},{"line":51,"address":[2083520],"length":1,"stats":{"Line":1}},{"line":56,"address":[2083536],"length":1,"stats":{"Line":1}},{"line":59,"address":[2083589],"length":1,"stats":{"Line":1}},{"line":60,"address":[2083619],"length":1,"stats":{"Line":1}},{"line":62,"address":[2083603],"length":1,"stats":{"Line":1}},{"line":66,"address":[2083664],"length":1,"stats":{"Line":1}},{"line":69,"address":[2083714],"length":1,"stats":{"Line":1}},{"line":70,"address":[2083744],"length":1,"stats":{"Line":1}},{"line":72,"address":[2083728],"length":1,"stats":{"Line":1}}],"covered":13,"coverable":13},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","db","mod.rs"],"content":"//! Central DB helper – connection bootstrap, migrations **and** most\n//! data-access helpers (tags, links, collections, saved views, …).\n\nmod database;\npub use database::{Database, IndexOptions};\n\nuse std::{\n fs,\n path::{Path, PathBuf},\n};\n\nuse std::result::Result as StdResult;\nuse anyhow::{Context, Result};\nuse chrono::Local;\nuse rusqlite::{\n backup::{Backup, StepResult},\n params,\n Connection,\n OpenFlags,\n OptionalExtension,\n TransactionBehavior,\n};\nuse tracing::{debug, info, warn};\n\n/* ─── embedded migrations ─────────────────────────────────────────── */\n\nconst MIGRATIONS: \u0026[(\u0026str, \u0026str)] = \u0026[\n (\"0001_initial_schema.sql\", include_str!(\"migrations/0001_initial_schema.sql\")),\n (\"0002_update_fts_and_triggers.sql\", include_str!(\"migrations/0002_update_fts_and_triggers.sql\")),\n (\"0003_create_links_collections_views.sql\", include_str!(\"migrations/0003_create_links_collections_views.sql\")),\n (\"0004_fix_hierarchical_tags_fts.sql\", include_str!(\"migrations/0004_fix_hierarchical_tags_fts.sql\")),\n (\"0005_add_dirty_table.sql\", include_str!(\"migrations/0005_add_dirty_table.sql\")),\n];\n\n/* ─── connection bootstrap ────────────────────────────────────────── */\n\npub fn open\u003cP: AsRef\u003cPath\u003e\u003e(db_path: P) -\u003e Result\u003cConnection\u003e {\n let db_path_ref = db_path.as_ref();\n let mut conn = Connection::open(db_path_ref)\n .with_context(|| format!(\"failed to open DB at {}\", db_path_ref.display()))?;\n\n conn.pragma_update(None, \"journal_mode\", \"WAL\")?;\n conn.pragma_update(None, \"foreign_keys\", \"ON\")?;\n\n // Wait up to 30 s for a competing writer before giving up\n conn.busy_timeout(std::time::Duration::from_secs(30))?;\n\n apply_migrations(\u0026mut conn)?;\n Ok(conn)\n}\n\n/* ─── migration runner ────────────────────────────────────────────── */\n\npub(crate) fn apply_migrations(conn: \u0026mut Connection) -\u003e Result\u003c()\u003e {\n // Ensure schema_version bookkeeping table exists\n conn.execute_batch(\n \"CREATE TABLE IF NOT EXISTS schema_version (\n version INTEGER PRIMARY KEY,\n applied_on TEXT NOT NULL\n );\",\n )?;\n\n // Legacy patch – ignore errors if column already exists\n let _ = conn.execute(\"ALTER TABLE schema_version ADD COLUMN applied_on TEXT\", []);\n\n // Grab the write-lock up-front so migrations can run uninterrupted\n let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;\n\n for (fname, sql) in MIGRATIONS {\n let version: i64 = fname\n .split('_')\n .next()\n .and_then(|s| s.parse().ok())\n .expect(\"migration filenames start with number\");\n\n let already: Option\u003ci64\u003e = tx\n .query_row(\n \"SELECT version FROM schema_version WHERE version = ?1\",\n [version],\n |r| r.get(0),\n )\n .optional()?;\n\n if already.is_some() {\n debug!(\"migration {} already applied\", fname);\n continue;\n }\n\n info!(\"applying migration {}\", fname);\n tx.execute_batch(sql)\n .with_context(|| format!(\"could not apply migration {}\", fname))?;\n\n tx.execute(\n \"INSERT INTO schema_version (version, applied_on) VALUES (?1, ?2)\",\n params![version, Local::now().to_rfc3339()],\n )?;\n }\n\n tx.commit()?;\n\n // sanity – warn if any embedded migration got skipped\n let mut missing = Vec::new();\n for (fname, _) in MIGRATIONS {\n let v: i64 = fname.split('_').next().unwrap().parse().unwrap();\n let ok: bool = conn\n .query_row(\n \"SELECT 1 FROM schema_version WHERE version = ?1\",\n [v],\n |_| Ok(true),\n )\n .optional()?\n .unwrap_or(false);\n if !ok {\n missing.push(v);\n }\n }\n if !missing.is_empty() {\n warn!(\"migrations not applied: {:?}\", missing);\n }\n\n Ok(())\n}\n\n/* ─── tag helpers ─────────────────────────────────────────────────── */\n\npub fn ensure_tag_path(conn: \u0026Connection, path: \u0026str) -\u003e Result\u003ci64\u003e {\n let mut parent: Option\u003ci64\u003e = None;\n for segment in path.split('/').filter(|s| !s.is_empty()) {\n conn.execute(\n \"INSERT OR IGNORE INTO tags(name, parent_id) VALUES (?1, ?2)\",\n params![segment, parent],\n )?;\n let id: i64 = conn.query_row(\n \"SELECT id FROM tags WHERE name = ?1 AND (parent_id IS ?2 OR parent_id = ?2)\",\n params![segment, parent],\n |r| r.get(0),\n )?;\n parent = Some(id);\n }\n parent.ok_or_else(|| anyhow::anyhow!(\"empty tag path\"))\n}\n\npub fn file_id(conn: \u0026Connection, path: \u0026str) -\u003e Result\u003ci64\u003e {\n conn.query_row(\"SELECT id FROM files WHERE path = ?1\", [path], |r| r.get(0))\n .map_err(|_| anyhow::anyhow!(\"file not indexed: {}\", path))\n}\n\n/* ─── attributes ──────────────────────────────────────────────────── */\n\npub fn upsert_attr(conn: \u0026Connection, file_id: i64, key: \u0026str, value: \u0026str) -\u003e Result\u003c()\u003e {\n conn.execute(\n r#\"\n INSERT INTO attributes(file_id, key, value)\n VALUES (?1, ?2, ?3)\n ON CONFLICT(file_id, key) DO UPDATE SET value = excluded.value\n \"#,\n params![file_id, key, value],\n )?;\n Ok(())\n}\n\n/* ─── links ───────────────────────────────────────────────────────── */\n\npub fn add_link(\n conn: \u0026Connection,\n src_file_id: i64,\n dst_file_id: i64,\n link_type: Option\u003c\u0026str\u003e,\n) -\u003e Result\u003c()\u003e {\n conn.execute(\n \"INSERT INTO links(src_file_id, dst_file_id, type)\n VALUES (?1, ?2, ?3)\n ON CONFLICT(src_file_id, dst_file_id, type) DO NOTHING\",\n params![src_file_id, dst_file_id, link_type],\n )?;\n Ok(())\n}\n\npub fn remove_link(\n conn: \u0026Connection,\n src_file_id: i64,\n dst_file_id: i64,\n link_type: Option\u003c\u0026str\u003e,\n) -\u003e Result\u003c()\u003e {\n conn.execute(\n \"DELETE FROM links\n WHERE src_file_id = ?1\n AND dst_file_id = ?2\n AND (type IS ?3 OR type = ?3)\",\n params![src_file_id, dst_file_id, link_type],\n )?;\n Ok(())\n}\n\npub fn list_links(\n conn: \u0026Connection,\n pattern: \u0026str,\n direction: Option\u003c\u0026str\u003e,\n link_type: Option\u003c\u0026str\u003e,\n) -\u003e Result\u003cVec\u003c(String, String, Option\u003cString\u003e)\u003e\u003e {\n let like_pattern = pattern.replace('*', \"%\");\n\n // Files matching pattern\n let mut stmt = conn.prepare(\"SELECT id, path FROM files WHERE path LIKE ?1\")?;\n let rows = stmt\n .query_map(params![like_pattern], |r| {\n Ok((r.get::\u003c_, i64\u003e(0)?, r.get::\u003c_, String\u003e(1)?))\n })?\n .collect::\u003cStdResult\u003cVec\u003c_\u003e, _\u003e\u003e()?;\n\n let mut out = Vec::new();\n for (fid, fpath) in rows {\n let (src_col, dst_col) = match direction {\n Some(\"in\") =\u003e (\"dst_file_id\", \"src_file_id\"),\n _ =\u003e (\"src_file_id\", \"dst_file_id\"),\n };\n\n let sql = format!(\n \"SELECT f2.path, l.type\n FROM links l\n JOIN files f2 ON f2.id = l.{dst_col}\n WHERE l.{src_col} = ?1\n AND (?2 IS NULL OR l.type = ?2)\",\n );\n\n let mut stmt2 = conn.prepare(\u0026sql)?;\n let links = stmt2\n .query_map(params![fid, link_type], |r| {\n Ok((r.get::\u003c_, String\u003e(0)?, r.get::\u003c_, Option\u003cString\u003e\u003e(1)?))\n })?\n .collect::\u003cStdResult\u003cVec\u003c_\u003e, _\u003e\u003e()?;\n\n for (other, typ) in links {\n out.push((fpath.clone(), other, typ));\n }\n }\n Ok(out)\n}\n\npub fn find_backlinks(\n conn: \u0026Connection,\n pattern: \u0026str,\n) -\u003e Result\u003cVec\u003c(String, Option\u003cString\u003e)\u003e\u003e {\n let like = pattern.replace('*', \"%\");\n\n let mut stmt = conn.prepare(\n \"SELECT f1.path, l.type\n FROM links l\n JOIN files f1 ON f1.id = l.src_file_id\n JOIN files f2 ON f2.id = l.dst_file_id\n WHERE f2.path LIKE ?1\",\n )?;\n\n let rows = stmt.query_map([like], |r| {\n Ok((r.get::\u003c_, String\u003e(0)?, r.get::\u003c_, Option\u003cString\u003e\u003e(1)?))\n })?;\n\n let out = rows.collect::\u003cStdResult\u003cVec\u003c_\u003e, _\u003e\u003e()?;\n Ok(out)\n}\n\n/* ─── collections helpers ────────────────────────────────────────── */\n\npub fn ensure_collection(conn: \u0026Connection, name: \u0026str) -\u003e Result\u003ci64\u003e {\n conn.execute(\n \"INSERT OR IGNORE INTO collections(name) VALUES (?1)\",\n params![name],\n )?;\n conn.query_row(\n \"SELECT id FROM collections WHERE name = ?1\",\n params![name],\n |r| r.get(0),\n )\n .context(\"collection lookup failed\")\n}\n\npub fn add_file_to_collection(conn: \u0026Connection, coll_id: i64, file_id: i64) -\u003e Result\u003c()\u003e {\n conn.execute(\n \"INSERT OR IGNORE INTO collection_files(collection_id, file_id)\n VALUES (?1, ?2)\",\n params![coll_id, file_id],\n )?;\n Ok(())\n}\n\npub fn list_collection(conn: \u0026Connection, name: \u0026str) -\u003e Result\u003cVec\u003cString\u003e\u003e {\n let mut stmt = conn.prepare(\n r#\"SELECT f.path\n FROM collections c\n JOIN collection_files cf ON cf.collection_id = c.id\n JOIN files f ON f.id = cf.file_id\n WHERE c.name = ?1\n ORDER BY f.path\"#,\n )?;\n\n let rows = stmt.query_map([name], |r| r.get::\u003c_, String\u003e(0))?;\n let list = rows.collect::\u003cStdResult\u003cVec\u003c_\u003e, _\u003e\u003e()?;\n Ok(list)\n}\n\n/* ─── saved views (smart folders) ───────────────────────────────── */\n\npub fn save_view(conn: \u0026Connection, name: \u0026str, query: \u0026str) -\u003e Result\u003c()\u003e {\n conn.execute(\n \"INSERT INTO views(name, query)\n VALUES (?1, ?2)\n ON CONFLICT(name) DO UPDATE SET query = excluded.query\",\n params![name, query],\n )?;\n Ok(())\n}\n\npub fn list_views(conn: \u0026Connection) -\u003e Result\u003cVec\u003c(String, String)\u003e\u003e {\n let mut stmt = conn.prepare(\"SELECT name, query FROM views ORDER BY name\")?;\n let rows = stmt.query_map([], |r| Ok((r.get::\u003c_, String\u003e(0)?, r.get::\u003c_, String\u003e(1)?)))?;\n let list = rows.collect::\u003cStdResult\u003cVec\u003c_\u003e, _\u003e\u003e()?;\n Ok(list)\n}\n\npub fn view_query(conn: \u0026Connection, name: \u0026str) -\u003e Result\u003cString\u003e {\n conn.query_row(\n \"SELECT query FROM views WHERE name = ?1\",\n [name],\n |r| r.get::\u003c_, String\u003e(0),\n )\n .context(format!(\"no view called '{}'\", name))\n}\n\n/* ─── dirty‐scan helpers ─────────────────────────────────────────── */\n\n/// Mark a file as “dirty” so it’ll be picked up by `scan_dirty`.\npub fn mark_dirty(conn: \u0026Connection, file_id: i64) -\u003e Result\u003c()\u003e {\n conn.execute(\n \"INSERT OR IGNORE INTO file_changes(file_id, marked_at)\n VALUES (?1, strftime('%s','now'))\",\n params![file_id],\n )?;\n Ok(())\n}\n\n/// Take and clear all dirty file IDs for incremental re-scan.\npub fn take_dirty(conn: \u0026Connection) -\u003e Result\u003cVec\u003ci64\u003e\u003e {\n let mut ids = Vec::new();\n {\n let mut stmt = conn.prepare(\"SELECT file_id FROM file_changes\")?;\n for row in stmt.query_map([], |r| r.get(0))? {\n ids.push(row?);\n }\n }\n conn.execute(\"DELETE FROM file_changes\", [])?;\n Ok(ids)\n}\n\n/* ─── backup / restore helpers ────────────────────────────────────── */\n\npub fn backup\u003cP: AsRef\u003cPath\u003e\u003e(db_path: P) -\u003e Result\u003cPathBuf\u003e {\n let src = db_path.as_ref();\n let dir = src\n .parent()\n .ok_or_else(|| anyhow::anyhow!(\"invalid DB path: {}\", src.display()))?\n .join(\"backups\");\n fs::create_dir_all(\u0026dir)?;\n\n let stamp = Local::now().format(\"%Y-%m-%d_%H-%M-%S\");\n let dst = dir.join(format!(\"backup_{stamp}.db\"));\n\n let src_conn = Connection::open_with_flags(src, OpenFlags::SQLITE_OPEN_READ_ONLY)?;\n let mut dst_conn = Connection::open(\u0026dst)?;\n\n let bk = Backup::new(\u0026src_conn, \u0026mut dst_conn)?;\n while let StepResult::More = bk.step(100)? {}\n Ok(dst)\n}\n\npub fn restore\u003cP: AsRef\u003cPath\u003e\u003e(backup_path: P, live_db_path: P) -\u003e Result\u003c()\u003e {\n fs::copy(\u0026backup_path, \u0026live_db_path)?;\n Ok(())\n}\n\n/* ─── tests ───────────────────────────────────────────────────────── */\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn migrations_apply_in_memory() {\n open(\":memory:\").expect(\"all migrations apply\");\n }\n}\n","traces":[{"line":37,"address":[5479584,5479632,5480989],"length":1,"stats":{"Line":9}},{"line":38,"address":[],"length":0,"stats":{"Line":20}},{"line":39,"address":[5479709,5479827,5480995],"length":1,"stats":{"Line":10}},{"line":40,"address":[],"length":0,"stats":{"Line":0}},{"line":42,"address":[1761913,1760418,1759050,1760529,1761802,1759161,1761337,1762721,1759969],"length":1,"stats":{"Line":14}},{"line":43,"address":[1762719,1760687,1759967,1762071,1759319,1761335],"length":1,"stats":{"Line":5}},{"line":46,"address":[1762304,1762717,1759965,1760920,1761333,1759552],"length":1,"stats":{"Line":6}},{"line":48,"address":[],"length":0,"stats":{"Line":5}},{"line":49,"address":[5480888],"length":1,"stats":{"Line":4}},{"line":54,"address":[5373047,5370432,5375769],"length":1,"stats":{"Line":1}},{"line":56,"address":[5370458,5370577],"length":1,"stats":{"Line":1}},{"line":64,"address":[5370619],"length":1,"stats":{"Line":1}},{"line":67,"address":[2110984],"length":1,"stats":{"Line":1}},{"line":69,"address":[2111183,2111275],"length":1,"stats":{"Line":2}},{"line":70,"address":[2111405,2113314],"length":1,"stats":{"Line":2}},{"line":73,"address":[5481184,5481204],"length":1,"stats":{"Line":2}},{"line":76,"address":[5375727,5373266,5373476,5373227],"length":1,"stats":{"Line":4}},{"line":79,"address":[5373250],"length":1,"stats":{"Line":1}},{"line":80,"address":[5481248,5481264],"length":1,"stats":{"Line":2}},{"line":84,"address":[5373561],"length":1,"stats":{"Line":1}},{"line":85,"address":[5373628,5375061],"length":1,"stats":{"Line":2}},{"line":89,"address":[5373666,5373600,5373957],"length":1,"stats":{"Line":4}},{"line":90,"address":[2114628,2114759,2114154,2115228],"length":1,"stats":{"Line":3}},{"line":91,"address":[2114743],"length":1,"stats":{"Line":0}},{"line":93,"address":[2114995,2115123,2114910,2114789,2115193],"length":1,"stats":{"Line":15}},{"line":95,"address":[2114801],"length":1,"stats":{"Line":1}},{"line":99,"address":[5371140,5373061],"length":1,"stats":{"Line":2}},{"line":102,"address":[5371343],"length":1,"stats":{"Line":6}},{"line":103,"address":[2111661,2111752],"length":1,"stats":{"Line":8}},{"line":104,"address":[2112731,2111854],"length":1,"stats":{"Line":8}},{"line":105,"address":[2112913,2113111,2113248],"length":1,"stats":{"Line":2}},{"line":108,"address":[2112905],"length":1,"stats":{"Line":6}},{"line":109,"address":[1763504,1763512],"length":1,"stats":{"Line":8}},{"line":113,"address":[2113197],"length":1,"stats":{"Line":6}},{"line":114,"address":[5372962],"length":1,"stats":{"Line":0}},{"line":117,"address":[5371601],"length":1,"stats":{"Line":2}},{"line":118,"address":[2111988,2111923],"length":1,"stats":{"Line":0}},{"line":121,"address":[2111951],"length":1,"stats":{"Line":6}},{"line":126,"address":[5375808],"length":1,"stats":{"Line":1}},{"line":127,"address":[5375844],"length":1,"stats":{"Line":1}},{"line":128,"address":[2116766,2116029],"length":1,"stats":{"Line":4}},{"line":129,"address":[2116433,2116264],"length":1,"stats":{"Line":1}},{"line":131,"address":[5376032],"length":1,"stats":{"Line":1}},{"line":133,"address":[2116545,2116669],"length":1,"stats":{"Line":1}},{"line":135,"address":[5376316],"length":1,"stats":{"Line":1}},{"line":136,"address":[1763600,1763584],"length":1,"stats":{"Line":2}},{"line":138,"address":[2116720],"length":1,"stats":{"Line":1}},{"line":140,"address":[2116364],"length":1,"stats":{"Line":1}},{"line":143,"address":[2116784],"length":1,"stats":{"Line":0}},{"line":144,"address":[2116809],"length":1,"stats":{"Line":0}},{"line":145,"address":[1763761,1763744],"length":1,"stats":{"Line":0}},{"line":150,"address":[2116896],"length":1,"stats":{"Line":1}},{"line":151,"address":[2117135,2117026],"length":1,"stats":{"Line":1}},{"line":157,"address":[2116942],"length":1,"stats":{"Line":1}},{"line":159,"address":[2117175],"length":1,"stats":{"Line":1}},{"line":164,"address":[5377024],"length":1,"stats":{"Line":1}},{"line":170,"address":[2117431,2117322],"length":1,"stats":{"Line":1}},{"line":174,"address":[5377064],"length":1,"stats":{"Line":1}},{"line":176,"address":[2117471],"length":1,"stats":{"Line":1}},{"line":179,"address":[2117504],"length":1,"stats":{"Line":1}},{"line":185,"address":[2117735,2117626],"length":1,"stats":{"Line":1}},{"line":190,"address":[2117544],"length":1,"stats":{"Line":1}},{"line":192,"address":[2117775],"length":1,"stats":{"Line":1}},{"line":195,"address":[2121829,2117808,2121615],"length":1,"stats":{"Line":1}},{"line":201,"address":[2117916],"length":1,"stats":{"Line":1}},{"line":204,"address":[2117985,2121827,2118053],"length":1,"stats":{"Line":2}},{"line":205,"address":[2118803,2118489,2118422,2118602,2121807],"length":1,"stats":{"Line":2}},{"line":206,"address":[2118370],"length":1,"stats":{"Line":2}},{"line":207,"address":[1764030],"length":1,"stats":{"Line":1}},{"line":211,"address":[2118908],"length":1,"stats":{"Line":1}},{"line":212,"address":[2119056,2118956,2119183],"length":1,"stats":{"Line":3}},{"line":213,"address":[2119644,2119284],"length":1,"stats":{"Line":2}},{"line":214,"address":[5379460,5379323],"length":1,"stats":{"Line":0}},{"line":215,"address":[2119480],"length":1,"stats":{"Line":1}},{"line":218,"address":[5379584],"length":1,"stats":{"Line":1}},{"line":226,"address":[2121711,2119886,2119970],"length":1,"stats":{"Line":2}},{"line":227,"address":[2120758,2121661,2120377,2120557,2120444,2121690],"length":1,"stats":{"Line":2}},{"line":228,"address":[2120297],"length":1,"stats":{"Line":2}},{"line":229,"address":[5483060,5482462],"length":1,"stats":{"Line":1}},{"line":233,"address":[2120855,2120959,2121576,2121086],"length":1,"stats":{"Line":4}},{"line":234,"address":[5381119,5381277],"length":1,"stats":{"Line":2}},{"line":237,"address":[5379217],"length":1,"stats":{"Line":1}},{"line":240,"address":[2123080,2121856,2123125],"length":1,"stats":{"Line":1}},{"line":244,"address":[2121926],"length":1,"stats":{"Line":1}},{"line":246,"address":[5382118,5381991,5381923,5383068],"length":1,"stats":{"Line":2}},{"line":254,"address":[5382434,5382567,5383054,5382295],"length":1,"stats":{"Line":3}},{"line":255,"address":[1765246,1765829],"length":1,"stats":{"Line":1}},{"line":258,"address":[2123036,2122796,2122700],"length":1,"stats":{"Line":2}},{"line":259,"address":[5382932],"length":1,"stats":{"Line":1}},{"line":264,"address":[2123168],"length":1,"stats":{"Line":1}},{"line":265,"address":[5383309,5383199],"length":1,"stats":{"Line":1}},{"line":267,"address":[2123203],"length":1,"stats":{"Line":1}},{"line":269,"address":[5383387],"length":1,"stats":{"Line":1}},{"line":271,"address":[5383359],"length":1,"stats":{"Line":1}},{"line":272,"address":[1765904,1765920],"length":1,"stats":{"Line":2}},{"line":277,"address":[2123520],"length":1,"stats":{"Line":1}},{"line":278,"address":[2123713,2123604],"length":1,"stats":{"Line":1}},{"line":281,"address":[5383518],"length":1,"stats":{"Line":1}},{"line":283,"address":[2123753],"length":1,"stats":{"Line":1}},{"line":286,"address":[2124809,2124801,2123776],"length":1,"stats":{"Line":1}},{"line":287,"address":[2123826,2123938],"length":1,"stats":{"Line":1}},{"line":296,"address":[1765987,1765952],"length":1,"stats":{"Line":4}},{"line":297,"address":[2124409,2124505,2124760],"length":1,"stats":{"Line":2}},{"line":298,"address":[5384702],"length":1,"stats":{"Line":1}},{"line":303,"address":[2124832],"length":1,"stats":{"Line":1}},{"line":304,"address":[2125035,2124926],"length":1,"stats":{"Line":1}},{"line":308,"address":[2124872],"length":1,"stats":{"Line":1}},{"line":310,"address":[2125075],"length":1,"stats":{"Line":1}},{"line":313,"address":[2126071,2125104,2126063],"length":1,"stats":{"Line":1}},{"line":314,"address":[2125128],"length":1,"stats":{"Line":1}},{"line":315,"address":[5484014,5483968],"length":1,"stats":{"Line":4}},{"line":316,"address":[2126022,2125673,2125769],"length":1,"stats":{"Line":2}},{"line":317,"address":[2125955],"length":1,"stats":{"Line":1}},{"line":320,"address":[2126430,2126405,2126096],"length":1,"stats":{"Line":1}},{"line":321,"address":[2126158,2126359,2126130],"length":1,"stats":{"Line":3}},{"line":323,"address":[2126138],"length":1,"stats":{"Line":1}},{"line":324,"address":[1766736,1766720],"length":1,"stats":{"Line":2}},{"line":326,"address":[2126423,2126217,2126389,2126193],"length":1,"stats":{"Line":2}},{"line":332,"address":[2126448],"length":1,"stats":{"Line":0}},{"line":333,"address":[2126611,2126502],"length":1,"stats":{"Line":0}},{"line":336,"address":[2126474],"length":1,"stats":{"Line":0}},{"line":338,"address":[2126651],"length":1,"stats":{"Line":0}},{"line":342,"address":[2128134,2128121,2126688],"length":1,"stats":{"Line":0}},{"line":343,"address":[2126723],"length":1,"stats":{"Line":0}},{"line":345,"address":[2126733,2126797,2128129],"length":1,"stats":{"Line":0}},{"line":346,"address":[2127448,2127174,2127110,2128127],"length":1,"stats":{"Line":0}},{"line":347,"address":[2127549,2127919],"length":1,"stats":{"Line":0}},{"line":350,"address":[2127905,2127615],"length":1,"stats":{"Line":0}},{"line":351,"address":[2127818],"length":1,"stats":{"Line":0}},{"line":356,"address":[],"length":0,"stats":{"Line":1}},{"line":357,"address":[],"length":0,"stats":{"Line":2}},{"line":358,"address":[1766965,1767096,1769333],"length":1,"stats":{"Line":1}},{"line":360,"address":[],"length":0,"stats":{"Line":0}},{"line":362,"address":[],"length":0,"stats":{"Line":2}},{"line":364,"address":[],"length":0,"stats":{"Line":1}},{"line":365,"address":[],"length":0,"stats":{"Line":2}},{"line":367,"address":[2497145,2498760,2497253],"length":1,"stats":{"Line":2}},{"line":368,"address":[1768084,1769239,1768151],"length":1,"stats":{"Line":2}},{"line":370,"address":[1769218,1768549,1768482],"length":1,"stats":{"Line":2}},{"line":371,"address":[1768756,1768807],"length":1,"stats":{"Line":2}},{"line":372,"address":[],"length":0,"stats":{"Line":1}},{"line":375,"address":[],"length":0,"stats":{"Line":1}},{"line":376,"address":[],"length":0,"stats":{"Line":2}},{"line":377,"address":[],"length":0,"stats":{"Line":1}}],"covered":124,"coverable":144},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","db_tests.rs"],"content":"// libmarlin/src/db_tests.rs\n\nuse super::db;\nuse rusqlite::Connection;\nuse tempfile::tempdir;\n\nfn open_mem() -\u003e Connection {\n // helper to open an in-memory DB with migrations applied\n db::open(\":memory:\").expect(\"open in-memory DB\")\n}\n\n#[test]\nfn ensure_tag_path_creates_hierarchy() {\n let conn = open_mem();\n // create foo/bar/baz\n let leaf = db::ensure_tag_path(\u0026conn, \"foo/bar/baz\").unwrap();\n // foo should exist as a root tag\n let foo: i64 = conn\n .query_row(\n \"SELECT id FROM tags WHERE name='foo' AND parent_id IS NULL\",\n [],\n |r| r.get(0),\n )\n .unwrap();\n // bar should be child of foo\n let bar: i64 = conn\n .query_row(\n \"SELECT id FROM tags WHERE name='bar' AND parent_id = ?1\",\n [foo],\n |r| r.get(0),\n )\n .unwrap();\n // baz should be child of bar, and its ID is what we got back\n let baz: i64 = conn\n .query_row(\n \"SELECT id FROM tags WHERE name='baz' AND parent_id = ?1\",\n [bar],\n |r| r.get(0),\n )\n .unwrap();\n assert_eq!(leaf, baz);\n}\n\n#[test]\nfn upsert_attr_inserts_and_updates() {\n let conn = open_mem();\n // insert a dummy file\n conn.execute(\n \"INSERT INTO files(path, size, mtime) VALUES (?1, 0, 0)\",\n [\"a.txt\"],\n )\n .unwrap();\n let fid: i64 = conn\n .query_row(\"SELECT id FROM files WHERE path='a.txt'\", [], |r| r.get(0))\n .unwrap();\n\n // insert\n db::upsert_attr(\u0026conn, fid, \"k\", \"v\").unwrap();\n let v1: String = conn\n .query_row(\n \"SELECT value FROM attributes WHERE file_id=?1 AND key='k'\",\n [fid],\n |r| r.get(0),\n )\n .unwrap();\n assert_eq!(v1, \"v\");\n\n // update\n db::upsert_attr(\u0026conn, fid, \"k\", \"v2\").unwrap();\n let v2: String = conn\n .query_row(\n \"SELECT value FROM attributes WHERE file_id=?1 AND key='k'\",\n [fid],\n |r| r.get(0),\n )\n .unwrap();\n assert_eq!(v2, \"v2\");\n}\n\n#[test]\nfn add_and_remove_links_and_backlinks() {\n let conn = open_mem();\n // create two files\n conn.execute(\n \"INSERT INTO files(path, size, mtime) VALUES (?1, 0, 0)\",\n [\"one.txt\"],\n )\n .unwrap();\n conn.execute(\n \"INSERT INTO files(path, size, mtime) VALUES (?1, 0, 0)\",\n [\"two.txt\"],\n )\n .unwrap();\n let src: i64 = conn\n .query_row(\"SELECT id FROM files WHERE path='one.txt'\", [], |r| r.get(0))\n .unwrap();\n let dst: i64 = conn\n .query_row(\"SELECT id FROM files WHERE path='two.txt'\", [], |r| r.get(0))\n .unwrap();\n\n // add a link of type \"ref\"\n db::add_link(\u0026conn, src, dst, Some(\"ref\")).unwrap();\n let out = db::list_links(\u0026conn, \"one%\", None, None).unwrap();\n assert_eq!(out.len(), 1);\n assert_eq!(out[0].2.as_deref(), Some(\"ref\"));\n\n // backlinks should mirror\n let back = db::find_backlinks(\u0026conn, \"two%\").unwrap();\n assert_eq!(back.len(), 1);\n assert_eq!(back[0].1.as_deref(), Some(\"ref\"));\n\n // remove it\n db::remove_link(\u0026conn, src, dst, Some(\"ref\")).unwrap();\n let empty = db::list_links(\u0026conn, \"one%\", None, None).unwrap();\n assert!(empty.is_empty());\n}\n\n#[test]\nfn collections_roundtrip() {\n let conn = open_mem();\n // create collection \"C\"\n let cid = db::ensure_collection(\u0026conn, \"C\").unwrap();\n\n // add a file\n conn.execute(\n \"INSERT INTO files(path, size, mtime) VALUES (?1, 0, 0)\",\n [\"f.txt\"],\n )\n .unwrap();\n let fid: i64 = conn\n .query_row(\"SELECT id FROM files WHERE path='f.txt'\", [], |r| r.get(0))\n .unwrap();\n\n db::add_file_to_collection(\u0026conn, cid, fid).unwrap();\n let files = db::list_collection(\u0026conn, \"C\").unwrap();\n assert_eq!(files, vec![\"f.txt\".to_string()]);\n}\n\n#[test]\nfn views_save_and_query() {\n let conn = open_mem();\n db::save_view(\u0026conn, \"v1\", \"some_query\").unwrap();\n let all = db::list_views(\u0026conn).unwrap();\n assert_eq!(all, vec![(\"v1\".to_string(), \"some_query\".to_string())]);\n\n let q = db::view_query(\u0026conn, \"v1\").unwrap();\n assert_eq!(q, \"some_query\");\n}\n\n#[test]\nfn backup_and_restore_cycle() {\n let tmp = tempdir().unwrap();\n let db_path = tmp.path().join(\"data.db\");\n let live = db::open(\u0026db_path).unwrap();\n\n // insert a file\n live.execute(\n \"INSERT INTO files(path, size, mtime) VALUES (?1, 0, 0)\",\n [\"x.bin\"],\n )\n .unwrap();\n\n // backup\n let backup = db::backup(\u0026db_path).unwrap();\n // remove original\n std::fs::remove_file(\u0026db_path).unwrap();\n // restore\n db::restore(\u0026backup, \u0026db_path).unwrap();\n\n // reopen and check that x.bin survived\n let conn2 = db::open(\u0026db_path).unwrap();\n let cnt: i64 =\n conn2.query_row(\"SELECT COUNT(*) FROM files WHERE path='x.bin'\", [], |r| r.get(0)).unwrap();\n assert_eq!(cnt, 1);\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","error.rs"],"content":"// libmarlin/src/error.rs\n\nuse std::io;\nuse std::fmt;\n// Ensure these are present if Error enum variants use them directly\n// use rusqlite;\n// use notify;\n\npub type Result\u003cT\u003e = std::result::Result\u003cT, Error\u003e;\n\n#[derive(Debug)]\npub enum Error {\n Io(io::Error),\n Database(rusqlite::Error), \n Watch(notify::Error), \n InvalidState(String),\n NotFound(String),\n Config(String),\n Other(String),\n}\n\nimpl fmt::Display for Error {\n fn fmt(\u0026self, f: \u0026mut fmt::Formatter\u003c'_\u003e) -\u003e fmt::Result {\n match self {\n Self::Io(err) =\u003e write!(f, \"IO error: {}\", err),\n Self::Database(err) =\u003e write!(f, \"Database error: {}\", err),\n Self::Watch(err) =\u003e write!(f, \"Watch error: {}\", err),\n Self::InvalidState(msg) =\u003e write!(f, \"Invalid state: {}\", msg),\n Self::NotFound(path) =\u003e write!(f, \"Not found: {}\", path),\n Self::Config(msg) =\u003e write!(f, \"Configuration error: {}\", msg),\n Self::Other(msg) =\u003e write!(f, \"Error: {}\", msg),\n }\n }\n}\n\nimpl std::error::Error for Error {\n fn source(\u0026self) -\u003e Option\u003c\u0026(dyn std::error::Error + 'static)\u003e {\n match self {\n Self::Io(err) =\u003e Some(err),\n Self::Database(err) =\u003e Some(err),\n Self::Watch(err) =\u003e Some(err),\n Self::InvalidState(_) | Self::NotFound(_) | Self::Config(_) | Self::Other(_) =\u003e None,\n }\n }\n}\n\nimpl From\u003cio::Error\u003e for Error {\n fn from(err: io::Error) -\u003e Self {\n Self::Io(err)\n }\n}\n\nimpl From\u003crusqlite::Error\u003e for Error {\n fn from(err: rusqlite::Error) -\u003e Self {\n Self::Database(err)\n }\n}\n\nimpl From\u003cnotify::Error\u003e for Error {\n fn from(err: notify::Error) -\u003e Self {\n Self::Watch(err)\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n use std::error::Error as StdError; \n\n #[test]\n fn test_error_display_and_from() {\n // Test Io variant\n let io_err_inner_for_source_check = io::Error::new(io::ErrorKind::NotFound, \"test io error\");\n let io_err_marlin = Error::from(io::Error::new(io::ErrorKind::NotFound, \"test io error\"));\n assert_eq!(io_err_marlin.to_string(), \"IO error: test io error\");\n let source = io_err_marlin.source();\n assert!(source.is_some(), \"Io error should have a source\");\n if let Some(s) = source {\n // Compare details of the source if necessary, or just its string representation\n assert_eq!(s.to_string(), io_err_inner_for_source_check.to_string());\n }\n\n // Test Database variant\n let rusqlite_err_inner_for_source_check = rusqlite::Error::SqliteFailure(\n rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_ERROR), \n Some(\"test db error\".to_string()),\n );\n // We need to create the error again for the From conversion if we want to compare the source\n let db_err_marlin = Error::from(rusqlite::Error::SqliteFailure(\n rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_ERROR), \n Some(\"test db error\".to_string()),\n ));\n assert!(db_err_marlin.to_string().contains(\"Database error: test db error\"));\n let source = db_err_marlin.source();\n assert!(source.is_some(), \"Database error should have a source\");\n if let Some(s) = source {\n assert_eq!(s.to_string(), rusqlite_err_inner_for_source_check.to_string());\n }\n\n\n // Test Watch variant\n let notify_raw_err_inner_for_source_check = notify::Error::new(notify::ErrorKind::Generic(\"test watch error\".to_string()));\n let watch_err_marlin = Error::from(notify::Error::new(notify::ErrorKind::Generic(\"test watch error\".to_string())));\n assert!(watch_err_marlin.to_string().contains(\"Watch error: test watch error\"));\n let source = watch_err_marlin.source();\n assert!(source.is_some(), \"Watch error should have a source\");\n if let Some(s) = source {\n assert_eq!(s.to_string(), notify_raw_err_inner_for_source_check.to_string());\n }\n\n\n let invalid_state_err = Error::InvalidState(\"bad state\".to_string());\n assert_eq!(invalid_state_err.to_string(), \"Invalid state: bad state\");\n assert!(invalid_state_err.source().is_none());\n\n let not_found_err = Error::NotFound(\"missing_file.txt\".to_string());\n assert_eq!(not_found_err.to_string(), \"Not found: missing_file.txt\");\n assert!(not_found_err.source().is_none());\n\n let config_err = Error::Config(\"bad config\".to_string());\n assert_eq!(config_err.to_string(), \"Configuration error: bad config\");\n assert!(config_err.source().is_none());\n\n let other_err = Error::Other(\"some other issue\".to_string());\n assert_eq!(other_err.to_string(), \"Error: some other issue\");\n assert!(other_err.source().is_none());\n }\n\n #[test]\n fn test_rusqlite_error_without_message() {\n let sqlite_busy_error = rusqlite::Error::SqliteFailure(\n rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_BUSY),\n None,\n );\n let db_err_no_msg = Error::from(sqlite_busy_error);\n \n let expected_rusqlite_msg = rusqlite::Error::SqliteFailure(\n rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_BUSY),\n None,\n ).to_string(); \n \n let expected_marlin_msg = format!(\"Database error: {}\", expected_rusqlite_msg);\n \n // Verify the string matches the expected format\n assert_eq!(db_err_no_msg.to_string(), expected_marlin_msg);\n \n // Check the error code directly instead of the string\n if let Error::Database(rusqlite::Error::SqliteFailure(err, _)) = \u0026db_err_no_msg {\n assert_eq!(err.code, rusqlite::ffi::ErrorCode::DatabaseBusy);\n } else {\n panic!(\"Expected Error::Database variant\");\n }\n \n // Verify the source exists\n assert!(db_err_no_msg.source().is_some());\n }\n}","traces":[{"line":23,"address":[5592736],"length":1,"stats":{"Line":1}},{"line":24,"address":[2070560],"length":1,"stats":{"Line":1}},{"line":25,"address":[2070624],"length":1,"stats":{"Line":1}},{"line":26,"address":[2070722],"length":1,"stats":{"Line":1}},{"line":27,"address":[2070839],"length":1,"stats":{"Line":1}},{"line":28,"address":[2070966],"length":1,"stats":{"Line":1}},{"line":29,"address":[2071094],"length":1,"stats":{"Line":1}},{"line":30,"address":[2071222],"length":1,"stats":{"Line":1}},{"line":31,"address":[2071347],"length":1,"stats":{"Line":1}},{"line":37,"address":[2071488],"length":1,"stats":{"Line":1}},{"line":38,"address":[2071498],"length":1,"stats":{"Line":1}},{"line":39,"address":[2071563],"length":1,"stats":{"Line":1}},{"line":40,"address":[5593804],"length":1,"stats":{"Line":1}},{"line":41,"address":[2071625],"length":1,"stats":{"Line":1}},{"line":42,"address":[2071653],"length":1,"stats":{"Line":1}},{"line":48,"address":[2071680],"length":1,"stats":{"Line":1}},{"line":49,"address":[2071688],"length":1,"stats":{"Line":1}},{"line":54,"address":[2071712],"length":1,"stats":{"Line":1}},{"line":55,"address":[2071720],"length":1,"stats":{"Line":1}},{"line":60,"address":[2071744],"length":1,"stats":{"Line":1}},{"line":61,"address":[2071761],"length":1,"stats":{"Line":1}}],"covered":21,"coverable":21},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","facade_tests.rs"],"content":"// libmarlin/src/facade_tests.rs\n\nuse super::*; // brings Marlin, config, etc.\nuse std::{env, fs};\nuse tempfile::tempdir;\n\n#[test]\nfn open_at_and_scan_and_search() {\n // 1) Prepare a temp workspace with one file\n let tmp = tempdir().unwrap();\n let file = tmp.path().join(\"hello.txt\");\n fs::write(\u0026file, \"hello FAÇT\").unwrap();\n\n // 2) Use open_at to create a fresh DB\n let db_path = tmp.path().join(\"explicit.db\");\n let mut m = Marlin::open_at(\u0026db_path).expect(\"open_at should succeed\");\n assert!(db_path.exists(), \"DB file should be created\");\n\n // 3) Scan the directory\n let count = m.scan(\u0026[tmp.path()]).expect(\"scan should succeed\");\n assert_eq!(count, 1, \"we created exactly one file\");\n\n // 4) Search using an FTS hit\n let hits = m.search(\"hello\").expect(\"search must not error\");\n assert_eq!(hits.len(), 1);\n assert!(hits[0].ends_with(\"hello.txt\"));\n\n // 5) Search a substring that isn't a valid token (fires fallback)\n let fallback_hits = m.search(\"FAÇT\").expect(\"fallback search works\");\n assert_eq!(fallback_hits.len(), 1);\n assert!(fallback_hits[0].ends_with(\"hello.txt\"));\n}\n\n#[test]\nfn tag_and_search_by_tag() {\n let tmp = tempdir().unwrap();\n let a = tmp.path().join(\"a.md\");\n let b = tmp.path().join(\"b.md\");\n fs::write(\u0026a, \"# a\").unwrap();\n fs::write(\u0026b, \"# b\").unwrap();\n\n let db_path = tmp.path().join(\"my.db\");\n env::set_var(\"MARLIN_DB_PATH\", \u0026db_path);\n\n let mut m = Marlin::open_default().unwrap();\n m.scan(\u0026[tmp.path()]).unwrap();\n\n let changed = m.tag(\"*.md\", \"foo/bar\").unwrap();\n assert_eq!(changed, 2);\n\n let tagged = m.search(\"tags_text:\\\"foo/bar\\\"\").unwrap();\n assert_eq!(tagged.len(), 2);\n\n env::remove_var(\"MARLIN_DB_PATH\");\n}\n\n#[test]\nfn open_default_fallback_config() {\n // Unset all overrides\n env::remove_var(\"MARLIN_DB_PATH\");\n env::remove_var(\"XDG_DATA_HOME\");\n\n // Simulate no XDG: temporarily point HOME to a read-only dir\n let fake_home = tempdir().unwrap();\n env::set_var(\"HOME\", fake_home.path());\n // This should fall back to \"./index_\u003chash\u003e.db\"\n let cfg = config::Config::load().unwrap();\n let fname = cfg.db_path.file_name().unwrap().to_string_lossy();\n assert!(fname.starts_with(\"index_\") \u0026\u0026 fname.ends_with(\".db\"));\n\n // Clean up\n env::remove_var(\"HOME\");\n}\n\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","lib.rs"],"content":"//! libmarlin – public API surface for the Marlin core.\n//!\n//! Down-stream crates (`cli-bin`, `tui-bin`, tests, plugins) should depend\n//! *only* on the helpers re-exported here, never on internal modules\n//! directly. That gives us room to refactor internals without breaking\n//! callers.\n\n#![deny(warnings)]\n\npub mod backup;\npub mod config;\npub mod db;\npub mod error;\npub mod logging;\npub mod scan;\npub mod utils;\npub mod watcher;\n\n#[cfg(test)]\nmod utils_tests;\n#[cfg(test)]\nmod config_tests;\n#[cfg(test)]\nmod scan_tests;\n#[cfg(test)]\nmod logging_tests;\n#[cfg(test)]\nmod db_tests;\n#[cfg(test)]\nmod facade_tests;\n#[cfg(test)]\nmod watcher_tests;\n\nuse anyhow::{Context, Result};\nuse rusqlite::Connection;\nuse std::{fs, path::Path, sync::{Arc, Mutex}};\n\n/// Main handle for interacting with a Marlin database.\npub struct Marlin {\n #[allow(dead_code)]\n cfg: config::Config,\n conn: Connection,\n}\n\nimpl Marlin {\n /// Open using the default config (env override or XDG/CWD fallback),\n /// ensuring parent directories exist and applying migrations.\n pub fn open_default() -\u003e Result\u003cSelf\u003e {\n // 1) Load configuration\n let cfg = config::Config::load()?;\n // 2) Ensure the DB's parent directory exists\n if let Some(parent) = cfg.db_path.parent() {\n fs::create_dir_all(parent)?;\n }\n // 3) Open the database and run migrations\n let conn = db::open(\u0026cfg.db_path)\n .context(format!(\"opening database at {}\", cfg.db_path.display()))?;\n Ok(Marlin { cfg, conn })\n }\n\n /// Open a Marlin instance at the specified database path,\n /// creating parent directories and applying migrations.\n pub fn open_at\u003cP: AsRef\u003cPath\u003e\u003e(db_path: P) -\u003e Result\u003cSelf\u003e {\n let db_path = db_path.as_ref();\n // Ensure the specified DB directory exists\n if let Some(parent) = db_path.parent() {\n fs::create_dir_all(parent)?;\n }\n // Build a minimal Config so callers can still inspect cfg.db_path\n let cfg = config::Config { db_path: db_path.to_path_buf() };\n // Open the database and run migrations\n let conn = db::open(db_path)\n .context(format!(\"opening database at {}\", db_path.display()))?;\n Ok(Marlin { cfg, conn })\n }\n\n /// Recursively index one or more directories.\n pub fn scan\u003cP: AsRef\u003cPath\u003e\u003e(\u0026mut self, paths: \u0026[P]) -\u003e Result\u003cusize\u003e {\n let mut total = 0;\n for p in paths {\n total += scan::scan_directory(\u0026mut self.conn, p.as_ref())?;\n }\n Ok(total)\n }\n\n /// Attach a hierarchical tag (`foo/bar`) to every _indexed_ file\n /// matching the glob. Returns the number of files actually updated.\n pub fn tag(\u0026mut self, pattern: \u0026str, tag_path: \u0026str) -\u003e Result\u003cusize\u003e {\n use glob::Pattern;\n\n // 1) ensure tag hierarchy\n let leaf = db::ensure_tag_path(\u0026self.conn, tag_path)?;\n\n // 2) collect leaf + ancestors\n let mut tag_ids = Vec::new();\n let mut cur = Some(leaf);\n while let Some(id) = cur {\n tag_ids.push(id);\n cur = self.conn.query_row(\n \"SELECT parent_id FROM tags WHERE id = ?1\",\n [id],\n |r| r.get::\u003c_, Option\u003ci64\u003e\u003e(0),\n )?;\n }\n\n // 3) match files by glob against stored paths\n let expanded = shellexpand::tilde(pattern).into_owned();\n let pat = Pattern::new(\u0026expanded)\n .with_context(|| format!(\"Invalid glob pattern `{}`\", expanded))?;\n\n let mut stmt_all = self.conn.prepare(\"SELECT id, path FROM files\")?;\n let rows = stmt_all.query_map([], |r| Ok((r.get(0)?, r.get(1)?)))?;\n\n let mut stmt_ins = self.conn.prepare(\n \"INSERT OR IGNORE INTO file_tags(file_id, tag_id) VALUES (?1, ?2)\",\n )?;\n\n let mut changed = 0;\n for row in rows {\n let (fid, path_str): (i64, String) = row?;\n let is_match = if expanded.contains(std::path::MAIN_SEPARATOR) {\n pat.matches(\u0026path_str)\n } else {\n Path::new(\u0026path_str)\n .file_name()\n .and_then(|n| n.to_str())\n .map(|n| pat.matches(n))\n .unwrap_or(false)\n };\n if !is_match {\n continue;\n }\n\n let mut newly = false;\n for \u0026tid in \u0026tag_ids {\n if stmt_ins.execute([fid, tid])? \u003e 0 {\n newly = true;\n }\n }\n if newly {\n changed += 1;\n }\n }\n Ok(changed)\n }\n\n /// Full-text search over path, tags, and attrs, with substring fallback.\n pub fn search(\u0026self, query: \u0026str) -\u003e Result\u003cVec\u003cString\u003e\u003e {\n let mut stmt = self.conn.prepare(\n \"SELECT f.path FROM files_fts JOIN files f ON f.rowid = files_fts.rowid WHERE files_fts MATCH ?1 ORDER BY rank\",\n )?;\n let mut hits = stmt.query_map([query], |r| r.get(0))?\n .collect::\u003cstd::result::Result\u003cVec\u003c_\u003e, rusqlite::Error\u003e\u003e()?;\n\n if hits.is_empty() \u0026\u0026 !query.contains(':') {\n hits = self.fallback_search(query)?;\n }\n Ok(hits)\n }\n\n fn fallback_search(\u0026self, term: \u0026str) -\u003e Result\u003cVec\u003cString\u003e\u003e {\n let needle = term.to_lowercase();\n let mut stmt = self.conn.prepare(\"SELECT path FROM files\")?;\n let rows = stmt.query_map([], |r| r.get(0))?;\n let mut out = Vec::new();\n for res in rows {\n let p: String = res?;\n if p.to_lowercase().contains(\u0026needle) {\n out.push(p.clone());\n continue;\n }\n if let Ok(meta) = fs::metadata(\u0026p) {\n if meta.len() \u003c= 65_536 { \n if let Ok(body) = fs::read_to_string(\u0026p) {\n if body.to_lowercase().contains(\u0026needle) {\n out.push(p.clone());\n }\n }\n }\n }\n }\n Ok(out)\n }\n\n /// Borrow the raw SQLite connection.\n pub fn conn(\u0026self) -\u003e \u0026Connection {\n \u0026self.conn\n }\n\n /// Spawn a file-watcher that indexes changes in real time.\n pub fn watch\u003cP: AsRef\u003cPath\u003e\u003e(\n \u0026mut self,\n path: P,\n config: Option\u003cwatcher::WatcherConfig\u003e,\n ) -\u003e Result\u003cwatcher::FileWatcher\u003e {\n let cfg = config.unwrap_or_default();\n let p = path.as_ref().to_path_buf();\n let new_conn = db::open(\u0026self.cfg.db_path)\n .context(\"opening database for watcher\")?;\n let watcher_db = Arc::new(Mutex::new(db::Database::new(new_conn)));\n \n let mut owned_w = watcher::FileWatcher::new(vec![p], cfg)?;\n owned_w.with_database(watcher_db); // Modifies owned_w in place\n owned_w.start()?; // Start the watcher after it has been fully configured\n \n Ok(owned_w) // Return the owned FileWatcher\n }\n}","traces":[{"line":48,"address":[5390765,5390771,5389632],"length":1,"stats":{"Line":1}},{"line":50,"address":[2099265],"length":1,"stats":{"Line":1}},{"line":52,"address":[2099509,2099445],"length":1,"stats":{"Line":2}},{"line":53,"address":[2099619,2099656],"length":1,"stats":{"Line":2}},{"line":56,"address":[2100322,2099768,2100017,2100080,2100122,2099644],"length":1,"stats":{"Line":4}},{"line":57,"address":[2099798,2099781,2100335,2100056,2100106],"length":1,"stats":{"Line":2}},{"line":58,"address":[2100169],"length":1,"stats":{"Line":1}},{"line":63,"address":[],"length":0,"stats":{"Line":1}},{"line":64,"address":[1683494,1683573],"length":1,"stats":{"Line":2}},{"line":66,"address":[],"length":0,"stats":{"Line":1}},{"line":67,"address":[],"length":0,"stats":{"Line":2}},{"line":70,"address":[1683849,1683722],"length":1,"stats":{"Line":2}},{"line":72,"address":[],"length":0,"stats":{"Line":4}},{"line":73,"address":[1683954,1684199,1684249,1684500,1683970],"length":1,"stats":{"Line":2}},{"line":74,"address":[],"length":0,"stats":{"Line":1}},{"line":78,"address":[],"length":0,"stats":{"Line":1}},{"line":79,"address":[],"length":0,"stats":{"Line":1}},{"line":80,"address":[],"length":0,"stats":{"Line":2}},{"line":81,"address":[1684843,1684666,1684765],"length":1,"stats":{"Line":1}},{"line":83,"address":[],"length":0,"stats":{"Line":1}},{"line":88,"address":[5390784,5395118,5394802],"length":1,"stats":{"Line":1}},{"line":92,"address":[2100477],"length":1,"stats":{"Line":1}},{"line":95,"address":[5391027],"length":1,"stats":{"Line":1}},{"line":96,"address":[2100629],"length":1,"stats":{"Line":1}},{"line":97,"address":[5391516,5391069],"length":1,"stats":{"Line":2}},{"line":98,"address":[5391107],"length":1,"stats":{"Line":1}},{"line":99,"address":[2101093,2100799,2100811,2100986],"length":1,"stats":{"Line":2}},{"line":101,"address":[2100803],"length":1,"stats":{"Line":1}},{"line":102,"address":[1684864,1684880],"length":1,"stats":{"Line":2}},{"line":107,"address":[5391149,5391572],"length":1,"stats":{"Line":2}},{"line":108,"address":[5395100,5391824,5391690,5391607],"length":1,"stats":{"Line":2}},{"line":109,"address":[5391808],"length":1,"stats":{"Line":0}},{"line":111,"address":[2101540,2104506,2101615],"length":1,"stats":{"Line":2}},{"line":112,"address":[5600174,5600128],"length":1,"stats":{"Line":4}},{"line":114,"address":[5392779,5392707,5392901,5394889],"length":1,"stats":{"Line":2}},{"line":118,"address":[5393106],"length":1,"stats":{"Line":1}},{"line":119,"address":[2102748,2102641,2102824],"length":1,"stats":{"Line":3}},{"line":120,"address":[2102941,2104294,2103115],"length":1,"stats":{"Line":2}},{"line":121,"address":[2103436,2103353],"length":1,"stats":{"Line":2}},{"line":122,"address":[5394005,5394179],"length":1,"stats":{"Line":0}},{"line":124,"address":[2103529,2103474],"length":1,"stats":{"Line":2}},{"line":126,"address":[5600654,5600640],"length":1,"stats":{"Line":2}},{"line":127,"address":[5600672,5600690],"length":1,"stats":{"Line":2}},{"line":130,"address":[2103643],"length":1,"stats":{"Line":1}},{"line":134,"address":[5394239],"length":1,"stats":{"Line":1}},{"line":135,"address":[2103741],"length":1,"stats":{"Line":1}},{"line":136,"address":[2104015,2104208,2103879],"length":1,"stats":{"Line":3}},{"line":137,"address":[5394726],"length":1,"stats":{"Line":1}},{"line":140,"address":[5394448,5394515],"length":1,"stats":{"Line":2}},{"line":141,"address":[2103995,2103958],"length":1,"stats":{"Line":1}},{"line":144,"address":[2102966],"length":1,"stats":{"Line":1}},{"line":148,"address":[5396601,5396611,5395136],"length":1,"stats":{"Line":1}},{"line":149,"address":[2104781,2104664],"length":1,"stats":{"Line":1}},{"line":152,"address":[1685667,1685632],"length":1,"stats":{"Line":4}},{"line":155,"address":[2105536,2105992,2105465,2105656],"length":1,"stats":{"Line":4}},{"line":156,"address":[5396430,5396266],"length":1,"stats":{"Line":1}},{"line":158,"address":[2105547],"length":1,"stats":{"Line":1}},{"line":161,"address":[5399481,5396624,5399133],"length":1,"stats":{"Line":1}},{"line":162,"address":[2106145],"length":1,"stats":{"Line":1}},{"line":163,"address":[2108847,2106261,2106186],"length":1,"stats":{"Line":2}},{"line":164,"address":[5397140,5399458,5397211],"length":1,"stats":{"Line":4}},{"line":165,"address":[2106856],"length":1,"stats":{"Line":1}},{"line":166,"address":[2107084,2107008,2106916],"length":1,"stats":{"Line":3}},{"line":167,"address":[2107201,2107346],"length":1,"stats":{"Line":2}},{"line":168,"address":[2107623,2107552],"length":1,"stats":{"Line":2}},{"line":169,"address":[2108664],"length":1,"stats":{"Line":0}},{"line":172,"address":[2107804,2107894],"length":1,"stats":{"Line":2}},{"line":173,"address":[2108574,2107962,2107901],"length":1,"stats":{"Line":3}},{"line":174,"address":[2108088,2107990],"length":1,"stats":{"Line":2}},{"line":175,"address":[2108136,2108207],"length":1,"stats":{"Line":2}},{"line":176,"address":[2108391],"length":1,"stats":{"Line":1}},{"line":182,"address":[2107234],"length":1,"stats":{"Line":1}},{"line":186,"address":[2108864],"length":1,"stats":{"Line":0}},{"line":187,"address":[2108872],"length":1,"stats":{"Line":0}},{"line":191,"address":[2308196,2308112,2309672],"length":1,"stats":{"Line":0}},{"line":196,"address":[],"length":0,"stats":{"Line":0}},{"line":197,"address":[],"length":0,"stats":{"Line":0}},{"line":198,"address":[],"length":0,"stats":{"Line":0}},{"line":200,"address":[],"length":0,"stats":{"Line":0}},{"line":202,"address":[],"length":0,"stats":{"Line":0}},{"line":203,"address":[2309358],"length":1,"stats":{"Line":0}},{"line":204,"address":[2309429],"length":1,"stats":{"Line":0}},{"line":206,"address":[2309555],"length":1,"stats":{"Line":0}}],"covered":69,"coverable":83},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","logging.rs"],"content":"use tracing_subscriber::{fmt, EnvFilter};\n\n/// Initialise global tracing subscriber.\n///\n/// Reads `RUST_LOG` for filtering, falls back to `info`.\npub fn init() {\n let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(\"info\"));\n\n // All tracing output (INFO, WARN, ERROR …) now goes to *stderr* so the\n // integration tests can assert on warnings / errors reliably.\n fmt()\n .with_target(false) // hide module targets\n .with_level(true) // include log level\n .with_env_filter(filter) // respect RUST_LOG\n .with_writer(std::io::stderr) // \u003c-- NEW: send to stderr\n .init();\n}\n","traces":[{"line":6,"address":[2109278,2108944,2109307],"length":1,"stats":{"Line":1}},{"line":7,"address":[2108981],"length":1,"stats":{"Line":1}},{"line":11,"address":[2109119,2109197,2109041],"length":1,"stats":{"Line":3}},{"line":14,"address":[2109154],"length":1,"stats":{"Line":1}}],"covered":4,"coverable":4},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","logging_tests.rs"],"content":"// libmarlin/src/logging_tests.rs\n\nuse super::logging;\nuse tracing::Level;\n\n#[test]\nfn init_sets_up_subscriber() {\n // set RUST_LOG to something to test the EnvFilter path\n std::env::set_var(\"RUST_LOG\", \"debug\");\n logging::init();\n tracing::event!(Level::INFO, \"this is a test log\");\n // if we made it here without panic, we’re good\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","scan.rs"],"content":"// src/scan.rs\n\nuse std::fs;\nuse std::path::Path;\n\nuse anyhow::Result;\nuse rusqlite::{params, Connection};\nuse tracing::{debug, info};\nuse walkdir::WalkDir;\n\n/// Recursively walk `root` and upsert file metadata.\n/// Triggers keep the FTS table in sync.\npub fn scan_directory(conn: \u0026mut Connection, root: \u0026Path) -\u003e Result\u003cusize\u003e {\n // Begin a transaction so we batch many inserts/updates together\n let tx = conn.transaction()?;\n\n // Prepare the upsert statement once\n let mut stmt = tx.prepare(\n r#\"\n INSERT INTO files(path, size, mtime)\n VALUES (?1, ?2, ?3)\n ON CONFLICT(path) DO UPDATE\n SET size = excluded.size,\n mtime = excluded.mtime\n \"#,\n )?;\n\n let mut count = 0usize;\n\n // Walk the directory recursively\n for entry in WalkDir::new(root)\n .into_iter()\n .filter_map(Result::ok)\n .filter(|e| e.file_type().is_file())\n {\n let path = entry.path();\n\n // Skip the database file and its WAL/SHM siblings\n if let Some(name) = path.file_name().and_then(|n| n.to_str()) {\n if name.ends_with(\".db\") || name.ends_with(\"-wal\") || name.ends_with(\"-shm\") {\n continue;\n }\n }\n\n // Gather file metadata\n let meta = fs::metadata(path)?;\n let size = meta.len() as i64;\n let mtime = meta\n .modified()?\n .duration_since(std::time::UNIX_EPOCH)?\n .as_secs() as i64;\n\n // Execute the upsert\n let path_str = path.to_string_lossy();\n stmt.execute(params![path_str, size, mtime])?;\n count += 1;\n\n debug!(file = %path_str, \"indexed\");\n }\n\n // Finalize and commit\n drop(stmt);\n tx.commit()?;\n\n info!(indexed = count, \"scan complete\");\n Ok(count)\n}\n","traces":[{"line":13,"address":[5442107,5436832,5441856],"length":1,"stats":{"Line":1}},{"line":15,"address":[1954756],"length":1,"stats":{"Line":1}},{"line":18,"address":[1955184,1955035,1954971,1959810],"length":1,"stats":{"Line":2}},{"line":28,"address":[1955409],"length":1,"stats":{"Line":1}},{"line":31,"address":[1955429,1955662,1955477],"length":1,"stats":{"Line":5}},{"line":34,"address":[2057035,2057008],"length":1,"stats":{"Line":4}},{"line":36,"address":[1957173,1955747],"length":1,"stats":{"Line":4}},{"line":39,"address":[1957211],"length":1,"stats":{"Line":5}},{"line":40,"address":[1957388,1957544,1957463],"length":1,"stats":{"Line":3}},{"line":46,"address":[1957434,1959697,1957612],"length":1,"stats":{"Line":2}},{"line":47,"address":[5440026],"length":1,"stats":{"Line":1}},{"line":48,"address":[1957947,1958123,1957851,1959629],"length":1,"stats":{"Line":1}},{"line":49,"address":[5440141],"length":1,"stats":{"Line":0}},{"line":50,"address":[5440311],"length":1,"stats":{"Line":0}},{"line":54,"address":[1958255],"length":1,"stats":{"Line":1}},{"line":55,"address":[5440654,5441802,5440492],"length":1,"stats":{"Line":3}},{"line":56,"address":[1958667,1958607],"length":1,"stats":{"Line":1}},{"line":58,"address":[1958936,1958691,1958642],"length":1,"stats":{"Line":3}},{"line":62,"address":[1955800],"length":1,"stats":{"Line":1}},{"line":63,"address":[1957078,1955903],"length":1,"stats":{"Line":2}},{"line":65,"address":[1956110,1956459],"length":1,"stats":{"Line":2}},{"line":66,"address":[1956410],"length":1,"stats":{"Line":1}}],"covered":20,"coverable":22},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","scan_tests.rs"],"content":"// libmarlin/src/scan_tests.rs\n\nuse super::scan::scan_directory;\nuse super::db;\nuse tempfile::tempdir;\nuse std::fs::File;\n\n#[test]\nfn scan_directory_counts_files() {\n let tmp = tempdir().unwrap();\n\n // create a couple of files\n File::create(tmp.path().join(\"a.txt\")).unwrap();\n File::create(tmp.path().join(\"b.log\")).unwrap();\n\n // open an in-memory DB (runs migrations)\n let mut conn = db::open(\":memory:\").unwrap();\n\n let count = scan_directory(\u0026mut conn, tmp.path()).unwrap();\n assert_eq!(count, 2);\n\n // ensure the paths were inserted\n let mut stmt = conn.prepare(\"SELECT COUNT(*) FROM files\").unwrap();\n let total: i64 = stmt.query_row([], |r| r.get(0)).unwrap();\n assert_eq!(total, 2);\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","utils.rs"],"content":"//! Misc shared helpers.\n\nuse std::path::PathBuf;\n\n/// Determine a filesystem root to limit recursive walking on glob scans.\n///\n/// If the pattern contains any of `*?[`, we take everything up to the\n/// first such character, and then (if that still contains metacharacters)\n/// walk up until there aren’t any left. If there are *no* metachars at\n/// all, we treat the entire string as a path and return its parent\n/// directory (or `.` if it has no parent).\npub fn determine_scan_root(pattern: \u0026str) -\u003e PathBuf {\n // find first wildcard char\n let first_wild = pattern\n .find(|c| matches!(c, '*' | '?' | '['))\n .unwrap_or(pattern.len());\n\n // everything up to the wildcard (or the whole string if none)\n let prefix = \u0026pattern[..first_wild];\n let mut root = PathBuf::from(prefix);\n\n // If there were NO wildcards at all, just return the parent directory\n if first_wild == pattern.len() {\n return root.parent().map(|p| p.to_path_buf()).unwrap_or_else(|| PathBuf::from(\".\"));\n }\n\n // Otherwise, if the prefix still has any wildcards (e.g. \"foo*/bar\"),\n // walk back up until it doesn’t\n while root\n .as_os_str()\n .to_string_lossy()\n .chars()\n .any(|c| matches!(c, '*' | '?' | '['))\n {\n root = root.parent().map(|p| p.to_path_buf()).unwrap_or_default();\n }\n\n if root.as_os_str().is_empty() {\n PathBuf::from(\".\")\n } else {\n root\n }\n}\n","traces":[{"line":12,"address":[5518304,5519621,5519490],"length":1,"stats":{"Line":1}},{"line":14,"address":[1785101,1785170],"length":1,"stats":{"Line":4}},{"line":15,"address":[2163341,2163328],"length":1,"stats":{"Line":2}},{"line":16,"address":[1785146],"length":1,"stats":{"Line":2}},{"line":19,"address":[1785217],"length":1,"stats":{"Line":2}},{"line":20,"address":[1785252],"length":1,"stats":{"Line":2}},{"line":23,"address":[1785293,1785368],"length":1,"stats":{"Line":4}},{"line":24,"address":[5629712,5629760,5629734,5629772],"length":1,"stats":{"Line":4}},{"line":29,"address":[1786189,1785454,1785381,1785591],"length":1,"stats":{"Line":6}},{"line":33,"address":[1785663,1785537],"length":1,"stats":{"Line":3}},{"line":35,"address":[1786050,1785935],"length":1,"stats":{"Line":0}},{"line":38,"address":[1785712,1785838],"length":1,"stats":{"Line":2}},{"line":39,"address":[1785883,1785848],"length":1,"stats":{"Line":2}},{"line":41,"address":[1785795],"length":1,"stats":{"Line":1}}],"covered":13,"coverable":14},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","utils_tests.rs"],"content":"// libmarlin/src/utils_tests.rs\n\nuse super::utils::determine_scan_root;\nuse std::path::PathBuf;\n\n#[test]\nfn determine_scan_root_plain_path() {\n let root = determine_scan_root(\"foo/bar/baz.txt\");\n assert_eq!(root, PathBuf::from(\"foo/bar\"));\n}\n\n#[test]\nfn determine_scan_root_glob() {\n let root = determine_scan_root(\"foo/*/baz.rs\");\n assert_eq!(root, PathBuf::from(\"foo\"));\n}\n\n#[test]\nfn determine_scan_root_only_wildcards() {\n let root = determine_scan_root(\"**/*.txt\");\n assert_eq!(root, PathBuf::from(\".\"));\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","watcher.rs"],"content":"// libmarlin/src/watcher.rs\n\n//! File system watcher implementation for Marlin\n//!\n//! This module provides real-time index updates by monitoring file system events\n//! (create, modify, delete) using the `notify` crate. It implements event debouncing,\n//! batch processing, and a state machine for robust lifecycle management.\n\nuse anyhow::{Result, Context};\nuse crate::db::Database;\nuse crossbeam_channel::{bounded, Receiver};\nuse notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcherTrait};\nuse std::collections::HashMap;\nuse std::path::PathBuf;\nuse std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};\nuse std::sync::{Arc, Mutex};\nuse std::thread::{self, JoinHandle};\nuse std::time::{Duration, Instant};\n// REMOVED: use std::fs; // \u003c\u003c\u003c\u003c\u003c\u003c\u003c\u003c\u003c\u003c\u003c\u003c THIS LINE WAS REMOVED\n\n/// Configuration for the file watcher\n#[derive(Debug, Clone)]\npub struct WatcherConfig {\n /// Time in milliseconds to debounce file events\n pub debounce_ms: u64,\n\n /// Maximum number of events to process in a single batch\n pub batch_size: usize,\n\n /// Maximum size of the event queue before applying backpressure\n pub max_queue_size: usize,\n\n /// Time in milliseconds to wait for events to drain during shutdown\n pub drain_timeout_ms: u64,\n}\n\nimpl Default for WatcherConfig {\n fn default() -\u003e Self {\n Self {\n debounce_ms: 100,\n batch_size: 1000,\n max_queue_size: 100_000,\n drain_timeout_ms: 5000,\n }\n }\n}\n\n/// State of the file watcher\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum WatcherState {\n Initializing,\n Watching,\n Paused,\n ShuttingDown,\n Stopped,\n}\n\n/// Status information about the file watcher\n#[derive(Debug, Clone)]\npub struct WatcherStatus {\n pub state: WatcherState,\n pub events_processed: usize,\n pub queue_size: usize,\n pub start_time: Option\u003cInstant\u003e,\n pub watched_paths: Vec\u003cPathBuf\u003e,\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]\nenum EventPriority {\n Create = 0,\n Delete = 1,\n Modify = 2,\n Access = 3,\n}\n\n#[derive(Debug, Clone)]\nstruct ProcessedEvent {\n path: PathBuf,\n kind: EventKind,\n priority: EventPriority,\n timestamp: Instant,\n}\n\nstruct EventDebouncer {\n events: HashMap\u003cPathBuf, ProcessedEvent\u003e,\n debounce_window_ms: u64,\n last_flush: Instant,\n}\n\nimpl EventDebouncer {\n fn new(debounce_window_ms: u64) -\u003e Self {\n Self {\n events: HashMap::new(),\n debounce_window_ms,\n last_flush: Instant::now(),\n }\n }\n\n fn add_event(\u0026mut self, event: ProcessedEvent) {\n let path = event.path.clone();\n if path.is_dir() { // This relies on the PathBuf itself knowing if it's a directory\n // or on the underlying FS. For unit tests, ensure paths are created.\n self.events.retain(|file_path, _| !file_path.starts_with(\u0026path) || file_path == \u0026path );\n }\n match self.events.get_mut(\u0026path) {\n Some(existing) =\u003e {\n if event.priority \u003c existing.priority {\n existing.priority = event.priority;\n }\n existing.timestamp = event.timestamp;\n existing.kind = event.kind;\n }\n None =\u003e {\n self.events.insert(path, event);\n }\n }\n }\n\n fn is_ready_to_flush(\u0026self) -\u003e bool {\n self.last_flush.elapsed() \u003e= Duration::from_millis(self.debounce_window_ms)\n }\n\n fn flush(\u0026mut self) -\u003e Vec\u003cProcessedEvent\u003e {\n let mut events: Vec\u003cProcessedEvent\u003e = self.events.drain().map(|(_, e)| e).collect();\n events.sort_by_key(|e| e.priority);\n self.last_flush = Instant::now();\n events\n }\n\n #[allow(dead_code)]\n fn len(\u0026self) -\u003e usize {\n self.events.len()\n }\n}\n\n#[cfg(test)]\nmod event_debouncer_tests {\n use super::*;\n use notify::event::{CreateKind, DataChange, ModifyKind, RemoveKind, RenameMode};\n use std::fs; // fs is needed for these tests to create dirs/files\n use tempfile; \n\n #[test]\n fn debouncer_add_and_flush() {\n let mut debouncer = EventDebouncer::new(100);\n std::thread::sleep(Duration::from_millis(110)); \n assert!(debouncer.is_ready_to_flush());\n assert_eq!(debouncer.len(), 0);\n\n let path1 = PathBuf::from(\"file1.txt\");\n debouncer.add_event(ProcessedEvent {\n path: path1.clone(),\n kind: EventKind::Create(CreateKind::File),\n priority: EventPriority::Create,\n timestamp: Instant::now(),\n });\n assert_eq!(debouncer.len(), 1);\n \n debouncer.last_flush = Instant::now(); \n assert!(!debouncer.is_ready_to_flush());\n\n std::thread::sleep(Duration::from_millis(110));\n assert!(debouncer.is_ready_to_flush());\n\n let flushed = debouncer.flush();\n assert_eq!(flushed.len(), 1);\n assert_eq!(flushed[0].path, path1);\n assert_eq!(debouncer.len(), 0);\n assert!(!debouncer.is_ready_to_flush()); \n }\n\n #[test]\n fn debouncer_coalesce_events() {\n let mut debouncer = EventDebouncer::new(100);\n let path1 = PathBuf::from(\"file1.txt\");\n\n let t1 = Instant::now();\n debouncer.add_event(ProcessedEvent {\n path: path1.clone(),\n kind: EventKind::Create(CreateKind::File),\n priority: EventPriority::Create,\n timestamp: t1,\n });\n std::thread::sleep(Duration::from_millis(10));\n let t2 = Instant::now();\n debouncer.add_event(ProcessedEvent {\n path: path1.clone(),\n kind: EventKind::Modify(ModifyKind::Data(DataChange::Any)),\n priority: EventPriority::Modify,\n timestamp: t2,\n });\n \n assert_eq!(debouncer.len(), 1);\n \n std::thread::sleep(Duration::from_millis(110));\n let flushed = debouncer.flush();\n assert_eq!(flushed.len(), 1);\n assert_eq!(flushed[0].path, path1);\n assert_eq!(flushed[0].priority, EventPriority::Create); \n assert_eq!( \n flushed[0].kind,\n EventKind::Modify(ModifyKind::Data(DataChange::Any))\n );\n assert_eq!(flushed[0].timestamp, t2);\n }\n\n #[test]\n fn debouncer_hierarchical() {\n let mut debouncer_h = EventDebouncer::new(100);\n let temp_dir_obj = tempfile::tempdir().expect(\"Failed to create temp dir\");\n let p_dir = temp_dir_obj.path().to_path_buf(); \n let p_file = p_dir.join(\"file.txt\");\n \n fs::File::create(\u0026p_file).expect(\"Failed to create test file for hierarchical debounce\");\n\n debouncer_h.add_event(ProcessedEvent {\n path: p_file.clone(),\n kind: EventKind::Create(CreateKind::File),\n priority: EventPriority::Create,\n timestamp: Instant::now(),\n });\n assert_eq!(debouncer_h.len(), 1);\n \n debouncer_h.add_event(ProcessedEvent {\n path: p_dir.clone(), \n kind: EventKind::Remove(RemoveKind::Folder), \n priority: EventPriority::Delete,\n timestamp: Instant::now(),\n });\n assert_eq!(debouncer_h.len(), 1, \"Hierarchical debounce should remove child event, leaving only parent dir event\");\n \n std::thread::sleep(Duration::from_millis(110));\n let flushed = debouncer_h.flush();\n assert_eq!(flushed.len(), 1);\n assert_eq!(flushed[0].path, p_dir);\n }\n\n #[test]\n fn debouncer_different_files() {\n let mut debouncer = EventDebouncer::new(100);\n let path1 = PathBuf::from(\"file1.txt\");\n let path2 = PathBuf::from(\"file2.txt\");\n\n debouncer.add_event(ProcessedEvent {\n path: path1.clone(),\n kind: EventKind::Create(CreateKind::File),\n priority: EventPriority::Create,\n timestamp: Instant::now(),\n });\n debouncer.add_event(ProcessedEvent {\n path: path2.clone(),\n kind: EventKind::Create(CreateKind::File),\n priority: EventPriority::Create,\n timestamp: Instant::now(),\n });\n assert_eq!(debouncer.len(), 2);\n std::thread::sleep(Duration::from_millis(110));\n let flushed = debouncer.flush();\n assert_eq!(flushed.len(), 2);\n }\n\n #[test]\n fn debouncer_priority_sorting_on_flush() {\n let mut debouncer = EventDebouncer::new(100);\n let path1 = PathBuf::from(\"file1.txt\"); \n let path2 = PathBuf::from(\"file2.txt\"); \n let path3 = PathBuf::from(\"file3.txt\"); \n\n debouncer.add_event(ProcessedEvent { path: path1, kind: EventKind::Modify(ModifyKind::Name(RenameMode::To)), priority: EventPriority::Modify, timestamp: Instant::now() });\n debouncer.add_event(ProcessedEvent { path: path2, kind: EventKind::Create(CreateKind::File), priority: EventPriority::Create, timestamp: Instant::now() });\n debouncer.add_event(ProcessedEvent { path: path3, kind: EventKind::Remove(RemoveKind::File), priority: EventPriority::Delete, timestamp: Instant::now() });\n \n std::thread::sleep(Duration::from_millis(110));\n let flushed = debouncer.flush();\n assert_eq!(flushed.len(), 3);\n assert_eq!(flushed[0].priority, EventPriority::Create); \n assert_eq!(flushed[1].priority, EventPriority::Delete); \n assert_eq!(flushed[2].priority, EventPriority::Modify); \n }\n\n #[test]\n fn debouncer_no_events_flush_empty() {\n let mut debouncer = EventDebouncer::new(100);\n std::thread::sleep(Duration::from_millis(110));\n let flushed = debouncer.flush();\n assert!(flushed.is_empty());\n assert_eq!(debouncer.len(), 0);\n }\n}\n\n\npub struct FileWatcher {\n state: Arc\u003cMutex\u003cWatcherState\u003e\u003e,\n #[allow(dead_code)]\n config: WatcherConfig,\n watched_paths: Vec\u003cPathBuf\u003e,\n #[allow(dead_code)]\n event_receiver: Receiver\u003cstd::result::Result\u003cEvent, notify::Error\u003e\u003e,\n #[allow(dead_code)] \n watcher: RecommendedWatcher,\n processor_thread: Option\u003cJoinHandle\u003c()\u003e\u003e,\n stop_flag: Arc\u003cAtomicBool\u003e,\n events_processed: Arc\u003cAtomicUsize\u003e,\n queue_size: Arc\u003cAtomicUsize\u003e,\n start_time: Instant,\n db_shared: Arc\u003cMutex\u003cOption\u003cArc\u003cMutex\u003cDatabase\u003e\u003e\u003e\u003e\u003e,\n}\n\nimpl FileWatcher {\n pub fn new(paths: Vec\u003cPathBuf\u003e, config: WatcherConfig) -\u003e Result\u003cSelf\u003e {\n let stop_flag = Arc::new(AtomicBool::new(false));\n let events_processed = Arc::new(AtomicUsize::new(0));\n let queue_size = Arc::new(AtomicUsize::new(0));\n let state = Arc::new(Mutex::new(WatcherState::Initializing));\n\n let (tx, rx) = bounded(config.max_queue_size);\n\n let event_tx = tx.clone();\n let mut actual_watcher = RecommendedWatcher::new(\n move |event_res: std::result::Result\u003cEvent, notify::Error\u003e| {\n if event_tx.send(event_res).is_err() {\n // Receiver dropped\n }\n },\n notify::Config::default(),\n )?;\n\n for path_to_watch in \u0026paths {\n actual_watcher\n .watch(path_to_watch, RecursiveMode::Recursive)\n .with_context(|| format!(\"Failed to watch path: {}\", path_to_watch.display()))?;\n }\n\n let config_clone = config.clone();\n let stop_flag_clone = stop_flag.clone();\n let events_processed_clone = events_processed.clone();\n let queue_size_clone = queue_size.clone();\n let state_clone = state.clone();\n let receiver_clone = rx.clone(); \n\n let db_shared_for_thread = Arc::new(Mutex::new(None::\u003cArc\u003cMutex\u003cDatabase\u003e\u003e\u003e));\n let db_captured_for_thread = db_shared_for_thread.clone();\n\n let processor_thread = thread::spawn(move || {\n let mut debouncer = EventDebouncer::new(config_clone.debounce_ms);\n\n while !stop_flag_clone.load(Ordering::Relaxed) { \n let current_state = { state_clone.lock().unwrap().clone() };\n\n if current_state == WatcherState::Paused {\n thread::sleep(Duration::from_millis(100));\n continue;\n }\n if current_state == WatcherState::ShuttingDown || current_state == WatcherState::Stopped {\n break;\n }\n\n let mut received_in_batch = 0;\n while let Ok(evt_res) = receiver_clone.try_recv() {\n received_in_batch +=1;\n match evt_res {\n Ok(event) =\u003e {\n for path in event.paths {\n let prio = match event.kind {\n EventKind::Create(_) =\u003e EventPriority::Create,\n EventKind::Remove(_) =\u003e EventPriority::Delete,\n EventKind::Modify(_) =\u003e EventPriority::Modify,\n EventKind::Access(_) =\u003e EventPriority::Access,\n _ =\u003e EventPriority::Modify,\n };\n debouncer.add_event(ProcessedEvent {\n path,\n kind: event.kind.clone(),\n priority: prio,\n timestamp: Instant::now(),\n });\n }\n }\n Err(e) =\u003e {\n eprintln!(\"Watcher channel error: {:?}\", e);\n }\n }\n if received_in_batch \u003e= config_clone.batch_size {\n break;\n }\n }\n\n queue_size_clone.store(debouncer.len(), Ordering::SeqCst);\n\n if debouncer.is_ready_to_flush() \u0026\u0026 debouncer.len() \u003e 0 {\n let evts_to_process = debouncer.flush();\n let num_evts = evts_to_process.len();\n events_processed_clone.fetch_add(num_evts, Ordering::SeqCst);\n\n let db_guard_option = db_captured_for_thread.lock().unwrap();\n if let Some(db_mutex) = \u0026*db_guard_option {\n let mut _db_instance_guard = db_mutex.lock().unwrap();\n for event_item in \u0026evts_to_process {\n println!( \n \"Processing event (DB available): {:?} for path {:?}\",\n event_item.kind, event_item.path\n );\n }\n } else {\n for event_item in \u0026evts_to_process {\n println!( \n \"Processing event (no DB): {:?} for path {:?}\",\n event_item.kind, event_item.path\n );\n }\n }\n }\n thread::sleep(Duration::from_millis(50));\n }\n\n if debouncer.len() \u003e 0 {\n let final_evts = debouncer.flush();\n events_processed_clone.fetch_add(final_evts.len(), Ordering::SeqCst);\n for processed_event in final_evts {\n println!(\n \"Processing final event: {:?} for path {:?}\",\n processed_event.kind, processed_event.path\n );\n }\n }\n let mut final_state_guard = state_clone.lock().unwrap();\n *final_state_guard = WatcherState::Stopped;\n });\n\n Ok(Self {\n state,\n config,\n watched_paths: paths,\n event_receiver: rx,\n watcher: actual_watcher,\n processor_thread: Some(processor_thread),\n stop_flag,\n events_processed,\n queue_size,\n start_time: Instant::now(),\n db_shared: db_shared_for_thread,\n })\n }\n\n pub fn with_database(\u0026mut self, db_arc: Arc\u003cMutex\u003cDatabase\u003e\u003e) -\u003e \u0026mut Self {\n { \n let mut shared_db_guard = self.db_shared.lock().unwrap();\n *shared_db_guard = Some(db_arc);\n } \n self\n }\n\n pub fn start(\u0026mut self) -\u003e Result\u003c()\u003e {\n let mut state_guard = self.state.lock().unwrap();\n if *state_guard == WatcherState::Watching || self.processor_thread.is_none() {\n if self.processor_thread.is_none() {\n return Err(anyhow::anyhow!(\"Watcher thread not available to start.\"));\n }\n if *state_guard == WatcherState::Initializing {\n *state_guard = WatcherState::Watching;\n }\n return Ok(());\n }\n if *state_guard != WatcherState::Initializing \u0026\u0026 *state_guard != WatcherState::Stopped \u0026\u0026 *state_guard != WatcherState::Paused {\n return Err(anyhow::anyhow!(format!(\"Cannot start watcher from state {:?}\", *state_guard)));\n }\n\n *state_guard = WatcherState::Watching;\n Ok(())\n }\n\n pub fn pause(\u0026mut self) -\u003e Result\u003c()\u003e {\n let mut state_guard = self.state.lock().unwrap();\n match *state_guard {\n WatcherState::Watching =\u003e {\n *state_guard = WatcherState::Paused;\n Ok(())\n }\n WatcherState::Paused =\u003e Ok(()), \n _ =\u003e Err(anyhow::anyhow!(format!(\"Watcher not in watching state to pause (current: {:?})\", *state_guard))),\n }\n }\n\n pub fn resume(\u0026mut self) -\u003e Result\u003c()\u003e {\n let mut state_guard = self.state.lock().unwrap();\n match *state_guard {\n WatcherState::Paused =\u003e {\n *state_guard = WatcherState::Watching;\n Ok(())\n }\n WatcherState::Watching =\u003e Ok(()), \n _ =\u003e Err(anyhow::anyhow!(format!(\"Watcher not in paused state to resume (current: {:?})\", *state_guard))),\n }\n }\n\n pub fn stop(\u0026mut self) -\u003e Result\u003c()\u003e {\n let mut current_state_guard = self.state.lock().unwrap();\n if *current_state_guard == WatcherState::Stopped || *current_state_guard == WatcherState::ShuttingDown {\n return Ok(());\n }\n *current_state_guard = WatcherState::ShuttingDown;\n drop(current_state_guard);\n\n self.stop_flag.store(true, Ordering::SeqCst);\n\n if let Some(handle) = self.processor_thread.take() {\n match handle.join() {\n Ok(_) =\u003e { /* Thread joined cleanly */ }\n Err(join_err) =\u003e {\n eprintln!(\"Watcher processor thread panicked: {:?}\", join_err);\n }\n }\n }\n \n let mut final_state_guard = self.state.lock().unwrap();\n *final_state_guard = WatcherState::Stopped;\n Ok(())\n }\n\n pub fn status(\u0026self) -\u003e WatcherStatus {\n let state_guard = self.state.lock().unwrap().clone();\n WatcherStatus {\n state: state_guard,\n events_processed: self.events_processed.load(Ordering::SeqCst),\n queue_size: self.queue_size.load(Ordering::SeqCst),\n start_time: Some(self.start_time),\n watched_paths: self.watched_paths.clone(),\n }\n }\n}\n\nimpl Drop for FileWatcher {\n fn drop(\u0026mut self) {\n if let Err(e) = self.stop() {\n eprintln!(\"Error stopping watcher in Drop: {:?}\", e);\n }\n }\n}\n\n\n#[cfg(test)]\nmod file_watcher_state_tests { \n use super::*;\n use tempfile::tempdir;\n use std::fs as FsMod; // Alias to avoid conflict with local `fs` module name if any\n\n #[test]\n fn test_watcher_pause_resume_stop() {\n let tmp_dir = tempdir().unwrap();\n let watch_path = tmp_dir.path().to_path_buf();\n FsMod::create_dir_all(\u0026watch_path).expect(\"Failed to create temp dir for watching\");\n\n let config = WatcherConfig::default();\n\n let mut watcher = FileWatcher::new(vec![watch_path], config).expect(\"Failed to create watcher\");\n\n assert_eq!(watcher.status().state, WatcherState::Initializing);\n\n watcher.start().expect(\"Start failed\");\n assert_eq!(watcher.status().state, WatcherState::Watching);\n\n watcher.pause().expect(\"Pause failed\");\n assert_eq!(watcher.status().state, WatcherState::Paused);\n\n watcher.pause().expect(\"Second pause failed\");\n assert_eq!(watcher.status().state, WatcherState::Paused);\n\n watcher.resume().expect(\"Resume failed\");\n assert_eq!(watcher.status().state, WatcherState::Watching);\n \n watcher.resume().expect(\"Second resume failed\");\n assert_eq!(watcher.status().state, WatcherState::Watching);\n\n watcher.stop().expect(\"Stop failed\");\n assert_eq!(watcher.status().state, WatcherState::Stopped);\n\n watcher.stop().expect(\"Second stop failed\");\n assert_eq!(watcher.status().state, WatcherState::Stopped);\n }\n\n #[test]\n fn test_watcher_start_errors() {\n let tmp_dir = tempdir().unwrap();\n FsMod::create_dir_all(tmp_dir.path()).expect(\"Failed to create temp dir for watching\");\n let mut watcher = FileWatcher::new(vec![tmp_dir.path().to_path_buf()], WatcherConfig::default()).unwrap();\n \n {\n let mut state_guard = watcher.state.lock().unwrap();\n *state_guard = WatcherState::Watching; \n }\n assert!(watcher.start().is_ok(), \"Should be able to call start when already Watching (idempotent state change)\");\n assert_eq!(watcher.status().state, WatcherState::Watching);\n \n {\n let mut state_guard = watcher.state.lock().unwrap();\n *state_guard = WatcherState::ShuttingDown;\n }\n assert!(watcher.start().is_err(), \"Should not be able to start from ShuttingDown\");\n }\n\n #[test]\n fn test_new_watcher_with_nonexistent_path() {\n let non_existent_path = PathBuf::from(\"/path/that/REALLY/does/not/exist/for/sure/and/cannot/be/created\");\n let config = WatcherConfig::default();\n let watcher_result = FileWatcher::new(vec![non_existent_path], config);\n assert!(watcher_result.is_err());\n if let Err(e) = watcher_result {\n let err_string = e.to_string();\n assert!(err_string.contains(\"Failed to watch path\") || err_string.contains(\"os error 2\"), \"Error was: {}\", err_string);\n }\n }\n\n #[test]\n fn test_watcher_default_config() {\n let config = WatcherConfig::default(); \n assert_eq!(config.debounce_ms, 100);\n assert_eq!(config.batch_size, 1000);\n assert_eq!(config.max_queue_size, 100_000);\n assert_eq!(config.drain_timeout_ms, 5000);\n }\n}","traces":[{"line":38,"address":[5542400],"length":1,"stats":{"Line":1}},{"line":91,"address":[2059012,2059006,2058864],"length":1,"stats":{"Line":1}},{"line":93,"address":[2058893],"length":1,"stats":{"Line":1}},{"line":95,"address":[2058898],"length":1,"stats":{"Line":1}},{"line":99,"address":[2059024,2059653],"length":1,"stats":{"Line":1}},{"line":100,"address":[2059049,2059126],"length":1,"stats":{"Line":2}},{"line":101,"address":[2059204,2059139],"length":1,"stats":{"Line":4}},{"line":103,"address":[2059261],"length":1,"stats":{"Line":3}},{"line":105,"address":[2059239,2059275],"length":1,"stats":{"Line":4}},{"line":106,"address":[2059313],"length":1,"stats":{"Line":1}},{"line":107,"address":[2059576,2059331,2059496],"length":1,"stats":{"Line":2}},{"line":108,"address":[2059570],"length":1,"stats":{"Line":0}},{"line":110,"address":[2059512],"length":1,"stats":{"Line":1}},{"line":111,"address":[5543146],"length":1,"stats":{"Line":1}},{"line":114,"address":[2059363],"length":1,"stats":{"Line":2}},{"line":119,"address":[2059696],"length":1,"stats":{"Line":1}},{"line":120,"address":[2059710],"length":1,"stats":{"Line":1}},{"line":123,"address":[2060031,2060025,2059776],"length":1,"stats":{"Line":2}},{"line":124,"address":[2059819],"length":1,"stats":{"Line":5}},{"line":125,"address":[2004170,2004160],"length":1,"stats":{"Line":5}},{"line":126,"address":[2059940],"length":1,"stats":{"Line":1}},{"line":127,"address":[2059991],"length":1,"stats":{"Line":1}},{"line":131,"address":[2060048],"length":1,"stats":{"Line":2}},{"line":132,"address":[5543701],"length":1,"stats":{"Line":2}},{"line":310,"address":[2060064,2063925,2063163],"length":1,"stats":{"Line":1}},{"line":311,"address":[5543751,5543927],"length":1,"stats":{"Line":2}},{"line":312,"address":[2060433,2060369],"length":1,"stats":{"Line":2}},{"line":313,"address":[2060524,2060588],"length":1,"stats":{"Line":2}},{"line":314,"address":[2060675,2060755],"length":1,"stats":{"Line":2}},{"line":316,"address":[2060910,2060850],"length":1,"stats":{"Line":2}},{"line":318,"address":[2060990,2061070],"length":1,"stats":{"Line":2}},{"line":320,"address":[2004268,2004176,2004274],"length":1,"stats":{"Line":2}},{"line":321,"address":[2004191],"length":1,"stats":{"Line":1}},{"line":325,"address":[2061110],"length":1,"stats":{"Line":1}},{"line":328,"address":[5545137,5545221],"length":1,"stats":{"Line":2}},{"line":329,"address":[2063555,2063405],"length":1,"stats":{"Line":2}},{"line":330,"address":[2061644,2063397],"length":1,"stats":{"Line":2}},{"line":331,"address":[2004288,2004310],"length":1,"stats":{"Line":3}},{"line":334,"address":[2061680],"length":1,"stats":{"Line":1}},{"line":335,"address":[2061687],"length":1,"stats":{"Line":1}},{"line":336,"address":[2061740,2061798],"length":1,"stats":{"Line":2}},{"line":337,"address":[5545506,5545573],"length":1,"stats":{"Line":2}},{"line":338,"address":[2061901,2061956],"length":1,"stats":{"Line":2}},{"line":339,"address":[2062045,2061980],"length":1,"stats":{"Line":2}},{"line":341,"address":[2062069,2062150],"length":1,"stats":{"Line":2}},{"line":342,"address":[2062237,2062183],"length":1,"stats":{"Line":2}},{"line":344,"address":[2062245],"length":1,"stats":{"Line":2}},{"line":345,"address":[2004479,2004570],"length":1,"stats":{"Line":2}},{"line":347,"address":[2004580,2004648],"length":1,"stats":{"Line":2}},{"line":348,"address":[2004760,2004699],"length":1,"stats":{"Line":2}},{"line":350,"address":[2004950],"length":1,"stats":{"Line":1}},{"line":351,"address":[2005026,2008058],"length":1,"stats":{"Line":0}},{"line":354,"address":[2005058,2004992],"length":1,"stats":{"Line":2}},{"line":358,"address":[2005114],"length":1,"stats":{"Line":1}},{"line":359,"address":[2006547,2005222,2005142],"length":1,"stats":{"Line":3}},{"line":360,"address":[2005272,2005346],"length":1,"stats":{"Line":1}},{"line":361,"address":[2005313],"length":1,"stats":{"Line":1}},{"line":362,"address":[2005521],"length":1,"stats":{"Line":1}},{"line":363,"address":[2005577,2005815,2005680],"length":1,"stats":{"Line":3}},{"line":364,"address":[2005884],"length":1,"stats":{"Line":1}},{"line":365,"address":[2006022],"length":1,"stats":{"Line":1}},{"line":366,"address":[2006042],"length":1,"stats":{"Line":1}},{"line":367,"address":[2006032],"length":1,"stats":{"Line":1}},{"line":368,"address":[2006012],"length":1,"stats":{"Line":1}},{"line":369,"address":[2006002],"length":1,"stats":{"Line":0}},{"line":371,"address":[2006268],"length":1,"stats":{"Line":1}},{"line":372,"address":[5524074],"length":1,"stats":{"Line":1}},{"line":373,"address":[2006082,2006155],"length":1,"stats":{"Line":2}},{"line":374,"address":[2006206],"length":1,"stats":{"Line":1}},{"line":375,"address":[2006220],"length":1,"stats":{"Line":1}},{"line":379,"address":[2005423],"length":1,"stats":{"Line":0}},{"line":380,"address":[2005511,2006432],"length":1,"stats":{"Line":0}},{"line":383,"address":[2005979],"length":1,"stats":{"Line":1}},{"line":388,"address":[2006663,2006719],"length":1,"stats":{"Line":2}},{"line":390,"address":[2006781,2006846],"length":1,"stats":{"Line":2}},{"line":391,"address":[2006891],"length":1,"stats":{"Line":1}},{"line":392,"address":[2006906,2006977],"length":1,"stats":{"Line":2}},{"line":393,"address":[2006985],"length":1,"stats":{"Line":1}},{"line":395,"address":[5525091],"length":1,"stats":{"Line":1}},{"line":396,"address":[5525293,5525214],"length":1,"stats":{"Line":2}},{"line":397,"address":[5525335,5525397],"length":1,"stats":{"Line":0}},{"line":398,"address":[2007409,2007476],"length":1,"stats":{"Line":0}},{"line":399,"address":[5525665,5525734],"length":1,"stats":{"Line":0}},{"line":405,"address":[5525856,5525354],"length":1,"stats":{"Line":2}},{"line":406,"address":[5526046,5525965],"length":1,"stats":{"Line":2}},{"line":413,"address":[5526025,5524841],"length":1,"stats":{"Line":2}},{"line":416,"address":[2008588,2004726,2008082],"length":1,"stats":{"Line":2}},{"line":417,"address":[5526247],"length":1,"stats":{"Line":0}},{"line":418,"address":[5526331,5526270],"length":1,"stats":{"Line":0}},{"line":419,"address":[5526388,5526590],"length":1,"stats":{"Line":0}},{"line":420,"address":[2008539,2008843],"length":1,"stats":{"Line":0}},{"line":426,"address":[5526742,5526208],"length":1,"stats":{"Line":2}},{"line":427,"address":[2008743,2008670],"length":1,"stats":{"Line":2}},{"line":430,"address":[5546512],"length":1,"stats":{"Line":1}},{"line":431,"address":[5546176],"length":1,"stats":{"Line":1}},{"line":433,"address":[2062476],"length":1,"stats":{"Line":1}},{"line":434,"address":[2062507],"length":1,"stats":{"Line":1}},{"line":435,"address":[5546271],"length":1,"stats":{"Line":1}},{"line":436,"address":[5546311],"length":1,"stats":{"Line":1}},{"line":437,"address":[5546343],"length":1,"stats":{"Line":1}},{"line":438,"address":[2062643],"length":1,"stats":{"Line":1}},{"line":439,"address":[2062667],"length":1,"stats":{"Line":1}},{"line":440,"address":[5546415],"length":1,"stats":{"Line":1}},{"line":441,"address":[2062776],"length":1,"stats":{"Line":1}},{"line":445,"address":[2064322,2064316,2063968],"length":1,"stats":{"Line":0}},{"line":447,"address":[2064066,2064005],"length":1,"stats":{"Line":0}},{"line":448,"address":[5548035,5547941,5547982,5548140],"length":1,"stats":{"Line":0}},{"line":450,"address":[5547798],"length":1,"stats":{"Line":0}},{"line":453,"address":[5549403,5549178,5548192],"length":1,"stats":{"Line":1}},{"line":454,"address":[5548215],"length":1,"stats":{"Line":1}},{"line":455,"address":[5548480,5548306,5548385],"length":1,"stats":{"Line":3}},{"line":456,"address":[2064587,2065274],"length":1,"stats":{"Line":2}},{"line":457,"address":[5549218,5549350],"length":1,"stats":{"Line":0}},{"line":459,"address":[5549249,5549194,5549340],"length":1,"stats":{"Line":2}},{"line":460,"address":[2065376],"length":1,"stats":{"Line":0}},{"line":462,"address":[5549277],"length":1,"stats":{"Line":1}},{"line":464,"address":[5548582,5548486],"length":1,"stats":{"Line":2}},{"line":465,"address":[5548986,5549156,5548707,5548792],"length":1,"stats":{"Line":2}},{"line":468,"address":[5548547,5548736],"length":1,"stats":{"Line":2}},{"line":469,"address":[5548745],"length":1,"stats":{"Line":1}},{"line":472,"address":[5549424,5550085,5550079],"length":1,"stats":{"Line":1}},{"line":473,"address":[2065519],"length":1,"stats":{"Line":1}},{"line":474,"address":[2065588,2065642],"length":1,"stats":{"Line":2}},{"line":476,"address":[5549674,5549632],"length":1,"stats":{"Line":2}},{"line":477,"address":[2065732],"length":1,"stats":{"Line":1}},{"line":479,"address":[2065710],"length":1,"stats":{"Line":1}},{"line":480,"address":[2065676,2065777,2065949],"length":1,"stats":{"Line":0}},{"line":484,"address":[5550769,5550775,5550112],"length":1,"stats":{"Line":1}},{"line":485,"address":[2066159],"length":1,"stats":{"Line":1}},{"line":486,"address":[5550203,5550270],"length":1,"stats":{"Line":2}},{"line":488,"address":[2066389,2066339],"length":1,"stats":{"Line":2}},{"line":489,"address":[2066395],"length":1,"stats":{"Line":1}},{"line":491,"address":[5550320],"length":1,"stats":{"Line":1}},{"line":492,"address":[2066591,2066316,2066419],"length":1,"stats":{"Line":0}},{"line":496,"address":[2067872,2067606,2066784],"length":1,"stats":{"Line":1}},{"line":497,"address":[2066804],"length":1,"stats":{"Line":1}},{"line":498,"address":[5550918,5550985,5551072],"length":1,"stats":{"Line":3}},{"line":499,"address":[2066998],"length":1,"stats":{"Line":1}},{"line":501,"address":[5551100],"length":1,"stats":{"Line":1}},{"line":502,"address":[2067102],"length":1,"stats":{"Line":1}},{"line":504,"address":[2067135],"length":1,"stats":{"Line":1}},{"line":506,"address":[2067193],"length":1,"stats":{"Line":1}},{"line":507,"address":[5551422,5551343],"length":1,"stats":{"Line":2}},{"line":509,"address":[2067416],"length":1,"stats":{"Line":0}},{"line":510,"address":[2067464,2067515],"length":1,"stats":{"Line":0}},{"line":515,"address":[2067683,2067635],"length":1,"stats":{"Line":2}},{"line":516,"address":[2067747,2067818],"length":1,"stats":{"Line":2}},{"line":517,"address":[2067827],"length":1,"stats":{"Line":1}},{"line":520,"address":[5551984,5552406,5552412],"length":1,"stats":{"Line":1}},{"line":521,"address":[2067942],"length":1,"stats":{"Line":1}},{"line":524,"address":[5552195],"length":1,"stats":{"Line":1}},{"line":525,"address":[2068130],"length":1,"stats":{"Line":1}},{"line":526,"address":[2068167],"length":1,"stats":{"Line":1}},{"line":527,"address":[2068183],"length":1,"stats":{"Line":1}},{"line":533,"address":[2001554,2001560,2001328],"length":1,"stats":{"Line":1}},{"line":534,"address":[2001340],"length":1,"stats":{"Line":1}},{"line":535,"address":[2001410,2001484],"length":1,"stats":{"Line":0}}],"covered":134,"coverable":157},{"path":["/","home","user","Documents","GitHub","Marlin","libmarlin","src","watcher_tests.rs"],"content":"//! Tests for the file system watcher functionality\n\n#[cfg(test)]\nmod tests {\n // Updated import for BackupManager from the new backup module\n use crate::backup::BackupManager;\n // These are still from the watcher module\n use crate::watcher::{FileWatcher, WatcherConfig, WatcherState};\n use crate::db::open as open_marlin_db; // Use your project's DB open function\n\n\n use std::fs::{self, File};\n use std::io::Write;\n // No longer need: use std::path::PathBuf;\n use std::thread;\n use std::time::Duration;\n use tempfile::tempdir;\n\n #[test]\n fn test_watcher_lifecycle() {\n // Create a temp directory for testing\n let temp_dir = tempdir().expect(\"Failed to create temp directory\");\n let temp_path = temp_dir.path();\n\n // Create a test file\n let test_file_path = temp_path.join(\"test.txt\");\n let mut file = File::create(\u0026test_file_path).expect(\"Failed to create test file\");\n writeln!(file, \"Test content\").expect(\"Failed to write to test file\");\n drop(file);\n\n // Configure and start the watcher\n let config = WatcherConfig {\n debounce_ms: 100,\n batch_size: 10,\n max_queue_size: 100,\n drain_timeout_ms: 1000,\n };\n\n let mut watcher = FileWatcher::new(vec![temp_path.to_path_buf()], config)\n .expect(\"Failed to create watcher\");\n\n watcher.start().expect(\"Failed to start watcher\");\n assert_eq!(watcher.status().state, WatcherState::Watching);\n\n thread::sleep(Duration::from_millis(200));\n let new_file_path = temp_path.join(\"new_file.txt\");\n let mut new_file_handle = File::create(\u0026new_file_path).expect(\"Failed to create new file\");\n writeln!(new_file_handle, \"New file content\").expect(\"Failed to write to new file\");\n drop(new_file_handle);\n\n thread::sleep(Duration::from_millis(200));\n let mut existing_file_handle = fs::OpenOptions::new()\n .write(true)\n .append(true)\n .open(\u0026test_file_path)\n .expect(\"Failed to open test file for modification\");\n writeln!(existing_file_handle, \"Additional content\").expect(\"Failed to append to test file\");\n drop(existing_file_handle);\n\n thread::sleep(Duration::from_millis(200));\n fs::remove_file(\u0026new_file_path).expect(\"Failed to remove file\");\n\n thread::sleep(Duration::from_millis(500));\n watcher.stop().expect(\"Failed to stop watcher\");\n\n assert_eq!(watcher.status().state, WatcherState::Stopped);\n assert!(watcher.status().events_processed \u003e 0, \"Expected some file events to be processed\");\n }\n\n #[test]\n fn test_backup_manager_related_functionality() {\n let live_db_tmp_dir = tempdir().expect(\"Failed to create temp directory for live DB\");\n let backups_storage_tmp_dir = tempdir().expect(\"Failed to create temp directory for backups storage\");\n \n let live_db_path = live_db_tmp_dir.path().join(\"test_live_watcher.db\"); // Unique name\n let backups_actual_dir = backups_storage_tmp_dir.path().join(\"my_backups_watcher\"); // Unique name\n\n // Initialize a proper SQLite DB for the \"live\" database\n let _conn = open_marlin_db(\u0026live_db_path).expect(\"Failed to open test_live_watcher.db for backup test\");\n \n let backup_manager = BackupManager::new(\u0026live_db_path, \u0026backups_actual_dir)\n .expect(\"Failed to create BackupManager instance\");\n \n let backup_info = backup_manager.create_backup().expect(\"Failed to create first backup\");\n \n assert!(backups_actual_dir.join(\u0026backup_info.id).exists(), \"Backup file should exist\");\n assert!(backup_info.size_bytes \u003e 0, \"Backup size should be greater than 0\");\n \n for i in 0..3 {\n std::thread::sleep(std::time::Duration::from_millis(30)); // Ensure timestamp difference\n backup_manager.create_backup().unwrap_or_else(|e| panic!(\"Failed to create additional backup {}: {:?}\", i, e));\n }\n \n let backups = backup_manager.list_backups().expect(\"Failed to list backups\");\n assert_eq!(backups.len(), 4, \"Should have 4 backups listed\");\n \n let prune_result = backup_manager.prune(2).expect(\"Failed to prune backups\");\n \n assert_eq!(prune_result.kept.len(), 2, \"Should have kept 2 backups\");\n assert_eq!(prune_result.removed.len(), 2, \"Should have removed 2 backups (4 initial - 2 kept)\");\n \n let remaining_backups = backup_manager.list_backups().expect(\"Failed to list backups after prune\");\n assert_eq!(remaining_backups.len(), 2, \"Should have 2 backups remaining after prune\");\n\n for removed_info in prune_result.removed {\n assert!(!backups_actual_dir.join(\u0026removed_info.id).exists(), \"Removed backup file {} should not exist\", removed_info.id);\n }\n for kept_info in prune_result.kept {\n assert!(backups_actual_dir.join(\u0026kept_info.id).exists(), \"Kept backup file {} should exist\", kept_info.id);\n }\n }\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","user","Documents","GitHub","Marlin","tui-bin","src","main.rs"],"content":"// tui-bin/src/main.rs\n\nfn main() {\n eprintln!(\"marlin-tui is not yet implemented. Stay tuned!\");\n}\n","traces":[{"line":3,"address":[127488],"length":1,"stats":{"Line":0}},{"line":4,"address":[127492],"length":1,"stats":{"Line":0}}],"covered":0,"coverable":2}]};
|
||
</script>
|
||
<script crossorigin>/** @license React v16.13.1
|
||
* react.production.min.js
|
||
*
|
||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||
*
|
||
* This source code is licensed under the MIT license found in the
|
||
* LICENSE file in the root directory of this source tree.
|
||
*/
|
||
'use strict';(function(d,r){"object"===typeof exports&&"undefined"!==typeof module?r(exports):"function"===typeof define&&define.amd?define(["exports"],r):(d=d||self,r(d.React={}))})(this,function(d){function r(a){for(var b="https://reactjs.org/docs/error-decoder.html?invariant="+a,c=1;c<arguments.length;c++)b+="&args[]="+encodeURIComponent(arguments[c]);return"Minified React error #"+a+"; visit "+b+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}
|
||
function w(a,b,c){this.props=a;this.context=b;this.refs=ba;this.updater=c||ca}function da(){}function L(a,b,c){this.props=a;this.context=b;this.refs=ba;this.updater=c||ca}function ea(a,b,c){var g,e={},fa=null,d=null;if(null!=b)for(g in void 0!==b.ref&&(d=b.ref),void 0!==b.key&&(fa=""+b.key),b)ha.call(b,g)&&!ia.hasOwnProperty(g)&&(e[g]=b[g]);var h=arguments.length-2;if(1===h)e.children=c;else if(1<h){for(var k=Array(h),f=0;f<h;f++)k[f]=arguments[f+2];e.children=k}if(a&&a.defaultProps)for(g in h=a.defaultProps,
|
||
h)void 0===e[g]&&(e[g]=h[g]);return{$$typeof:x,type:a,key:fa,ref:d,props:e,_owner:M.current}}function va(a,b){return{$$typeof:x,type:a.type,key:b,ref:a.ref,props:a.props,_owner:a._owner}}function N(a){return"object"===typeof a&&null!==a&&a.$$typeof===x}function wa(a){var b={"=":"=0",":":"=2"};return"$"+(""+a).replace(/[=:]/g,function(a){return b[a]})}function ja(a,b,c,g){if(C.length){var e=C.pop();e.result=a;e.keyPrefix=b;e.func=c;e.context=g;e.count=0;return e}return{result:a,keyPrefix:b,func:c,
|
||
context:g,count:0}}function ka(a){a.result=null;a.keyPrefix=null;a.func=null;a.context=null;a.count=0;10>C.length&&C.push(a)}function O(a,b,c,g){var e=typeof a;if("undefined"===e||"boolean"===e)a=null;var d=!1;if(null===a)d=!0;else switch(e){case "string":case "number":d=!0;break;case "object":switch(a.$$typeof){case x:case xa:d=!0}}if(d)return c(g,a,""===b?"."+P(a,0):b),1;d=0;b=""===b?".":b+":";if(Array.isArray(a))for(var f=0;f<a.length;f++){e=a[f];var h=b+P(e,f);d+=O(e,h,c,g)}else if(null===a||
|
||
"object"!==typeof a?h=null:(h=la&&a[la]||a["@@iterator"],h="function"===typeof h?h:null),"function"===typeof h)for(a=h.call(a),f=0;!(e=a.next()).done;)e=e.value,h=b+P(e,f++),d+=O(e,h,c,g);else if("object"===e)throw c=""+a,Error(r(31,"[object Object]"===c?"object with keys {"+Object.keys(a).join(", ")+"}":c,""));return d}function Q(a,b,c){return null==a?0:O(a,"",b,c)}function P(a,b){return"object"===typeof a&&null!==a&&null!=a.key?wa(a.key):b.toString(36)}function ya(a,b,c){a.func.call(a.context,b,
|
||
a.count++)}function za(a,b,c){var g=a.result,e=a.keyPrefix;a=a.func.call(a.context,b,a.count++);Array.isArray(a)?R(a,g,c,function(a){return a}):null!=a&&(N(a)&&(a=va(a,e+(!a.key||b&&b.key===a.key?"":(""+a.key).replace(ma,"$&/")+"/")+c)),g.push(a))}function R(a,b,c,g,e){var d="";null!=c&&(d=(""+c).replace(ma,"$&/")+"/");b=ja(b,d,g,e);Q(a,za,b);ka(b)}function t(){var a=na.current;if(null===a)throw Error(r(321));return a}function S(a,b){var c=a.length;a.push(b);a:for(;;){var g=c-1>>>1,e=a[g];if(void 0!==
|
||
e&&0<D(e,b))a[g]=b,a[c]=e,c=g;else break a}}function n(a){a=a[0];return void 0===a?null:a}function E(a){var b=a[0];if(void 0!==b){var c=a.pop();if(c!==b){a[0]=c;a:for(var g=0,e=a.length;g<e;){var d=2*(g+1)-1,f=a[d],h=d+1,k=a[h];if(void 0!==f&&0>D(f,c))void 0!==k&&0>D(k,f)?(a[g]=k,a[h]=c,g=h):(a[g]=f,a[d]=c,g=d);else if(void 0!==k&&0>D(k,c))a[g]=k,a[h]=c,g=h;else break a}}return b}return null}function D(a,b){var c=a.sortIndex-b.sortIndex;return 0!==c?c:a.id-b.id}function F(a){for(var b=n(u);null!==
|
||
b;){if(null===b.callback)E(u);else if(b.startTime<=a)E(u),b.sortIndex=b.expirationTime,S(p,b);else break;b=n(u)}}function T(a){y=!1;F(a);if(!v)if(null!==n(p))v=!0,z(U);else{var b=n(u);null!==b&&G(T,b.startTime-a)}}function U(a,b){v=!1;y&&(y=!1,V());H=!0;var c=m;try{F(b);for(l=n(p);null!==l&&(!(l.expirationTime>b)||a&&!W());){var g=l.callback;if(null!==g){l.callback=null;m=l.priorityLevel;var e=g(l.expirationTime<=b);b=q();"function"===typeof e?l.callback=e:l===n(p)&&E(p);F(b)}else E(p);l=n(p)}if(null!==
|
||
l)var d=!0;else{var f=n(u);null!==f&&G(T,f.startTime-b);d=!1}return d}finally{l=null,m=c,H=!1}}function oa(a){switch(a){case 1:return-1;case 2:return 250;case 5:return 1073741823;case 4:return 1E4;default:return 5E3}}var f="function"===typeof Symbol&&Symbol.for,x=f?Symbol.for("react.element"):60103,xa=f?Symbol.for("react.portal"):60106,Aa=f?Symbol.for("react.fragment"):60107,Ba=f?Symbol.for("react.strict_mode"):60108,Ca=f?Symbol.for("react.profiler"):60114,Da=f?Symbol.for("react.provider"):60109,
|
||
Ea=f?Symbol.for("react.context"):60110,Fa=f?Symbol.for("react.forward_ref"):60112,Ga=f?Symbol.for("react.suspense"):60113,Ha=f?Symbol.for("react.memo"):60115,Ia=f?Symbol.for("react.lazy"):60116,la="function"===typeof Symbol&&Symbol.iterator,pa=Object.getOwnPropertySymbols,Ja=Object.prototype.hasOwnProperty,Ka=Object.prototype.propertyIsEnumerable,I=function(){try{if(!Object.assign)return!1;var a=new String("abc");a[5]="de";if("5"===Object.getOwnPropertyNames(a)[0])return!1;var b={};for(a=0;10>a;a++)b["_"+
|
||
String.fromCharCode(a)]=a;if("0123456789"!==Object.getOwnPropertyNames(b).map(function(a){return b[a]}).join(""))return!1;var c={};"abcdefghijklmnopqrst".split("").forEach(function(a){c[a]=a});return"abcdefghijklmnopqrst"!==Object.keys(Object.assign({},c)).join("")?!1:!0}catch(g){return!1}}()?Object.assign:function(a,b){if(null===a||void 0===a)throw new TypeError("Object.assign cannot be called with null or undefined");var c=Object(a);for(var g,e=1;e<arguments.length;e++){var d=Object(arguments[e]);
|
||
for(var f in d)Ja.call(d,f)&&(c[f]=d[f]);if(pa){g=pa(d);for(var h=0;h<g.length;h++)Ka.call(d,g[h])&&(c[g[h]]=d[g[h]])}}return c},ca={isMounted:function(a){return!1},enqueueForceUpdate:function(a,b,c){},enqueueReplaceState:function(a,b,c,d){},enqueueSetState:function(a,b,c,d){}},ba={};w.prototype.isReactComponent={};w.prototype.setState=function(a,b){if("object"!==typeof a&&"function"!==typeof a&&null!=a)throw Error(r(85));this.updater.enqueueSetState(this,a,b,"setState")};w.prototype.forceUpdate=
|
||
function(a){this.updater.enqueueForceUpdate(this,a,"forceUpdate")};da.prototype=w.prototype;f=L.prototype=new da;f.constructor=L;I(f,w.prototype);f.isPureReactComponent=!0;var M={current:null},ha=Object.prototype.hasOwnProperty,ia={key:!0,ref:!0,__self:!0,__source:!0},ma=/\/+/g,C=[],na={current:null},X;if("undefined"===typeof window||"function"!==typeof MessageChannel){var A=null,qa=null,ra=function(){if(null!==A)try{var a=q();A(!0,a);A=null}catch(b){throw setTimeout(ra,0),b;}},La=Date.now();var q=
|
||
function(){return Date.now()-La};var z=function(a){null!==A?setTimeout(z,0,a):(A=a,setTimeout(ra,0))};var G=function(a,b){qa=setTimeout(a,b)};var V=function(){clearTimeout(qa)};var W=function(){return!1};f=X=function(){}}else{var Y=window.performance,sa=window.Date,Ma=window.setTimeout,Na=window.clearTimeout;"undefined"!==typeof console&&(f=window.cancelAnimationFrame,"function"!==typeof window.requestAnimationFrame&&console.error("This browser doesn't support requestAnimationFrame. Make sure that you load a polyfill in older browsers. https://fb.me/react-polyfills"),
|
||
"function"!==typeof f&&console.error("This browser doesn't support cancelAnimationFrame. Make sure that you load a polyfill in older browsers. https://fb.me/react-polyfills"));if("object"===typeof Y&&"function"===typeof Y.now)q=function(){return Y.now()};else{var Oa=sa.now();q=function(){return sa.now()-Oa}}var J=!1,K=null,Z=-1,ta=5,ua=0;W=function(){return q()>=ua};f=function(){};X=function(a){0>a||125<a?console.error("forceFrameRate takes a positive int between 0 and 125, forcing framerates higher than 125 fps is not unsupported"):
|
||
ta=0<a?Math.floor(1E3/a):5};var B=new MessageChannel,aa=B.port2;B.port1.onmessage=function(){if(null!==K){var a=q();ua=a+ta;try{K(!0,a)?aa.postMessage(null):(J=!1,K=null)}catch(b){throw aa.postMessage(null),b;}}else J=!1};z=function(a){K=a;J||(J=!0,aa.postMessage(null))};G=function(a,b){Z=Ma(function(){a(q())},b)};V=function(){Na(Z);Z=-1}}var p=[],u=[],Pa=1,l=null,m=3,H=!1,v=!1,y=!1,Qa=0;B={ReactCurrentDispatcher:na,ReactCurrentOwner:M,IsSomeRendererActing:{current:!1},assign:I};I(B,{Scheduler:{__proto__:null,
|
||
unstable_ImmediatePriority:1,unstable_UserBlockingPriority:2,unstable_NormalPriority:3,unstable_IdlePriority:5,unstable_LowPriority:4,unstable_runWithPriority:function(a,b){switch(a){case 1:case 2:case 3:case 4:case 5:break;default:a=3}var c=m;m=a;try{return b()}finally{m=c}},unstable_next:function(a){switch(m){case 1:case 2:case 3:var b=3;break;default:b=m}var c=m;m=b;try{return a()}finally{m=c}},unstable_scheduleCallback:function(a,b,c){var d=q();if("object"===typeof c&&null!==c){var e=c.delay;
|
||
e="number"===typeof e&&0<e?d+e:d;c="number"===typeof c.timeout?c.timeout:oa(a)}else c=oa(a),e=d;c=e+c;a={id:Pa++,callback:b,priorityLevel:a,startTime:e,expirationTime:c,sortIndex:-1};e>d?(a.sortIndex=e,S(u,a),null===n(p)&&a===n(u)&&(y?V():y=!0,G(T,e-d))):(a.sortIndex=c,S(p,a),v||H||(v=!0,z(U)));return a},unstable_cancelCallback:function(a){a.callback=null},unstable_wrapCallback:function(a){var b=m;return function(){var c=m;m=b;try{return a.apply(this,arguments)}finally{m=c}}},unstable_getCurrentPriorityLevel:function(){return m},
|
||
unstable_shouldYield:function(){var a=q();F(a);var b=n(p);return b!==l&&null!==l&&null!==b&&null!==b.callback&&b.startTime<=a&&b.expirationTime<l.expirationTime||W()},unstable_requestPaint:f,unstable_continueExecution:function(){v||H||(v=!0,z(U))},unstable_pauseExecution:function(){},unstable_getFirstCallbackNode:function(){return n(p)},get unstable_now(){return q},get unstable_forceFrameRate(){return X},unstable_Profiling:null},SchedulerTracing:{__proto__:null,__interactionsRef:null,__subscriberRef:null,
|
||
unstable_clear:function(a){return a()},unstable_getCurrent:function(){return null},unstable_getThreadID:function(){return++Qa},unstable_trace:function(a,b,c){return c()},unstable_wrap:function(a){return a},unstable_subscribe:function(a){},unstable_unsubscribe:function(a){}}});d.Children={map:function(a,b,c){if(null==a)return a;var d=[];R(a,d,null,b,c);return d},forEach:function(a,b,c){if(null==a)return a;b=ja(null,null,b,c);Q(a,ya,b);ka(b)},count:function(a){return Q(a,function(){return null},null)},
|
||
toArray:function(a){var b=[];R(a,b,null,function(a){return a});return b},only:function(a){if(!N(a))throw Error(r(143));return a}};d.Component=w;d.Fragment=Aa;d.Profiler=Ca;d.PureComponent=L;d.StrictMode=Ba;d.Suspense=Ga;d.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=B;d.cloneElement=function(a,b,c){if(null===a||void 0===a)throw Error(r(267,a));var d=I({},a.props),e=a.key,f=a.ref,m=a._owner;if(null!=b){void 0!==b.ref&&(f=b.ref,m=M.current);void 0!==b.key&&(e=""+b.key);if(a.type&&a.type.defaultProps)var h=
|
||
a.type.defaultProps;for(k in b)ha.call(b,k)&&!ia.hasOwnProperty(k)&&(d[k]=void 0===b[k]&&void 0!==h?h[k]:b[k])}var k=arguments.length-2;if(1===k)d.children=c;else if(1<k){h=Array(k);for(var l=0;l<k;l++)h[l]=arguments[l+2];d.children=h}return{$$typeof:x,type:a.type,key:e,ref:f,props:d,_owner:m}};d.createContext=function(a,b){void 0===b&&(b=null);a={$$typeof:Ea,_calculateChangedBits:b,_currentValue:a,_currentValue2:a,_threadCount:0,Provider:null,Consumer:null};a.Provider={$$typeof:Da,_context:a};return a.Consumer=
|
||
a};d.createElement=ea;d.createFactory=function(a){var b=ea.bind(null,a);b.type=a;return b};d.createRef=function(){return{current:null}};d.forwardRef=function(a){return{$$typeof:Fa,render:a}};d.isValidElement=N;d.lazy=function(a){return{$$typeof:Ia,_ctor:a,_status:-1,_result:null}};d.memo=function(a,b){return{$$typeof:Ha,type:a,compare:void 0===b?null:b}};d.useCallback=function(a,b){return t().useCallback(a,b)};d.useContext=function(a,b){return t().useContext(a,b)};d.useDebugValue=function(a,b){};
|
||
d.useEffect=function(a,b){return t().useEffect(a,b)};d.useImperativeHandle=function(a,b,c){return t().useImperativeHandle(a,b,c)};d.useLayoutEffect=function(a,b){return t().useLayoutEffect(a,b)};d.useMemo=function(a,b){return t().useMemo(a,b)};d.useReducer=function(a,b,c){return t().useReducer(a,b,c)};d.useRef=function(a){return t().useRef(a)};d.useState=function(a){return t().useState(a)};d.version="16.13.1"});
|
||
</script>
|
||
<script crossorigin>/** @license React v16.13.1
|
||
* react-dom.production.min.js
|
||
*
|
||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||
*
|
||
* This source code is licensed under the MIT license found in the
|
||
* LICENSE file in the root directory of this source tree.
|
||
*/
|
||
/*
|
||
Modernizr 3.0.0pre (Custom Build) | MIT
|
||
*/
|
||
'use strict';(function(I,ea){"object"===typeof exports&&"undefined"!==typeof module?ea(exports,require("react")):"function"===typeof define&&define.amd?define(["exports","react"],ea):(I=I||self,ea(I.ReactDOM={},I.React))})(this,function(I,ea){function k(a){for(var b="https://reactjs.org/docs/error-decoder.html?invariant="+a,c=1;c<arguments.length;c++)b+="&args[]="+encodeURIComponent(arguments[c]);return"Minified React error #"+a+"; visit "+b+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}
|
||
function ji(a,b,c,d,e,f,g,h,m){yb=!1;gc=null;ki.apply(li,arguments)}function mi(a,b,c,d,e,f,g,h,m){ji.apply(this,arguments);if(yb){if(yb){var n=gc;yb=!1;gc=null}else throw Error(k(198));hc||(hc=!0,pd=n)}}function lf(a,b,c){var d=a.type||"unknown-event";a.currentTarget=mf(c);mi(d,b,void 0,a);a.currentTarget=null}function nf(){if(ic)for(var a in cb){var b=cb[a],c=ic.indexOf(a);if(!(-1<c))throw Error(k(96,a));if(!jc[c]){if(!b.extractEvents)throw Error(k(97,a));jc[c]=b;c=b.eventTypes;for(var d in c){var e=
|
||
void 0;var f=c[d],g=b,h=d;if(qd.hasOwnProperty(h))throw Error(k(99,h));qd[h]=f;var m=f.phasedRegistrationNames;if(m){for(e in m)m.hasOwnProperty(e)&&of(m[e],g,h);e=!0}else f.registrationName?(of(f.registrationName,g,h),e=!0):e=!1;if(!e)throw Error(k(98,d,a));}}}}function of(a,b,c){if(db[a])throw Error(k(100,a));db[a]=b;rd[a]=b.eventTypes[c].dependencies}function pf(a){var b=!1,c;for(c in a)if(a.hasOwnProperty(c)){var d=a[c];if(!cb.hasOwnProperty(c)||cb[c]!==d){if(cb[c])throw Error(k(102,c));cb[c]=
|
||
d;b=!0}}b&&nf()}function qf(a){if(a=rf(a)){if("function"!==typeof sd)throw Error(k(280));var b=a.stateNode;b&&(b=td(b),sd(a.stateNode,a.type,b))}}function sf(a){eb?fb?fb.push(a):fb=[a]:eb=a}function tf(){if(eb){var a=eb,b=fb;fb=eb=null;qf(a);if(b)for(a=0;a<b.length;a++)qf(b[a])}}function ud(){if(null!==eb||null!==fb)vd(),tf()}function uf(a,b,c){if(wd)return a(b,c);wd=!0;try{return vf(a,b,c)}finally{wd=!1,ud()}}function ni(a){if(wf.call(xf,a))return!0;if(wf.call(yf,a))return!1;if(oi.test(a))return xf[a]=
|
||
!0;yf[a]=!0;return!1}function pi(a,b,c,d){if(null!==c&&0===c.type)return!1;switch(typeof b){case "function":case "symbol":return!0;case "boolean":if(d)return!1;if(null!==c)return!c.acceptsBooleans;a=a.toLowerCase().slice(0,5);return"data-"!==a&&"aria-"!==a;default:return!1}}function qi(a,b,c,d){if(null===b||"undefined"===typeof b||pi(a,b,c,d))return!0;if(d)return!1;if(null!==c)switch(c.type){case 3:return!b;case 4:return!1===b;case 5:return isNaN(b);case 6:return isNaN(b)||1>b}return!1}function L(a,
|
||
b,c,d,e,f){this.acceptsBooleans=2===b||3===b||4===b;this.attributeName=d;this.attributeNamespace=e;this.mustUseProperty=c;this.propertyName=a;this.type=b;this.sanitizeURL=f}function xd(a,b,c,d){var e=E.hasOwnProperty(b)?E[b]:null;var f=null!==e?0===e.type:d?!1:!(2<b.length)||"o"!==b[0]&&"O"!==b[0]||"n"!==b[1]&&"N"!==b[1]?!1:!0;f||(qi(b,c,e,d)&&(c=null),d||null===e?ni(b)&&(null===c?a.removeAttribute(b):a.setAttribute(b,""+c)):e.mustUseProperty?a[e.propertyName]=null===c?3===e.type?!1:"":c:(b=e.attributeName,
|
||
d=e.attributeNamespace,null===c?a.removeAttribute(b):(e=e.type,c=3===e||4===e&&!0===c?"":""+c,d?a.setAttributeNS(d,b,c):a.setAttribute(b,c))))}function zb(a){if(null===a||"object"!==typeof a)return null;a=zf&&a[zf]||a["@@iterator"];return"function"===typeof a?a:null}function ri(a){if(-1===a._status){a._status=0;var b=a._ctor;b=b();a._result=b;b.then(function(b){0===a._status&&(b=b.default,a._status=1,a._result=b)},function(b){0===a._status&&(a._status=2,a._result=b)})}}function na(a){if(null==a)return null;
|
||
if("function"===typeof a)return a.displayName||a.name||null;if("string"===typeof a)return a;switch(a){case Ma:return"Fragment";case gb:return"Portal";case kc:return"Profiler";case Af:return"StrictMode";case lc:return"Suspense";case yd:return"SuspenseList"}if("object"===typeof a)switch(a.$$typeof){case Bf:return"Context.Consumer";case Cf:return"Context.Provider";case zd:var b=a.render;b=b.displayName||b.name||"";return a.displayName||(""!==b?"ForwardRef("+b+")":"ForwardRef");case Ad:return na(a.type);
|
||
case Df:return na(a.render);case Ef:if(a=1===a._status?a._result:null)return na(a)}return null}function Bd(a){var b="";do{a:switch(a.tag){case 3:case 4:case 6:case 7:case 10:case 9:var c="";break a;default:var d=a._debugOwner,e=a._debugSource,f=na(a.type);c=null;d&&(c=na(d.type));d=f;f="";e?f=" (at "+e.fileName.replace(si,"")+":"+e.lineNumber+")":c&&(f=" (created by "+c+")");c="\n in "+(d||"Unknown")+f}b+=c;a=a.return}while(a);return b}function va(a){switch(typeof a){case "boolean":case "number":case "object":case "string":case "undefined":return a;
|
||
default:return""}}function Ff(a){var b=a.type;return(a=a.nodeName)&&"input"===a.toLowerCase()&&("checkbox"===b||"radio"===b)}function ti(a){var b=Ff(a)?"checked":"value",c=Object.getOwnPropertyDescriptor(a.constructor.prototype,b),d=""+a[b];if(!a.hasOwnProperty(b)&&"undefined"!==typeof c&&"function"===typeof c.get&&"function"===typeof c.set){var e=c.get,f=c.set;Object.defineProperty(a,b,{configurable:!0,get:function(){return e.call(this)},set:function(a){d=""+a;f.call(this,a)}});Object.defineProperty(a,
|
||
b,{enumerable:c.enumerable});return{getValue:function(){return d},setValue:function(a){d=""+a},stopTracking:function(){a._valueTracker=null;delete a[b]}}}}function mc(a){a._valueTracker||(a._valueTracker=ti(a))}function Gf(a){if(!a)return!1;var b=a._valueTracker;if(!b)return!0;var c=b.getValue();var d="";a&&(d=Ff(a)?a.checked?"true":"false":a.value);a=d;return a!==c?(b.setValue(a),!0):!1}function Cd(a,b){var c=b.checked;return M({},b,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:null!=
|
||
c?c:a._wrapperState.initialChecked})}function Hf(a,b){var c=null==b.defaultValue?"":b.defaultValue,d=null!=b.checked?b.checked:b.defaultChecked;c=va(null!=b.value?b.value:c);a._wrapperState={initialChecked:d,initialValue:c,controlled:"checkbox"===b.type||"radio"===b.type?null!=b.checked:null!=b.value}}function If(a,b){b=b.checked;null!=b&&xd(a,"checked",b,!1)}function Dd(a,b){If(a,b);var c=va(b.value),d=b.type;if(null!=c)if("number"===d){if(0===c&&""===a.value||a.value!=c)a.value=""+c}else a.value!==
|
||
""+c&&(a.value=""+c);else if("submit"===d||"reset"===d){a.removeAttribute("value");return}b.hasOwnProperty("value")?Ed(a,b.type,c):b.hasOwnProperty("defaultValue")&&Ed(a,b.type,va(b.defaultValue));null==b.checked&&null!=b.defaultChecked&&(a.defaultChecked=!!b.defaultChecked)}function Jf(a,b,c){if(b.hasOwnProperty("value")||b.hasOwnProperty("defaultValue")){var d=b.type;if(!("submit"!==d&&"reset"!==d||void 0!==b.value&&null!==b.value))return;b=""+a._wrapperState.initialValue;c||b===a.value||(a.value=
|
||
b);a.defaultValue=b}c=a.name;""!==c&&(a.name="");a.defaultChecked=!!a._wrapperState.initialChecked;""!==c&&(a.name=c)}function Ed(a,b,c){if("number"!==b||a.ownerDocument.activeElement!==a)null==c?a.defaultValue=""+a._wrapperState.initialValue:a.defaultValue!==""+c&&(a.defaultValue=""+c)}function ui(a){var b="";ea.Children.forEach(a,function(a){null!=a&&(b+=a)});return b}function Fd(a,b){a=M({children:void 0},b);if(b=ui(b.children))a.children=b;return a}function hb(a,b,c,d){a=a.options;if(b){b={};
|
||
for(var e=0;e<c.length;e++)b["$"+c[e]]=!0;for(c=0;c<a.length;c++)e=b.hasOwnProperty("$"+a[c].value),a[c].selected!==e&&(a[c].selected=e),e&&d&&(a[c].defaultSelected=!0)}else{c=""+va(c);b=null;for(e=0;e<a.length;e++){if(a[e].value===c){a[e].selected=!0;d&&(a[e].defaultSelected=!0);return}null!==b||a[e].disabled||(b=a[e])}null!==b&&(b.selected=!0)}}function Gd(a,b){if(null!=b.dangerouslySetInnerHTML)throw Error(k(91));return M({},b,{value:void 0,defaultValue:void 0,children:""+a._wrapperState.initialValue})}
|
||
function Kf(a,b){var c=b.value;if(null==c){c=b.children;b=b.defaultValue;if(null!=c){if(null!=b)throw Error(k(92));if(Array.isArray(c)){if(!(1>=c.length))throw Error(k(93));c=c[0]}b=c}null==b&&(b="");c=b}a._wrapperState={initialValue:va(c)}}function Lf(a,b){var c=va(b.value),d=va(b.defaultValue);null!=c&&(c=""+c,c!==a.value&&(a.value=c),null==b.defaultValue&&a.defaultValue!==c&&(a.defaultValue=c));null!=d&&(a.defaultValue=""+d)}function Mf(a,b){b=a.textContent;b===a._wrapperState.initialValue&&""!==
|
||
b&&null!==b&&(a.value=b)}function Nf(a){switch(a){case "svg":return"http://www.w3.org/2000/svg";case "math":return"http://www.w3.org/1998/Math/MathML";default:return"http://www.w3.org/1999/xhtml"}}function Hd(a,b){return null==a||"http://www.w3.org/1999/xhtml"===a?Nf(b):"http://www.w3.org/2000/svg"===a&&"foreignObject"===b?"http://www.w3.org/1999/xhtml":a}function nc(a,b){var c={};c[a.toLowerCase()]=b.toLowerCase();c["Webkit"+a]="webkit"+b;c["Moz"+a]="moz"+b;return c}function oc(a){if(Id[a])return Id[a];
|
||
if(!ib[a])return a;var b=ib[a],c;for(c in b)if(b.hasOwnProperty(c)&&c in Of)return Id[a]=b[c];return a}function Jd(a){var b=Pf.get(a);void 0===b&&(b=new Map,Pf.set(a,b));return b}function Na(a){var b=a,c=a;if(a.alternate)for(;b.return;)b=b.return;else{a=b;do b=a,0!==(b.effectTag&1026)&&(c=b.return),a=b.return;while(a)}return 3===b.tag?c:null}function Qf(a){if(13===a.tag){var b=a.memoizedState;null===b&&(a=a.alternate,null!==a&&(b=a.memoizedState));if(null!==b)return b.dehydrated}return null}function Rf(a){if(Na(a)!==
|
||
a)throw Error(k(188));}function vi(a){var b=a.alternate;if(!b){b=Na(a);if(null===b)throw Error(k(188));return b!==a?null:a}for(var c=a,d=b;;){var e=c.return;if(null===e)break;var f=e.alternate;if(null===f){d=e.return;if(null!==d){c=d;continue}break}if(e.child===f.child){for(f=e.child;f;){if(f===c)return Rf(e),a;if(f===d)return Rf(e),b;f=f.sibling}throw Error(k(188));}if(c.return!==d.return)c=e,d=f;else{for(var g=!1,h=e.child;h;){if(h===c){g=!0;c=e;d=f;break}if(h===d){g=!0;d=e;c=f;break}h=h.sibling}if(!g){for(h=
|
||
f.child;h;){if(h===c){g=!0;c=f;d=e;break}if(h===d){g=!0;d=f;c=e;break}h=h.sibling}if(!g)throw Error(k(189));}}if(c.alternate!==d)throw Error(k(190));}if(3!==c.tag)throw Error(k(188));return c.stateNode.current===c?a:b}function Sf(a){a=vi(a);if(!a)return null;for(var b=a;;){if(5===b.tag||6===b.tag)return b;if(b.child)b.child.return=b,b=b.child;else{if(b===a)break;for(;!b.sibling;){if(!b.return||b.return===a)return null;b=b.return}b.sibling.return=b.return;b=b.sibling}}return null}function jb(a,b){if(null==
|
||
b)throw Error(k(30));if(null==a)return b;if(Array.isArray(a)){if(Array.isArray(b))return a.push.apply(a,b),a;a.push(b);return a}return Array.isArray(b)?[a].concat(b):[a,b]}function Kd(a,b,c){Array.isArray(a)?a.forEach(b,c):a&&b.call(c,a)}function pc(a){null!==a&&(Ab=jb(Ab,a));a=Ab;Ab=null;if(a){Kd(a,wi);if(Ab)throw Error(k(95));if(hc)throw a=pd,hc=!1,pd=null,a;}}function Ld(a){a=a.target||a.srcElement||window;a.correspondingUseElement&&(a=a.correspondingUseElement);return 3===a.nodeType?a.parentNode:
|
||
a}function Tf(a){if(!wa)return!1;a="on"+a;var b=a in document;b||(b=document.createElement("div"),b.setAttribute(a,"return;"),b="function"===typeof b[a]);return b}function Uf(a){a.topLevelType=null;a.nativeEvent=null;a.targetInst=null;a.ancestors.length=0;10>qc.length&&qc.push(a)}function Vf(a,b,c,d){if(qc.length){var e=qc.pop();e.topLevelType=a;e.eventSystemFlags=d;e.nativeEvent=b;e.targetInst=c;return e}return{topLevelType:a,eventSystemFlags:d,nativeEvent:b,targetInst:c,ancestors:[]}}function Wf(a){var b=
|
||
a.targetInst,c=b;do{if(!c){a.ancestors.push(c);break}var d=c;if(3===d.tag)d=d.stateNode.containerInfo;else{for(;d.return;)d=d.return;d=3!==d.tag?null:d.stateNode.containerInfo}if(!d)break;b=c.tag;5!==b&&6!==b||a.ancestors.push(c);c=Bb(d)}while(c);for(c=0;c<a.ancestors.length;c++){b=a.ancestors[c];var e=Ld(a.nativeEvent);d=a.topLevelType;var f=a.nativeEvent,g=a.eventSystemFlags;0===c&&(g|=64);for(var h=null,m=0;m<jc.length;m++){var n=jc[m];n&&(n=n.extractEvents(d,b,f,e,g))&&(h=jb(h,n))}pc(h)}}function Md(a,
|
||
b,c){if(!c.has(a)){switch(a){case "scroll":Cb(b,"scroll",!0);break;case "focus":case "blur":Cb(b,"focus",!0);Cb(b,"blur",!0);c.set("blur",null);c.set("focus",null);break;case "cancel":case "close":Tf(a)&&Cb(b,a,!0);break;case "invalid":case "submit":case "reset":break;default:-1===Db.indexOf(a)&&w(a,b)}c.set(a,null)}}function xi(a,b){var c=Jd(b);Nd.forEach(function(a){Md(a,b,c)});yi.forEach(function(a){Md(a,b,c)})}function Od(a,b,c,d,e){return{blockedOn:a,topLevelType:b,eventSystemFlags:c|32,nativeEvent:e,
|
||
container:d}}function Xf(a,b){switch(a){case "focus":case "blur":xa=null;break;case "dragenter":case "dragleave":ya=null;break;case "mouseover":case "mouseout":za=null;break;case "pointerover":case "pointerout":Eb.delete(b.pointerId);break;case "gotpointercapture":case "lostpointercapture":Fb.delete(b.pointerId)}}function Gb(a,b,c,d,e,f){if(null===a||a.nativeEvent!==f)return a=Od(b,c,d,e,f),null!==b&&(b=Hb(b),null!==b&&Yf(b)),a;a.eventSystemFlags|=d;return a}function zi(a,b,c,d,e){switch(b){case "focus":return xa=
|
||
Gb(xa,a,b,c,d,e),!0;case "dragenter":return ya=Gb(ya,a,b,c,d,e),!0;case "mouseover":return za=Gb(za,a,b,c,d,e),!0;case "pointerover":var f=e.pointerId;Eb.set(f,Gb(Eb.get(f)||null,a,b,c,d,e));return!0;case "gotpointercapture":return f=e.pointerId,Fb.set(f,Gb(Fb.get(f)||null,a,b,c,d,e)),!0}return!1}function Ai(a){var b=Bb(a.target);if(null!==b){var c=Na(b);if(null!==c)if(b=c.tag,13===b){if(b=Qf(c),null!==b){a.blockedOn=b;Pd(a.priority,function(){Bi(c)});return}}else if(3===b&&c.stateNode.hydrate){a.blockedOn=
|
||
3===c.tag?c.stateNode.containerInfo:null;return}}a.blockedOn=null}function rc(a){if(null!==a.blockedOn)return!1;var b=Qd(a.topLevelType,a.eventSystemFlags,a.container,a.nativeEvent);if(null!==b){var c=Hb(b);null!==c&&Yf(c);a.blockedOn=b;return!1}return!0}function Zf(a,b,c){rc(a)&&c.delete(b)}function Ci(){for(Rd=!1;0<fa.length;){var a=fa[0];if(null!==a.blockedOn){a=Hb(a.blockedOn);null!==a&&Di(a);break}var b=Qd(a.topLevelType,a.eventSystemFlags,a.container,a.nativeEvent);null!==b?a.blockedOn=b:fa.shift()}null!==
|
||
xa&&rc(xa)&&(xa=null);null!==ya&&rc(ya)&&(ya=null);null!==za&&rc(za)&&(za=null);Eb.forEach(Zf);Fb.forEach(Zf)}function Ib(a,b){a.blockedOn===b&&(a.blockedOn=null,Rd||(Rd=!0,$f(ag,Ci)))}function bg(a){if(0<fa.length){Ib(fa[0],a);for(var b=1;b<fa.length;b++){var c=fa[b];c.blockedOn===a&&(c.blockedOn=null)}}null!==xa&&Ib(xa,a);null!==ya&&Ib(ya,a);null!==za&&Ib(za,a);b=function(b){return Ib(b,a)};Eb.forEach(b);Fb.forEach(b);for(b=0;b<Jb.length;b++)c=Jb[b],c.blockedOn===a&&(c.blockedOn=null);for(;0<Jb.length&&
|
||
(b=Jb[0],null===b.blockedOn);)Ai(b),null===b.blockedOn&&Jb.shift()}function Sd(a,b){for(var c=0;c<a.length;c+=2){var d=a[c],e=a[c+1],f="on"+(e[0].toUpperCase()+e.slice(1));f={phasedRegistrationNames:{bubbled:f,captured:f+"Capture"},dependencies:[d],eventPriority:b};Td.set(d,b);cg.set(d,f);dg[e]=f}}function w(a,b){Cb(b,a,!1)}function Cb(a,b,c){var d=Td.get(b);switch(void 0===d?2:d){case 0:d=Ei.bind(null,b,1,a);break;case 1:d=Fi.bind(null,b,1,a);break;default:d=sc.bind(null,b,1,a)}c?a.addEventListener(b,
|
||
d,!0):a.addEventListener(b,d,!1)}function Ei(a,b,c,d){Oa||vd();var e=sc,f=Oa;Oa=!0;try{eg(e,a,b,c,d)}finally{(Oa=f)||ud()}}function Fi(a,b,c,d){Gi(Hi,sc.bind(null,a,b,c,d))}function sc(a,b,c,d){if(tc)if(0<fa.length&&-1<Nd.indexOf(a))a=Od(null,a,b,c,d),fa.push(a);else{var e=Qd(a,b,c,d);if(null===e)Xf(a,d);else if(-1<Nd.indexOf(a))a=Od(e,a,b,c,d),fa.push(a);else if(!zi(e,a,b,c,d)){Xf(a,d);a=Vf(a,d,null,b);try{uf(Wf,a)}finally{Uf(a)}}}}function Qd(a,b,c,d){c=Ld(d);c=Bb(c);if(null!==c){var e=Na(c);if(null===
|
||
e)c=null;else{var f=e.tag;if(13===f){c=Qf(e);if(null!==c)return c;c=null}else if(3===f){if(e.stateNode.hydrate)return 3===e.tag?e.stateNode.containerInfo:null;c=null}else e!==c&&(c=null)}}a=Vf(a,d,c,b);try{uf(Wf,a)}finally{Uf(a)}return null}function fg(a,b,c){return null==b||"boolean"===typeof b||""===b?"":c||"number"!==typeof b||0===b||Kb.hasOwnProperty(a)&&Kb[a]?(""+b).trim():b+"px"}function gg(a,b){a=a.style;for(var c in b)if(b.hasOwnProperty(c)){var d=0===c.indexOf("--"),e=fg(c,b[c],d);"float"===
|
||
c&&(c="cssFloat");d?a.setProperty(c,e):a[c]=e}}function Ud(a,b){if(b){if(Ii[a]&&(null!=b.children||null!=b.dangerouslySetInnerHTML))throw Error(k(137,a,""));if(null!=b.dangerouslySetInnerHTML){if(null!=b.children)throw Error(k(60));if(!("object"===typeof b.dangerouslySetInnerHTML&&"__html"in b.dangerouslySetInnerHTML))throw Error(k(61));}if(null!=b.style&&"object"!==typeof b.style)throw Error(k(62,""));}}function Vd(a,b){if(-1===a.indexOf("-"))return"string"===typeof b.is;switch(a){case "annotation-xml":case "color-profile":case "font-face":case "font-face-src":case "font-face-uri":case "font-face-format":case "font-face-name":case "missing-glyph":return!1;
|
||
default:return!0}}function oa(a,b){a=9===a.nodeType||11===a.nodeType?a:a.ownerDocument;var c=Jd(a);b=rd[b];for(var d=0;d<b.length;d++)Md(b[d],a,c)}function uc(){}function Wd(a){a=a||("undefined"!==typeof document?document:void 0);if("undefined"===typeof a)return null;try{return a.activeElement||a.body}catch(b){return a.body}}function hg(a){for(;a&&a.firstChild;)a=a.firstChild;return a}function ig(a,b){var c=hg(a);a=0;for(var d;c;){if(3===c.nodeType){d=a+c.textContent.length;if(a<=b&&d>=b)return{node:c,
|
||
offset:b-a};a=d}a:{for(;c;){if(c.nextSibling){c=c.nextSibling;break a}c=c.parentNode}c=void 0}c=hg(c)}}function jg(a,b){return a&&b?a===b?!0:a&&3===a.nodeType?!1:b&&3===b.nodeType?jg(a,b.parentNode):"contains"in a?a.contains(b):a.compareDocumentPosition?!!(a.compareDocumentPosition(b)&16):!1:!1}function kg(){for(var a=window,b=Wd();b instanceof a.HTMLIFrameElement;){try{var c="string"===typeof b.contentWindow.location.href}catch(d){c=!1}if(c)a=b.contentWindow;else break;b=Wd(a.document)}return b}
|
||
function Xd(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return b&&("input"===b&&("text"===a.type||"search"===a.type||"tel"===a.type||"url"===a.type||"password"===a.type)||"textarea"===b||"true"===a.contentEditable)}function lg(a,b){switch(a){case "button":case "input":case "select":case "textarea":return!!b.autoFocus}return!1}function Yd(a,b){return"textarea"===a||"option"===a||"noscript"===a||"string"===typeof b.children||"number"===typeof b.children||"object"===typeof b.dangerouslySetInnerHTML&&
|
||
null!==b.dangerouslySetInnerHTML&&null!=b.dangerouslySetInnerHTML.__html}function kb(a){for(;null!=a;a=a.nextSibling){var b=a.nodeType;if(1===b||3===b)break}return a}function mg(a){a=a.previousSibling;for(var b=0;a;){if(8===a.nodeType){var c=a.data;if(c===ng||c===Zd||c===$d){if(0===b)return a;b--}else c===og&&b++}a=a.previousSibling}return null}function Bb(a){var b=a[Aa];if(b)return b;for(var c=a.parentNode;c;){if(b=c[Lb]||c[Aa]){c=b.alternate;if(null!==b.child||null!==c&&null!==c.child)for(a=mg(a);null!==
|
||
a;){if(c=a[Aa])return c;a=mg(a)}return b}a=c;c=a.parentNode}return null}function Hb(a){a=a[Aa]||a[Lb];return!a||5!==a.tag&&6!==a.tag&&13!==a.tag&&3!==a.tag?null:a}function Pa(a){if(5===a.tag||6===a.tag)return a.stateNode;throw Error(k(33));}function ae(a){return a[vc]||null}function pa(a){do a=a.return;while(a&&5!==a.tag);return a?a:null}function pg(a,b){var c=a.stateNode;if(!c)return null;var d=td(c);if(!d)return null;c=d[b];a:switch(b){case "onClick":case "onClickCapture":case "onDoubleClick":case "onDoubleClickCapture":case "onMouseDown":case "onMouseDownCapture":case "onMouseMove":case "onMouseMoveCapture":case "onMouseUp":case "onMouseUpCapture":case "onMouseEnter":(d=
|
||
!d.disabled)||(a=a.type,d=!("button"===a||"input"===a||"select"===a||"textarea"===a));a=!d;break a;default:a=!1}if(a)return null;if(c&&"function"!==typeof c)throw Error(k(231,b,typeof c));return c}function qg(a,b,c){if(b=pg(a,c.dispatchConfig.phasedRegistrationNames[b]))c._dispatchListeners=jb(c._dispatchListeners,b),c._dispatchInstances=jb(c._dispatchInstances,a)}function Ji(a){if(a&&a.dispatchConfig.phasedRegistrationNames){for(var b=a._targetInst,c=[];b;)c.push(b),b=pa(b);for(b=c.length;0<b--;)qg(c[b],
|
||
"captured",a);for(b=0;b<c.length;b++)qg(c[b],"bubbled",a)}}function be(a,b,c){a&&c&&c.dispatchConfig.registrationName&&(b=pg(a,c.dispatchConfig.registrationName))&&(c._dispatchListeners=jb(c._dispatchListeners,b),c._dispatchInstances=jb(c._dispatchInstances,a))}function Ki(a){a&&a.dispatchConfig.registrationName&&be(a._targetInst,null,a)}function lb(a){Kd(a,Ji)}function rg(){if(wc)return wc;var a,b=ce,c=b.length,d,e="value"in Ba?Ba.value:Ba.textContent,f=e.length;for(a=0;a<c&&b[a]===e[a];a++);var g=
|
||
c-a;for(d=1;d<=g&&b[c-d]===e[f-d];d++);return wc=e.slice(a,1<d?1-d:void 0)}function xc(){return!0}function yc(){return!1}function R(a,b,c,d){this.dispatchConfig=a;this._targetInst=b;this.nativeEvent=c;a=this.constructor.Interface;for(var e in a)a.hasOwnProperty(e)&&((b=a[e])?this[e]=b(c):"target"===e?this.target=d:this[e]=c[e]);this.isDefaultPrevented=(null!=c.defaultPrevented?c.defaultPrevented:!1===c.returnValue)?xc:yc;this.isPropagationStopped=yc;return this}function Li(a,b,c,d){if(this.eventPool.length){var e=
|
||
this.eventPool.pop();this.call(e,a,b,c,d);return e}return new this(a,b,c,d)}function Mi(a){if(!(a instanceof this))throw Error(k(279));a.destructor();10>this.eventPool.length&&this.eventPool.push(a)}function sg(a){a.eventPool=[];a.getPooled=Li;a.release=Mi}function tg(a,b){switch(a){case "keyup":return-1!==Ni.indexOf(b.keyCode);case "keydown":return 229!==b.keyCode;case "keypress":case "mousedown":case "blur":return!0;default:return!1}}function ug(a){a=a.detail;return"object"===typeof a&&"data"in
|
||
a?a.data:null}function Oi(a,b){switch(a){case "compositionend":return ug(b);case "keypress":if(32!==b.which)return null;vg=!0;return wg;case "textInput":return a=b.data,a===wg&&vg?null:a;default:return null}}function Pi(a,b){if(mb)return"compositionend"===a||!de&&tg(a,b)?(a=rg(),wc=ce=Ba=null,mb=!1,a):null;switch(a){case "paste":return null;case "keypress":if(!(b.ctrlKey||b.altKey||b.metaKey)||b.ctrlKey&&b.altKey){if(b.char&&1<b.char.length)return b.char;if(b.which)return String.fromCharCode(b.which)}return null;
|
||
case "compositionend":return xg&&"ko"!==b.locale?null:b.data;default:return null}}function yg(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return"input"===b?!!Qi[a.type]:"textarea"===b?!0:!1}function zg(a,b,c){a=R.getPooled(Ag.change,a,b,c);a.type="change";sf(c);lb(a);return a}function Ri(a){pc(a)}function zc(a){var b=Pa(a);if(Gf(b))return a}function Si(a,b){if("change"===a)return b}function Bg(){Mb&&(Mb.detachEvent("onpropertychange",Cg),Nb=Mb=null)}function Cg(a){if("value"===a.propertyName&&
|
||
zc(Nb))if(a=zg(Nb,a,Ld(a)),Oa)pc(a);else{Oa=!0;try{ee(Ri,a)}finally{Oa=!1,ud()}}}function Ti(a,b,c){"focus"===a?(Bg(),Mb=b,Nb=c,Mb.attachEvent("onpropertychange",Cg)):"blur"===a&&Bg()}function Ui(a,b){if("selectionchange"===a||"keyup"===a||"keydown"===a)return zc(Nb)}function Vi(a,b){if("click"===a)return zc(b)}function Wi(a,b){if("input"===a||"change"===a)return zc(b)}function Xi(a){var b=this.nativeEvent;return b.getModifierState?b.getModifierState(a):(a=Yi[a])?!!b[a]:!1}function fe(a){return Xi}
|
||
function Zi(a,b){return a===b&&(0!==a||1/a===1/b)||a!==a&&b!==b}function Ob(a,b){if(Qa(a,b))return!0;if("object"!==typeof a||null===a||"object"!==typeof b||null===b)return!1;var c=Object.keys(a),d=Object.keys(b);if(c.length!==d.length)return!1;for(d=0;d<c.length;d++)if(!$i.call(b,c[d])||!Qa(a[c[d]],b[c[d]]))return!1;return!0}function Dg(a,b){var c=b.window===b?b.document:9===b.nodeType?b:b.ownerDocument;if(ge||null==nb||nb!==Wd(c))return null;c=nb;"selectionStart"in c&&Xd(c)?c={start:c.selectionStart,
|
||
end:c.selectionEnd}:(c=(c.ownerDocument&&c.ownerDocument.defaultView||window).getSelection(),c={anchorNode:c.anchorNode,anchorOffset:c.anchorOffset,focusNode:c.focusNode,focusOffset:c.focusOffset});return Pb&&Ob(Pb,c)?null:(Pb=c,a=R.getPooled(Eg.select,he,a,b),a.type="select",a.target=nb,lb(a),a)}function Ac(a){var b=a.keyCode;"charCode"in a?(a=a.charCode,0===a&&13===b&&(a=13)):a=b;10===a&&(a=13);return 32<=a||13===a?a:0}function q(a,b){0>ob||(a.current=ie[ob],ie[ob]=null,ob--)}function y(a,b,c){ob++;
|
||
ie[ob]=a.current;a.current=b}function pb(a,b){var c=a.type.contextTypes;if(!c)return Ca;var d=a.stateNode;if(d&&d.__reactInternalMemoizedUnmaskedChildContext===b)return d.__reactInternalMemoizedMaskedChildContext;var e={},f;for(f in c)e[f]=b[f];d&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=b,a.__reactInternalMemoizedMaskedChildContext=e);return e}function N(a){a=a.childContextTypes;return null!==a&&void 0!==a}function Fg(a,b,c){if(B.current!==Ca)throw Error(k(168));y(B,b);y(G,c)}
|
||
function Gg(a,b,c){var d=a.stateNode;a=b.childContextTypes;if("function"!==typeof d.getChildContext)return c;d=d.getChildContext();for(var e in d)if(!(e in a))throw Error(k(108,na(b)||"Unknown",e));return M({},c,{},d)}function Bc(a){a=(a=a.stateNode)&&a.__reactInternalMemoizedMergedChildContext||Ca;Ra=B.current;y(B,a);y(G,G.current);return!0}function Hg(a,b,c){var d=a.stateNode;if(!d)throw Error(k(169));c?(a=Gg(a,b,Ra),d.__reactInternalMemoizedMergedChildContext=a,q(G),q(B),y(B,a)):q(G);y(G,c)}function Cc(){switch(aj()){case Dc:return 99;
|
||
case Ig:return 98;case Jg:return 97;case Kg:return 96;case Lg:return 95;default:throw Error(k(332));}}function Mg(a){switch(a){case 99:return Dc;case 98:return Ig;case 97:return Jg;case 96:return Kg;case 95:return Lg;default:throw Error(k(332));}}function Da(a,b){a=Mg(a);return bj(a,b)}function Ng(a,b,c){a=Mg(a);return je(a,b,c)}function Og(a){null===qa?(qa=[a],Ec=je(Dc,Pg)):qa.push(a);return Qg}function ha(){if(null!==Ec){var a=Ec;Ec=null;Rg(a)}Pg()}function Pg(){if(!ke&&null!==qa){ke=!0;var a=0;
|
||
try{var b=qa;Da(99,function(){for(;a<b.length;a++){var c=b[a];do c=c(!0);while(null!==c)}});qa=null}catch(c){throw null!==qa&&(qa=qa.slice(a+1)),je(Dc,ha),c;}finally{ke=!1}}}function Fc(a,b,c){c/=10;return 1073741821-(((1073741821-a+b/10)/c|0)+1)*c}function aa(a,b){if(a&&a.defaultProps){b=M({},b);a=a.defaultProps;for(var c in a)void 0===b[c]&&(b[c]=a[c])}return b}function le(){Gc=qb=Hc=null}function me(a){var b=Ic.current;q(Ic);a.type._context._currentValue=b}function Sg(a,b){for(;null!==a;){var c=
|
||
a.alternate;if(a.childExpirationTime<b)a.childExpirationTime=b,null!==c&&c.childExpirationTime<b&&(c.childExpirationTime=b);else if(null!==c&&c.childExpirationTime<b)c.childExpirationTime=b;else break;a=a.return}}function rb(a,b){Hc=a;Gc=qb=null;a=a.dependencies;null!==a&&null!==a.firstContext&&(a.expirationTime>=b&&(ia=!0),a.firstContext=null)}function W(a,b){if(Gc!==a&&!1!==b&&0!==b){if("number"!==typeof b||1073741823===b)Gc=a,b=1073741823;b={context:a,observedBits:b,next:null};if(null===qb){if(null===
|
||
Hc)throw Error(k(308));qb=b;Hc.dependencies={expirationTime:0,firstContext:b,responders:null}}else qb=qb.next=b}return a._currentValue}function ne(a){a.updateQueue={baseState:a.memoizedState,baseQueue:null,shared:{pending:null},effects:null}}function oe(a,b){a=a.updateQueue;b.updateQueue===a&&(b.updateQueue={baseState:a.baseState,baseQueue:a.baseQueue,shared:a.shared,effects:a.effects})}function Ea(a,b){a={expirationTime:a,suspenseConfig:b,tag:Tg,payload:null,callback:null,next:null};return a.next=
|
||
a}function Fa(a,b){a=a.updateQueue;if(null!==a){a=a.shared;var c=a.pending;null===c?b.next=b:(b.next=c.next,c.next=b);a.pending=b}}function Ug(a,b){var c=a.alternate;null!==c&&oe(c,a);a=a.updateQueue;c=a.baseQueue;null===c?(a.baseQueue=b.next=b,b.next=b):(b.next=c.next,c.next=b)}function Qb(a,b,c,d){var e=a.updateQueue;Ga=!1;var f=e.baseQueue,g=e.shared.pending;if(null!==g){if(null!==f){var h=f.next;f.next=g.next;g.next=h}f=g;e.shared.pending=null;h=a.alternate;null!==h&&(h=h.updateQueue,null!==h&&
|
||
(h.baseQueue=g))}if(null!==f){h=f.next;var m=e.baseState,n=0,k=null,ba=null,l=null;if(null!==h){var p=h;do{g=p.expirationTime;if(g<d){var t={expirationTime:p.expirationTime,suspenseConfig:p.suspenseConfig,tag:p.tag,payload:p.payload,callback:p.callback,next:null};null===l?(ba=l=t,k=m):l=l.next=t;g>n&&(n=g)}else{null!==l&&(l=l.next={expirationTime:1073741823,suspenseConfig:p.suspenseConfig,tag:p.tag,payload:p.payload,callback:p.callback,next:null});Vg(g,p.suspenseConfig);a:{var q=a,r=p;g=b;t=c;switch(r.tag){case 1:q=
|
||
r.payload;if("function"===typeof q){m=q.call(t,m,g);break a}m=q;break a;case 3:q.effectTag=q.effectTag&-4097|64;case Tg:q=r.payload;g="function"===typeof q?q.call(t,m,g):q;if(null===g||void 0===g)break a;m=M({},m,g);break a;case Jc:Ga=!0}}null!==p.callback&&(a.effectTag|=32,g=e.effects,null===g?e.effects=[p]:g.push(p))}p=p.next;if(null===p||p===h)if(g=e.shared.pending,null===g)break;else p=f.next=g.next,g.next=h,e.baseQueue=f=g,e.shared.pending=null}while(1)}null===l?k=m:l.next=ba;e.baseState=k;e.baseQueue=
|
||
l;Kc(n);a.expirationTime=n;a.memoizedState=m}}function Wg(a,b,c){a=b.effects;b.effects=null;if(null!==a)for(b=0;b<a.length;b++){var d=a[b],e=d.callback;if(null!==e){d.callback=null;d=e;e=c;if("function"!==typeof d)throw Error(k(191,d));d.call(e)}}}function Lc(a,b,c,d){b=a.memoizedState;c=c(d,b);c=null===c||void 0===c?b:M({},b,c);a.memoizedState=c;0===a.expirationTime&&(a.updateQueue.baseState=c)}function Xg(a,b,c,d,e,f,g){a=a.stateNode;return"function"===typeof a.shouldComponentUpdate?a.shouldComponentUpdate(d,
|
||
f,g):b.prototype&&b.prototype.isPureReactComponent?!Ob(c,d)||!Ob(e,f):!0}function Yg(a,b,c){var d=!1,e=Ca;var f=b.contextType;"object"===typeof f&&null!==f?f=W(f):(e=N(b)?Ra:B.current,d=b.contextTypes,f=(d=null!==d&&void 0!==d)?pb(a,e):Ca);b=new b(c,f);a.memoizedState=null!==b.state&&void 0!==b.state?b.state:null;b.updater=Mc;a.stateNode=b;b._reactInternalFiber=a;d&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=e,a.__reactInternalMemoizedMaskedChildContext=f);return b}function Zg(a,
|
||
b,c,d){a=b.state;"function"===typeof b.componentWillReceiveProps&&b.componentWillReceiveProps(c,d);"function"===typeof b.UNSAFE_componentWillReceiveProps&&b.UNSAFE_componentWillReceiveProps(c,d);b.state!==a&&Mc.enqueueReplaceState(b,b.state,null)}function pe(a,b,c,d){var e=a.stateNode;e.props=c;e.state=a.memoizedState;e.refs=$g;ne(a);var f=b.contextType;"object"===typeof f&&null!==f?e.context=W(f):(f=N(b)?Ra:B.current,e.context=pb(a,f));Qb(a,c,e,d);e.state=a.memoizedState;f=b.getDerivedStateFromProps;
|
||
"function"===typeof f&&(Lc(a,b,f,c),e.state=a.memoizedState);"function"===typeof b.getDerivedStateFromProps||"function"===typeof e.getSnapshotBeforeUpdate||"function"!==typeof e.UNSAFE_componentWillMount&&"function"!==typeof e.componentWillMount||(b=e.state,"function"===typeof e.componentWillMount&&e.componentWillMount(),"function"===typeof e.UNSAFE_componentWillMount&&e.UNSAFE_componentWillMount(),b!==e.state&&Mc.enqueueReplaceState(e,e.state,null),Qb(a,c,e,d),e.state=a.memoizedState);"function"===
|
||
typeof e.componentDidMount&&(a.effectTag|=4)}function Rb(a,b,c){a=c.ref;if(null!==a&&"function"!==typeof a&&"object"!==typeof a){if(c._owner){c=c._owner;if(c){if(1!==c.tag)throw Error(k(309));var d=c.stateNode}if(!d)throw Error(k(147,a));var e=""+a;if(null!==b&&null!==b.ref&&"function"===typeof b.ref&&b.ref._stringRef===e)return b.ref;b=function(a){var b=d.refs;b===$g&&(b=d.refs={});null===a?delete b[e]:b[e]=a};b._stringRef=e;return b}if("string"!==typeof a)throw Error(k(284));if(!c._owner)throw Error(k(290,
|
||
a));}return a}function Nc(a,b){if("textarea"!==a.type)throw Error(k(31,"[object Object]"===Object.prototype.toString.call(b)?"object with keys {"+Object.keys(b).join(", ")+"}":b,""));}function ah(a){function b(b,c){if(a){var d=b.lastEffect;null!==d?(d.nextEffect=c,b.lastEffect=c):b.firstEffect=b.lastEffect=c;c.nextEffect=null;c.effectTag=8}}function c(c,d){if(!a)return null;for(;null!==d;)b(c,d),d=d.sibling;return null}function d(a,b){for(a=new Map;null!==b;)null!==b.key?a.set(b.key,b):a.set(b.index,
|
||
b),b=b.sibling;return a}function e(a,b){a=Sa(a,b);a.index=0;a.sibling=null;return a}function f(b,c,d){b.index=d;if(!a)return c;d=b.alternate;if(null!==d)return d=d.index,d<c?(b.effectTag=2,c):d;b.effectTag=2;return c}function g(b){a&&null===b.alternate&&(b.effectTag=2);return b}function h(a,b,c,d){if(null===b||6!==b.tag)return b=qe(c,a.mode,d),b.return=a,b;b=e(b,c);b.return=a;return b}function m(a,b,c,d){if(null!==b&&b.elementType===c.type)return d=e(b,c.props),d.ref=Rb(a,b,c),d.return=a,d;d=Oc(c.type,
|
||
c.key,c.props,null,a.mode,d);d.ref=Rb(a,b,c);d.return=a;return d}function n(a,b,c,d){if(null===b||4!==b.tag||b.stateNode.containerInfo!==c.containerInfo||b.stateNode.implementation!==c.implementation)return b=re(c,a.mode,d),b.return=a,b;b=e(b,c.children||[]);b.return=a;return b}function l(a,b,c,d,f){if(null===b||7!==b.tag)return b=Ha(c,a.mode,d,f),b.return=a,b;b=e(b,c);b.return=a;return b}function ba(a,b,c){if("string"===typeof b||"number"===typeof b)return b=qe(""+b,a.mode,c),b.return=a,b;if("object"===
|
||
typeof b&&null!==b){switch(b.$$typeof){case Pc:return c=Oc(b.type,b.key,b.props,null,a.mode,c),c.ref=Rb(a,null,b),c.return=a,c;case gb:return b=re(b,a.mode,c),b.return=a,b}if(Qc(b)||zb(b))return b=Ha(b,a.mode,c,null),b.return=a,b;Nc(a,b)}return null}function p(a,b,c,d){var e=null!==b?b.key:null;if("string"===typeof c||"number"===typeof c)return null!==e?null:h(a,b,""+c,d);if("object"===typeof c&&null!==c){switch(c.$$typeof){case Pc:return c.key===e?c.type===Ma?l(a,b,c.props.children,d,e):m(a,b,c,
|
||
d):null;case gb:return c.key===e?n(a,b,c,d):null}if(Qc(c)||zb(c))return null!==e?null:l(a,b,c,d,null);Nc(a,c)}return null}function t(a,b,c,d,e){if("string"===typeof d||"number"===typeof d)return a=a.get(c)||null,h(b,a,""+d,e);if("object"===typeof d&&null!==d){switch(d.$$typeof){case Pc:return a=a.get(null===d.key?c:d.key)||null,d.type===Ma?l(b,a,d.props.children,e,d.key):m(b,a,d,e);case gb:return a=a.get(null===d.key?c:d.key)||null,n(b,a,d,e)}if(Qc(d)||zb(d))return a=a.get(c)||null,l(b,a,d,e,null);
|
||
Nc(b,d)}return null}function q(e,g,h,m){for(var n=null,k=null,l=g,r=g=0,C=null;null!==l&&r<h.length;r++){l.index>r?(C=l,l=null):C=l.sibling;var O=p(e,l,h[r],m);if(null===O){null===l&&(l=C);break}a&&l&&null===O.alternate&&b(e,l);g=f(O,g,r);null===k?n=O:k.sibling=O;k=O;l=C}if(r===h.length)return c(e,l),n;if(null===l){for(;r<h.length;r++)l=ba(e,h[r],m),null!==l&&(g=f(l,g,r),null===k?n=l:k.sibling=l,k=l);return n}for(l=d(e,l);r<h.length;r++)C=t(l,e,r,h[r],m),null!==C&&(a&&null!==C.alternate&&l.delete(null===
|
||
C.key?r:C.key),g=f(C,g,r),null===k?n=C:k.sibling=C,k=C);a&&l.forEach(function(a){return b(e,a)});return n}function w(e,g,h,n){var m=zb(h);if("function"!==typeof m)throw Error(k(150));h=m.call(h);if(null==h)throw Error(k(151));for(var l=m=null,r=g,C=g=0,O=null,v=h.next();null!==r&&!v.done;C++,v=h.next()){r.index>C?(O=r,r=null):O=r.sibling;var q=p(e,r,v.value,n);if(null===q){null===r&&(r=O);break}a&&r&&null===q.alternate&&b(e,r);g=f(q,g,C);null===l?m=q:l.sibling=q;l=q;r=O}if(v.done)return c(e,r),m;
|
||
if(null===r){for(;!v.done;C++,v=h.next())v=ba(e,v.value,n),null!==v&&(g=f(v,g,C),null===l?m=v:l.sibling=v,l=v);return m}for(r=d(e,r);!v.done;C++,v=h.next())v=t(r,e,C,v.value,n),null!==v&&(a&&null!==v.alternate&&r.delete(null===v.key?C:v.key),g=f(v,g,C),null===l?m=v:l.sibling=v,l=v);a&&r.forEach(function(a){return b(e,a)});return m}return function(a,d,f,h){var m="object"===typeof f&&null!==f&&f.type===Ma&&null===f.key;m&&(f=f.props.children);var n="object"===typeof f&&null!==f;if(n)switch(f.$$typeof){case Pc:a:{n=
|
||
f.key;for(m=d;null!==m;){if(m.key===n){switch(m.tag){case 7:if(f.type===Ma){c(a,m.sibling);d=e(m,f.props.children);d.return=a;a=d;break a}break;default:if(m.elementType===f.type){c(a,m.sibling);d=e(m,f.props);d.ref=Rb(a,m,f);d.return=a;a=d;break a}}c(a,m);break}else b(a,m);m=m.sibling}f.type===Ma?(d=Ha(f.props.children,a.mode,h,f.key),d.return=a,a=d):(h=Oc(f.type,f.key,f.props,null,a.mode,h),h.ref=Rb(a,d,f),h.return=a,a=h)}return g(a);case gb:a:{for(m=f.key;null!==d;){if(d.key===m)if(4===d.tag&&d.stateNode.containerInfo===
|
||
f.containerInfo&&d.stateNode.implementation===f.implementation){c(a,d.sibling);d=e(d,f.children||[]);d.return=a;a=d;break a}else{c(a,d);break}else b(a,d);d=d.sibling}d=re(f,a.mode,h);d.return=a;a=d}return g(a)}if("string"===typeof f||"number"===typeof f)return f=""+f,null!==d&&6===d.tag?(c(a,d.sibling),d=e(d,f),d.return=a,a=d):(c(a,d),d=qe(f,a.mode,h),d.return=a,a=d),g(a);if(Qc(f))return q(a,d,f,h);if(zb(f))return w(a,d,f,h);n&&Nc(a,f);if("undefined"===typeof f&&!m)switch(a.tag){case 1:case 0:throw a=
|
||
a.type,Error(k(152,a.displayName||a.name||"Component"));}return c(a,d)}}function Ta(a){if(a===Sb)throw Error(k(174));return a}function se(a,b){y(Tb,b);y(Ub,a);y(ja,Sb);a=b.nodeType;switch(a){case 9:case 11:b=(b=b.documentElement)?b.namespaceURI:Hd(null,"");break;default:a=8===a?b.parentNode:b,b=a.namespaceURI||null,a=a.tagName,b=Hd(b,a)}q(ja);y(ja,b)}function tb(a){q(ja);q(Ub);q(Tb)}function bh(a){Ta(Tb.current);var b=Ta(ja.current);var c=Hd(b,a.type);b!==c&&(y(Ub,a),y(ja,c))}function te(a){Ub.current===
|
||
a&&(q(ja),q(Ub))}function Rc(a){for(var b=a;null!==b;){if(13===b.tag){var c=b.memoizedState;if(null!==c&&(c=c.dehydrated,null===c||c.data===$d||c.data===Zd))return b}else if(19===b.tag&&void 0!==b.memoizedProps.revealOrder){if(0!==(b.effectTag&64))return b}else if(null!==b.child){b.child.return=b;b=b.child;continue}if(b===a)break;for(;null===b.sibling;){if(null===b.return||b.return===a)return null;b=b.return}b.sibling.return=b.return;b=b.sibling}return null}function ue(a,b){return{responder:a,props:b}}
|
||
function S(){throw Error(k(321));}function ve(a,b){if(null===b)return!1;for(var c=0;c<b.length&&c<a.length;c++)if(!Qa(a[c],b[c]))return!1;return!0}function we(a,b,c,d,e,f){Ia=f;z=b;b.memoizedState=null;b.updateQueue=null;b.expirationTime=0;Sc.current=null===a||null===a.memoizedState?dj:ej;a=c(d,e);if(b.expirationTime===Ia){f=0;do{b.expirationTime=0;if(!(25>f))throw Error(k(301));f+=1;J=K=null;b.updateQueue=null;Sc.current=fj;a=c(d,e)}while(b.expirationTime===Ia)}Sc.current=Tc;b=null!==K&&null!==K.next;
|
||
Ia=0;J=K=z=null;Uc=!1;if(b)throw Error(k(300));return a}function ub(){var a={memoizedState:null,baseState:null,baseQueue:null,queue:null,next:null};null===J?z.memoizedState=J=a:J=J.next=a;return J}function vb(){if(null===K){var a=z.alternate;a=null!==a?a.memoizedState:null}else a=K.next;var b=null===J?z.memoizedState:J.next;if(null!==b)J=b,K=a;else{if(null===a)throw Error(k(310));K=a;a={memoizedState:K.memoizedState,baseState:K.baseState,baseQueue:K.baseQueue,queue:K.queue,next:null};null===J?z.memoizedState=
|
||
J=a:J=J.next=a}return J}function Ua(a,b){return"function"===typeof b?b(a):b}function Vc(a,b,c){b=vb();c=b.queue;if(null===c)throw Error(k(311));c.lastRenderedReducer=a;var d=K,e=d.baseQueue,f=c.pending;if(null!==f){if(null!==e){var g=e.next;e.next=f.next;f.next=g}d.baseQueue=e=f;c.pending=null}if(null!==e){e=e.next;d=d.baseState;var h=g=f=null,m=e;do{var n=m.expirationTime;if(n<Ia){var l={expirationTime:m.expirationTime,suspenseConfig:m.suspenseConfig,action:m.action,eagerReducer:m.eagerReducer,eagerState:m.eagerState,
|
||
next:null};null===h?(g=h=l,f=d):h=h.next=l;n>z.expirationTime&&(z.expirationTime=n,Kc(n))}else null!==h&&(h=h.next={expirationTime:1073741823,suspenseConfig:m.suspenseConfig,action:m.action,eagerReducer:m.eagerReducer,eagerState:m.eagerState,next:null}),Vg(n,m.suspenseConfig),d=m.eagerReducer===a?m.eagerState:a(d,m.action);m=m.next}while(null!==m&&m!==e);null===h?f=d:h.next=g;Qa(d,b.memoizedState)||(ia=!0);b.memoizedState=d;b.baseState=f;b.baseQueue=h;c.lastRenderedState=d}return[b.memoizedState,
|
||
c.dispatch]}function Wc(a,b,c){b=vb();c=b.queue;if(null===c)throw Error(k(311));c.lastRenderedReducer=a;var d=c.dispatch,e=c.pending,f=b.memoizedState;if(null!==e){c.pending=null;var g=e=e.next;do f=a(f,g.action),g=g.next;while(g!==e);Qa(f,b.memoizedState)||(ia=!0);b.memoizedState=f;null===b.baseQueue&&(b.baseState=f);c.lastRenderedState=f}return[f,d]}function xe(a){var b=ub();"function"===typeof a&&(a=a());b.memoizedState=b.baseState=a;a=b.queue={pending:null,dispatch:null,lastRenderedReducer:Ua,
|
||
lastRenderedState:a};a=a.dispatch=ch.bind(null,z,a);return[b.memoizedState,a]}function ye(a,b,c,d){a={tag:a,create:b,destroy:c,deps:d,next:null};b=z.updateQueue;null===b?(b={lastEffect:null},z.updateQueue=b,b.lastEffect=a.next=a):(c=b.lastEffect,null===c?b.lastEffect=a.next=a:(d=c.next,c.next=a,a.next=d,b.lastEffect=a));return a}function dh(a){return vb().memoizedState}function ze(a,b,c,d){var e=ub();z.effectTag|=a;e.memoizedState=ye(1|b,c,void 0,void 0===d?null:d)}function Ae(a,b,c,d){var e=vb();
|
||
d=void 0===d?null:d;var f=void 0;if(null!==K){var g=K.memoizedState;f=g.destroy;if(null!==d&&ve(d,g.deps)){ye(b,c,f,d);return}}z.effectTag|=a;e.memoizedState=ye(1|b,c,f,d)}function eh(a,b){return ze(516,4,a,b)}function Xc(a,b){return Ae(516,4,a,b)}function fh(a,b){return Ae(4,2,a,b)}function gh(a,b){if("function"===typeof b)return a=a(),b(a),function(){b(null)};if(null!==b&&void 0!==b)return a=a(),b.current=a,function(){b.current=null}}function hh(a,b,c){c=null!==c&&void 0!==c?c.concat([a]):null;
|
||
return Ae(4,2,gh.bind(null,b,a),c)}function Be(a,b){}function ih(a,b){ub().memoizedState=[a,void 0===b?null:b];return a}function Yc(a,b){var c=vb();b=void 0===b?null:b;var d=c.memoizedState;if(null!==d&&null!==b&&ve(b,d[1]))return d[0];c.memoizedState=[a,b];return a}function jh(a,b){var c=vb();b=void 0===b?null:b;var d=c.memoizedState;if(null!==d&&null!==b&&ve(b,d[1]))return d[0];a=a();c.memoizedState=[a,b];return a}function Ce(a,b,c){var d=Cc();Da(98>d?98:d,function(){a(!0)});Da(97<d?97:d,function(){var d=
|
||
X.suspense;X.suspense=void 0===b?null:b;try{a(!1),c()}finally{X.suspense=d}})}function ch(a,b,c){var d=ka(),e=Vb.suspense;d=Va(d,a,e);e={expirationTime:d,suspenseConfig:e,action:c,eagerReducer:null,eagerState:null,next:null};var f=b.pending;null===f?e.next=e:(e.next=f.next,f.next=e);b.pending=e;f=a.alternate;if(a===z||null!==f&&f===z)Uc=!0,e.expirationTime=Ia,z.expirationTime=Ia;else{if(0===a.expirationTime&&(null===f||0===f.expirationTime)&&(f=b.lastRenderedReducer,null!==f))try{var g=b.lastRenderedState,
|
||
h=f(g,c);e.eagerReducer=f;e.eagerState=h;if(Qa(h,g))return}catch(m){}finally{}Ja(a,d)}}function kh(a,b){var c=la(5,null,null,0);c.elementType="DELETED";c.type="DELETED";c.stateNode=b;c.return=a;c.effectTag=8;null!==a.lastEffect?(a.lastEffect.nextEffect=c,a.lastEffect=c):a.firstEffect=a.lastEffect=c}function lh(a,b){switch(a.tag){case 5:var c=a.type;b=1!==b.nodeType||c.toLowerCase()!==b.nodeName.toLowerCase()?null:b;return null!==b?(a.stateNode=b,!0):!1;case 6:return b=""===a.pendingProps||3!==b.nodeType?
|
||
null:b,null!==b?(a.stateNode=b,!0):!1;case 13:return!1;default:return!1}}function De(a){if(Wa){var b=Ka;if(b){var c=b;if(!lh(a,b)){b=kb(c.nextSibling);if(!b||!lh(a,b)){a.effectTag=a.effectTag&-1025|2;Wa=!1;ra=a;return}kh(ra,c)}ra=a;Ka=kb(b.firstChild)}else a.effectTag=a.effectTag&-1025|2,Wa=!1,ra=a}}function mh(a){for(a=a.return;null!==a&&5!==a.tag&&3!==a.tag&&13!==a.tag;)a=a.return;ra=a}function Zc(a){if(a!==ra)return!1;if(!Wa)return mh(a),Wa=!0,!1;var b=a.type;if(5!==a.tag||"head"!==b&&"body"!==
|
||
b&&!Yd(b,a.memoizedProps))for(b=Ka;b;)kh(a,b),b=kb(b.nextSibling);mh(a);if(13===a.tag){a=a.memoizedState;a=null!==a?a.dehydrated:null;if(!a)throw Error(k(317));a:{a=a.nextSibling;for(b=0;a;){if(8===a.nodeType){var c=a.data;if(c===og){if(0===b){Ka=kb(a.nextSibling);break a}b--}else c!==ng&&c!==Zd&&c!==$d||b++}a=a.nextSibling}Ka=null}}else Ka=ra?kb(a.stateNode.nextSibling):null;return!0}function Ee(){Ka=ra=null;Wa=!1}function T(a,b,c,d){b.child=null===a?Fe(b,null,c,d):wb(b,a.child,c,d)}function nh(a,
|
||
b,c,d,e){c=c.render;var f=b.ref;rb(b,e);d=we(a,b,c,d,f,e);if(null!==a&&!ia)return b.updateQueue=a.updateQueue,b.effectTag&=-517,a.expirationTime<=e&&(a.expirationTime=0),sa(a,b,e);b.effectTag|=1;T(a,b,d,e);return b.child}function oh(a,b,c,d,e,f){if(null===a){var g=c.type;if("function"===typeof g&&!Ge(g)&&void 0===g.defaultProps&&null===c.compare&&void 0===c.defaultProps)return b.tag=15,b.type=g,ph(a,b,g,d,e,f);a=Oc(c.type,null,d,null,b.mode,f);a.ref=b.ref;a.return=b;return b.child=a}g=a.child;if(e<
|
||
f&&(e=g.memoizedProps,c=c.compare,c=null!==c?c:Ob,c(e,d)&&a.ref===b.ref))return sa(a,b,f);b.effectTag|=1;a=Sa(g,d);a.ref=b.ref;a.return=b;return b.child=a}function ph(a,b,c,d,e,f){return null!==a&&Ob(a.memoizedProps,d)&&a.ref===b.ref&&(ia=!1,e<f)?(b.expirationTime=a.expirationTime,sa(a,b,f)):He(a,b,c,d,f)}function qh(a,b){var c=b.ref;if(null===a&&null!==c||null!==a&&a.ref!==c)b.effectTag|=128}function He(a,b,c,d,e){var f=N(c)?Ra:B.current;f=pb(b,f);rb(b,e);c=we(a,b,c,d,f,e);if(null!==a&&!ia)return b.updateQueue=
|
||
a.updateQueue,b.effectTag&=-517,a.expirationTime<=e&&(a.expirationTime=0),sa(a,b,e);b.effectTag|=1;T(a,b,c,e);return b.child}function rh(a,b,c,d,e){if(N(c)){var f=!0;Bc(b)}else f=!1;rb(b,e);if(null===b.stateNode)null!==a&&(a.alternate=null,b.alternate=null,b.effectTag|=2),Yg(b,c,d),pe(b,c,d,e),d=!0;else if(null===a){var g=b.stateNode,h=b.memoizedProps;g.props=h;var m=g.context,n=c.contextType;"object"===typeof n&&null!==n?n=W(n):(n=N(c)?Ra:B.current,n=pb(b,n));var l=c.getDerivedStateFromProps,k="function"===
|
||
typeof l||"function"===typeof g.getSnapshotBeforeUpdate;k||"function"!==typeof g.UNSAFE_componentWillReceiveProps&&"function"!==typeof g.componentWillReceiveProps||(h!==d||m!==n)&&Zg(b,g,d,n);Ga=!1;var p=b.memoizedState;g.state=p;Qb(b,d,g,e);m=b.memoizedState;h!==d||p!==m||G.current||Ga?("function"===typeof l&&(Lc(b,c,l,d),m=b.memoizedState),(h=Ga||Xg(b,c,h,d,p,m,n))?(k||"function"!==typeof g.UNSAFE_componentWillMount&&"function"!==typeof g.componentWillMount||("function"===typeof g.componentWillMount&&
|
||
g.componentWillMount(),"function"===typeof g.UNSAFE_componentWillMount&&g.UNSAFE_componentWillMount()),"function"===typeof g.componentDidMount&&(b.effectTag|=4)):("function"===typeof g.componentDidMount&&(b.effectTag|=4),b.memoizedProps=d,b.memoizedState=m),g.props=d,g.state=m,g.context=n,d=h):("function"===typeof g.componentDidMount&&(b.effectTag|=4),d=!1)}else g=b.stateNode,oe(a,b),h=b.memoizedProps,g.props=b.type===b.elementType?h:aa(b.type,h),m=g.context,n=c.contextType,"object"===typeof n&&null!==
|
||
n?n=W(n):(n=N(c)?Ra:B.current,n=pb(b,n)),l=c.getDerivedStateFromProps,(k="function"===typeof l||"function"===typeof g.getSnapshotBeforeUpdate)||"function"!==typeof g.UNSAFE_componentWillReceiveProps&&"function"!==typeof g.componentWillReceiveProps||(h!==d||m!==n)&&Zg(b,g,d,n),Ga=!1,m=b.memoizedState,g.state=m,Qb(b,d,g,e),p=b.memoizedState,h!==d||m!==p||G.current||Ga?("function"===typeof l&&(Lc(b,c,l,d),p=b.memoizedState),(l=Ga||Xg(b,c,h,d,m,p,n))?(k||"function"!==typeof g.UNSAFE_componentWillUpdate&&
|
||
"function"!==typeof g.componentWillUpdate||("function"===typeof g.componentWillUpdate&&g.componentWillUpdate(d,p,n),"function"===typeof g.UNSAFE_componentWillUpdate&&g.UNSAFE_componentWillUpdate(d,p,n)),"function"===typeof g.componentDidUpdate&&(b.effectTag|=4),"function"===typeof g.getSnapshotBeforeUpdate&&(b.effectTag|=256)):("function"!==typeof g.componentDidUpdate||h===a.memoizedProps&&m===a.memoizedState||(b.effectTag|=4),"function"!==typeof g.getSnapshotBeforeUpdate||h===a.memoizedProps&&m===
|
||
a.memoizedState||(b.effectTag|=256),b.memoizedProps=d,b.memoizedState=p),g.props=d,g.state=p,g.context=n,d=l):("function"!==typeof g.componentDidUpdate||h===a.memoizedProps&&m===a.memoizedState||(b.effectTag|=4),"function"!==typeof g.getSnapshotBeforeUpdate||h===a.memoizedProps&&m===a.memoizedState||(b.effectTag|=256),d=!1);return Ie(a,b,c,d,f,e)}function Ie(a,b,c,d,e,f){qh(a,b);var g=0!==(b.effectTag&64);if(!d&&!g)return e&&Hg(b,c,!1),sa(a,b,f);d=b.stateNode;gj.current=b;var h=g&&"function"!==typeof c.getDerivedStateFromError?
|
||
null:d.render();b.effectTag|=1;null!==a&&g?(b.child=wb(b,a.child,null,f),b.child=wb(b,null,h,f)):T(a,b,h,f);b.memoizedState=d.state;e&&Hg(b,c,!0);return b.child}function sh(a){var b=a.stateNode;b.pendingContext?Fg(a,b.pendingContext,b.pendingContext!==b.context):b.context&&Fg(a,b.context,!1);se(a,b.containerInfo)}function th(a,b,c){var d=b.mode,e=b.pendingProps,f=D.current,g=!1,h;(h=0!==(b.effectTag&64))||(h=0!==(f&2)&&(null===a||null!==a.memoizedState));h?(g=!0,b.effectTag&=-65):null!==a&&null===
|
||
a.memoizedState||void 0===e.fallback||!0===e.unstable_avoidThisFallback||(f|=1);y(D,f&1);if(null===a){void 0!==e.fallback&&De(b);if(g){g=e.fallback;e=Ha(null,d,0,null);e.return=b;if(0===(b.mode&2))for(a=null!==b.memoizedState?b.child.child:b.child,e.child=a;null!==a;)a.return=e,a=a.sibling;c=Ha(g,d,c,null);c.return=b;e.sibling=c;b.memoizedState=Je;b.child=e;return c}d=e.children;b.memoizedState=null;return b.child=Fe(b,null,d,c)}if(null!==a.memoizedState){a=a.child;d=a.sibling;if(g){e=e.fallback;
|
||
c=Sa(a,a.pendingProps);c.return=b;if(0===(b.mode&2)&&(g=null!==b.memoizedState?b.child.child:b.child,g!==a.child))for(c.child=g;null!==g;)g.return=c,g=g.sibling;d=Sa(d,e);d.return=b;c.sibling=d;c.childExpirationTime=0;b.memoizedState=Je;b.child=c;return d}c=wb(b,a.child,e.children,c);b.memoizedState=null;return b.child=c}a=a.child;if(g){g=e.fallback;e=Ha(null,d,0,null);e.return=b;e.child=a;null!==a&&(a.return=e);if(0===(b.mode&2))for(a=null!==b.memoizedState?b.child.child:b.child,e.child=a;null!==
|
||
a;)a.return=e,a=a.sibling;c=Ha(g,d,c,null);c.return=b;e.sibling=c;c.effectTag|=2;e.childExpirationTime=0;b.memoizedState=Je;b.child=e;return c}b.memoizedState=null;return b.child=wb(b,a,e.children,c)}function uh(a,b){a.expirationTime<b&&(a.expirationTime=b);var c=a.alternate;null!==c&&c.expirationTime<b&&(c.expirationTime=b);Sg(a.return,b)}function Ke(a,b,c,d,e,f){var g=a.memoizedState;null===g?a.memoizedState={isBackwards:b,rendering:null,renderingStartTime:0,last:d,tail:c,tailExpiration:0,tailMode:e,
|
||
lastEffect:f}:(g.isBackwards=b,g.rendering=null,g.renderingStartTime=0,g.last=d,g.tail=c,g.tailExpiration=0,g.tailMode=e,g.lastEffect=f)}function vh(a,b,c){var d=b.pendingProps,e=d.revealOrder,f=d.tail;T(a,b,d.children,c);d=D.current;if(0!==(d&2))d=d&1|2,b.effectTag|=64;else{if(null!==a&&0!==(a.effectTag&64))a:for(a=b.child;null!==a;){if(13===a.tag)null!==a.memoizedState&&uh(a,c);else if(19===a.tag)uh(a,c);else if(null!==a.child){a.child.return=a;a=a.child;continue}if(a===b)break a;for(;null===a.sibling;){if(null===
|
||
a.return||a.return===b)break a;a=a.return}a.sibling.return=a.return;a=a.sibling}d&=1}y(D,d);if(0===(b.mode&2))b.memoizedState=null;else switch(e){case "forwards":c=b.child;for(e=null;null!==c;)a=c.alternate,null!==a&&null===Rc(a)&&(e=c),c=c.sibling;c=e;null===c?(e=b.child,b.child=null):(e=c.sibling,c.sibling=null);Ke(b,!1,e,c,f,b.lastEffect);break;case "backwards":c=null;e=b.child;for(b.child=null;null!==e;){a=e.alternate;if(null!==a&&null===Rc(a)){b.child=e;break}a=e.sibling;e.sibling=c;c=e;e=a}Ke(b,
|
||
!0,c,null,f,b.lastEffect);break;case "together":Ke(b,!1,null,null,void 0,b.lastEffect);break;default:b.memoizedState=null}return b.child}function sa(a,b,c){null!==a&&(b.dependencies=a.dependencies);var d=b.expirationTime;0!==d&&Kc(d);if(b.childExpirationTime<c)return null;if(null!==a&&b.child!==a.child)throw Error(k(153));if(null!==b.child){a=b.child;c=Sa(a,a.pendingProps);b.child=c;for(c.return=b;null!==a.sibling;)a=a.sibling,c=c.sibling=Sa(a,a.pendingProps),c.return=b;c.sibling=null}return b.child}
|
||
function $c(a,b){switch(a.tailMode){case "hidden":b=a.tail;for(var c=null;null!==b;)null!==b.alternate&&(c=b),b=b.sibling;null===c?a.tail=null:c.sibling=null;break;case "collapsed":c=a.tail;for(var d=null;null!==c;)null!==c.alternate&&(d=c),c=c.sibling;null===d?b||null===a.tail?a.tail=null:a.tail.sibling=null:d.sibling=null}}function hj(a,b,c){var d=b.pendingProps;switch(b.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return null;case 1:return N(b.type)&&(q(G),q(B)),
|
||
null;case 3:return tb(),q(G),q(B),c=b.stateNode,c.pendingContext&&(c.context=c.pendingContext,c.pendingContext=null),null!==a&&null!==a.child||!Zc(b)||(b.effectTag|=4),wh(b),null;case 5:te(b);c=Ta(Tb.current);var e=b.type;if(null!==a&&null!=b.stateNode)ij(a,b,e,d,c),a.ref!==b.ref&&(b.effectTag|=128);else{if(!d){if(null===b.stateNode)throw Error(k(166));return null}a=Ta(ja.current);if(Zc(b)){d=b.stateNode;e=b.type;var f=b.memoizedProps;d[Aa]=b;d[vc]=f;switch(e){case "iframe":case "object":case "embed":w("load",
|
||
d);break;case "video":case "audio":for(a=0;a<Db.length;a++)w(Db[a],d);break;case "source":w("error",d);break;case "img":case "image":case "link":w("error",d);w("load",d);break;case "form":w("reset",d);w("submit",d);break;case "details":w("toggle",d);break;case "input":Hf(d,f);w("invalid",d);oa(c,"onChange");break;case "select":d._wrapperState={wasMultiple:!!f.multiple};w("invalid",d);oa(c,"onChange");break;case "textarea":Kf(d,f),w("invalid",d),oa(c,"onChange")}Ud(e,f);a=null;for(var g in f)if(f.hasOwnProperty(g)){var h=
|
||
f[g];"children"===g?"string"===typeof h?d.textContent!==h&&(a=["children",h]):"number"===typeof h&&d.textContent!==""+h&&(a=["children",""+h]):db.hasOwnProperty(g)&&null!=h&&oa(c,g)}switch(e){case "input":mc(d);Jf(d,f,!0);break;case "textarea":mc(d);Mf(d);break;case "select":case "option":break;default:"function"===typeof f.onClick&&(d.onclick=uc)}c=a;b.updateQueue=c;null!==c&&(b.effectTag|=4)}else{g=9===c.nodeType?c:c.ownerDocument;"http://www.w3.org/1999/xhtml"===a&&(a=Nf(e));"http://www.w3.org/1999/xhtml"===
|
||
a?"script"===e?(a=g.createElement("div"),a.innerHTML="<script>\x3c/script>",a=a.removeChild(a.firstChild)):"string"===typeof d.is?a=g.createElement(e,{is:d.is}):(a=g.createElement(e),"select"===e&&(g=a,d.multiple?g.multiple=!0:d.size&&(g.size=d.size))):a=g.createElementNS(a,e);a[Aa]=b;a[vc]=d;jj(a,b,!1,!1);b.stateNode=a;g=Vd(e,d);switch(e){case "iframe":case "object":case "embed":w("load",a);h=d;break;case "video":case "audio":for(h=0;h<Db.length;h++)w(Db[h],a);h=d;break;case "source":w("error",a);
|
||
h=d;break;case "img":case "image":case "link":w("error",a);w("load",a);h=d;break;case "form":w("reset",a);w("submit",a);h=d;break;case "details":w("toggle",a);h=d;break;case "input":Hf(a,d);h=Cd(a,d);w("invalid",a);oa(c,"onChange");break;case "option":h=Fd(a,d);break;case "select":a._wrapperState={wasMultiple:!!d.multiple};h=M({},d,{value:void 0});w("invalid",a);oa(c,"onChange");break;case "textarea":Kf(a,d);h=Gd(a,d);w("invalid",a);oa(c,"onChange");break;default:h=d}Ud(e,h);var m=h;for(f in m)if(m.hasOwnProperty(f)){var n=
|
||
m[f];"style"===f?gg(a,n):"dangerouslySetInnerHTML"===f?(n=n?n.__html:void 0,null!=n&&xh(a,n)):"children"===f?"string"===typeof n?("textarea"!==e||""!==n)&&Wb(a,n):"number"===typeof n&&Wb(a,""+n):"suppressContentEditableWarning"!==f&&"suppressHydrationWarning"!==f&&"autoFocus"!==f&&(db.hasOwnProperty(f)?null!=n&&oa(c,f):null!=n&&xd(a,f,n,g))}switch(e){case "input":mc(a);Jf(a,d,!1);break;case "textarea":mc(a);Mf(a);break;case "option":null!=d.value&&a.setAttribute("value",""+va(d.value));break;case "select":a.multiple=
|
||
!!d.multiple;c=d.value;null!=c?hb(a,!!d.multiple,c,!1):null!=d.defaultValue&&hb(a,!!d.multiple,d.defaultValue,!0);break;default:"function"===typeof h.onClick&&(a.onclick=uc)}lg(e,d)&&(b.effectTag|=4)}null!==b.ref&&(b.effectTag|=128)}return null;case 6:if(a&&null!=b.stateNode)kj(a,b,a.memoizedProps,d);else{if("string"!==typeof d&&null===b.stateNode)throw Error(k(166));c=Ta(Tb.current);Ta(ja.current);Zc(b)?(c=b.stateNode,d=b.memoizedProps,c[Aa]=b,c.nodeValue!==d&&(b.effectTag|=4)):(c=(9===c.nodeType?
|
||
c:c.ownerDocument).createTextNode(d),c[Aa]=b,b.stateNode=c)}return null;case 13:q(D);d=b.memoizedState;if(0!==(b.effectTag&64))return b.expirationTime=c,b;c=null!==d;d=!1;null===a?void 0!==b.memoizedProps.fallback&&Zc(b):(e=a.memoizedState,d=null!==e,c||null===e||(e=a.child.sibling,null!==e&&(f=b.firstEffect,null!==f?(b.firstEffect=e,e.nextEffect=f):(b.firstEffect=b.lastEffect=e,e.nextEffect=null),e.effectTag=8)));if(c&&!d&&0!==(b.mode&2))if(null===a&&!0!==b.memoizedProps.unstable_avoidThisFallback||
|
||
0!==(D.current&1))F===Xa&&(F=ad);else{if(F===Xa||F===ad)F=bd;0!==Xb&&null!==U&&(Ya(U,P),yh(U,Xb))}if(c||d)b.effectTag|=4;return null;case 4:return tb(),wh(b),null;case 10:return me(b),null;case 17:return N(b.type)&&(q(G),q(B)),null;case 19:q(D);d=b.memoizedState;if(null===d)return null;e=0!==(b.effectTag&64);f=d.rendering;if(null===f)if(e)$c(d,!1);else{if(F!==Xa||null!==a&&0!==(a.effectTag&64))for(f=b.child;null!==f;){a=Rc(f);if(null!==a){b.effectTag|=64;$c(d,!1);e=a.updateQueue;null!==e&&(b.updateQueue=
|
||
e,b.effectTag|=4);null===d.lastEffect&&(b.firstEffect=null);b.lastEffect=d.lastEffect;for(d=b.child;null!==d;)e=d,f=c,e.effectTag&=2,e.nextEffect=null,e.firstEffect=null,e.lastEffect=null,a=e.alternate,null===a?(e.childExpirationTime=0,e.expirationTime=f,e.child=null,e.memoizedProps=null,e.memoizedState=null,e.updateQueue=null,e.dependencies=null):(e.childExpirationTime=a.childExpirationTime,e.expirationTime=a.expirationTime,e.child=a.child,e.memoizedProps=a.memoizedProps,e.memoizedState=a.memoizedState,
|
||
e.updateQueue=a.updateQueue,f=a.dependencies,e.dependencies=null===f?null:{expirationTime:f.expirationTime,firstContext:f.firstContext,responders:f.responders}),d=d.sibling;y(D,D.current&1|2);return b.child}f=f.sibling}}else{if(!e)if(a=Rc(f),null!==a){if(b.effectTag|=64,e=!0,c=a.updateQueue,null!==c&&(b.updateQueue=c,b.effectTag|=4),$c(d,!0),null===d.tail&&"hidden"===d.tailMode&&!f.alternate)return b=b.lastEffect=d.lastEffect,null!==b&&(b.nextEffect=null),null}else 2*Y()-d.renderingStartTime>d.tailExpiration&&
|
||
1<c&&(b.effectTag|=64,e=!0,$c(d,!1),b.expirationTime=b.childExpirationTime=c-1);d.isBackwards?(f.sibling=b.child,b.child=f):(c=d.last,null!==c?c.sibling=f:b.child=f,d.last=f)}return null!==d.tail?(0===d.tailExpiration&&(d.tailExpiration=Y()+500),c=d.tail,d.rendering=c,d.tail=c.sibling,d.lastEffect=b.lastEffect,d.renderingStartTime=Y(),c.sibling=null,b=D.current,y(D,e?b&1|2:b&1),c):null}throw Error(k(156,b.tag));}function lj(a,b){switch(a.tag){case 1:return N(a.type)&&(q(G),q(B)),b=a.effectTag,b&4096?
|
||
(a.effectTag=b&-4097|64,a):null;case 3:tb();q(G);q(B);b=a.effectTag;if(0!==(b&64))throw Error(k(285));a.effectTag=b&-4097|64;return a;case 5:return te(a),null;case 13:return q(D),b=a.effectTag,b&4096?(a.effectTag=b&-4097|64,a):null;case 19:return q(D),null;case 4:return tb(),null;case 10:return me(a),null;default:return null}}function Le(a,b){return{value:a,source:b,stack:Bd(b)}}function Me(a,b){var c=b.source,d=b.stack;null===d&&null!==c&&(d=Bd(c));null!==c&&na(c.type);b=b.value;null!==a&&1===a.tag&&
|
||
na(a.type);try{console.error(b)}catch(e){setTimeout(function(){throw e;})}}function mj(a,b){try{b.props=a.memoizedProps,b.state=a.memoizedState,b.componentWillUnmount()}catch(c){Za(a,c)}}function zh(a){var b=a.ref;if(null!==b)if("function"===typeof b)try{b(null)}catch(c){Za(a,c)}else b.current=null}function nj(a,b){switch(b.tag){case 0:case 11:case 15:case 22:return;case 1:if(b.effectTag&256&&null!==a){var c=a.memoizedProps,d=a.memoizedState;a=b.stateNode;b=a.getSnapshotBeforeUpdate(b.elementType===
|
||
b.type?c:aa(b.type,c),d);a.__reactInternalSnapshotBeforeUpdate=b}return;case 3:case 5:case 6:case 4:case 17:return}throw Error(k(163));}function Ah(a,b){b=b.updateQueue;b=null!==b?b.lastEffect:null;if(null!==b){var c=b=b.next;do{if((c.tag&a)===a){var d=c.destroy;c.destroy=void 0;void 0!==d&&d()}c=c.next}while(c!==b)}}function Bh(a,b){b=b.updateQueue;b=null!==b?b.lastEffect:null;if(null!==b){var c=b=b.next;do{if((c.tag&a)===a){var d=c.create;c.destroy=d()}c=c.next}while(c!==b)}}function oj(a,b,c,d){switch(c.tag){case 0:case 11:case 15:case 22:Bh(3,
|
||
c);return;case 1:a=c.stateNode;c.effectTag&4&&(null===b?a.componentDidMount():(d=c.elementType===c.type?b.memoizedProps:aa(c.type,b.memoizedProps),a.componentDidUpdate(d,b.memoizedState,a.__reactInternalSnapshotBeforeUpdate)));b=c.updateQueue;null!==b&&Wg(c,b,a);return;case 3:b=c.updateQueue;if(null!==b){a=null;if(null!==c.child)switch(c.child.tag){case 5:a=c.child.stateNode;break;case 1:a=c.child.stateNode}Wg(c,b,a)}return;case 5:a=c.stateNode;null===b&&c.effectTag&4&&lg(c.type,c.memoizedProps)&&
|
||
a.focus();return;case 6:return;case 4:return;case 12:return;case 13:null===c.memoizedState&&(c=c.alternate,null!==c&&(c=c.memoizedState,null!==c&&(c=c.dehydrated,null!==c&&bg(c))));return;case 19:case 17:case 20:case 21:return}throw Error(k(163));}function Ch(a,b,c){"function"===typeof Ne&&Ne(b);switch(b.tag){case 0:case 11:case 14:case 15:case 22:a=b.updateQueue;if(null!==a&&(a=a.lastEffect,null!==a)){var d=a.next;Da(97<c?97:c,function(){var a=d;do{var c=a.destroy;if(void 0!==c){var g=b;try{c()}catch(h){Za(g,
|
||
h)}}a=a.next}while(a!==d)})}break;case 1:zh(b);c=b.stateNode;"function"===typeof c.componentWillUnmount&&mj(b,c);break;case 5:zh(b);break;case 4:Dh(a,b,c)}}function Eh(a){var b=a.alternate;a.return=null;a.child=null;a.memoizedState=null;a.updateQueue=null;a.dependencies=null;a.alternate=null;a.firstEffect=null;a.lastEffect=null;a.pendingProps=null;a.memoizedProps=null;a.stateNode=null;null!==b&&Eh(b)}function Fh(a){return 5===a.tag||3===a.tag||4===a.tag}function Gh(a){a:{for(var b=a.return;null!==
|
||
b;){if(Fh(b)){var c=b;break a}b=b.return}throw Error(k(160));}b=c.stateNode;switch(c.tag){case 5:var d=!1;break;case 3:b=b.containerInfo;d=!0;break;case 4:b=b.containerInfo;d=!0;break;default:throw Error(k(161));}c.effectTag&16&&(Wb(b,""),c.effectTag&=-17);a:b:for(c=a;;){for(;null===c.sibling;){if(null===c.return||Fh(c.return)){c=null;break a}c=c.return}c.sibling.return=c.return;for(c=c.sibling;5!==c.tag&&6!==c.tag&&18!==c.tag;){if(c.effectTag&2)continue b;if(null===c.child||4===c.tag)continue b;
|
||
else c.child.return=c,c=c.child}if(!(c.effectTag&2)){c=c.stateNode;break a}}d?Oe(a,c,b):Pe(a,c,b)}function Oe(a,b,c){var d=a.tag,e=5===d||6===d;if(e)a=e?a.stateNode:a.stateNode.instance,b?8===c.nodeType?c.parentNode.insertBefore(a,b):c.insertBefore(a,b):(8===c.nodeType?(b=c.parentNode,b.insertBefore(a,c)):(b=c,b.appendChild(a)),c=c._reactRootContainer,null!==c&&void 0!==c||null!==b.onclick||(b.onclick=uc));else if(4!==d&&(a=a.child,null!==a))for(Oe(a,b,c),a=a.sibling;null!==a;)Oe(a,b,c),a=a.sibling}
|
||
function Pe(a,b,c){var d=a.tag,e=5===d||6===d;if(e)a=e?a.stateNode:a.stateNode.instance,b?c.insertBefore(a,b):c.appendChild(a);else if(4!==d&&(a=a.child,null!==a))for(Pe(a,b,c),a=a.sibling;null!==a;)Pe(a,b,c),a=a.sibling}function Dh(a,b,c){for(var d=b,e=!1,f,g;;){if(!e){e=d.return;a:for(;;){if(null===e)throw Error(k(160));f=e.stateNode;switch(e.tag){case 5:g=!1;break a;case 3:f=f.containerInfo;g=!0;break a;case 4:f=f.containerInfo;g=!0;break a}e=e.return}e=!0}if(5===d.tag||6===d.tag){a:for(var h=
|
||
a,m=d,n=c,l=m;;)if(Ch(h,l,n),null!==l.child&&4!==l.tag)l.child.return=l,l=l.child;else{if(l===m)break a;for(;null===l.sibling;){if(null===l.return||l.return===m)break a;l=l.return}l.sibling.return=l.return;l=l.sibling}g?(h=f,m=d.stateNode,8===h.nodeType?h.parentNode.removeChild(m):h.removeChild(m)):f.removeChild(d.stateNode)}else if(4===d.tag){if(null!==d.child){f=d.stateNode.containerInfo;g=!0;d.child.return=d;d=d.child;continue}}else if(Ch(a,d,c),null!==d.child){d.child.return=d;d=d.child;continue}if(d===
|
||
b)break;for(;null===d.sibling;){if(null===d.return||d.return===b)return;d=d.return;4===d.tag&&(e=!1)}d.sibling.return=d.return;d=d.sibling}}function Qe(a,b){switch(b.tag){case 0:case 11:case 14:case 15:case 22:Ah(3,b);return;case 1:return;case 5:var c=b.stateNode;if(null!=c){var d=b.memoizedProps,e=null!==a?a.memoizedProps:d;a=b.type;var f=b.updateQueue;b.updateQueue=null;if(null!==f){c[vc]=d;"input"===a&&"radio"===d.type&&null!=d.name&&If(c,d);Vd(a,e);b=Vd(a,d);for(e=0;e<f.length;e+=2){var g=f[e],
|
||
h=f[e+1];"style"===g?gg(c,h):"dangerouslySetInnerHTML"===g?xh(c,h):"children"===g?Wb(c,h):xd(c,g,h,b)}switch(a){case "input":Dd(c,d);break;case "textarea":Lf(c,d);break;case "select":b=c._wrapperState.wasMultiple,c._wrapperState.wasMultiple=!!d.multiple,a=d.value,null!=a?hb(c,!!d.multiple,a,!1):b!==!!d.multiple&&(null!=d.defaultValue?hb(c,!!d.multiple,d.defaultValue,!0):hb(c,!!d.multiple,d.multiple?[]:"",!1))}}}return;case 6:if(null===b.stateNode)throw Error(k(162));b.stateNode.nodeValue=b.memoizedProps;
|
||
return;case 3:b=b.stateNode;b.hydrate&&(b.hydrate=!1,bg(b.containerInfo));return;case 12:return;case 13:c=b;null===b.memoizedState?d=!1:(d=!0,c=b.child,Re=Y());if(null!==c)a:for(a=c;;){if(5===a.tag)f=a.stateNode,d?(f=f.style,"function"===typeof f.setProperty?f.setProperty("display","none","important"):f.display="none"):(f=a.stateNode,e=a.memoizedProps.style,e=void 0!==e&&null!==e&&e.hasOwnProperty("display")?e.display:null,f.style.display=fg("display",e));else if(6===a.tag)a.stateNode.nodeValue=d?
|
||
"":a.memoizedProps;else if(13===a.tag&&null!==a.memoizedState&&null===a.memoizedState.dehydrated){f=a.child.sibling;f.return=a;a=f;continue}else if(null!==a.child){a.child.return=a;a=a.child;continue}if(a===c)break;for(;null===a.sibling;){if(null===a.return||a.return===c)break a;a=a.return}a.sibling.return=a.return;a=a.sibling}Hh(b);return;case 19:Hh(b);return;case 17:return}throw Error(k(163));}function Hh(a){var b=a.updateQueue;if(null!==b){a.updateQueue=null;var c=a.stateNode;null===c&&(c=a.stateNode=
|
||
new pj);b.forEach(function(b){var d=qj.bind(null,a,b);c.has(b)||(c.add(b),b.then(d,d))})}}function Ih(a,b,c){c=Ea(c,null);c.tag=3;c.payload={element:null};var d=b.value;c.callback=function(){cd||(cd=!0,Se=d);Me(a,b)};return c}function Jh(a,b,c){c=Ea(c,null);c.tag=3;var d=a.type.getDerivedStateFromError;if("function"===typeof d){var e=b.value;c.payload=function(){Me(a,b);return d(e)}}var f=a.stateNode;null!==f&&"function"===typeof f.componentDidCatch&&(c.callback=function(){"function"!==typeof d&&
|
||
(null===La?La=new Set([this]):La.add(this),Me(a,b));var c=b.stack;this.componentDidCatch(b.value,{componentStack:null!==c?c:""})});return c}function ka(){return(p&(ca|ma))!==H?1073741821-(Y()/10|0):0!==dd?dd:dd=1073741821-(Y()/10|0)}function Va(a,b,c){b=b.mode;if(0===(b&2))return 1073741823;var d=Cc();if(0===(b&4))return 99===d?1073741823:1073741822;if((p&ca)!==H)return P;if(null!==c)a=Fc(a,c.timeoutMs|0||5E3,250);else switch(d){case 99:a=1073741823;break;case 98:a=Fc(a,150,100);break;case 97:case 96:a=
|
||
Fc(a,5E3,250);break;case 95:a=2;break;default:throw Error(k(326));}null!==U&&a===P&&--a;return a}function ed(a,b){a.expirationTime<b&&(a.expirationTime=b);var c=a.alternate;null!==c&&c.expirationTime<b&&(c.expirationTime=b);var d=a.return,e=null;if(null===d&&3===a.tag)e=a.stateNode;else for(;null!==d;){c=d.alternate;d.childExpirationTime<b&&(d.childExpirationTime=b);null!==c&&c.childExpirationTime<b&&(c.childExpirationTime=b);if(null===d.return&&3===d.tag){e=d.stateNode;break}d=d.return}null!==e&&
|
||
(U===e&&(Kc(b),F===bd&&Ya(e,P)),yh(e,b));return e}function fd(a){var b=a.lastExpiredTime;if(0!==b)return b;b=a.firstPendingTime;if(!Kh(a,b))return b;var c=a.lastPingedTime;a=a.nextKnownPendingLevel;a=c>a?c:a;return 2>=a&&b!==a?0:a}function V(a){if(0!==a.lastExpiredTime)a.callbackExpirationTime=1073741823,a.callbackPriority=99,a.callbackNode=Og(Te.bind(null,a));else{var b=fd(a),c=a.callbackNode;if(0===b)null!==c&&(a.callbackNode=null,a.callbackExpirationTime=0,a.callbackPriority=90);else{var d=ka();
|
||
1073741823===b?d=99:1===b||2===b?d=95:(d=10*(1073741821-b)-10*(1073741821-d),d=0>=d?99:250>=d?98:5250>=d?97:95);if(null!==c){var e=a.callbackPriority;if(a.callbackExpirationTime===b&&e>=d)return;c!==Qg&&Rg(c)}a.callbackExpirationTime=b;a.callbackPriority=d;b=1073741823===b?Og(Te.bind(null,a)):Ng(d,Lh.bind(null,a),{timeout:10*(1073741821-b)-Y()});a.callbackNode=b}}}function Lh(a,b){dd=0;if(b)return b=ka(),Ue(a,b),V(a),null;var c=fd(a);if(0!==c){b=a.callbackNode;if((p&(ca|ma))!==H)throw Error(k(327));
|
||
xb();a===U&&c===P||$a(a,c);if(null!==t){var d=p;p|=ca;var e=Mh();do try{rj();break}catch(h){Nh(a,h)}while(1);le();p=d;gd.current=e;if(F===hd)throw b=id,$a(a,c),Ya(a,c),V(a),b;if(null===t)switch(e=a.finishedWork=a.current.alternate,a.finishedExpirationTime=c,d=F,U=null,d){case Xa:case hd:throw Error(k(345));case Oh:Ue(a,2<c?2:c);break;case ad:Ya(a,c);d=a.lastSuspendedTime;c===d&&(a.nextKnownPendingLevel=Ve(e));if(1073741823===ta&&(e=Re+Ph-Y(),10<e)){if(jd){var f=a.lastPingedTime;if(0===f||f>=c){a.lastPingedTime=
|
||
c;$a(a,c);break}}f=fd(a);if(0!==f&&f!==c)break;if(0!==d&&d!==c){a.lastPingedTime=d;break}a.timeoutHandle=We(ab.bind(null,a),e);break}ab(a);break;case bd:Ya(a,c);d=a.lastSuspendedTime;c===d&&(a.nextKnownPendingLevel=Ve(e));if(jd&&(e=a.lastPingedTime,0===e||e>=c)){a.lastPingedTime=c;$a(a,c);break}e=fd(a);if(0!==e&&e!==c)break;if(0!==d&&d!==c){a.lastPingedTime=d;break}1073741823!==Yb?d=10*(1073741821-Yb)-Y():1073741823===ta?d=0:(d=10*(1073741821-ta)-5E3,e=Y(),c=10*(1073741821-c)-e,d=e-d,0>d&&(d=0),d=
|
||
(120>d?120:480>d?480:1080>d?1080:1920>d?1920:3E3>d?3E3:4320>d?4320:1960*sj(d/1960))-d,c<d&&(d=c));if(10<d){a.timeoutHandle=We(ab.bind(null,a),d);break}ab(a);break;case Xe:if(1073741823!==ta&&null!==kd){f=ta;var g=kd;d=g.busyMinDurationMs|0;0>=d?d=0:(e=g.busyDelayMs|0,f=Y()-(10*(1073741821-f)-(g.timeoutMs|0||5E3)),d=f<=e?0:e+d-f);if(10<d){Ya(a,c);a.timeoutHandle=We(ab.bind(null,a),d);break}}ab(a);break;default:throw Error(k(329));}V(a);if(a.callbackNode===b)return Lh.bind(null,a)}}return null}function Te(a){var b=
|
||
a.lastExpiredTime;b=0!==b?b:1073741823;if((p&(ca|ma))!==H)throw Error(k(327));xb();a===U&&b===P||$a(a,b);if(null!==t){var c=p;p|=ca;var d=Mh();do try{tj();break}catch(e){Nh(a,e)}while(1);le();p=c;gd.current=d;if(F===hd)throw c=id,$a(a,b),Ya(a,b),V(a),c;if(null!==t)throw Error(k(261));a.finishedWork=a.current.alternate;a.finishedExpirationTime=b;U=null;ab(a);V(a)}return null}function uj(){if(null!==bb){var a=bb;bb=null;a.forEach(function(a,c){Ue(c,a);V(c)});ha()}}function Qh(a,b){var c=p;p|=1;try{return a(b)}finally{p=
|
||
c,p===H&&ha()}}function Rh(a,b){var c=p;p&=-2;p|=Ye;try{return a(b)}finally{p=c,p===H&&ha()}}function $a(a,b){a.finishedWork=null;a.finishedExpirationTime=0;var c=a.timeoutHandle;-1!==c&&(a.timeoutHandle=-1,vj(c));if(null!==t)for(c=t.return;null!==c;){var d=c;switch(d.tag){case 1:d=d.type.childContextTypes;null!==d&&void 0!==d&&(q(G),q(B));break;case 3:tb();q(G);q(B);break;case 5:te(d);break;case 4:tb();break;case 13:q(D);break;case 19:q(D);break;case 10:me(d)}c=c.return}U=a;t=Sa(a.current,null);
|
||
P=b;F=Xa;id=null;Yb=ta=1073741823;kd=null;Xb=0;jd=!1}function Nh(a,b){do{try{le();Sc.current=Tc;if(Uc)for(var c=z.memoizedState;null!==c;){var d=c.queue;null!==d&&(d.pending=null);c=c.next}Ia=0;J=K=z=null;Uc=!1;if(null===t||null===t.return)return F=hd,id=b,t=null;a:{var e=a,f=t.return,g=t,h=b;b=P;g.effectTag|=2048;g.firstEffect=g.lastEffect=null;if(null!==h&&"object"===typeof h&&"function"===typeof h.then){var m=h;if(0===(g.mode&2)){var n=g.alternate;n?(g.updateQueue=n.updateQueue,g.memoizedState=
|
||
n.memoizedState,g.expirationTime=n.expirationTime):(g.updateQueue=null,g.memoizedState=null)}var l=0!==(D.current&1),k=f;do{var p;if(p=13===k.tag){var q=k.memoizedState;if(null!==q)p=null!==q.dehydrated?!0:!1;else{var w=k.memoizedProps;p=void 0===w.fallback?!1:!0!==w.unstable_avoidThisFallback?!0:l?!1:!0}}if(p){var y=k.updateQueue;if(null===y){var r=new Set;r.add(m);k.updateQueue=r}else y.add(m);if(0===(k.mode&2)){k.effectTag|=64;g.effectTag&=-2981;if(1===g.tag)if(null===g.alternate)g.tag=17;else{var O=
|
||
Ea(1073741823,null);O.tag=Jc;Fa(g,O)}g.expirationTime=1073741823;break a}h=void 0;g=b;var v=e.pingCache;null===v?(v=e.pingCache=new wj,h=new Set,v.set(m,h)):(h=v.get(m),void 0===h&&(h=new Set,v.set(m,h)));if(!h.has(g)){h.add(g);var x=xj.bind(null,e,m,g);m.then(x,x)}k.effectTag|=4096;k.expirationTime=b;break a}k=k.return}while(null!==k);h=Error((na(g.type)||"A React component")+" suspended while rendering, but no fallback UI was specified.\n\nAdd a <Suspense fallback=...> component higher in the tree to provide a loading indicator or placeholder to display."+
|
||
Bd(g))}F!==Xe&&(F=Oh);h=Le(h,g);k=f;do{switch(k.tag){case 3:m=h;k.effectTag|=4096;k.expirationTime=b;var A=Ih(k,m,b);Ug(k,A);break a;case 1:m=h;var u=k.type,B=k.stateNode;if(0===(k.effectTag&64)&&("function"===typeof u.getDerivedStateFromError||null!==B&&"function"===typeof B.componentDidCatch&&(null===La||!La.has(B)))){k.effectTag|=4096;k.expirationTime=b;var H=Jh(k,m,b);Ug(k,H);break a}}k=k.return}while(null!==k)}t=Sh(t)}catch(cj){b=cj;continue}break}while(1)}function Mh(a){a=gd.current;gd.current=
|
||
Tc;return null===a?Tc:a}function Vg(a,b){a<ta&&2<a&&(ta=a);null!==b&&a<Yb&&2<a&&(Yb=a,kd=b)}function Kc(a){a>Xb&&(Xb=a)}function tj(){for(;null!==t;)t=Th(t)}function rj(){for(;null!==t&&!yj();)t=Th(t)}function Th(a){var b=zj(a.alternate,a,P);a.memoizedProps=a.pendingProps;null===b&&(b=Sh(a));Uh.current=null;return b}function Sh(a){t=a;do{var b=t.alternate;a=t.return;if(0===(t.effectTag&2048)){b=hj(b,t,P);if(1===P||1!==t.childExpirationTime){for(var c=0,d=t.child;null!==d;){var e=d.expirationTime,
|
||
f=d.childExpirationTime;e>c&&(c=e);f>c&&(c=f);d=d.sibling}t.childExpirationTime=c}if(null!==b)return b;null!==a&&0===(a.effectTag&2048)&&(null===a.firstEffect&&(a.firstEffect=t.firstEffect),null!==t.lastEffect&&(null!==a.lastEffect&&(a.lastEffect.nextEffect=t.firstEffect),a.lastEffect=t.lastEffect),1<t.effectTag&&(null!==a.lastEffect?a.lastEffect.nextEffect=t:a.firstEffect=t,a.lastEffect=t))}else{b=lj(t);if(null!==b)return b.effectTag&=2047,b;null!==a&&(a.firstEffect=a.lastEffect=null,a.effectTag|=
|
||
2048)}b=t.sibling;if(null!==b)return b;t=a}while(null!==t);F===Xa&&(F=Xe);return null}function Ve(a){var b=a.expirationTime;a=a.childExpirationTime;return b>a?b:a}function ab(a){var b=Cc();Da(99,Aj.bind(null,a,b));return null}function Aj(a,b){do xb();while(null!==Zb);if((p&(ca|ma))!==H)throw Error(k(327));var c=a.finishedWork,d=a.finishedExpirationTime;if(null===c)return null;a.finishedWork=null;a.finishedExpirationTime=0;if(c===a.current)throw Error(k(177));a.callbackNode=null;a.callbackExpirationTime=
|
||
0;a.callbackPriority=90;a.nextKnownPendingLevel=0;var e=Ve(c);a.firstPendingTime=e;d<=a.lastSuspendedTime?a.firstSuspendedTime=a.lastSuspendedTime=a.nextKnownPendingLevel=0:d<=a.firstSuspendedTime&&(a.firstSuspendedTime=d-1);d<=a.lastPingedTime&&(a.lastPingedTime=0);d<=a.lastExpiredTime&&(a.lastExpiredTime=0);a===U&&(t=U=null,P=0);1<c.effectTag?null!==c.lastEffect?(c.lastEffect.nextEffect=c,e=c.firstEffect):e=c:e=c.firstEffect;if(null!==e){var f=p;p|=ma;Uh.current=null;Ze=tc;var g=kg();if(Xd(g)){if("selectionStart"in
|
||
g)var h={start:g.selectionStart,end:g.selectionEnd};else a:{h=(h=g.ownerDocument)&&h.defaultView||window;var m=h.getSelection&&h.getSelection();if(m&&0!==m.rangeCount){h=m.anchorNode;var n=m.anchorOffset,q=m.focusNode;m=m.focusOffset;try{h.nodeType,q.nodeType}catch(sb){h=null;break a}var ba=0,w=-1,y=-1,B=0,D=0,r=g,z=null;b:for(;;){for(var v;;){r!==h||0!==n&&3!==r.nodeType||(w=ba+n);r!==q||0!==m&&3!==r.nodeType||(y=ba+m);3===r.nodeType&&(ba+=r.nodeValue.length);if(null===(v=r.firstChild))break;z=r;
|
||
r=v}for(;;){if(r===g)break b;z===h&&++B===n&&(w=ba);z===q&&++D===m&&(y=ba);if(null!==(v=r.nextSibling))break;r=z;z=r.parentNode}r=v}h=-1===w||-1===y?null:{start:w,end:y}}else h=null}h=h||{start:0,end:0}}else h=null;$e={activeElementDetached:null,focusedElem:g,selectionRange:h};tc=!1;l=e;do try{Bj()}catch(sb){if(null===l)throw Error(k(330));Za(l,sb);l=l.nextEffect}while(null!==l);l=e;do try{for(g=a,h=b;null!==l;){var x=l.effectTag;x&16&&Wb(l.stateNode,"");if(x&128){var A=l.alternate;if(null!==A){var u=
|
||
A.ref;null!==u&&("function"===typeof u?u(null):u.current=null)}}switch(x&1038){case 2:Gh(l);l.effectTag&=-3;break;case 6:Gh(l);l.effectTag&=-3;Qe(l.alternate,l);break;case 1024:l.effectTag&=-1025;break;case 1028:l.effectTag&=-1025;Qe(l.alternate,l);break;case 4:Qe(l.alternate,l);break;case 8:n=l,Dh(g,n,h),Eh(n)}l=l.nextEffect}}catch(sb){if(null===l)throw Error(k(330));Za(l,sb);l=l.nextEffect}while(null!==l);u=$e;A=kg();x=u.focusedElem;h=u.selectionRange;if(A!==x&&x&&x.ownerDocument&&jg(x.ownerDocument.documentElement,
|
||
x)){null!==h&&Xd(x)&&(A=h.start,u=h.end,void 0===u&&(u=A),"selectionStart"in x?(x.selectionStart=A,x.selectionEnd=Math.min(u,x.value.length)):(u=(A=x.ownerDocument||document)&&A.defaultView||window,u.getSelection&&(u=u.getSelection(),n=x.textContent.length,g=Math.min(h.start,n),h=void 0===h.end?g:Math.min(h.end,n),!u.extend&&g>h&&(n=h,h=g,g=n),n=ig(x,g),q=ig(x,h),n&&q&&(1!==u.rangeCount||u.anchorNode!==n.node||u.anchorOffset!==n.offset||u.focusNode!==q.node||u.focusOffset!==q.offset)&&(A=A.createRange(),
|
||
A.setStart(n.node,n.offset),u.removeAllRanges(),g>h?(u.addRange(A),u.extend(q.node,q.offset)):(A.setEnd(q.node,q.offset),u.addRange(A))))));A=[];for(u=x;u=u.parentNode;)1===u.nodeType&&A.push({element:u,left:u.scrollLeft,top:u.scrollTop});"function"===typeof x.focus&&x.focus();for(x=0;x<A.length;x++)u=A[x],u.element.scrollLeft=u.left,u.element.scrollTop=u.top}tc=!!Ze;$e=Ze=null;a.current=c;l=e;do try{for(x=a;null!==l;){var F=l.effectTag;F&36&&oj(x,l.alternate,l);if(F&128){A=void 0;var E=l.ref;if(null!==
|
||
E){var G=l.stateNode;switch(l.tag){case 5:A=G;break;default:A=G}"function"===typeof E?E(A):E.current=A}}l=l.nextEffect}}catch(sb){if(null===l)throw Error(k(330));Za(l,sb);l=l.nextEffect}while(null!==l);l=null;Cj();p=f}else a.current=c;if(ld)ld=!1,Zb=a,$b=b;else for(l=e;null!==l;)b=l.nextEffect,l.nextEffect=null,l=b;b=a.firstPendingTime;0===b&&(La=null);1073741823===b?a===af?ac++:(ac=0,af=a):ac=0;"function"===typeof bf&&bf(c.stateNode,d);V(a);if(cd)throw cd=!1,a=Se,Se=null,a;if((p&Ye)!==H)return null;
|
||
ha();return null}function Bj(){for(;null!==l;){var a=l.effectTag;0!==(a&256)&&nj(l.alternate,l);0===(a&512)||ld||(ld=!0,Ng(97,function(){xb();return null}));l=l.nextEffect}}function xb(){if(90!==$b){var a=97<$b?97:$b;$b=90;return Da(a,Dj)}}function Dj(){if(null===Zb)return!1;var a=Zb;Zb=null;if((p&(ca|ma))!==H)throw Error(k(331));var b=p;p|=ma;for(a=a.current.firstEffect;null!==a;){try{var c=a;if(0!==(c.effectTag&512))switch(c.tag){case 0:case 11:case 15:case 22:Ah(5,c),Bh(5,c)}}catch(d){if(null===
|
||
a)throw Error(k(330));Za(a,d)}c=a.nextEffect;a.nextEffect=null;a=c}p=b;ha();return!0}function Vh(a,b,c){b=Le(c,b);b=Ih(a,b,1073741823);Fa(a,b);a=ed(a,1073741823);null!==a&&V(a)}function Za(a,b){if(3===a.tag)Vh(a,a,b);else for(var c=a.return;null!==c;){if(3===c.tag){Vh(c,a,b);break}else if(1===c.tag){var d=c.stateNode;if("function"===typeof c.type.getDerivedStateFromError||"function"===typeof d.componentDidCatch&&(null===La||!La.has(d))){a=Le(b,a);a=Jh(c,a,1073741823);Fa(c,a);c=ed(c,1073741823);null!==
|
||
c&&V(c);break}}c=c.return}}function xj(a,b,c){var d=a.pingCache;null!==d&&d.delete(b);U===a&&P===c?F===bd||F===ad&&1073741823===ta&&Y()-Re<Ph?$a(a,P):jd=!0:Kh(a,c)&&(b=a.lastPingedTime,0!==b&&b<c||(a.lastPingedTime=c,V(a)))}function qj(a,b){var c=a.stateNode;null!==c&&c.delete(b);b=0;0===b&&(b=ka(),b=Va(b,a,null));a=ed(a,b);null!==a&&V(a)}function Ej(a){if("undefined"===typeof __REACT_DEVTOOLS_GLOBAL_HOOK__)return!1;var b=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(b.isDisabled||!b.supportsFiber)return!0;try{var c=
|
||
b.inject(a);bf=function(a,e){try{b.onCommitFiberRoot(c,a,void 0,64===(a.current.effectTag&64))}catch(f){}};Ne=function(a){try{b.onCommitFiberUnmount(c,a)}catch(e){}}}catch(d){}return!0}function Fj(a,b,c,d){this.tag=a;this.key=c;this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null;this.index=0;this.ref=null;this.pendingProps=b;this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null;this.mode=d;this.effectTag=0;this.lastEffect=this.firstEffect=this.nextEffect=
|
||
null;this.childExpirationTime=this.expirationTime=0;this.alternate=null}function Ge(a){a=a.prototype;return!(!a||!a.isReactComponent)}function Gj(a){if("function"===typeof a)return Ge(a)?1:0;if(void 0!==a&&null!==a){a=a.$$typeof;if(a===zd)return 11;if(a===Ad)return 14}return 2}function Sa(a,b){var c=a.alternate;null===c?(c=la(a.tag,b,a.key,a.mode),c.elementType=a.elementType,c.type=a.type,c.stateNode=a.stateNode,c.alternate=a,a.alternate=c):(c.pendingProps=b,c.effectTag=0,c.nextEffect=null,c.firstEffect=
|
||
null,c.lastEffect=null);c.childExpirationTime=a.childExpirationTime;c.expirationTime=a.expirationTime;c.child=a.child;c.memoizedProps=a.memoizedProps;c.memoizedState=a.memoizedState;c.updateQueue=a.updateQueue;b=a.dependencies;c.dependencies=null===b?null:{expirationTime:b.expirationTime,firstContext:b.firstContext,responders:b.responders};c.sibling=a.sibling;c.index=a.index;c.ref=a.ref;return c}function Oc(a,b,c,d,e,f){var g=2;d=a;if("function"===typeof a)Ge(a)&&(g=1);else if("string"===typeof a)g=
|
||
5;else a:switch(a){case Ma:return Ha(c.children,e,f,b);case Hj:g=8;e|=7;break;case Af:g=8;e|=1;break;case kc:return a=la(12,c,b,e|8),a.elementType=kc,a.type=kc,a.expirationTime=f,a;case lc:return a=la(13,c,b,e),a.type=lc,a.elementType=lc,a.expirationTime=f,a;case yd:return a=la(19,c,b,e),a.elementType=yd,a.expirationTime=f,a;default:if("object"===typeof a&&null!==a)switch(a.$$typeof){case Cf:g=10;break a;case Bf:g=9;break a;case zd:g=11;break a;case Ad:g=14;break a;case Ef:g=16;d=null;break a;case Df:g=
|
||
22;break a}throw Error(k(130,null==a?a:typeof a,""));}b=la(g,c,b,e);b.elementType=a;b.type=d;b.expirationTime=f;return b}function Ha(a,b,c,d){a=la(7,a,d,b);a.expirationTime=c;return a}function qe(a,b,c){a=la(6,a,null,b);a.expirationTime=c;return a}function re(a,b,c){b=la(4,null!==a.children?a.children:[],a.key,b);b.expirationTime=c;b.stateNode={containerInfo:a.containerInfo,pendingChildren:null,implementation:a.implementation};return b}function Ij(a,b,c){this.tag=b;this.current=null;this.containerInfo=
|
||
a;this.pingCache=this.pendingChildren=null;this.finishedExpirationTime=0;this.finishedWork=null;this.timeoutHandle=-1;this.pendingContext=this.context=null;this.hydrate=c;this.callbackNode=null;this.callbackPriority=90;this.lastExpiredTime=this.lastPingedTime=this.nextKnownPendingLevel=this.lastSuspendedTime=this.firstSuspendedTime=this.firstPendingTime=0}function Kh(a,b){var c=a.firstSuspendedTime;a=a.lastSuspendedTime;return 0!==c&&c>=b&&a<=b}function Ya(a,b){var c=a.firstSuspendedTime,d=a.lastSuspendedTime;
|
||
c<b&&(a.firstSuspendedTime=b);if(d>b||0===c)a.lastSuspendedTime=b;b<=a.lastPingedTime&&(a.lastPingedTime=0);b<=a.lastExpiredTime&&(a.lastExpiredTime=0)}function yh(a,b){b>a.firstPendingTime&&(a.firstPendingTime=b);var c=a.firstSuspendedTime;0!==c&&(b>=c?a.firstSuspendedTime=a.lastSuspendedTime=a.nextKnownPendingLevel=0:b>=a.lastSuspendedTime&&(a.lastSuspendedTime=b+1),b>a.nextKnownPendingLevel&&(a.nextKnownPendingLevel=b))}function Ue(a,b){var c=a.lastExpiredTime;if(0===c||c>b)a.lastExpiredTime=b}
|
||
function md(a,b,c,d){var e=b.current,f=ka(),g=Vb.suspense;f=Va(f,e,g);a:if(c){c=c._reactInternalFiber;b:{if(Na(c)!==c||1!==c.tag)throw Error(k(170));var h=c;do{switch(h.tag){case 3:h=h.stateNode.context;break b;case 1:if(N(h.type)){h=h.stateNode.__reactInternalMemoizedMergedChildContext;break b}}h=h.return}while(null!==h);throw Error(k(171));}if(1===c.tag){var m=c.type;if(N(m)){c=Gg(c,m,h);break a}}c=h}else c=Ca;null===b.context?b.context=c:b.pendingContext=c;b=Ea(f,g);b.payload={element:a};d=void 0===
|
||
d?null:d;null!==d&&(b.callback=d);Fa(e,b);Ja(e,f);return f}function cf(a){a=a.current;if(!a.child)return null;switch(a.child.tag){case 5:return a.child.stateNode;default:return a.child.stateNode}}function Wh(a,b){a=a.memoizedState;null!==a&&null!==a.dehydrated&&a.retryTime<b&&(a.retryTime=b)}function df(a,b){Wh(a,b);(a=a.alternate)&&Wh(a,b)}function ef(a,b,c){c=null!=c&&!0===c.hydrate;var d=new Ij(a,b,c),e=la(3,null,null,2===b?7:1===b?3:0);d.current=e;e.stateNode=d;ne(e);a[Lb]=d.current;c&&0!==b&&
|
||
xi(a,9===a.nodeType?a:a.ownerDocument);this._internalRoot=d}function bc(a){return!(!a||1!==a.nodeType&&9!==a.nodeType&&11!==a.nodeType&&(8!==a.nodeType||" react-mount-point-unstable "!==a.nodeValue))}function Jj(a,b){b||(b=a?9===a.nodeType?a.documentElement:a.firstChild:null,b=!(!b||1!==b.nodeType||!b.hasAttribute("data-reactroot")));if(!b)for(var c;c=a.lastChild;)a.removeChild(c);return new ef(a,0,b?{hydrate:!0}:void 0)}function nd(a,b,c,d,e){var f=c._reactRootContainer;if(f){var g=f._internalRoot;
|
||
if("function"===typeof e){var h=e;e=function(){var a=cf(g);h.call(a)}}md(b,g,a,e)}else{f=c._reactRootContainer=Jj(c,d);g=f._internalRoot;if("function"===typeof e){var m=e;e=function(){var a=cf(g);m.call(a)}}Rh(function(){md(b,g,a,e)})}return cf(g)}function Kj(a,b,c){var d=3<arguments.length&&void 0!==arguments[3]?arguments[3]:null;return{$$typeof:gb,key:null==d?null:""+d,children:a,containerInfo:b,implementation:c}}function Xh(a,b){var c=2<arguments.length&&void 0!==arguments[2]?arguments[2]:null;
|
||
if(!bc(b))throw Error(k(200));return Kj(a,b,null,c)}if(!ea)throw Error(k(227));var ki=function(a,b,c,d,e,f,g,h,m){var n=Array.prototype.slice.call(arguments,3);try{b.apply(c,n)}catch(C){this.onError(C)}},yb=!1,gc=null,hc=!1,pd=null,li={onError:function(a){yb=!0;gc=a}},td=null,rf=null,mf=null,ic=null,cb={},jc=[],qd={},db={},rd={},wa=!("undefined"===typeof window||"undefined"===typeof window.document||"undefined"===typeof window.document.createElement),M=ea.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.assign,
|
||
sd=null,eb=null,fb=null,ee=function(a,b){return a(b)},eg=function(a,b,c,d,e){return a(b,c,d,e)},vd=function(){},vf=ee,Oa=!1,wd=!1,Z=ea.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler,Lj=Z.unstable_cancelCallback,ff=Z.unstable_now,$f=Z.unstable_scheduleCallback,Mj=Z.unstable_shouldYield,Yh=Z.unstable_requestPaint,Pd=Z.unstable_runWithPriority,Nj=Z.unstable_getCurrentPriorityLevel,Oj=Z.unstable_ImmediatePriority,Zh=Z.unstable_UserBlockingPriority,ag=Z.unstable_NormalPriority,Pj=Z.unstable_LowPriority,
|
||
Qj=Z.unstable_IdlePriority,oi=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,wf=Object.prototype.hasOwnProperty,yf={},xf={},E={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(a){E[a]=
|
||
new L(a,0,!1,a,null,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(a){var b=a[0];E[b]=new L(b,1,!1,a[1],null,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(a){E[a]=new L(a,2,!1,a.toLowerCase(),null,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(a){E[a]=new L(a,2,!1,a,null,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(a){E[a]=
|
||
new L(a,3,!1,a.toLowerCase(),null,!1)});["checked","multiple","muted","selected"].forEach(function(a){E[a]=new L(a,3,!0,a,null,!1)});["capture","download"].forEach(function(a){E[a]=new L(a,4,!1,a,null,!1)});["cols","rows","size","span"].forEach(function(a){E[a]=new L(a,6,!1,a,null,!1)});["rowSpan","start"].forEach(function(a){E[a]=new L(a,5,!1,a.toLowerCase(),null,!1)});var gf=/[\-:]([a-z])/g,hf=function(a){return a[1].toUpperCase()};"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(a){var b=
|
||
a.replace(gf,hf);E[b]=new L(b,1,!1,a,null,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(a){var b=a.replace(gf,hf);E[b]=new L(b,1,!1,a,"http://www.w3.org/1999/xlink",!1)});["xml:base","xml:lang","xml:space"].forEach(function(a){var b=a.replace(gf,hf);E[b]=new L(b,1,!1,a,"http://www.w3.org/XML/1998/namespace",!1)});["tabIndex","crossOrigin"].forEach(function(a){E[a]=new L(a,1,!1,a.toLowerCase(),null,!1)});E.xlinkHref=new L("xlinkHref",1,
|
||
!1,"xlink:href","http://www.w3.org/1999/xlink",!0);["src","href","action","formAction"].forEach(function(a){E[a]=new L(a,1,!1,a.toLowerCase(),null,!0)});var da=ea.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;da.hasOwnProperty("ReactCurrentDispatcher")||(da.ReactCurrentDispatcher={current:null});da.hasOwnProperty("ReactCurrentBatchConfig")||(da.ReactCurrentBatchConfig={suspense:null});var si=/^(.*)[\\\/]/,Q="function"===typeof Symbol&&Symbol.for,Pc=Q?Symbol.for("react.element"):60103,gb=Q?Symbol.for("react.portal"):
|
||
60106,Ma=Q?Symbol.for("react.fragment"):60107,Af=Q?Symbol.for("react.strict_mode"):60108,kc=Q?Symbol.for("react.profiler"):60114,Cf=Q?Symbol.for("react.provider"):60109,Bf=Q?Symbol.for("react.context"):60110,Hj=Q?Symbol.for("react.concurrent_mode"):60111,zd=Q?Symbol.for("react.forward_ref"):60112,lc=Q?Symbol.for("react.suspense"):60113,yd=Q?Symbol.for("react.suspense_list"):60120,Ad=Q?Symbol.for("react.memo"):60115,Ef=Q?Symbol.for("react.lazy"):60116,Df=Q?Symbol.for("react.block"):60121,zf="function"===
|
||
typeof Symbol&&Symbol.iterator,od,xh=function(a){return"undefined"!==typeof MSApp&&MSApp.execUnsafeLocalFunction?function(b,c,d,e){MSApp.execUnsafeLocalFunction(function(){return a(b,c,d,e)})}:a}(function(a,b){if("http://www.w3.org/2000/svg"!==a.namespaceURI||"innerHTML"in a)a.innerHTML=b;else{od=od||document.createElement("div");od.innerHTML="<svg>"+b.valueOf().toString()+"</svg>";for(b=od.firstChild;a.firstChild;)a.removeChild(a.firstChild);for(;b.firstChild;)a.appendChild(b.firstChild)}}),Wb=function(a,
|
||
b){if(b){var c=a.firstChild;if(c&&c===a.lastChild&&3===c.nodeType){c.nodeValue=b;return}}a.textContent=b},ib={animationend:nc("Animation","AnimationEnd"),animationiteration:nc("Animation","AnimationIteration"),animationstart:nc("Animation","AnimationStart"),transitionend:nc("Transition","TransitionEnd")},Id={},Of={};wa&&(Of=document.createElement("div").style,"AnimationEvent"in window||(delete ib.animationend.animation,delete ib.animationiteration.animation,delete ib.animationstart.animation),"TransitionEvent"in
|
||
window||delete ib.transitionend.transition);var $h=oc("animationend"),ai=oc("animationiteration"),bi=oc("animationstart"),ci=oc("transitionend"),Db="abort canplay canplaythrough durationchange emptied encrypted ended error loadeddata loadedmetadata loadstart pause play playing progress ratechange seeked seeking stalled suspend timeupdate volumechange waiting".split(" "),Pf=new ("function"===typeof WeakMap?WeakMap:Map),Ab=null,wi=function(a){if(a){var b=a._dispatchListeners,c=a._dispatchInstances;
|
||
if(Array.isArray(b))for(var d=0;d<b.length&&!a.isPropagationStopped();d++)lf(a,b[d],c[d]);else b&&lf(a,b,c);a._dispatchListeners=null;a._dispatchInstances=null;a.isPersistent()||a.constructor.release(a)}},qc=[],Rd=!1,fa=[],xa=null,ya=null,za=null,Eb=new Map,Fb=new Map,Jb=[],Nd="mousedown mouseup touchcancel touchend touchstart auxclick dblclick pointercancel pointerdown pointerup dragend dragstart drop compositionend compositionstart keydown keypress keyup input textInput close cancel copy cut paste click change contextmenu reset submit".split(" "),
|
||
yi="focus blur dragenter dragleave mouseover mouseout pointerover pointerout gotpointercapture lostpointercapture".split(" "),dg={},cg=new Map,Td=new Map,Rj=["abort","abort",$h,"animationEnd",ai,"animationIteration",bi,"animationStart","canplay","canPlay","canplaythrough","canPlayThrough","durationchange","durationChange","emptied","emptied","encrypted","encrypted","ended","ended","error","error","gotpointercapture","gotPointerCapture","load","load","loadeddata","loadedData","loadedmetadata","loadedMetadata",
|
||
"loadstart","loadStart","lostpointercapture","lostPointerCapture","playing","playing","progress","progress","seeking","seeking","stalled","stalled","suspend","suspend","timeupdate","timeUpdate",ci,"transitionEnd","waiting","waiting"];Sd("blur blur cancel cancel click click close close contextmenu contextMenu copy copy cut cut auxclick auxClick dblclick doubleClick dragend dragEnd dragstart dragStart drop drop focus focus input input invalid invalid keydown keyDown keypress keyPress keyup keyUp mousedown mouseDown mouseup mouseUp paste paste pause pause play play pointercancel pointerCancel pointerdown pointerDown pointerup pointerUp ratechange rateChange reset reset seeked seeked submit submit touchcancel touchCancel touchend touchEnd touchstart touchStart volumechange volumeChange".split(" "),
|
||
0);Sd("drag drag dragenter dragEnter dragexit dragExit dragleave dragLeave dragover dragOver mousemove mouseMove mouseout mouseOut mouseover mouseOver pointermove pointerMove pointerout pointerOut pointerover pointerOver scroll scroll toggle toggle touchmove touchMove wheel wheel".split(" "),1);Sd(Rj,2);(function(a,b){for(var c=0;c<a.length;c++)Td.set(a[c],b)})("change selectionchange textInput compositionstart compositionend compositionupdate".split(" "),0);var Hi=Zh,Gi=Pd,tc=!0,Kb={animationIterationCount:!0,
|
||
borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,
|
||
strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Sj=["Webkit","ms","Moz","O"];Object.keys(Kb).forEach(function(a){Sj.forEach(function(b){b=b+a.charAt(0).toUpperCase()+a.substring(1);Kb[b]=Kb[a]})});var Ii=M({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0}),ng="$",og="/$",$d="$?",Zd="$!",Ze=null,$e=null,We="function"===typeof setTimeout?setTimeout:void 0,vj="function"===
|
||
typeof clearTimeout?clearTimeout:void 0,jf=Math.random().toString(36).slice(2),Aa="__reactInternalInstance$"+jf,vc="__reactEventHandlers$"+jf,Lb="__reactContainere$"+jf,Ba=null,ce=null,wc=null;M(R.prototype,{preventDefault:function(){this.defaultPrevented=!0;var a=this.nativeEvent;a&&(a.preventDefault?a.preventDefault():"unknown"!==typeof a.returnValue&&(a.returnValue=!1),this.isDefaultPrevented=xc)},stopPropagation:function(){var a=this.nativeEvent;a&&(a.stopPropagation?a.stopPropagation():"unknown"!==
|
||
typeof a.cancelBubble&&(a.cancelBubble=!0),this.isPropagationStopped=xc)},persist:function(){this.isPersistent=xc},isPersistent:yc,destructor:function(){var a=this.constructor.Interface,b;for(b in a)this[b]=null;this.nativeEvent=this._targetInst=this.dispatchConfig=null;this.isPropagationStopped=this.isDefaultPrevented=yc;this._dispatchInstances=this._dispatchListeners=null}});R.Interface={type:null,target:null,currentTarget:function(){return null},eventPhase:null,bubbles:null,cancelable:null,timeStamp:function(a){return a.timeStamp||
|
||
Date.now()},defaultPrevented:null,isTrusted:null};R.extend=function(a){function b(){return c.apply(this,arguments)}var c=this,d=function(){};d.prototype=c.prototype;d=new d;M(d,b.prototype);b.prototype=d;b.prototype.constructor=b;b.Interface=M({},c.Interface,a);b.extend=c.extend;sg(b);return b};sg(R);var Tj=R.extend({data:null}),Uj=R.extend({data:null}),Ni=[9,13,27,32],de=wa&&"CompositionEvent"in window,cc=null;wa&&"documentMode"in document&&(cc=document.documentMode);var Vj=wa&&"TextEvent"in window&&
|
||
!cc,xg=wa&&(!de||cc&&8<cc&&11>=cc),wg=String.fromCharCode(32),ua={beforeInput:{phasedRegistrationNames:{bubbled:"onBeforeInput",captured:"onBeforeInputCapture"},dependencies:["compositionend","keypress","textInput","paste"]},compositionEnd:{phasedRegistrationNames:{bubbled:"onCompositionEnd",captured:"onCompositionEndCapture"},dependencies:"blur compositionend keydown keypress keyup mousedown".split(" ")},compositionStart:{phasedRegistrationNames:{bubbled:"onCompositionStart",captured:"onCompositionStartCapture"},
|
||
dependencies:"blur compositionstart keydown keypress keyup mousedown".split(" ")},compositionUpdate:{phasedRegistrationNames:{bubbled:"onCompositionUpdate",captured:"onCompositionUpdateCapture"},dependencies:"blur compositionupdate keydown keypress keyup mousedown".split(" ")}},vg=!1,mb=!1,Wj={eventTypes:ua,extractEvents:function(a,b,c,d,e){var f;if(de)b:{switch(a){case "compositionstart":var g=ua.compositionStart;break b;case "compositionend":g=ua.compositionEnd;break b;case "compositionupdate":g=
|
||
ua.compositionUpdate;break b}g=void 0}else mb?tg(a,c)&&(g=ua.compositionEnd):"keydown"===a&&229===c.keyCode&&(g=ua.compositionStart);g?(xg&&"ko"!==c.locale&&(mb||g!==ua.compositionStart?g===ua.compositionEnd&&mb&&(f=rg()):(Ba=d,ce="value"in Ba?Ba.value:Ba.textContent,mb=!0)),e=Tj.getPooled(g,b,c,d),f?e.data=f:(f=ug(c),null!==f&&(e.data=f)),lb(e),f=e):f=null;(a=Vj?Oi(a,c):Pi(a,c))?(b=Uj.getPooled(ua.beforeInput,b,c,d),b.data=a,lb(b)):b=null;return null===f?b:null===b?f:[f,b]}},Qi={color:!0,date:!0,
|
||
datetime:!0,"datetime-local":!0,email:!0,month:!0,number:!0,password:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0},Ag={change:{phasedRegistrationNames:{bubbled:"onChange",captured:"onChangeCapture"},dependencies:"blur change click focus input keydown keyup selectionchange".split(" ")}},Mb=null,Nb=null,kf=!1;wa&&(kf=Tf("input")&&(!document.documentMode||9<document.documentMode));var Xj={eventTypes:Ag,_isInputEventSupported:kf,extractEvents:function(a,b,c,d,e){e=b?Pa(b):window;var f=
|
||
e.nodeName&&e.nodeName.toLowerCase();if("select"===f||"input"===f&&"file"===e.type)var g=Si;else if(yg(e))if(kf)g=Wi;else{g=Ui;var h=Ti}else(f=e.nodeName)&&"input"===f.toLowerCase()&&("checkbox"===e.type||"radio"===e.type)&&(g=Vi);if(g&&(g=g(a,b)))return zg(g,c,d);h&&h(a,e,b);"blur"===a&&(a=e._wrapperState)&&a.controlled&&"number"===e.type&&Ed(e,"number",e.value)}},dc=R.extend({view:null,detail:null}),Yi={Alt:"altKey",Control:"ctrlKey",Meta:"metaKey",Shift:"shiftKey"},di=0,ei=0,fi=!1,gi=!1,ec=dc.extend({screenX:null,
|
||
screenY:null,clientX:null,clientY:null,pageX:null,pageY:null,ctrlKey:null,shiftKey:null,altKey:null,metaKey:null,getModifierState:fe,button:null,buttons:null,relatedTarget:function(a){return a.relatedTarget||(a.fromElement===a.srcElement?a.toElement:a.fromElement)},movementX:function(a){if("movementX"in a)return a.movementX;var b=di;di=a.screenX;return fi?"mousemove"===a.type?a.screenX-b:0:(fi=!0,0)},movementY:function(a){if("movementY"in a)return a.movementY;var b=ei;ei=a.screenY;return gi?"mousemove"===
|
||
a.type?a.screenY-b:0:(gi=!0,0)}}),hi=ec.extend({pointerId:null,width:null,height:null,pressure:null,tangentialPressure:null,tiltX:null,tiltY:null,twist:null,pointerType:null,isPrimary:null}),fc={mouseEnter:{registrationName:"onMouseEnter",dependencies:["mouseout","mouseover"]},mouseLeave:{registrationName:"onMouseLeave",dependencies:["mouseout","mouseover"]},pointerEnter:{registrationName:"onPointerEnter",dependencies:["pointerout","pointerover"]},pointerLeave:{registrationName:"onPointerLeave",dependencies:["pointerout",
|
||
"pointerover"]}},Yj={eventTypes:fc,extractEvents:function(a,b,c,d,e){var f="mouseover"===a||"pointerover"===a,g="mouseout"===a||"pointerout"===a;if(f&&0===(e&32)&&(c.relatedTarget||c.fromElement)||!g&&!f)return null;f=d.window===d?d:(f=d.ownerDocument)?f.defaultView||f.parentWindow:window;if(g){if(g=b,b=(b=c.relatedTarget||c.toElement)?Bb(b):null,null!==b){var h=Na(b);if(b!==h||5!==b.tag&&6!==b.tag)b=null}}else g=null;if(g===b)return null;if("mouseout"===a||"mouseover"===a){var m=ec;var n=fc.mouseLeave;
|
||
var l=fc.mouseEnter;var k="mouse"}else if("pointerout"===a||"pointerover"===a)m=hi,n=fc.pointerLeave,l=fc.pointerEnter,k="pointer";a=null==g?f:Pa(g);f=null==b?f:Pa(b);n=m.getPooled(n,g,c,d);n.type=k+"leave";n.target=a;n.relatedTarget=f;c=m.getPooled(l,b,c,d);c.type=k+"enter";c.target=f;c.relatedTarget=a;d=g;k=b;if(d&&k)a:{m=d;l=k;g=0;for(a=m;a;a=pa(a))g++;a=0;for(b=l;b;b=pa(b))a++;for(;0<g-a;)m=pa(m),g--;for(;0<a-g;)l=pa(l),a--;for(;g--;){if(m===l||m===l.alternate)break a;m=pa(m);l=pa(l)}m=null}else m=
|
||
null;l=m;for(m=[];d&&d!==l;){g=d.alternate;if(null!==g&&g===l)break;m.push(d);d=pa(d)}for(d=[];k&&k!==l;){g=k.alternate;if(null!==g&&g===l)break;d.push(k);k=pa(k)}for(k=0;k<m.length;k++)be(m[k],"bubbled",n);for(k=d.length;0<k--;)be(d[k],"captured",c);return 0===(e&64)?[n]:[n,c]}},Qa="function"===typeof Object.is?Object.is:Zi,$i=Object.prototype.hasOwnProperty,Zj=wa&&"documentMode"in document&&11>=document.documentMode,Eg={select:{phasedRegistrationNames:{bubbled:"onSelect",captured:"onSelectCapture"},
|
||
dependencies:"blur contextmenu dragend focus keydown keyup mousedown mouseup selectionchange".split(" ")}},nb=null,he=null,Pb=null,ge=!1,ak={eventTypes:Eg,extractEvents:function(a,b,c,d,e,f){e=f||(d.window===d?d.document:9===d.nodeType?d:d.ownerDocument);if(!(f=!e)){a:{e=Jd(e);f=rd.onSelect;for(var g=0;g<f.length;g++)if(!e.has(f[g])){e=!1;break a}e=!0}f=!e}if(f)return null;e=b?Pa(b):window;switch(a){case "focus":if(yg(e)||"true"===e.contentEditable)nb=e,he=b,Pb=null;break;case "blur":Pb=he=nb=null;
|
||
break;case "mousedown":ge=!0;break;case "contextmenu":case "mouseup":case "dragend":return ge=!1,Dg(c,d);case "selectionchange":if(Zj)break;case "keydown":case "keyup":return Dg(c,d)}return null}},bk=R.extend({animationName:null,elapsedTime:null,pseudoElement:null}),ck=R.extend({clipboardData:function(a){return"clipboardData"in a?a.clipboardData:window.clipboardData}}),dk=dc.extend({relatedTarget:null}),ek={Esc:"Escape",Spacebar:" ",Left:"ArrowLeft",Up:"ArrowUp",Right:"ArrowRight",Down:"ArrowDown",
|
||
Del:"Delete",Win:"OS",Menu:"ContextMenu",Apps:"ContextMenu",Scroll:"ScrollLock",MozPrintableKey:"Unidentified"},fk={8:"Backspace",9:"Tab",12:"Clear",13:"Enter",16:"Shift",17:"Control",18:"Alt",19:"Pause",20:"CapsLock",27:"Escape",32:" ",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown",45:"Insert",46:"Delete",112:"F1",113:"F2",114:"F3",115:"F4",116:"F5",117:"F6",118:"F7",119:"F8",120:"F9",121:"F10",122:"F11",123:"F12",144:"NumLock",145:"ScrollLock",
|
||
224:"Meta"},gk=dc.extend({key:function(a){if(a.key){var b=ek[a.key]||a.key;if("Unidentified"!==b)return b}return"keypress"===a.type?(a=Ac(a),13===a?"Enter":String.fromCharCode(a)):"keydown"===a.type||"keyup"===a.type?fk[a.keyCode]||"Unidentified":""},location:null,ctrlKey:null,shiftKey:null,altKey:null,metaKey:null,repeat:null,locale:null,getModifierState:fe,charCode:function(a){return"keypress"===a.type?Ac(a):0},keyCode:function(a){return"keydown"===a.type||"keyup"===a.type?a.keyCode:0},which:function(a){return"keypress"===
|
||
a.type?Ac(a):"keydown"===a.type||"keyup"===a.type?a.keyCode:0}}),hk=ec.extend({dataTransfer:null}),ik=dc.extend({touches:null,targetTouches:null,changedTouches:null,altKey:null,metaKey:null,ctrlKey:null,shiftKey:null,getModifierState:fe}),jk=R.extend({propertyName:null,elapsedTime:null,pseudoElement:null}),kk=ec.extend({deltaX:function(a){return"deltaX"in a?a.deltaX:"wheelDeltaX"in a?-a.wheelDeltaX:0},deltaY:function(a){return"deltaY"in a?a.deltaY:"wheelDeltaY"in a?-a.wheelDeltaY:"wheelDelta"in a?
|
||
-a.wheelDelta:0},deltaZ:null,deltaMode:null}),lk={eventTypes:dg,extractEvents:function(a,b,c,d,e){e=cg.get(a);if(!e)return null;switch(a){case "keypress":if(0===Ac(c))return null;case "keydown":case "keyup":a=gk;break;case "blur":case "focus":a=dk;break;case "click":if(2===c.button)return null;case "auxclick":case "dblclick":case "mousedown":case "mousemove":case "mouseup":case "mouseout":case "mouseover":case "contextmenu":a=ec;break;case "drag":case "dragend":case "dragenter":case "dragexit":case "dragleave":case "dragover":case "dragstart":case "drop":a=
|
||
hk;break;case "touchcancel":case "touchend":case "touchmove":case "touchstart":a=ik;break;case $h:case ai:case bi:a=bk;break;case ci:a=jk;break;case "scroll":a=dc;break;case "wheel":a=kk;break;case "copy":case "cut":case "paste":a=ck;break;case "gotpointercapture":case "lostpointercapture":case "pointercancel":case "pointerdown":case "pointermove":case "pointerout":case "pointerover":case "pointerup":a=hi;break;default:a=R}b=a.getPooled(e,b,c,d);lb(b);return b}};(function(a){if(ic)throw Error(k(101));
|
||
ic=Array.prototype.slice.call(a);nf()})("ResponderEventPlugin SimpleEventPlugin EnterLeaveEventPlugin ChangeEventPlugin SelectEventPlugin BeforeInputEventPlugin".split(" "));(function(a,b,c){td=a;rf=b;mf=c})(ae,Hb,Pa);pf({SimpleEventPlugin:lk,EnterLeaveEventPlugin:Yj,ChangeEventPlugin:Xj,SelectEventPlugin:ak,BeforeInputEventPlugin:Wj});var ie=[],ob=-1,Ca={},B={current:Ca},G={current:!1},Ra=Ca,bj=Pd,je=$f,Rg=Lj,aj=Nj,Dc=Oj,Ig=Zh,Jg=ag,Kg=Pj,Lg=Qj,Qg={},yj=Mj,Cj=void 0!==Yh?Yh:function(){},qa=null,
|
||
Ec=null,ke=!1,ii=ff(),Y=1E4>ii?ff:function(){return ff()-ii},Ic={current:null},Hc=null,qb=null,Gc=null,Tg=0,Jc=2,Ga=!1,Vb=da.ReactCurrentBatchConfig,$g=(new ea.Component).refs,Mc={isMounted:function(a){return(a=a._reactInternalFiber)?Na(a)===a:!1},enqueueSetState:function(a,b,c){a=a._reactInternalFiber;var d=ka(),e=Vb.suspense;d=Va(d,a,e);e=Ea(d,e);e.payload=b;void 0!==c&&null!==c&&(e.callback=c);Fa(a,e);Ja(a,d)},enqueueReplaceState:function(a,b,c){a=a._reactInternalFiber;var d=ka(),e=Vb.suspense;
|
||
d=Va(d,a,e);e=Ea(d,e);e.tag=1;e.payload=b;void 0!==c&&null!==c&&(e.callback=c);Fa(a,e);Ja(a,d)},enqueueForceUpdate:function(a,b){a=a._reactInternalFiber;var c=ka(),d=Vb.suspense;c=Va(c,a,d);d=Ea(c,d);d.tag=Jc;void 0!==b&&null!==b&&(d.callback=b);Fa(a,d);Ja(a,c)}},Qc=Array.isArray,wb=ah(!0),Fe=ah(!1),Sb={},ja={current:Sb},Ub={current:Sb},Tb={current:Sb},D={current:0},Sc=da.ReactCurrentDispatcher,X=da.ReactCurrentBatchConfig,Ia=0,z=null,K=null,J=null,Uc=!1,Tc={readContext:W,useCallback:S,useContext:S,
|
||
useEffect:S,useImperativeHandle:S,useLayoutEffect:S,useMemo:S,useReducer:S,useRef:S,useState:S,useDebugValue:S,useResponder:S,useDeferredValue:S,useTransition:S},dj={readContext:W,useCallback:ih,useContext:W,useEffect:eh,useImperativeHandle:function(a,b,c){c=null!==c&&void 0!==c?c.concat([a]):null;return ze(4,2,gh.bind(null,b,a),c)},useLayoutEffect:function(a,b){return ze(4,2,a,b)},useMemo:function(a,b){var c=ub();b=void 0===b?null:b;a=a();c.memoizedState=[a,b];return a},useReducer:function(a,b,c){var d=
|
||
ub();b=void 0!==c?c(b):b;d.memoizedState=d.baseState=b;a=d.queue={pending:null,dispatch:null,lastRenderedReducer:a,lastRenderedState:b};a=a.dispatch=ch.bind(null,z,a);return[d.memoizedState,a]},useRef:function(a){var b=ub();a={current:a};return b.memoizedState=a},useState:xe,useDebugValue:Be,useResponder:ue,useDeferredValue:function(a,b){var c=xe(a),d=c[0],e=c[1];eh(function(){var c=X.suspense;X.suspense=void 0===b?null:b;try{e(a)}finally{X.suspense=c}},[a,b]);return d},useTransition:function(a){var b=
|
||
xe(!1),c=b[0];b=b[1];return[ih(Ce.bind(null,b,a),[b,a]),c]}},ej={readContext:W,useCallback:Yc,useContext:W,useEffect:Xc,useImperativeHandle:hh,useLayoutEffect:fh,useMemo:jh,useReducer:Vc,useRef:dh,useState:function(a){return Vc(Ua)},useDebugValue:Be,useResponder:ue,useDeferredValue:function(a,b){var c=Vc(Ua),d=c[0],e=c[1];Xc(function(){var c=X.suspense;X.suspense=void 0===b?null:b;try{e(a)}finally{X.suspense=c}},[a,b]);return d},useTransition:function(a){var b=Vc(Ua),c=b[0];b=b[1];return[Yc(Ce.bind(null,
|
||
b,a),[b,a]),c]}},fj={readContext:W,useCallback:Yc,useContext:W,useEffect:Xc,useImperativeHandle:hh,useLayoutEffect:fh,useMemo:jh,useReducer:Wc,useRef:dh,useState:function(a){return Wc(Ua)},useDebugValue:Be,useResponder:ue,useDeferredValue:function(a,b){var c=Wc(Ua),d=c[0],e=c[1];Xc(function(){var c=X.suspense;X.suspense=void 0===b?null:b;try{e(a)}finally{X.suspense=c}},[a,b]);return d},useTransition:function(a){var b=Wc(Ua),c=b[0];b=b[1];return[Yc(Ce.bind(null,b,a),[b,a]),c]}},ra=null,Ka=null,Wa=
|
||
!1,gj=da.ReactCurrentOwner,ia=!1,Je={dehydrated:null,retryTime:0};var jj=function(a,b,c,d){for(c=b.child;null!==c;){if(5===c.tag||6===c.tag)a.appendChild(c.stateNode);else if(4!==c.tag&&null!==c.child){c.child.return=c;c=c.child;continue}if(c===b)break;for(;null===c.sibling;){if(null===c.return||c.return===b)return;c=c.return}c.sibling.return=c.return;c=c.sibling}};var wh=function(a){};var ij=function(a,b,c,d,e){var f=a.memoizedProps;if(f!==d){var g=b.stateNode;Ta(ja.current);a=null;switch(c){case "input":f=
|
||
Cd(g,f);d=Cd(g,d);a=[];break;case "option":f=Fd(g,f);d=Fd(g,d);a=[];break;case "select":f=M({},f,{value:void 0});d=M({},d,{value:void 0});a=[];break;case "textarea":f=Gd(g,f);d=Gd(g,d);a=[];break;default:"function"!==typeof f.onClick&&"function"===typeof d.onClick&&(g.onclick=uc)}Ud(c,d);var h,m;c=null;for(h in f)if(!d.hasOwnProperty(h)&&f.hasOwnProperty(h)&&null!=f[h])if("style"===h)for(m in g=f[h],g)g.hasOwnProperty(m)&&(c||(c={}),c[m]="");else"dangerouslySetInnerHTML"!==h&&"children"!==h&&"suppressContentEditableWarning"!==
|
||
h&&"suppressHydrationWarning"!==h&&"autoFocus"!==h&&(db.hasOwnProperty(h)?a||(a=[]):(a=a||[]).push(h,null));for(h in d){var k=d[h];g=null!=f?f[h]:void 0;if(d.hasOwnProperty(h)&&k!==g&&(null!=k||null!=g))if("style"===h)if(g){for(m in g)!g.hasOwnProperty(m)||k&&k.hasOwnProperty(m)||(c||(c={}),c[m]="");for(m in k)k.hasOwnProperty(m)&&g[m]!==k[m]&&(c||(c={}),c[m]=k[m])}else c||(a||(a=[]),a.push(h,c)),c=k;else"dangerouslySetInnerHTML"===h?(k=k?k.__html:void 0,g=g?g.__html:void 0,null!=k&&g!==k&&(a=a||
|
||
[]).push(h,k)):"children"===h?g===k||"string"!==typeof k&&"number"!==typeof k||(a=a||[]).push(h,""+k):"suppressContentEditableWarning"!==h&&"suppressHydrationWarning"!==h&&(db.hasOwnProperty(h)?(null!=k&&oa(e,h),a||g===k||(a=[])):(a=a||[]).push(h,k))}c&&(a=a||[]).push("style",c);e=a;if(b.updateQueue=e)b.effectTag|=4}};var kj=function(a,b,c,d){c!==d&&(b.effectTag|=4)};var pj="function"===typeof WeakSet?WeakSet:Set,wj="function"===typeof WeakMap?WeakMap:Map,sj=Math.ceil,gd=da.ReactCurrentDispatcher,
|
||
Uh=da.ReactCurrentOwner,H=0,Ye=8,ca=16,ma=32,Xa=0,hd=1,Oh=2,ad=3,bd=4,Xe=5,p=H,U=null,t=null,P=0,F=Xa,id=null,ta=1073741823,Yb=1073741823,kd=null,Xb=0,jd=!1,Re=0,Ph=500,l=null,cd=!1,Se=null,La=null,ld=!1,Zb=null,$b=90,bb=null,ac=0,af=null,dd=0,Ja=function(a,b){if(50<ac)throw ac=0,af=null,Error(k(185));a=ed(a,b);if(null!==a){var c=Cc();1073741823===b?(p&Ye)!==H&&(p&(ca|ma))===H?Te(a):(V(a),p===H&&ha()):V(a);(p&4)===H||98!==c&&99!==c||(null===bb?bb=new Map([[a,b]]):(c=bb.get(a),(void 0===c||c>b)&&bb.set(a,
|
||
b)))}};var zj=function(a,b,c){var d=b.expirationTime;if(null!==a){var e=b.pendingProps;if(a.memoizedProps!==e||G.current)ia=!0;else{if(d<c){ia=!1;switch(b.tag){case 3:sh(b);Ee();break;case 5:bh(b);if(b.mode&4&&1!==c&&e.hidden)return b.expirationTime=b.childExpirationTime=1,null;break;case 1:N(b.type)&&Bc(b);break;case 4:se(b,b.stateNode.containerInfo);break;case 10:d=b.memoizedProps.value;e=b.type._context;y(Ic,e._currentValue);e._currentValue=d;break;case 13:if(null!==b.memoizedState){d=b.child.childExpirationTime;
|
||
if(0!==d&&d>=c)return th(a,b,c);y(D,D.current&1);b=sa(a,b,c);return null!==b?b.sibling:null}y(D,D.current&1);break;case 19:d=b.childExpirationTime>=c;if(0!==(a.effectTag&64)){if(d)return vh(a,b,c);b.effectTag|=64}e=b.memoizedState;null!==e&&(e.rendering=null,e.tail=null);y(D,D.current);if(!d)return null}return sa(a,b,c)}ia=!1}}else ia=!1;b.expirationTime=0;switch(b.tag){case 2:d=b.type;null!==a&&(a.alternate=null,b.alternate=null,b.effectTag|=2);a=b.pendingProps;e=pb(b,B.current);rb(b,c);e=we(null,
|
||
b,d,a,e,c);b.effectTag|=1;if("object"===typeof e&&null!==e&&"function"===typeof e.render&&void 0===e.$$typeof){b.tag=1;b.memoizedState=null;b.updateQueue=null;if(N(d)){var f=!0;Bc(b)}else f=!1;b.memoizedState=null!==e.state&&void 0!==e.state?e.state:null;ne(b);var g=d.getDerivedStateFromProps;"function"===typeof g&&Lc(b,d,g,a);e.updater=Mc;b.stateNode=e;e._reactInternalFiber=b;pe(b,d,a,c);b=Ie(null,b,d,!0,f,c)}else b.tag=0,T(null,b,e,c),b=b.child;return b;case 16:a:{e=b.elementType;null!==a&&(a.alternate=
|
||
null,b.alternate=null,b.effectTag|=2);a=b.pendingProps;ri(e);if(1!==e._status)throw e._result;e=e._result;b.type=e;f=b.tag=Gj(e);a=aa(e,a);switch(f){case 0:b=He(null,b,e,a,c);break a;case 1:b=rh(null,b,e,a,c);break a;case 11:b=nh(null,b,e,a,c);break a;case 14:b=oh(null,b,e,aa(e.type,a),d,c);break a}throw Error(k(306,e,""));}return b;case 0:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:aa(d,e),He(a,b,d,e,c);case 1:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:aa(d,e),rh(a,b,d,e,c);
|
||
case 3:sh(b);d=b.updateQueue;if(null===a||null===d)throw Error(k(282));d=b.pendingProps;e=b.memoizedState;e=null!==e?e.element:null;oe(a,b);Qb(b,d,null,c);d=b.memoizedState.element;if(d===e)Ee(),b=sa(a,b,c);else{if(e=b.stateNode.hydrate)Ka=kb(b.stateNode.containerInfo.firstChild),ra=b,e=Wa=!0;if(e)for(c=Fe(b,null,d,c),b.child=c;c;)c.effectTag=c.effectTag&-3|1024,c=c.sibling;else T(a,b,d,c),Ee();b=b.child}return b;case 5:return bh(b),null===a&&De(b),d=b.type,e=b.pendingProps,f=null!==a?a.memoizedProps:
|
||
null,g=e.children,Yd(d,e)?g=null:null!==f&&Yd(d,f)&&(b.effectTag|=16),qh(a,b),b.mode&4&&1!==c&&e.hidden?(b.expirationTime=b.childExpirationTime=1,b=null):(T(a,b,g,c),b=b.child),b;case 6:return null===a&&De(b),null;case 13:return th(a,b,c);case 4:return se(b,b.stateNode.containerInfo),d=b.pendingProps,null===a?b.child=wb(b,null,d,c):T(a,b,d,c),b.child;case 11:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:aa(d,e),nh(a,b,d,e,c);case 7:return T(a,b,b.pendingProps,c),b.child;case 8:return T(a,
|
||
b,b.pendingProps.children,c),b.child;case 12:return T(a,b,b.pendingProps.children,c),b.child;case 10:a:{d=b.type._context;e=b.pendingProps;g=b.memoizedProps;f=e.value;var h=b.type._context;y(Ic,h._currentValue);h._currentValue=f;if(null!==g)if(h=g.value,f=Qa(h,f)?0:("function"===typeof d._calculateChangedBits?d._calculateChangedBits(h,f):1073741823)|0,0===f){if(g.children===e.children&&!G.current){b=sa(a,b,c);break a}}else for(h=b.child,null!==h&&(h.return=b);null!==h;){var m=h.dependencies;if(null!==
|
||
m){g=h.child;for(var l=m.firstContext;null!==l;){if(l.context===d&&0!==(l.observedBits&f)){1===h.tag&&(l=Ea(c,null),l.tag=Jc,Fa(h,l));h.expirationTime<c&&(h.expirationTime=c);l=h.alternate;null!==l&&l.expirationTime<c&&(l.expirationTime=c);Sg(h.return,c);m.expirationTime<c&&(m.expirationTime=c);break}l=l.next}}else g=10===h.tag?h.type===b.type?null:h.child:h.child;if(null!==g)g.return=h;else for(g=h;null!==g;){if(g===b){g=null;break}h=g.sibling;if(null!==h){h.return=g.return;g=h;break}g=g.return}h=
|
||
g}T(a,b,e.children,c);b=b.child}return b;case 9:return e=b.type,f=b.pendingProps,d=f.children,rb(b,c),e=W(e,f.unstable_observedBits),d=d(e),b.effectTag|=1,T(a,b,d,c),b.child;case 14:return e=b.type,f=aa(e,b.pendingProps),f=aa(e.type,f),oh(a,b,e,f,d,c);case 15:return ph(a,b,b.type,b.pendingProps,d,c);case 17:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:aa(d,e),null!==a&&(a.alternate=null,b.alternate=null,b.effectTag|=2),b.tag=1,N(d)?(a=!0,Bc(b)):a=!1,rb(b,c),Yg(b,d,e),pe(b,d,e,c),Ie(null,
|
||
b,d,!0,a,c);case 19:return vh(a,b,c)}throw Error(k(156,b.tag));};var bf=null,Ne=null,la=function(a,b,c,d){return new Fj(a,b,c,d)};ef.prototype.render=function(a){md(a,this._internalRoot,null,null)};ef.prototype.unmount=function(){var a=this._internalRoot,b=a.containerInfo;md(null,a,null,function(){b[Lb]=null})};var Di=function(a){if(13===a.tag){var b=Fc(ka(),150,100);Ja(a,b);df(a,b)}};var Yf=function(a){13===a.tag&&(Ja(a,3),df(a,3))};var Bi=function(a){if(13===a.tag){var b=ka();b=Va(b,a,null);Ja(a,
|
||
b);df(a,b)}};sd=function(a,b,c){switch(b){case "input":Dd(a,c);b=c.name;if("radio"===c.type&&null!=b){for(c=a;c.parentNode;)c=c.parentNode;c=c.querySelectorAll("input[name="+JSON.stringify(""+b)+'][type="radio"]');for(b=0;b<c.length;b++){var d=c[b];if(d!==a&&d.form===a.form){var e=ae(d);if(!e)throw Error(k(90));Gf(d);Dd(d,e)}}}break;case "textarea":Lf(a,c);break;case "select":b=c.value,null!=b&&hb(a,!!c.multiple,b,!1)}};(function(a,b,c,d){ee=a;eg=b;vd=c;vf=d})(Qh,function(a,b,c,d,e){var f=p;p|=4;
|
||
try{return Da(98,a.bind(null,b,c,d,e))}finally{p=f,p===H&&ha()}},function(){(p&(1|ca|ma))===H&&(uj(),xb())},function(a,b){var c=p;p|=2;try{return a(b)}finally{p=c,p===H&&ha()}});var mk={Events:[Hb,Pa,ae,pf,qd,lb,function(a){Kd(a,Ki)},sf,tf,sc,pc,xb,{current:!1}]};(function(a){var b=a.findFiberByHostInstance;return Ej(M({},a,{overrideHookState:null,overrideProps:null,setSuspenseHandler:null,scheduleUpdate:null,currentDispatcherRef:da.ReactCurrentDispatcher,findHostInstanceByFiber:function(a){a=Sf(a);
|
||
return null===a?null:a.stateNode},findFiberByHostInstance:function(a){return b?b(a):null},findHostInstancesForRefresh:null,scheduleRefresh:null,scheduleRoot:null,setRefreshHandler:null,getCurrentFiber:null}))})({findFiberByHostInstance:Bb,bundleType:0,version:"16.13.1",rendererPackageName:"react-dom"});I.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=mk;I.createPortal=Xh;I.findDOMNode=function(a){if(null==a)return null;if(1===a.nodeType)return a;var b=a._reactInternalFiber;if(void 0===
|
||
b){if("function"===typeof a.render)throw Error(k(188));throw Error(k(268,Object.keys(a)));}a=Sf(b);a=null===a?null:a.stateNode;return a};I.flushSync=function(a,b){if((p&(ca|ma))!==H)throw Error(k(187));var c=p;p|=1;try{return Da(99,a.bind(null,b))}finally{p=c,ha()}};I.hydrate=function(a,b,c){if(!bc(b))throw Error(k(200));return nd(null,a,b,!0,c)};I.render=function(a,b,c){if(!bc(b))throw Error(k(200));return nd(null,a,b,!1,c)};I.unmountComponentAtNode=function(a){if(!bc(a))throw Error(k(40));return a._reactRootContainer?
|
||
(Rh(function(){nd(null,null,a,!1,function(){a._reactRootContainer=null;a[Lb]=null})}),!0):!1};I.unstable_batchedUpdates=Qh;I.unstable_createPortal=function(a,b){return Xh(a,b,2<arguments.length&&void 0!==arguments[2]?arguments[2]:null)};I.unstable_renderSubtreeIntoContainer=function(a,b,c,d){if(!bc(c))throw Error(k(200));if(null==a||void 0===a._reactInternalFiber)throw Error(k(38));return nd(a,b,c,!1,d)};I.version="16.13.1"});
|
||
</script>
|
||
<script>const e = React.createElement;
|
||
|
||
function pathToString(path) {
|
||
if (path[0] === '/') {
|
||
return '/' + path.slice(1).join('/');
|
||
} else {
|
||
return path.join('/');
|
||
}
|
||
}
|
||
|
||
function findCommonPath(files) {
|
||
if (!files || !files.length) {
|
||
return [];
|
||
}
|
||
|
||
function isPrefix(arr, prefix) {
|
||
if (arr.length < prefix.length) {
|
||
return false;
|
||
}
|
||
for (let i = prefix.length - 1; i >= 0; --i) {
|
||
if (arr[i] !== prefix[i]) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
let commonPath = files[0].path.slice(0, -1);
|
||
while (commonPath.length) {
|
||
if (files.every(file => isPrefix(file.path, commonPath))) {
|
||
break;
|
||
}
|
||
commonPath.pop();
|
||
}
|
||
return commonPath;
|
||
}
|
||
|
||
function findFolders(files) {
|
||
if (!files || !files.length) {
|
||
return [];
|
||
}
|
||
|
||
let folders = files.filter(file => file.path.length > 1).map(file => file.path[0]);
|
||
folders = [...new Set(folders)]; // unique
|
||
folders.sort();
|
||
|
||
folders = folders.map(folder => {
|
||
let filesInFolder = files
|
||
.filter(file => file.path[0] === folder)
|
||
.map(file => ({
|
||
...file,
|
||
path: file.path.slice(1),
|
||
parent: [...file.parent, file.path[0]],
|
||
}));
|
||
|
||
const children = findFolders(filesInFolder); // recursion
|
||
|
||
return {
|
||
is_folder: true,
|
||
path: [folder],
|
||
parent: files[0].parent,
|
||
children,
|
||
covered: children.reduce((sum, file) => sum + file.covered, 0),
|
||
coverable: children.reduce((sum, file) => sum + file.coverable, 0),
|
||
prevRun: {
|
||
covered: children.reduce((sum, file) => sum + file.prevRun.covered, 0),
|
||
coverable: children.reduce((sum, file) => sum + file.prevRun.coverable, 0),
|
||
}
|
||
};
|
||
});
|
||
|
||
return [
|
||
...folders,
|
||
...files.filter(file => file.path.length === 1),
|
||
];
|
||
}
|
||
|
||
class App extends React.Component {
|
||
constructor(...args) {
|
||
super(...args);
|
||
|
||
this.state = {
|
||
current: [],
|
||
};
|
||
}
|
||
|
||
componentDidMount() {
|
||
this.updateStateFromLocation();
|
||
window.addEventListener("hashchange", () => this.updateStateFromLocation(), false);
|
||
}
|
||
|
||
updateStateFromLocation() {
|
||
if (window.location.hash.length > 1) {
|
||
const current = window.location.hash.substr(1).split('/');
|
||
this.setState({current});
|
||
} else {
|
||
this.setState({current: []});
|
||
}
|
||
}
|
||
|
||
getCurrentPath() {
|
||
let file = this.props.root;
|
||
let path = [file];
|
||
for (let p of this.state.current) {
|
||
file = file.children.find(file => file.path[0] === p);
|
||
if (!file) {
|
||
return path;
|
||
}
|
||
path.push(file);
|
||
}
|
||
return path;
|
||
}
|
||
|
||
render() {
|
||
const path = this.getCurrentPath();
|
||
const file = path[path.length - 1];
|
||
|
||
let w = null;
|
||
if (file.is_folder) {
|
||
w = e(FilesList, {
|
||
folder: file,
|
||
onSelectFile: this.selectFile.bind(this),
|
||
onBack: path.length > 1 ? this.back.bind(this) : null,
|
||
});
|
||
} else {
|
||
w = e(DisplayFile, {
|
||
file,
|
||
onBack: this.back.bind(this),
|
||
});
|
||
}
|
||
|
||
return e('div', {className: 'app'}, w);
|
||
}
|
||
|
||
selectFile(file) {
|
||
this.setState(({current}) => {
|
||
return {current: [...current, file.path[0]]};
|
||
}, () => this.updateHash());
|
||
}
|
||
|
||
back(file) {
|
||
this.setState(({current}) => {
|
||
return {current: current.slice(0, current.length - 1)};
|
||
}, () => this.updateHash());
|
||
}
|
||
|
||
updateHash() {
|
||
if (!this.state.current || !this.state.current.length) {
|
||
window.location = '#';
|
||
} else {
|
||
window.location = '#' + this.state.current.join('/');
|
||
}
|
||
}
|
||
}
|
||
|
||
function FilesList({folder, onSelectFile, onBack}) {
|
||
let files = folder.children;
|
||
return e('div', {className: 'display-folder'},
|
||
e(FileHeader, {file: folder, onBack}),
|
||
e('table', {className: 'files-list'},
|
||
e('thead', {className: 'files-list__head'},
|
||
e('tr', null,
|
||
e('th', null, "Path"),
|
||
e('th', null, "Coverage")
|
||
)
|
||
),
|
||
e('tbody', {className: 'files-list__body'},
|
||
files.map(file => e(File, {file, onClick: onSelectFile}))
|
||
)
|
||
)
|
||
);
|
||
}
|
||
|
||
function File({file, onClick}) {
|
||
const coverage = file.coverable ? file.covered / file.coverable * 100 : -1;
|
||
const coverageDelta = file.prevRun &&
|
||
(file.covered / file.coverable * 100 - file.prevRun.covered / file.prevRun.coverable * 100);
|
||
|
||
return e('tr', {
|
||
className: 'files-list__file'
|
||
+ (coverage >= 0 && coverage < 50 ? ' files-list__file_low': '')
|
||
+ (coverage >= 50 && coverage < 80 ? ' files-list__file_medium': '')
|
||
+ (coverage >= 80 ? ' files-list__file_high': '')
|
||
+ (file.is_folder ? ' files-list__file_folder': ''),
|
||
onClick: () => onClick(file),
|
||
},
|
||
e('td', null, e('a', null, pathToString(file.path))),
|
||
e('td', null,
|
||
file.covered + ' / ' + file.coverable +
|
||
(coverage >= 0 ? ' (' + coverage.toFixed(2) + '%)' : ''),
|
||
e('span', {title: 'Change from the previous run'},
|
||
(coverageDelta ? ` (${coverageDelta > 0 ? '+' : ''}${coverageDelta.toFixed(2)}%)` : ''))
|
||
)
|
||
);
|
||
}
|
||
|
||
function DisplayFile({file, onBack}) {
|
||
return e('div', {className: 'display-file'},
|
||
e(FileHeader, {file, onBack}),
|
||
e(FileContent, {file})
|
||
);
|
||
}
|
||
|
||
function FileHeader({file, onBack}) {
|
||
const coverage = file.covered / file.coverable * 100;
|
||
const coverageDelta = file.prevRun && (coverage - file.prevRun.covered / file.prevRun.coverable * 100);
|
||
|
||
return e('div', {className: 'file-header'},
|
||
onBack ? e('a', {className: 'file-header__back', onClick: onBack}, 'Back') : null,
|
||
e('div', {className: 'file-header__name'}, pathToString([...file.parent, ...file.path])),
|
||
e('div', {className: 'file-header__stat'},
|
||
'Covered: ' + file.covered + ' of ' + file.coverable +
|
||
(file.coverable ? ' (' + coverage.toFixed(2) + '%)' : ''),
|
||
e('span', {title: 'Change from the previous run'},
|
||
(coverageDelta ? ` (${coverageDelta > 0 ? '+' : ''}${coverageDelta.toFixed(2)}%)` : ''))
|
||
)
|
||
);
|
||
}
|
||
|
||
function FileContent({file}) {
|
||
return e('pre', {className: 'file-content'},
|
||
file.content.split(/\r?\n/).map((line, index) => {
|
||
const trace = file.traces.find(trace => trace.line === index + 1);
|
||
const covered = trace && trace.stats.Line;
|
||
const uncovered = trace && !trace.stats.Line;
|
||
return e('code', {
|
||
className: 'code-line'
|
||
+ (covered ? ' code-line_covered' : '')
|
||
+ (uncovered ? ' code-line_uncovered' : ''),
|
||
title: trace ? JSON.stringify(trace.stats, null, 2) : null,
|
||
}, line);
|
||
})
|
||
);
|
||
}
|
||
|
||
(function(){
|
||
const commonPath = findCommonPath(data.files);
|
||
const prevFilesMap = new Map();
|
||
|
||
previousData && previousData.files.forEach((file) => {
|
||
const path = file.path.slice(commonPath.length).join('/');
|
||
prevFilesMap.set(path, file);
|
||
});
|
||
|
||
const files = data.files.map((file) => {
|
||
const path = file.path.slice(commonPath.length);
|
||
const { covered = 0, coverable = 0 } = prevFilesMap.get(path.join('/')) || {};
|
||
return {
|
||
...file,
|
||
path,
|
||
parent: commonPath,
|
||
prevRun: { covered, coverable },
|
||
};
|
||
});
|
||
|
||
const children = findFolders(files);
|
||
|
||
const root = {
|
||
is_folder: true,
|
||
children,
|
||
path: commonPath,
|
||
parent: [],
|
||
covered: children.reduce((sum, file) => sum + file.covered, 0),
|
||
coverable: children.reduce((sum, file) => sum + file.coverable, 0),
|
||
prevRun: {
|
||
covered: children.reduce((sum, file) => sum + file.prevRun.covered, 0),
|
||
coverable: children.reduce((sum, file) => sum + file.prevRun.coverable, 0),
|
||
}
|
||
};
|
||
|
||
ReactDOM.render(e(App, {root, prevFilesMap}), document.getElementById('root'));
|
||
}());
|
||
</script>
|
||
</body>
|
||
</html> |