11//! Output helpers for CLI search commands.
2+ //!
3+ //! Exception: This file exceeds 800 lines due to comprehensive test suite
4+ //! covering DataFrame/native output parity and footer formatting variations.
25
36extern crate alloc;
47
58use alloc:: borrow:: Cow ;
69use core:: fmt:: Write as _;
10+ use core:: time:: Duration ;
711use std:: fs:: File ;
812use std:: io:: { BufWriter , Write } ;
913#[ cfg( windows) ]
@@ -16,6 +20,16 @@ use tracing::info;
1620use uffs_core:: output:: { CPP_COLUMN_ORDER , OutputColumn , OutputConfig } ;
1721use uffs_core:: { export_json, export_table} ;
1822
23+ /// Context for C++ baseline-compatible footer formatting.
24+ pub ( super ) struct CppFooterContext < ' a > {
25+ /// Drive letters to include in the footer (e.g., `['C', 'D']`).
26+ pub ( super ) output_targets : & ' a [ char ] ,
27+ /// Elapsed time since search started.
28+ pub ( super ) elapsed : Duration ,
29+ /// Original search pattern string.
30+ pub ( super ) pattern : & ' a str ,
31+ }
32+
1933/// Streaming output writer for multi-drive search.
2034///
2135/// Supports CSV (header + rows) and NDJSON (one JSON object per line) formats.
@@ -257,7 +271,7 @@ pub(super) fn write_native_results(
257271 format : & str ,
258272 out : & str ,
259273 output_config : & OutputConfig ,
260- output_targets : & [ char ] ,
274+ footer_ctx : & CppFooterContext < ' _ > ,
261275) -> Result < ( ) > {
262276 let normalized_format = format. to_ascii_lowercase ( ) ;
263277 let is_console = matches ! (
@@ -274,7 +288,7 @@ pub(super) fn write_native_results(
274288 & normalized_format,
275289 & mut stdout,
276290 output_config,
277- output_targets ,
291+ footer_ctx ,
278292 ) ?;
279293 stdout. flush ( ) ?;
280294 } else {
@@ -287,7 +301,7 @@ pub(super) fn write_native_results(
287301 & normalized_format,
288302 & mut writer,
289303 output_config,
290- output_targets ,
304+ footer_ctx ,
291305 ) ?;
292306 writer. flush ( ) ?;
293307
@@ -304,7 +318,7 @@ fn write_native_results_to<W: Write>(
304318 format : & str ,
305319 writer : & mut W ,
306320 output_config : & OutputConfig ,
307- output_targets : & [ char ] ,
321+ footer_ctx : & CppFooterContext < ' _ > ,
308322) -> Result < ( ) > {
309323 let output_cols = selected_output_columns ( output_config) ;
310324 let fixed_tz = chrono:: FixedOffset :: east_opt ( output_config. timezone_offset_secs ) ;
@@ -338,7 +352,7 @@ fn write_native_results_to<W: Write>(
338352 }
339353
340354 if format == "custom" {
341- write_cpp_drive_footer ( writer, output_targets ) ?;
355+ write_cpp_drive_footer ( writer, footer_ctx ) ?;
342356 }
343357
344358 Ok ( ( ) )
@@ -882,12 +896,20 @@ pub(super) fn write_results(
882896 out : & str ,
883897 output_config : & OutputConfig ,
884898 output_targets : & [ char ] ,
899+ elapsed : Duration ,
900+ pattern : & str ,
885901) -> Result < ( ) > {
886902 let is_console = matches ! (
887903 out. to_lowercase( ) . as_str( ) ,
888904 "console" | "con" | "term" | "terminal"
889905 ) ;
890906
907+ let footer_ctx = CppFooterContext {
908+ output_targets,
909+ elapsed,
910+ pattern,
911+ } ;
912+
891913 if is_console {
892914 let stdout_handle = std:: io:: stdout ( ) ;
893915 let mut stdout = stdout_handle. lock ( ) ;
@@ -896,7 +918,7 @@ pub(super) fn write_results(
896918 "csv" => output_config. write ( results, & mut stdout) ?,
897919 "custom" => {
898920 output_config. write ( results, & mut stdout) ?;
899- write_cpp_drive_footer ( & mut stdout, output_targets ) ?;
921+ write_cpp_drive_footer ( & mut stdout, & footer_ctx ) ?;
900922 }
901923 _ => export_table ( results, & mut stdout) ?,
902924 }
@@ -910,7 +932,7 @@ pub(super) fn write_results(
910932 "json" => export_json ( results, & mut writer) ?,
911933 "custom" => {
912934 output_config. write ( results, & mut writer) ?;
913- write_cpp_drive_footer ( & mut writer, output_targets ) ?;
935+ write_cpp_drive_footer ( & mut writer, & footer_ctx ) ?;
914936 }
915937 _ => output_config. write ( results, & mut writer) ?,
916938 }
@@ -923,20 +945,33 @@ pub(super) fn write_results(
923945}
924946
925947/// Append the legacy C++ drive footer for baseline-compatible custom output.
926- fn write_cpp_drive_footer < W : Write > ( writer : & mut W , output_targets : & [ char ] ) -> Result < ( ) > {
927- if output_targets. is_empty ( ) {
948+ ///
949+ /// Uses CRLF line endings (`\r\n`) to match C++ baseline behavior.
950+ /// When `elapsed` is <= 1 second, appends the fast-scan message.
951+ fn write_cpp_drive_footer < W : Write > ( writer : & mut W , ctx : & CppFooterContext < ' _ > ) -> Result < ( ) > {
952+ if ctx. output_targets . is_empty ( ) {
928953 return Ok ( ( ) ) ;
929954 }
930955
931- writeln ! ( writer) ?;
932- writeln ! ( writer) ?;
933- writeln ! (
956+ write ! ( writer, " \r \n " ) ?;
957+ write ! ( writer, " \r \n " ) ?;
958+ write ! (
934959 writer,
935- "Drives? \t {}\t {}" ,
936- output_targets. len( ) ,
937- format_cpp_drive_letters( output_targets)
960+ "Drives? \t {}\t {}\r \n " ,
961+ ctx . output_targets. len( ) ,
962+ format_cpp_drive_letters( ctx . output_targets)
938963 ) ?;
939- writeln ! ( writer) ?;
964+ write ! ( writer, "\r \n " ) ?;
965+
966+ if ctx. elapsed . as_secs ( ) <= 1 {
967+ write ! (
968+ writer,
969+ "MMMmmm that was FAST ... maybe your searchstring was wrong?\t {pattern}\r \n " ,
970+ pattern = ctx. pattern
971+ ) ?;
972+ write ! ( writer, "Search path. E.g. 'C:/' or 'C:\\ Prog**' \r \n " ) ?;
973+ }
974+
940975 Ok ( ( ) )
941976}
942977
@@ -1065,13 +1100,20 @@ mod tests {
10651100 format : & str ,
10661101 output_config : & OutputConfig ,
10671102 output_targets : & [ char ] ,
1103+ elapsed : Duration ,
1104+ pattern : & str ,
10681105 ) -> Result < String > {
10691106 let mut output = Vec :: new ( ) ;
1107+ let footer_ctx = CppFooterContext {
1108+ output_targets,
1109+ elapsed,
1110+ pattern,
1111+ } ;
10701112 match format {
10711113 "json" => export_json ( results, & mut output) ?,
10721114 "custom" => {
10731115 output_config. write ( results, & mut output) ?;
1074- write_cpp_drive_footer ( & mut output, output_targets ) ?;
1116+ write_cpp_drive_footer ( & mut output, & footer_ctx ) ?;
10751117 }
10761118 _ => output_config. write ( results, & mut output) ?,
10771119 }
@@ -1084,15 +1126,22 @@ mod tests {
10841126 format : & str ,
10851127 output_config : & OutputConfig ,
10861128 output_targets : & [ char ] ,
1129+ elapsed : Duration ,
1130+ pattern : & str ,
10871131 ) -> Result < String > {
10881132 let mut output = Vec :: new ( ) ;
1133+ let footer_ctx = CppFooterContext {
1134+ output_targets,
1135+ elapsed,
1136+ pattern,
1137+ } ;
10891138 write_native_results_to (
10901139 index,
10911140 results,
10921141 format,
10931142 & mut output,
10941143 output_config,
1095- output_targets ,
1144+ & footer_ctx ,
10961145 ) ?;
10971146 String :: from_utf8 ( output) . map_err ( Into :: into)
10981147 }
@@ -1123,6 +1172,8 @@ mod tests {
11231172 & path. to_string_lossy ( ) ,
11241173 & output_config,
11251174 & [ 'C' , 'D' ] ,
1175+ Duration :: from_secs ( 2 ) ,
1176+ "*.txt" ,
11261177 ) ?;
11271178
11281179 let written = fs:: read_to_string ( & path) ?;
@@ -1146,6 +1197,8 @@ mod tests {
11461197 & path. to_string_lossy ( ) ,
11471198 & output_config,
11481199 & [ 'C' , 'D' ] ,
1200+ Duration :: from_secs ( 2 ) ,
1201+ "*.txt" ,
11491202 ) ?;
11501203
11511204 let written = fs:: read_to_string ( & path) ?;
@@ -1155,10 +1208,10 @@ mod tests {
11551208 written,
11561209 concat!(
11571210 "\" C:\\ Temp\\ file.txt\" ,\" file.txt\" \n " ,
1158- "\n " ,
1159- "\n " ,
1160- "Drives? \t 2\t C:|D:\n " ,
1161- "\n "
1211+ "\r \ n " ,
1212+ "\r \ n " ,
1213+ "Drives? \t 2\t C:|D:\r \ n " ,
1214+ "\r \ n "
11621215 )
11631216 ) ;
11641217 Ok ( ( ) )
@@ -1176,6 +1229,8 @@ mod tests {
11761229 & path. to_string_lossy ( ) ,
11771230 & output_config,
11781231 & [ 'C' , 'D' ] ,
1232+ Duration :: from_secs ( 2 ) ,
1233+ "*.txt" ,
11791234 ) ?;
11801235
11811236 let written = fs:: read_to_string ( & path) ?;
@@ -1239,15 +1294,26 @@ mod tests {
12391294 let output_config = OutputConfig :: new ( ) . with_columns (
12401295 "path,name,pathonly,size,sizeondisk,created,readonly,archive,hidden,directory,descendants,treesize,treeallocated,type" ,
12411296 ) ;
1297+ let elapsed = Duration :: from_secs ( 2 ) ;
1298+ let pattern = "*.txt" ;
12421299
12431300 let expected = write_results_to_string (
12441301 & results_to_dataframe ( & index, results. clone ( ) , true ) ?,
12451302 "custom" ,
12461303 & output_config,
12471304 & [ 'C' ] ,
1305+ elapsed,
1306+ pattern,
1307+ ) ?;
1308+ let actual = write_native_results_to_string (
1309+ & index,
1310+ & results,
1311+ "custom" ,
1312+ & output_config,
1313+ & [ 'C' ] ,
1314+ elapsed,
1315+ pattern,
12481316 ) ?;
1249- let actual =
1250- write_native_results_to_string ( & index, & results, "custom" , & output_config, & [ 'C' ] ) ?;
12511317
12521318 assert_eq ! ( actual, expected) ;
12531319 Ok ( ( ) )
@@ -1262,17 +1328,98 @@ mod tests {
12621328 . with_separator ( ";" )
12631329 . with_quote ( "'" )
12641330 . with_header ( false ) ;
1331+ let elapsed = Duration :: from_secs ( 2 ) ;
1332+ let pattern = "*.txt" ;
12651333
12661334 let expected = write_results_to_string (
12671335 & results_to_dataframe ( & index, results. clone ( ) , true ) ?,
12681336 "csv" ,
12691337 & output_config,
12701338 & [ 'C' ] ,
1339+ elapsed,
1340+ pattern,
1341+ ) ?;
1342+ let actual = write_native_results_to_string (
1343+ & index,
1344+ & results,
1345+ "csv" ,
1346+ & output_config,
1347+ & [ 'C' ] ,
1348+ elapsed,
1349+ pattern,
12711350 ) ?;
1272- let actual =
1273- write_native_results_to_string ( & index, & results, "csv" , & output_config, & [ 'C' ] ) ?;
12741351
12751352 assert_eq ! ( actual, expected) ;
12761353 Ok ( ( ) )
12771354 }
1355+
1356+ #[ test]
1357+ fn test_cpp_footer_includes_fast_scan_message_when_elapsed_le_1s ( ) -> TestResult {
1358+ let path = temp_output_path ( "txt" ) ;
1359+ let results = sample_df ( ) ?;
1360+ let output_config = OutputConfig :: new ( )
1361+ . with_columns ( "path,name" )
1362+ . with_header ( false ) ;
1363+
1364+ write_results (
1365+ & results,
1366+ "custom" ,
1367+ & path. to_string_lossy ( ) ,
1368+ & output_config,
1369+ & [ 'G' ] ,
1370+ Duration :: from_millis ( 999 ) ,
1371+ ">G:.*" ,
1372+ ) ?;
1373+
1374+ let written = fs:: read_to_string ( & path) ?;
1375+ drop ( fs:: remove_file ( & path) ) ;
1376+
1377+ assert_eq ! (
1378+ written,
1379+ concat!(
1380+ "\" C:\\ Temp\\ file.txt\" ,\" file.txt\" \n " ,
1381+ "\r \n " ,
1382+ "\r \n " ,
1383+ "Drives? \t 1\t G:\r \n " ,
1384+ "\r \n " ,
1385+ "MMMmmm that was FAST ... maybe your searchstring was wrong?\t >G:.*\r \n " ,
1386+ "Search path. E.g. 'C:/' or 'C:\\ Prog**' \r \n "
1387+ )
1388+ ) ;
1389+ Ok ( ( ) )
1390+ }
1391+
1392+ #[ test]
1393+ fn test_cpp_footer_omits_fast_scan_message_when_elapsed_gt_1s ( ) -> TestResult {
1394+ let path = temp_output_path ( "txt" ) ;
1395+ let results = sample_df ( ) ?;
1396+ let output_config = OutputConfig :: new ( )
1397+ . with_columns ( "path,name" )
1398+ . with_header ( false ) ;
1399+
1400+ write_results (
1401+ & results,
1402+ "custom" ,
1403+ & path. to_string_lossy ( ) ,
1404+ & output_config,
1405+ & [ 'G' ] ,
1406+ Duration :: from_secs ( 2 ) ,
1407+ ">G:.*" ,
1408+ ) ?;
1409+
1410+ let written = fs:: read_to_string ( & path) ?;
1411+ drop ( fs:: remove_file ( & path) ) ;
1412+
1413+ assert_eq ! (
1414+ written,
1415+ concat!(
1416+ "\" C:\\ Temp\\ file.txt\" ,\" file.txt\" \n " ,
1417+ "\r \n " ,
1418+ "\r \n " ,
1419+ "Drives? \t 1\t G:\r \n " ,
1420+ "\r \n "
1421+ )
1422+ ) ;
1423+ Ok ( ( ) )
1424+ }
12781425}
0 commit comments