1- use std:: path:: { Path , PathBuf } ;
1+ use std:: io:: { BufWriter , Write } ;
2+ use std:: path:: PathBuf ;
23
34use anyhow:: Result ;
45use async_trait:: async_trait;
56use ratatui:: { prelude:: * , widgets:: * } ;
6-
7- use tokio:: { sync:: mpsc:: UnboundedSender , task:: JoinHandle } ;
7+ use tokio:: { sync:: mpsc, task:: JoinHandle } ;
88use tokio_util:: sync:: CancellationToken ;
99
1010use 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
4141impl 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