Skip to content

Commit 63b4be3

Browse files
committed
improve memory usage
1 parent a0c5ca4 commit 63b4be3

File tree

1 file changed

+113
-53
lines changed

1 file changed

+113
-53
lines changed

src/ui/result_widget.rs

Lines changed: 113 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
use std::path::{Path, PathBuf};
1+
use std::io::{BufWriter, Write};
2+
use std::path::PathBuf;
23

34
use anyhow::Result;
45
use async_trait::async_trait;
56
use ratatui::{prelude::*, widgets::*};
6-
7-
use tokio::{sync::mpsc::UnboundedSender, task::JoinHandle};
7+
use tokio::{sync::mpsc, task::JoinHandle};
88
use tokio_util::sync::CancellationToken;
99

1010
use crate::{
1111
app::{actions::Action, config::AppConfig, key_bindings, AppContext, AppState},
1212
component::Component,
13-
file_handling::{DiskEntry, SearchResult},
13+
file_handling::SearchResult,
1414
models::Scrollable,
1515
tui::Event,
1616
ui::{
@@ -29,7 +29,7 @@ impl Default for ExportTask {
2929
fn default() -> Self {
3030
let cancellation_token = CancellationToken::new();
3131
let task = tokio::spawn(async {
32-
std::future::pending::<()>().await;
32+
std::future::ready(()).await;
3333
});
3434
Self {
3535
task,
@@ -39,70 +39,109 @@ impl Default for ExportTask {
3939
}
4040

4141
impl ExportTask {
42-
fn run<P: AsRef<Path>>(
42+
pub fn export_as_json(
4343
&mut self,
44-
search_query: &str,
45-
search_results: Vec<DiskEntry>,
46-
action_sender: UnboundedSender<Action>,
47-
export_dir: P,
44+
search_query: String,
45+
mut json_rx: mpsc::Receiver<serde_json::Value>,
46+
action_sender: mpsc::UnboundedSender<Action>,
47+
export_dir: PathBuf,
4848
) {
4949
self.cancel();
5050
self.cancellation_token = CancellationToken::new();
5151
let cancellation_token = self.cancellation_token.clone();
5252

53-
let search_query = search_query.to_owned();
54-
let export_path = export_dir.as_ref().join(format!(
53+
let export_path = export_dir.join(format!(
5554
"search_results_{}.json",
5655
chrono::Local::now().format("%Y-%m-%dT%H_%M_%S")
5756
));
5857

5958
self.task = tokio::task::spawn(async move {
60-
let mut success_count = 0_usize;
61-
let total_items = search_results.len();
62-
let mut json_values: Vec<serde_json::Value> = Vec::with_capacity(total_items);
63-
for entry in search_results {
64-
if cancellation_token.is_cancelled() {
65-
let _ = action_sender.send(Action::ForcedShutdown);
66-
break;
59+
let file = match std::fs::File::create(export_path) {
60+
Ok(file) => file,
61+
Err(err) => {
62+
log::error!("Failed to create export file - Details {:?}", err);
63+
let _ = action_sender
64+
.send(Action::ExportFailure("Failed to create export file".into()));
65+
return;
6766
}
68-
json_values.push(entry.build_as_json());
69-
success_count += 1;
70-
let update_msg = format!("Exporting results... {}/{}", success_count, total_items);
71-
let _ = action_sender.send(Action::UpdateAppState(AppState::Working(update_msg)));
72-
}
67+
};
7368

74-
let final_json_export = serde_json::json!(
75-
{
76-
"query": search_query,
77-
"results": json_values
78-
}
69+
let mut writer = BufWriter::new(file);
70+
71+
// Write the opening JSON structure
72+
let open_json = format!(
73+
"{{\n \"search_query\": \"{}\",\n \"results\": [\n",
74+
search_query
7975
);
8076

81-
// Convert JSON data to a pretty string
82-
match serde_json::to_string_pretty(&final_json_export) {
83-
Ok(pretty_json) => {
84-
// Write JSON to file asynchronously
85-
if let Err(io_err) = tokio::fs::write(&export_path, pretty_json).await {
77+
if let Err(err) = writer.write_all(open_json.as_bytes()) {
78+
log::error!(
79+
"Failed to write open JSON string to export file - Details {:?}",
80+
err
81+
);
82+
let _ = action_sender.send(Action::ExportFailure(
83+
"Failed to write to export file".into(),
84+
));
85+
return;
86+
};
87+
88+
let mut first = true;
89+
90+
// Write each JSON entry
91+
while let Some(entry) = json_rx.recv().await {
92+
if cancellation_token.is_cancelled() {
93+
let _ = action_sender.send(Action::ForcedShutdown);
94+
break;
95+
}
96+
if !first {
97+
if let Err(err) = writer.write_all(b",\n") {
8698
log::error!(
87-
"Failed to write search results into file '{}' - Details {:#?}",
88-
utils::absolute_path_as_string(&export_path),
89-
io_err
99+
"Failed to write indentation to export file - Details {:?}",
100+
err
90101
);
91102
let _ = action_sender.send(Action::ExportFailure(
92-
"Failed to export search results".into(),
103+
"Failed to write to export file".into(),
93104
));
94-
} else {
95-
let _ = action_sender.send(Action::ExportDone);
105+
return;
96106
}
97107
}
98-
Err(err) => {
99-
log::error!("Failed to serialize Search-Results as JSON: {}", err);
108+
first = false;
109+
110+
// Indent each entry with 4 spaces
111+
if let Err(err) = writer.write_all(b" ") {
112+
log::error!("Failed to write indentation - Details {:?}", err);
113+
let _ = action_sender.send(Action::ExportFailure(format!(
114+
"Failed to write indentation: {}",
115+
err
116+
)));
117+
return;
118+
}
119+
120+
if let Err(err) = serde_json::to_writer(&mut writer, &entry) {
121+
log::error!("Failed to search result to export file - Details {:?}", err);
100122
let _ = action_sender.send(Action::ExportFailure(
101-
"Failed to export search results".into(),
123+
"Failed to write to export file".into(),
102124
));
103-
}
125+
return;
126+
};
127+
}
128+
129+
// Write the closing JSON structure
130+
let close_json = "\n ]\n}".to_string();
131+
if let Err(err) = writer.write_all(close_json.as_bytes()) {
132+
log::error!(
133+
"Failed to write closing JSON string to export file - Details {:?}",
134+
err
135+
);
136+
let _ = action_sender.send(Action::ExportFailure(
137+
"Failed to write to export file".into(),
138+
));
139+
return;
104140
}
105-
})
141+
let _ = writer.flush();
142+
143+
let _ = action_sender.send(Action::ExportDone);
144+
});
106145
}
107146

108147
pub fn cancel(&self) {
@@ -414,16 +453,37 @@ impl Component for ResultWidget {
414453
if key.modifiers == crossterm::event::KeyModifiers::NONE =>
415454
{
416455
self.is_working = true;
417-
self.export_task.run(
418-
self.search_result.search_query(),
419-
self.search_result.items().clone(),
420-
self.action_sender.clone().unwrap(),
421-
&self.export_dir,
422-
);
456+
457+
self.send_app_action(Action::UpdateAppState(AppState::Working(
458+
"Exporting results...".into(),
459+
)))?;
460+
461+
let (tx, rx) = mpsc::channel(100);
462+
let search_query = self.search_result.search_query().to_string();
463+
let export_dir = self.export_dir.clone();
464+
let action_sender = self.action_sender.clone().unwrap();
465+
let items = self.search_result.items().to_vec();
466+
467+
self.export_task
468+
.export_as_json(search_query, rx, action_sender, export_dir);
469+
470+
let tx_clone = tx.clone();
471+
tokio::spawn(async move {
472+
for entry in items {
473+
let json_value = entry.build_as_json();
474+
// Send to writer
475+
if tx_clone.send(json_value).await.is_err() {
476+
println!("Writer task dropped, stopping producer");
477+
break;
478+
}
479+
}
480+
});
481+
482+
// Close the channel to indicate that no more values will be sent
483+
drop(tx);
423484
}
424485
crossterm::event::KeyCode::Esc => {
425486
self.app_context = AppContext::NotActive;
426-
// self.search_result.reset();
427487
self.search_result = SearchResult::default();
428488
self.table_state
429489
.select(self.search_result.selected().into());
@@ -636,7 +696,7 @@ impl Component for ResultWidget {
636696
left: 0,
637697
right: 0,
638698
top: 1,
639-
bottom: 0,
699+
bottom: 1,
640700
}))
641701
.highlight_symbol(
642702
Text::from(vec!["\n".into(), HIGHLIGHT_SYMBOL.into()])

0 commit comments

Comments
 (0)