1
- use std:: path:: { Path , PathBuf } ;
1
+ use std:: io:: { BufWriter , Write } ;
2
+ use std:: path:: PathBuf ;
2
3
3
4
use anyhow:: Result ;
4
5
use async_trait:: async_trait;
5
6
use ratatui:: { prelude:: * , widgets:: * } ;
6
-
7
- use tokio:: { sync:: mpsc:: UnboundedSender , task:: JoinHandle } ;
7
+ use tokio:: { sync:: mpsc, task:: JoinHandle } ;
8
8
use tokio_util:: sync:: CancellationToken ;
9
9
10
10
use crate :: {
11
11
app:: { actions:: Action , config:: AppConfig , key_bindings, AppContext , AppState } ,
12
12
component:: Component ,
13
- file_handling:: { DiskEntry , SearchResult } ,
13
+ file_handling:: SearchResult ,
14
14
models:: Scrollable ,
15
15
tui:: Event ,
16
16
ui:: {
@@ -29,7 +29,7 @@ impl Default for ExportTask {
29
29
fn default ( ) -> Self {
30
30
let cancellation_token = CancellationToken :: new ( ) ;
31
31
let task = tokio:: spawn ( async {
32
- std:: future:: pending :: < ( ) > ( ) . await ;
32
+ std:: future:: ready ( ( ) ) . await ;
33
33
} ) ;
34
34
Self {
35
35
task,
@@ -39,70 +39,109 @@ impl Default for ExportTask {
39
39
}
40
40
41
41
impl ExportTask {
42
- fn run < P : AsRef < Path > > (
42
+ pub fn export_as_json (
43
43
& 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 ,
48
48
) {
49
49
self . cancel ( ) ;
50
50
self . cancellation_token = CancellationToken :: new ( ) ;
51
51
let cancellation_token = self . cancellation_token . clone ( ) ;
52
52
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 ! (
55
54
"search_results_{}.json" ,
56
55
chrono:: Local :: now( ) . format( "%Y-%m-%dT%H_%M_%S" )
57
56
) ) ;
58
57
59
58
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 ;
67
66
}
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
+ } ;
73
68
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
79
75
) ;
80
76
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 " ) {
86
98
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
90
101
) ;
91
102
let _ = action_sender. send ( Action :: ExportFailure (
92
- "Failed to export search results " . into ( ) ,
103
+ "Failed to write to export file " . into ( ) ,
93
104
) ) ;
94
- } else {
95
- let _ = action_sender. send ( Action :: ExportDone ) ;
105
+ return ;
96
106
}
97
107
}
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) ;
100
122
let _ = action_sender. send ( Action :: ExportFailure (
101
- "Failed to export search results " . into ( ) ,
123
+ "Failed to write to export file " . into ( ) ,
102
124
) ) ;
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 ;
104
140
}
105
- } )
141
+ let _ = writer. flush ( ) ;
142
+
143
+ let _ = action_sender. send ( Action :: ExportDone ) ;
144
+ } ) ;
106
145
}
107
146
108
147
pub fn cancel ( & self ) {
@@ -414,16 +453,37 @@ impl Component for ResultWidget {
414
453
if key. modifiers == crossterm:: event:: KeyModifiers :: NONE =>
415
454
{
416
455
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) ;
423
484
}
424
485
crossterm:: event:: KeyCode :: Esc => {
425
486
self . app_context = AppContext :: NotActive ;
426
- // self.search_result.reset();
427
487
self . search_result = SearchResult :: default ( ) ;
428
488
self . table_state
429
489
. select ( self . search_result . selected ( ) . into ( ) ) ;
@@ -636,7 +696,7 @@ impl Component for ResultWidget {
636
696
left : 0 ,
637
697
right : 0 ,
638
698
top : 1 ,
639
- bottom : 0 ,
699
+ bottom : 1 ,
640
700
} ) )
641
701
. highlight_symbol (
642
702
Text :: from ( vec ! [ "\n " . into( ) , HIGHLIGHT_SYMBOL . into( ) ] )
0 commit comments