fur_cli/commands/
printed.rs1use crate::commands::provenance::{self, Options, Scope};
2use crate::commands::timeline::{run_timeline, TimelineArgs};
3use colored::*;
4use serde_json::Value;
5use std::fs;
6use std::path::Path;
7
8pub fn run_printed(
13 out: Option<String>,
14 verbose: bool,
15 provenance_scope: Option<Scope>,
16 id: Option<String>,
17) {
18 let fur_dir = Path::new(".fur");
19 let index_path = fur_dir.join("index.json");
20
21 if !index_path.exists() {
22 eprintln!("🚨 .fur/ not found. Run `fur new` first.");
23 return;
24 }
25
26 let index: Value = serde_json::from_str(&fs::read_to_string(&index_path).unwrap()).unwrap();
27
28 let target = match resolve_target(&index, id.as_deref()) {
29 Ok(t) => t,
30 Err(e) => return eprintln!("❌ {}", e),
31 };
32
33 if let Some(scope) = provenance_scope {
34 return run_provenance(fur_dir, &target, scope, out, verbose);
35 }
36
37 run_single(fur_dir, &target, out, verbose);
38}
39
40fn resolve_target(index: &Value, requested: Option<&str>) -> Result<String, String> {
42 let threads: Vec<String> = index["threads"]
43 .as_array()
44 .map(|arr| {
45 arr.iter()
46 .filter_map(|v| v.as_str().map(|s| s.to_string()))
47 .collect()
48 })
49 .unwrap_or_default();
50
51 let Some(needle) = requested else {
52 return index["active_thread"]
53 .as_str()
54 .filter(|s| !s.is_empty())
55 .map(|s| s.to_string())
56 .ok_or_else(|| "No active conversation found.".to_string());
57 };
58
59 if threads.iter().any(|t| t == needle) {
60 return Ok(needle.to_string());
61 }
62
63 let matches: Vec<&String> = threads.iter().filter(|t| t.starts_with(needle)).collect();
64
65 match matches.as_slice() {
66 [] => Err(format!("No conversation matches '{}'", needle)),
67 [single] => Ok((*single).clone()),
68 _ => Err(format!("Ambiguous prefix '{}'", needle)),
69 }
70}
71
72fn run_single(fur_dir: &Path, target: &str, out: Option<String>, verbose: bool) {
74 let convo_path = fur_dir.join("threads").join(format!("{}.json", target));
75 let conversation_json: Value =
76 serde_json::from_str(&fs::read_to_string(&convo_path).unwrap()).unwrap();
77
78 let title = conversation_json["title"].as_str().unwrap_or("untitled");
79
80 let out_path = out.unwrap_or_else(|| default_name(title, target, "md"));
81 let is_pdf = out_path.to_lowercase().ends_with(".pdf");
82
83 let args = TimelineArgs {
84 verbose,
85 contents: true,
86 out: Some(out_path.clone()),
87 conversation_override: Some(target.to_string()),
88 };
89
90 println!(
91 "{}",
92 format!("🖨️ Printing conversation: {} ({}) → {}", title, target, out_path)
93 .bright_green()
94 .bold()
95 );
96
97 run_timeline(args);
98
99 println!(
100 "{}",
101 if is_pdf {
102 format!("✔️ Exported PDF: {}", out_path)
103 } else {
104 format!("✔️ Exported Markdown: {}", out_path)
105 }
106 .bright_green()
107 .bold()
108 );
109}
110
111fn run_provenance(
113 fur_dir: &Path,
114 target: &str,
115 scope: Scope,
116 out: Option<String>,
117 verbose: bool,
118) {
119 let options = Options {
120 scope,
121 contents: verbose,
122 };
123
124 let (body, count) = match provenance::render(fur_dir, target, &options) {
125 Ok(result) => result,
126 Err(e) => return eprintln!("❌ {}", e),
127 };
128
129 let convo_path = fur_dir.join("threads").join(format!("{}.json", target));
130 let title = fs::read_to_string(&convo_path)
131 .ok()
132 .and_then(|c| serde_json::from_str::<Value>(&c).ok())
133 .and_then(|v| v["title"].as_str().map(|s| s.to_string()))
134 .unwrap_or_else(|| "untitled".to_string());
135
136 let out_path = out.unwrap_or_else(|| default_name(&format!("{}_PROVENANCE", title), target, "md"));
137
138 if out_path.to_lowercase().ends_with(".pdf") {
139 eprintln!("❌ Provenance records are Markdown only.");
140 return;
141 }
142
143 if let Err(e) = fs::write(&out_path, body) {
144 return eprintln!("❌ Could not write {}: {}", out_path, e);
145 }
146
147 println!(
148 "{}",
149 format!(
150 "🧬 Provenance record: {} conversation(s) → {}",
151 count, out_path
152 )
153 .bright_green()
154 .bold()
155 );
156}
157
158fn default_name(title: &str, id: &str, ext: &str) -> String {
161 let all_caps = title.to_uppercase().replace(' ', "_");
162 let short_id = id.split('-').next().unwrap_or(id);
163 format!("{}_{}.{}", all_caps, short_id, ext)
164}