@@ -264,6 +264,86 @@ impl UserOutput {
264264 pub fn data ( & mut self , data : & str ) {
265265 writeln ! ( self . stdout_writer, "{data}" ) . ok ( ) ;
266266 }
267+
268+ /// Display a blank line to stderr (Normal level and above)
269+ ///
270+ /// Used for spacing between sections of output to improve readability.
271+ ///
272+ /// # Examples
273+ ///
274+ /// ```rust
275+ /// use torrust_tracker_deployer_lib::presentation::user_output::{UserOutput, VerbosityLevel};
276+ ///
277+ /// let mut output = UserOutput::new(VerbosityLevel::Normal);
278+ /// output.success("Configuration template generated");
279+ /// output.blank_line();
280+ /// output.progress("Starting next steps...");
281+ /// ```
282+ pub fn blank_line ( & mut self ) {
283+ if self . verbosity >= VerbosityLevel :: Normal {
284+ writeln ! ( self . stderr_writer) . ok ( ) ;
285+ }
286+ }
287+
288+ /// Display a numbered list of steps to stderr (Normal level and above)
289+ ///
290+ /// Useful for displaying sequential instructions or action items.
291+ ///
292+ /// # Examples
293+ ///
294+ /// ```rust
295+ /// use torrust_tracker_deployer_lib::presentation::user_output::{UserOutput, VerbosityLevel};
296+ ///
297+ /// let mut output = UserOutput::new(VerbosityLevel::Normal);
298+ /// output.steps("Next steps:", &[
299+ /// "Edit the configuration file",
300+ /// "Review the settings",
301+ /// "Run the deploy command",
302+ /// ]);
303+ /// // Output to stderr:
304+ /// // Next steps:
305+ /// // 1. Edit the configuration file
306+ /// // 2. Review the settings
307+ /// // 3. Run the deploy command
308+ /// ```
309+ pub fn steps ( & mut self , title : & str , steps : & [ & str ] ) {
310+ if self . verbosity >= VerbosityLevel :: Normal {
311+ writeln ! ( self . stderr_writer, "{title}" ) . ok ( ) ;
312+ for ( idx, step) in steps. iter ( ) . enumerate ( ) {
313+ writeln ! ( self . stderr_writer, "{}. {}" , idx + 1 , step) . ok ( ) ;
314+ }
315+ }
316+ }
317+
318+ /// Display a multi-line information block to stderr (Normal level and above)
319+ ///
320+ /// Useful for displaying grouped information or detailed messages.
321+ ///
322+ /// # Examples
323+ ///
324+ /// ```rust
325+ /// use torrust_tracker_deployer_lib::presentation::user_output::{UserOutput, VerbosityLevel};
326+ ///
327+ /// let mut output = UserOutput::new(VerbosityLevel::Normal);
328+ /// output.info_block("Configuration options:", &[
329+ /// " - username: 'torrust' (default)",
330+ /// " - port: 22 (default SSH port)",
331+ /// " - key_path: path/to/key",
332+ /// ]);
333+ /// // Output to stderr:
334+ /// // Configuration options:
335+ /// // - username: 'torrust' (default)
336+ /// // - port: 22 (default SSH port)
337+ /// // - key_path: path/to/key
338+ /// ```
339+ pub fn info_block ( & mut self , title : & str , lines : & [ & str ] ) {
340+ if self . verbosity >= VerbosityLevel :: Normal {
341+ writeln ! ( self . stderr_writer, "{title}" ) . ok ( ) ;
342+ for line in lines {
343+ writeln ! ( self . stderr_writer, "{line}" ) . ok ( ) ;
344+ }
345+ }
346+ }
267347}
268348
269349#[ cfg( test) ]
@@ -472,4 +552,101 @@ mod tests {
472552 assert ! ( normal >= VerbosityLevel :: Normal ) ;
473553 assert ! ( normal < VerbosityLevel :: Verbose ) ;
474554 }
555+
556+ #[ test]
557+ fn it_should_write_blank_line_to_stderr ( ) {
558+ let ( mut output, stdout_buf, stderr_buf) = create_test_user_output ( VerbosityLevel :: Normal ) ;
559+
560+ output. blank_line ( ) ;
561+
562+ // Verify blank line went to stderr
563+ let stderr_content = String :: from_utf8 ( stderr_buf. lock ( ) . unwrap ( ) . clone ( ) ) . unwrap ( ) ;
564+ assert_eq ! ( stderr_content, "\n " ) ;
565+
566+ // Verify stdout is empty
567+ let stdout_content = String :: from_utf8 ( stdout_buf. lock ( ) . unwrap ( ) . clone ( ) ) . unwrap ( ) ;
568+ assert_eq ! ( stdout_content, "" ) ;
569+ }
570+
571+ #[ test]
572+ fn it_should_not_write_blank_line_at_quiet_level ( ) {
573+ let ( mut output, _stdout_buf, stderr_buf) = create_test_user_output ( VerbosityLevel :: Quiet ) ;
574+
575+ output. blank_line ( ) ;
576+
577+ // Verify no output at Quiet level
578+ let stderr_content = String :: from_utf8 ( stderr_buf. lock ( ) . unwrap ( ) . clone ( ) ) . unwrap ( ) ;
579+ assert_eq ! ( stderr_content, "" ) ;
580+ }
581+
582+ #[ test]
583+ fn it_should_write_steps_to_stderr ( ) {
584+ let ( mut output, stdout_buf, stderr_buf) = create_test_user_output ( VerbosityLevel :: Normal ) ;
585+
586+ output. steps (
587+ "Next steps:" ,
588+ & [
589+ "Edit the configuration file" ,
590+ "Review the settings" ,
591+ "Run the deploy command" ,
592+ ] ,
593+ ) ;
594+
595+ // Verify steps went to stderr with correct formatting
596+ let stderr_content = String :: from_utf8 ( stderr_buf. lock ( ) . unwrap ( ) . clone ( ) ) . unwrap ( ) ;
597+ assert_eq ! (
598+ stderr_content,
599+ "Next steps:\n 1. Edit the configuration file\n 2. Review the settings\n 3. Run the deploy command\n "
600+ ) ;
601+
602+ // Verify stdout is empty
603+ let stdout_content = String :: from_utf8 ( stdout_buf. lock ( ) . unwrap ( ) . clone ( ) ) . unwrap ( ) ;
604+ assert_eq ! ( stdout_content, "" ) ;
605+ }
606+
607+ #[ test]
608+ fn it_should_not_write_steps_at_quiet_level ( ) {
609+ let ( mut output, _stdout_buf, stderr_buf) = create_test_user_output ( VerbosityLevel :: Quiet ) ;
610+
611+ output. steps ( "Next steps:" , & [ "Step 1" , "Step 2" ] ) ;
612+
613+ // Verify no output at Quiet level
614+ let stderr_content = String :: from_utf8 ( stderr_buf. lock ( ) . unwrap ( ) . clone ( ) ) . unwrap ( ) ;
615+ assert_eq ! ( stderr_content, "" ) ;
616+ }
617+
618+ #[ test]
619+ fn it_should_write_info_block_to_stderr ( ) {
620+ let ( mut output, stdout_buf, stderr_buf) = create_test_user_output ( VerbosityLevel :: Normal ) ;
621+
622+ output. info_block (
623+ "Configuration options:" ,
624+ & [
625+ " - username: 'torrust' (default)" ,
626+ " - port: 22 (default SSH port)" ,
627+ ] ,
628+ ) ;
629+
630+ // Verify info block went to stderr
631+ let stderr_content = String :: from_utf8 ( stderr_buf. lock ( ) . unwrap ( ) . clone ( ) ) . unwrap ( ) ;
632+ assert_eq ! (
633+ stderr_content,
634+ "Configuration options:\n - username: 'torrust' (default)\n - port: 22 (default SSH port)\n "
635+ ) ;
636+
637+ // Verify stdout is empty
638+ let stdout_content = String :: from_utf8 ( stdout_buf. lock ( ) . unwrap ( ) . clone ( ) ) . unwrap ( ) ;
639+ assert_eq ! ( stdout_content, "" ) ;
640+ }
641+
642+ #[ test]
643+ fn it_should_not_write_info_block_at_quiet_level ( ) {
644+ let ( mut output, _stdout_buf, stderr_buf) = create_test_user_output ( VerbosityLevel :: Quiet ) ;
645+
646+ output. info_block ( "Info:" , & [ "Line 1" , "Line 2" ] ) ;
647+
648+ // Verify no output at Quiet level
649+ let stderr_content = String :: from_utf8 ( stderr_buf. lock ( ) . unwrap ( ) . clone ( ) ) . unwrap ( ) ;
650+ assert_eq ! ( stderr_content, "" ) ;
651+ }
475652}
0 commit comments