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;
7use std::process::{Command, Stdio};
8use std::env;
9use std::time::SystemTime;
10
11use crate::models::ContentType;
12use tempfile::NamedTempFile;
13
14pub fn read_file_content(path: &Path) -> Result<String> {
16 fs::read_to_string(path).map_err(|e| anyhow!("Failed to read file {}: {}", path.display(), e))
17}
18
19pub fn read_stdin_content() -> Result<String> {
21 let mut buffer = String::new();
22 io::stdin().read_to_string(&mut buffer)?;
23 Ok(buffer)
24}
25
26pub fn open_editor(initial_content: Option<&str>) -> Result<String> {
28 let editor = get_editor()?;
30
31 let mut temp_file = NamedTempFile::new()?;
33
34 if let Some(content) = initial_content {
36 temp_file.write_all(content.as_bytes())?;
37 temp_file.flush()?;
38 }
39
40 let temp_path = temp_file.path().to_path_buf();
42
43 let status = Command::new(&editor)
45 .arg(&temp_path)
46 .stdin(Stdio::inherit())
47 .stdout(Stdio::inherit())
48 .stderr(Stdio::inherit())
49 .status()
50 .with_context(|| format!("Failed to open editor: {}", editor))?;
51
52 if !status.success() {
53 return Err(anyhow!("Editor exited with non-zero status: {}", status));
54 }
55
56 let content = fs::read_to_string(&temp_path)
58 .with_context(|| format!("Failed to read from temporary file: {}", temp_path.display()))?;
59
60 Ok(content)
61}
62
63pub fn open_editor_with_type(content_type: ContentType, initial_content: Option<&str>) -> Result<String> {
65 let editor = get_editor()?;
67
68 let extension = match content_type {
70 ContentType::Code => ".rs", ContentType::Text => ".txt",
72 ContentType::Script => ".sh",
73 ContentType::Other(ref lang) => {
74 match lang.as_str() {
75 "javascript" | "js" => ".js",
76 "typescript" | "ts" => ".ts",
77 "python" | "py" => ".py",
78 "ruby" | "rb" => ".rb",
79 "html" => ".html",
80 "css" => ".css",
81 "json" => ".json",
82 "yaml" | "yml" => ".yml",
83 "markdown" | "md" => ".md",
84 "shell" | "sh" | "bash" => ".sh",
85 "sql" => ".sql",
86 _ => ".txt"
87 }
88 }
89 };
90
91 let temp_dir = tempfile::tempdir()?;
93 let timestamp = SystemTime::now()
94 .duration_since(SystemTime::UNIX_EPOCH)
95 .unwrap_or_default()
96 .as_secs();
97 let file_name = format!("pocket_temp_{}{}", timestamp, extension);
98 let temp_path = temp_dir.path().join(file_name);
99
100 if let Some(content) = initial_content {
102 fs::write(&temp_path, content)?;
103 } else {
104 let template = match content_type {
106 ContentType::Code => match extension {
107 ".rs" => "// Rust code snippet\n\nfn example() {\n // Your code here\n}\n",
108 ".js" => "// JavaScript code snippet\n\nfunction example() {\n // Your code here\n}\n",
109 ".ts" => "// TypeScript code snippet\n\nfunction example(): void {\n // Your code here\n}\n",
110 ".py" => "# Python code snippet\n\ndef example():\n # Your code here\n pass\n",
111 ".rb" => "# Ruby code snippet\n\ndef example\n # Your code here\nend\n",
112 ".html" => "<!DOCTYPE html>\n<html>\n<head>\n <title>Title</title>\n</head>\n<body>\n <!-- Your content here -->\n</body>\n</html>\n",
113 ".css" => "/* CSS snippet */\n\n.example {\n /* Your styles here */\n}\n",
114 ".json" => "{\n \"key\": \"value\"\n}\n",
115 ".yml" => "# YAML snippet\nkey: value\nnested:\n subkey: subvalue\n",
116 ".sh" => "#!/bin/bash\n\n# Your script here\necho \"Hello, world!\"\n",
117 ".sql" => "-- SQL snippet\nSELECT * FROM table WHERE condition;\n",
118 _ => "// Code snippet\n\n// Your code here\n"
119 },
120 ContentType::Text => "# Title\n\nYour text here...\n",
121 ContentType::Script => "#!/bin/bash\n\n# Your script here\necho \"Hello, world!\"\n",
122 ContentType::Other(_) => "# Content\n\nYour content here...\n"
123 };
124 fs::write(&temp_path, template)?;
125 }
126
127 let status = Command::new(&editor)
129 .arg(&temp_path)
130 .stdin(Stdio::inherit())
131 .stdout(Stdio::inherit())
132 .stderr(Stdio::inherit())
133 .status()
134 .with_context(|| format!("Failed to open editor: {}", editor))?;
135
136 if !status.success() {
137 return Err(anyhow!("Editor exited with non-zero status: {}", status));
138 }
139
140 let content = fs::read_to_string(&temp_path)
142 .with_context(|| format!("Failed to read from temporary file: {}", temp_path.display()))?;
143
144 Ok(content)
145}
146
147pub fn edit_entry(id: &str, content: &str, content_type: ContentType) -> Result<String> {
149 println!("Opening entry {} in editor. Make your changes and save to update.", id.cyan());
150 open_editor_with_type(content_type, Some(content))
151}
152
153fn get_editor() -> Result<String> {
155 if let Ok(storage) = crate::storage::StorageManager::new() {
157 if let Ok(config) = storage.load_config() {
158 if !config.user.editor.is_empty() {
159 return Ok(config.user.editor);
160 }
161 }
162 }
163
164 if let Ok(editor) = env::var("EDITOR") {
166 if !editor.is_empty() {
167 return Ok(editor);
168 }
169 }
170
171 if let Ok(editor) = env::var("VISUAL") {
172 if !editor.is_empty() {
173 return Ok(editor);
174 }
175 }
176
177 println!("{}", "No preferred editor found in config or environment variables.".yellow());
179 let editor = input::<String>("Please enter your preferred editor (e.g., vim, nano, code):", None)?;
180
181 if let Ok(storage) = crate::storage::StorageManager::new() {
183 if let Ok(mut config) = storage.load_config() {
184 config.user.editor = editor.clone();
185 let _ = storage.save_config(&config); }
187 }
188
189 Ok(editor)
190}
191
192pub fn detect_content_type(path: Option<&Path>, content: Option<&str>) -> ContentType {
194 if let Some(path) = path {
196 if let Some(extension) = path.extension().and_then(|e| e.to_str()) {
197 match extension.to_lowercase().as_str() {
198 "rs" => return ContentType::Code,
199 "go" => return ContentType::Code,
200 "js" | "ts" => return ContentType::Code,
201 "py" => return ContentType::Code,
202 "java" => return ContentType::Code,
203 "c" | "cpp" | "h" | "hpp" => return ContentType::Code,
204 "cs" => return ContentType::Code,
205 "rb" => return ContentType::Code,
206 "php" => return ContentType::Code,
207 "html" | "htm" => return ContentType::Other("html".to_string()),
208 "css" => return ContentType::Other("css".to_string()),
209 "json" => return ContentType::Other("json".to_string()),
210 "yaml" | "yml" => return ContentType::Other("yaml".to_string()),
211 "md" | "markdown" => return ContentType::Other("markdown".to_string()),
212 "sql" => return ContentType::Other("sql".to_string()),
213 "sh" | "bash" | "zsh" => return ContentType::Script,
214 _ => {}
215 }
216 }
217
218 if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
220 if filename.starts_with("Dockerfile") {
221 return ContentType::Other("dockerfile".to_string());
222 }
223
224 if filename == "Makefile" || filename == "makefile" {
225 return ContentType::Other("makefile".to_string());
226 }
227 }
228 }
229
230 if let Some(content) = content {
232 if content.starts_with("#!/bin/sh") ||
234 content.starts_with("#!/bin/bash") ||
235 content.starts_with("#!/usr/bin/env bash") ||
236 content.starts_with("#!/bin/zsh") ||
237 content.starts_with("#!/usr/bin/env zsh") {
238 return ContentType::Script;
239 }
240
241 let trimmed = content.trim();
243 if trimmed.starts_with("#include") || trimmed.starts_with("#define") ||
244 trimmed.starts_with("import ") || trimmed.starts_with("from ") ||
245 trimmed.starts_with("package ") || trimmed.starts_with("using ") ||
246 trimmed.starts_with("function ") || trimmed.starts_with("def ") ||
247 trimmed.starts_with("class ") || trimmed.starts_with("struct ") ||
248 trimmed.starts_with("enum ") || trimmed.starts_with("interface ") ||
249 trimmed.contains("public class ") || trimmed.contains("private class ") ||
250 trimmed.contains("fn ") || trimmed.contains("pub fn ") ||
251 trimmed.contains("impl ") || trimmed.contains("trait ") {
252 return ContentType::Code;
253 }
254
255 if (trimmed.starts_with('{') && trimmed.ends_with('}')) ||
257 (trimmed.starts_with('[') && trimmed.ends_with(']')) {
258 return ContentType::Other("json".to_string());
259 }
260
261 if trimmed.starts_with("<!DOCTYPE html>") ||
263 trimmed.starts_with("<html>") ||
264 trimmed.contains("<body>") {
265 return ContentType::Other("html".to_string());
266 }
267
268 if trimmed.starts_with("# ") ||
270 trimmed.contains("\n## ") ||
271 trimmed.contains("\n### ") {
272 return ContentType::Other("markdown".to_string());
273 }
274 }
275
276 ContentType::Text
278}
279
280pub fn confirm(message: &str, default: bool) -> Result<bool> {
282 Ok(Confirm::with_theme(&ColorfulTheme::default())
283 .with_prompt(message)
284 .default(default)
285 .interact()?)
286}
287
288pub fn input<T>(message: &str, default: Option<T>) -> Result<T>
290where
291 T: std::str::FromStr + std::fmt::Display + Clone,
292 T::Err: std::fmt::Display,
293{
294 let theme = ColorfulTheme::default();
295
296 if let Some(default_value) = default {
297 Ok(Input::<T>::with_theme(&theme)
298 .with_prompt(message)
299 .default(default_value)
300 .interact()?)
301 } else {
302 Ok(Input::<T>::with_theme(&theme)
303 .with_prompt(message)
304 .interact()?)
305 }
306}
307
308pub fn select<T>(message: &str, options: &[T]) -> Result<usize>
310where
311 T: std::fmt::Display,
312{
313 Ok(Select::with_theme(&ColorfulTheme::default())
314 .with_prompt(message)
315 .items(options)
316 .default(0)
317 .interact()?)
318}
319
320pub fn format_with_tag(tag: &str, content: &str) -> String {
322 format!("--- {} ---\n{}\n--- end {} ---\n", tag, content, tag)
323}
324
325pub fn truncate_string(s: &str, max_len: usize) -> String {
327 if s.len() <= max_len {
328 s.to_string()
329 } else {
330 let mut result = s.chars().take(max_len - 3).collect::<String>();
331 result.push_str("...");
332 result
333 }
334}
335
336pub fn first_line(s: &str) -> &str {
338 s.lines().next().unwrap_or(s)
339}
340
341pub fn get_title_from_content(content: &str) -> String {
343 let first = first_line(content);
344 if first.is_empty() {
345 truncate_string(content, 50)
346 } else {
347 truncate_string(first, 50)
348 }
349}