1use anyhow::{Result, anyhow, Context};
2use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};
3use owo_colors::OwoColorize;
4use std::fs;
5use std::io::{self, Read, Write};
6use std::path::{Path, PathBuf};
7use std::process::{Command, Stdio};
8use std::env;
9use std::time::SystemTime;
10
11use crate::models::ContentType;
12use tempfile::NamedTempFile;
13
14pub mod clipboard;
16
17pub mod summarization;
19
20pub use clipboard::{read_clipboard, write_clipboard};
22
23pub use summarization::{summarize_text, SummaryMetadata};
25
26pub fn read_file_content(path: &Path) -> Result<String> {
28 fs::read_to_string(path).map_err(|e| anyhow!("Failed to read file {}: {}", path.display(), e))
29}
30
31pub fn read_stdin_content() -> Result<String> {
33 let mut buffer = String::new();
34 io::stdin().read_to_string(&mut buffer)?;
35 Ok(buffer)
36}
37
38pub fn open_editor(initial_content: Option<&str>) -> Result<String> {
40 let editor = get_editor()?;
42
43 let mut temp_file = NamedTempFile::new()?;
45
46 if let Some(content) = initial_content {
48 temp_file.write_all(content.as_bytes())?;
49 temp_file.flush()?;
50 }
51
52 let temp_path = temp_file.path().to_path_buf();
54
55 let status = Command::new(&editor)
57 .arg(&temp_path)
58 .stdin(Stdio::inherit())
59 .stdout(Stdio::inherit())
60 .stderr(Stdio::inherit())
61 .status()
62 .with_context(|| format!("Failed to open editor: {}", editor))?;
63
64 if !status.success() {
65 return Err(anyhow!("Editor exited with non-zero status: {}", status));
66 }
67
68 let content = fs::read_to_string(&temp_path)
70 .with_context(|| format!("Failed to read from temporary file: {}", temp_path.display()))?;
71
72 Ok(content)
73}
74
75pub fn open_editor_with_type(content_type: ContentType, initial_content: Option<&str>) -> Result<String> {
77 let editor = get_editor()?;
79
80 let extension = match content_type {
82 ContentType::Code => ".rs", ContentType::Text => ".txt",
84 ContentType::Script => ".sh",
85 ContentType::Other(ref lang) => {
86 match lang.as_str() {
87 "javascript" | "js" => ".js",
88 "typescript" | "ts" => ".ts",
89 "python" | "py" => ".py",
90 "ruby" | "rb" => ".rb",
91 "html" => ".html",
92 "css" => ".css",
93 "json" => ".json",
94 "yaml" | "yml" => ".yml",
95 "markdown" | "md" => ".md",
96 "shell" | "sh" | "bash" => ".sh",
97 "sql" => ".sql",
98 _ => ".txt"
99 }
100 }
101 };
102
103 let temp_dir = tempfile::tempdir()?;
105 let timestamp = SystemTime::now()
106 .duration_since(SystemTime::UNIX_EPOCH)
107 .unwrap_or_default()
108 .as_secs();
109 let file_name = format!("pocket_temp_{}{}", timestamp, extension);
110 let temp_path = temp_dir.path().join(file_name);
111
112 if let Some(content) = initial_content {
114 fs::write(&temp_path, content)?;
115 } else {
116 let template = match content_type {
118 ContentType::Code => match extension {
119 ".rs" => "// Rust code snippet\n\nfn example() {\n // Your code here\n}\n",
120 ".js" => "// JavaScript code snippet\n\nfunction example() {\n // Your code here\n}\n",
121 ".ts" => "// TypeScript code snippet\n\nfunction example(): void {\n // Your code here\n}\n",
122 ".py" => "# Python code snippet\n\ndef example():\n # Your code here\n pass\n",
123 ".rb" => "# Ruby code snippet\n\ndef example\n # Your code here\nend\n",
124 ".html" => "<!DOCTYPE html>\n<html>\n<head>\n <title>Title</title>\n</head>\n<body>\n <!-- Your content here -->\n</body>\n</html>\n",
125 ".css" => "/* CSS snippet */\n\n.example {\n /* Your styles here */\n}\n",
126 ".json" => "{\n \"key\": \"value\"\n}\n",
127 ".yml" => "# YAML snippet\nkey: value\nnested:\n subkey: subvalue\n",
128 ".sh" => "#!/bin/bash\n\n# Your script here\necho \"Hello, world!\"\n",
129 ".sql" => "-- SQL snippet\nSELECT * FROM table WHERE condition;\n",
130 _ => "// Code snippet\n\n// Your code here\n"
131 },
132 ContentType::Text => "# Title\n\nYour text here...\n",
133 ContentType::Script => "#!/bin/bash\n\n# Your script here\necho \"Hello, world!\"\n",
134 ContentType::Other(_) => "# Content\n\nYour content here...\n"
135 };
136 fs::write(&temp_path, template)?;
137 }
138
139 let status = Command::new(&editor)
141 .arg(&temp_path)
142 .stdin(Stdio::inherit())
143 .stdout(Stdio::inherit())
144 .stderr(Stdio::inherit())
145 .status()
146 .with_context(|| format!("Failed to open editor: {}", editor))?;
147
148 if !status.success() {
149 return Err(anyhow!("Editor exited with non-zero status: {}", status));
150 }
151
152 let content = fs::read_to_string(&temp_path)
154 .with_context(|| format!("Failed to read from temporary file: {}", temp_path.display()))?;
155
156 Ok(content)
157}
158
159pub fn edit_entry(id: &str, content: &str, content_type: ContentType) -> Result<String> {
161 println!("Opening entry {} in editor. Make your changes and save to update.", id.cyan());
162 open_editor_with_type(content_type, Some(content))
163}
164
165fn get_editor() -> Result<String> {
167 if let Ok(storage) = crate::storage::StorageManager::new() {
169 if let Ok(config) = storage.load_config() {
170 if !config.user.editor.is_empty() {
171 return Ok(config.user.editor);
172 }
173 }
174 }
175
176 if let Ok(editor) = env::var("EDITOR") {
178 if !editor.is_empty() {
179 return Ok(editor);
180 }
181 }
182
183 if let Ok(editor) = env::var("VISUAL") {
184 if !editor.is_empty() {
185 return Ok(editor);
186 }
187 }
188
189 println!("{}", "No preferred editor found in config or environment variables.".yellow());
191 let editor = input::<String>("Please enter your preferred editor (e.g., vim, nano, code):", None)?;
192
193 if let Ok(storage) = crate::storage::StorageManager::new() {
195 if let Ok(mut config) = storage.load_config() {
196 config.user.editor = editor.clone();
197 let _ = storage.save_config(&config); }
199 }
200
201 Ok(editor)
202}
203
204pub fn detect_content_type(path: Option<&Path>, content: Option<&str>) -> ContentType {
206 if let Some(path) = path {
208 if let Some(extension) = path.extension().and_then(|e| e.to_str()) {
209 match extension.to_lowercase().as_str() {
210 "rs" => return ContentType::Code,
211 "go" => return ContentType::Code,
212 "js" | "ts" => return ContentType::Code,
213 "py" => return ContentType::Code,
214 "java" => return ContentType::Code,
215 "c" | "cpp" | "h" | "hpp" => return ContentType::Code,
216 "cs" => return ContentType::Code,
217 "rb" => return ContentType::Code,
218 "php" => return ContentType::Code,
219 "html" | "htm" => return ContentType::Other("html".to_string()),
220 "css" => return ContentType::Other("css".to_string()),
221 "json" => return ContentType::Other("json".to_string()),
222 "yaml" | "yml" => return ContentType::Other("yaml".to_string()),
223 "md" | "markdown" => return ContentType::Other("markdown".to_string()),
224 "sql" => return ContentType::Other("sql".to_string()),
225 "sh" | "bash" | "zsh" => return ContentType::Script,
226 _ => {}
227 }
228 }
229
230 if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
232 if filename.starts_with("Dockerfile") {
233 return ContentType::Other("dockerfile".to_string());
234 }
235
236 if filename == "Makefile" || filename == "makefile" {
237 return ContentType::Other("makefile".to_string());
238 }
239 }
240 }
241
242 if let Some(content) = content {
244 if content.starts_with("#!/bin/sh") ||
246 content.starts_with("#!/bin/bash") ||
247 content.starts_with("#!/usr/bin/env bash") ||
248 content.starts_with("#!/bin/zsh") ||
249 content.starts_with("#!/usr/bin/env zsh") {
250 return ContentType::Script;
251 }
252
253 let trimmed = content.trim();
255 if trimmed.starts_with("#include") || trimmed.starts_with("#define") ||
256 trimmed.starts_with("import ") || trimmed.starts_with("from ") ||
257 trimmed.starts_with("package ") || trimmed.starts_with("using ") ||
258 trimmed.starts_with("function ") || trimmed.starts_with("def ") ||
259 trimmed.starts_with("class ") || trimmed.starts_with("struct ") ||
260 trimmed.starts_with("enum ") || trimmed.starts_with("interface ") ||
261 trimmed.contains("public class ") || trimmed.contains("private class ") ||
262 trimmed.contains("fn ") || trimmed.contains("pub fn ") ||
263 trimmed.contains("impl ") || trimmed.contains("trait ") {
264 return ContentType::Code;
265 }
266
267 if (trimmed.starts_with('{') && trimmed.ends_with('}')) ||
269 (trimmed.starts_with('[') && trimmed.ends_with(']')) {
270 return ContentType::Other("json".to_string());
271 }
272
273 if trimmed.starts_with("<!DOCTYPE html>") ||
275 trimmed.starts_with("<html>") ||
276 trimmed.contains("<body>") {
277 return ContentType::Other("html".to_string());
278 }
279
280 if trimmed.starts_with("# ") ||
282 trimmed.contains("\n## ") ||
283 trimmed.contains("\n### ") {
284 return ContentType::Other("markdown".to_string());
285 }
286 }
287
288 ContentType::Text
290}
291
292pub fn confirm(message: &str, default: bool) -> Result<bool> {
294 Ok(Confirm::with_theme(&ColorfulTheme::default())
295 .with_prompt(message)
296 .default(default)
297 .interact()?)
298}
299
300pub fn input<T>(message: &str, default: Option<T>) -> Result<T>
302where
303 T: std::str::FromStr + std::fmt::Display + Clone,
304 T::Err: std::fmt::Display,
305{
306 let theme = ColorfulTheme::default();
307
308 if let Some(default_value) = default {
309 Ok(Input::<T>::with_theme(&theme)
310 .with_prompt(message)
311 .default(default_value)
312 .interact()?)
313 } else {
314 Ok(Input::<T>::with_theme(&theme)
315 .with_prompt(message)
316 .interact()?)
317 }
318}
319
320pub fn select<T>(message: &str, options: &[T]) -> Result<usize>
322where
323 T: std::fmt::Display,
324{
325 Ok(Select::with_theme(&ColorfulTheme::default())
326 .with_prompt(message)
327 .items(options)
328 .default(0)
329 .interact()?)
330}
331
332pub fn format_with_tag(tag: &str, content: &str) -> String {
334 format!("--- {} ---\n{}\n--- end {} ---\n", tag, content, tag)
335}
336
337pub fn truncate_string(s: &str, max_len: usize) -> String {
339 if s.len() <= max_len {
340 s.to_string()
341 } else {
342 let mut result = s.chars().take(max_len - 3).collect::<String>();
343 result.push_str("...");
344 result
345 }
346}
347
348pub fn first_line(s: &str) -> &str {
350 s.lines().next().unwrap_or(s)
351}
352
353pub fn get_title_from_content(content: &str) -> String {
355 let first = first_line(content);
356 if first.is_empty() {
357 truncate_string(content, 50)
358 } else {
359 truncate_string(first, 50)
360 }
361}
362
363pub fn expand_path(path: &str) -> Result<PathBuf> {
365 let expanded = if path.starts_with("~") {
366 if let Some(home) = dirs::home_dir() {
367 let path_without_tilde = path.strip_prefix("~").unwrap_or("");
368 home.join(path_without_tilde.strip_prefix("/").unwrap_or(path_without_tilde))
369 } else {
370 return Err(anyhow!("Could not determine home directory"));
371 }
372 } else {
373 PathBuf::from(path)
374 };
375
376 let mut result = String::new();
378 let mut in_var = false;
379 let mut var_name = String::new();
380
381 for c in expanded.to_str().unwrap_or(path).chars() {
382 if in_var {
383 if c.is_alphanumeric() || c == '_' {
384 var_name.push(c);
385 } else {
386 if !var_name.is_empty() {
387 if let Ok(value) = std::env::var(&var_name) {
388 result.push_str(&value);
389 }
390 var_name.clear();
391 } else {
392 result.push('$');
393 }
394 result.push(c);
395 in_var = false;
396 }
397 } else if c == '$' {
398 in_var = true;
399 } else {
400 result.push(c);
401 }
402 }
403
404 if in_var && !var_name.is_empty() {
405 if let Ok(value) = std::env::var(&var_name) {
406 result.push_str(&value);
407 }
408 }
409
410 Ok(PathBuf::from(result))
411}
412
413pub fn get_cursor_position(content: &str) -> Option<usize> {
416 for marker in ["// CURSOR", "# CURSOR", "<!-- CURSOR -->", "/* CURSOR */"] {
418 if let Some(pos) = content.find(marker) {
419 return Some(pos);
420 }
421 }
422
423 if let Some(pos) = content.find("\n\n\n") {
426 return Some(pos + 2); }
428
429 Some(content.len())
431}