gestura_core_tools/
file_async.rs

1//! Async file operations for pipeline integration
2//!
3//! Wraps the synchronous [`FileTools`] via
4//! `tokio::task::spawn_blocking` for use in async contexts (pipeline, GUI).
5
6use crate::error::AppError;
7use crate::error::Result;
8use crate::file::FileTools;
9use serde::Serialize;
10use std::path::Path;
11
12#[derive(Debug, Serialize)]
13struct ListDirOutput {
14    path: String,
15    show_hidden: bool,
16    truncated: bool,
17    entries: Vec<crate::file::FileEntry>,
18}
19
20#[derive(Debug, Serialize)]
21struct TreeDirOutput {
22    path: String,
23    max_depth: Option<usize>,
24    show_hidden: bool,
25    tree: crate::file::TreeNode,
26}
27
28#[derive(Debug, Serialize)]
29struct WriteFileOutput {
30    path: String,
31    bytes_written: usize,
32    created: bool,
33    changed: bool,
34}
35
36#[derive(Debug, Serialize)]
37struct EditFileOutput {
38    path: String,
39    replacements: usize,
40    changed: bool,
41}
42
43#[derive(Debug, Serialize)]
44struct SearchOutput {
45    pattern: String,
46    path: String,
47    recursive: bool,
48    truncated: bool,
49    matches: Vec<crate::file::SearchMatch>,
50}
51
52/// Read a file asynchronously.
53pub async fn read_file(path: &str) -> Result<String> {
54    read_file_range(path, None, None).await
55}
56
57/// Read a file asynchronously with an optional line range.
58pub async fn read_file_range(
59    path: &str,
60    start_line: Option<usize>,
61    end_line: Option<usize>,
62) -> Result<String> {
63    let path = path.to_string();
64    tokio::task::spawn_blocking(move || {
65        let tools = FileTools::new();
66        tools
67            .read(Path::new(&path), start_line, end_line)
68            .map(|r| r.content)
69    })
70    .await
71    .map_err(|e| AppError::Io(std::io::Error::other(format!("Task join error: {}", e))))?
72}
73
74/// Write to a file asynchronously.
75pub async fn write_file(path: &str, content: &str) -> Result<String> {
76    let path = path.to_string();
77    let content = content.to_string();
78    tokio::task::spawn_blocking(move || {
79        let tools = FileTools::new();
80        let res = tools.write(Path::new(&path), &content)?;
81
82        let out = WriteFileOutput {
83            path,
84            bytes_written: res.bytes_written,
85            created: res.created,
86            changed: res.changed,
87        };
88        serde_json::to_string_pretty(&out).map_err(|e| {
89            AppError::Io(std::io::Error::other(format!(
90                "Failed to serialize write output: {e}"
91            )))
92        })
93    })
94    .await
95    .map_err(|e| AppError::Io(std::io::Error::other(format!("Task join error: {}", e))))?
96}
97
98/// Edit a file asynchronously by replacing `old_str` with `new_str`.
99///
100/// Returns a small JSON payload with the number of replacements.
101pub async fn edit_file(path: &str, old_str: &str, new_str: &str) -> Result<String> {
102    let path = path.to_string();
103    let old_str = old_str.to_string();
104    let new_str = new_str.to_string();
105    tokio::task::spawn_blocking(move || {
106        let tools = FileTools::new();
107        let res = tools.edit(Path::new(&path), &old_str, &new_str)?;
108
109        let out = EditFileOutput {
110            path,
111            replacements: res.replacements,
112            changed: res.changed,
113        };
114        serde_json::to_string_pretty(&out).map_err(|e| {
115            AppError::Io(std::io::Error::other(format!(
116                "Failed to serialize edit output: {e}"
117            )))
118        })
119    })
120    .await
121    .map_err(|e| AppError::Io(std::io::Error::other(format!("Task join error: {}", e))))?
122}
123
124/// List directory entries asynchronously.
125///
126/// Returns a JSON string containing the entries.
127pub async fn list_dir(path: &str, show_hidden: bool, max_entries: Option<usize>) -> Result<String> {
128    let path = path.to_string();
129    tokio::task::spawn_blocking(move || {
130        let tools = FileTools::new();
131        let mut entries = tools.list(Path::new(&path), show_hidden)?;
132        let max = max_entries.unwrap_or(200);
133        let truncated = entries.len() > max;
134        if truncated {
135            entries.truncate(max);
136        }
137
138        let out = ListDirOutput {
139            path,
140            show_hidden,
141            truncated,
142            entries,
143        };
144        serde_json::to_string_pretty(&out).map_err(|e| {
145            AppError::Io(std::io::Error::other(format!(
146                "Failed to serialize list output: {e}"
147            )))
148        })
149    })
150    .await
151    .map_err(|e| AppError::Io(std::io::Error::other(format!("Task join error: {}", e))))?
152}
153
154/// Build a directory tree asynchronously.
155///
156/// Returns a JSON string containing the tree.
157pub async fn tree_dir(path: &str, max_depth: Option<usize>, show_hidden: bool) -> Result<String> {
158    let path = path.to_string();
159    let show_hidden_flag = show_hidden;
160    tokio::task::spawn_blocking(move || {
161        let tools = FileTools::new();
162        let tree = tools.tree(Path::new(&path), max_depth, show_hidden_flag)?;
163        let out = TreeDirOutput {
164            path,
165            max_depth,
166            show_hidden: show_hidden_flag,
167            tree,
168        };
169        serde_json::to_string_pretty(&out).map_err(|e| {
170            AppError::Io(std::io::Error::other(format!(
171                "Failed to serialize tree output: {e}"
172            )))
173        })
174    })
175    .await
176    .map_err(|e| AppError::Io(std::io::Error::other(format!("Task join error: {}", e))))?
177}
178
179/// Search files for a pattern asynchronously.
180///
181/// Returns a JSON string containing the matches.
182pub async fn search_files(
183    pattern: &str,
184    path: &str,
185    recursive: bool,
186    max_matches: Option<usize>,
187) -> Result<String> {
188    let pattern = pattern.to_string();
189    let path = path.to_string();
190    tokio::task::spawn_blocking(move || {
191        let tools = FileTools::new();
192        let mut matches = tools.search(&pattern, Path::new(&path), recursive)?;
193        let max = max_matches.unwrap_or(200);
194        let truncated = matches.len() > max;
195        if truncated {
196            matches.truncate(max);
197        }
198
199        let out = SearchOutput {
200            pattern,
201            path,
202            recursive,
203            truncated,
204            matches,
205        };
206        serde_json::to_string_pretty(&out).map_err(|e| {
207            AppError::Io(std::io::Error::other(format!(
208                "Failed to serialize search output: {e}"
209            )))
210        })
211    })
212    .await
213    .map_err(|e| AppError::Io(std::io::Error::other(format!("Task join error: {}", e))))?
214}