ricecoder_tui/
command_blocks.rs

1//! Command blocks widget for grouped command display
2
3use std::time::{SystemTime, UNIX_EPOCH};
4
5/// Command execution status
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum CommandStatus {
8    /// Command is pending execution
9    Pending,
10    /// Command is currently executing
11    Running,
12    /// Command completed successfully
13    Success,
14    /// Command failed with error
15    Failed,
16    /// Command was cancelled
17    Cancelled,
18}
19
20impl CommandStatus {
21    /// Get display text for the status
22    pub fn display_text(&self) -> &'static str {
23        match self {
24            CommandStatus::Pending => "⏳ Pending",
25            CommandStatus::Running => "⚙️  Running",
26            CommandStatus::Success => "✓ Success",
27            CommandStatus::Failed => "✗ Failed",
28            CommandStatus::Cancelled => "⊘ Cancelled",
29        }
30    }
31
32    /// Get short display text
33    pub fn short_text(&self) -> &'static str {
34        match self {
35            CommandStatus::Pending => "pending",
36            CommandStatus::Running => "running",
37            CommandStatus::Success => "success",
38            CommandStatus::Failed => "failed",
39            CommandStatus::Cancelled => "cancelled",
40        }
41    }
42}
43
44/// A single command in a block
45#[derive(Debug, Clone)]
46pub struct Command {
47    /// Command text
48    pub text: String,
49    /// Command status
50    pub status: CommandStatus,
51    /// Command output
52    pub output: String,
53    /// Exit code (if completed)
54    pub exit_code: Option<i32>,
55    /// Timestamp when command was created
56    pub created_at: u64,
57    /// Timestamp when command started
58    pub started_at: Option<u64>,
59    /// Timestamp when command finished
60    pub finished_at: Option<u64>,
61}
62
63impl Command {
64    /// Create a new command
65    pub fn new(text: impl Into<String>) -> Self {
66        let now = SystemTime::now()
67            .duration_since(UNIX_EPOCH)
68            .unwrap_or_default()
69            .as_secs();
70
71        Self {
72            text: text.into(),
73            status: CommandStatus::Pending,
74            output: String::new(),
75            exit_code: None,
76            created_at: now,
77            started_at: None,
78            finished_at: None,
79        }
80    }
81
82    /// Start executing the command
83    pub fn start(&mut self) {
84        self.status = CommandStatus::Running;
85        let now = SystemTime::now()
86            .duration_since(UNIX_EPOCH)
87            .unwrap_or_default()
88            .as_secs();
89        self.started_at = Some(now);
90    }
91
92    /// Append output to the command
93    pub fn append_output(&mut self, output: &str) {
94        self.output.push_str(output);
95    }
96
97    /// Mark command as completed
98    pub fn complete(&mut self, exit_code: i32) {
99        self.status = if exit_code == 0 {
100            CommandStatus::Success
101        } else {
102            CommandStatus::Failed
103        };
104        self.exit_code = Some(exit_code);
105        let now = SystemTime::now()
106            .duration_since(UNIX_EPOCH)
107            .unwrap_or_default()
108            .as_secs();
109        self.finished_at = Some(now);
110    }
111
112    /// Mark command as cancelled
113    pub fn cancel(&mut self) {
114        self.status = CommandStatus::Cancelled;
115        let now = SystemTime::now()
116            .duration_since(UNIX_EPOCH)
117            .unwrap_or_default()
118            .as_secs();
119        self.finished_at = Some(now);
120    }
121
122    /// Get the duration of command execution in seconds
123    pub fn duration_secs(&self) -> Option<u64> {
124        match (self.started_at, self.finished_at) {
125            (Some(start), Some(end)) => Some(end - start),
126            (Some(start), None) => {
127                let now = SystemTime::now()
128                    .duration_since(UNIX_EPOCH)
129                    .unwrap_or_default()
130                    .as_secs();
131                Some(now - start)
132            }
133            _ => None,
134        }
135    }
136
137    /// Check if command is complete
138    pub fn is_complete(&self) -> bool {
139        matches!(
140            self.status,
141            CommandStatus::Success | CommandStatus::Failed | CommandStatus::Cancelled
142        )
143    }
144}
145
146/// A block of related commands
147#[derive(Debug, Clone)]
148pub struct CommandBlock {
149    /// Block ID
150    pub id: String,
151    /// Block title/description
152    pub title: String,
153    /// Commands in the block
154    pub commands: Vec<Command>,
155    /// Whether block is collapsed
156    pub collapsed: bool,
157    /// Selected command index
158    pub selected_command: Option<usize>,
159    /// Block creation timestamp
160    pub created_at: u64,
161}
162
163impl CommandBlock {
164    /// Create a new command block
165    pub fn new(id: impl Into<String>, title: impl Into<String>) -> Self {
166        let now = SystemTime::now()
167            .duration_since(UNIX_EPOCH)
168            .unwrap_or_default()
169            .as_secs();
170
171        Self {
172            id: id.into(),
173            title: title.into(),
174            commands: Vec::new(),
175            collapsed: false,
176            selected_command: None,
177            created_at: now,
178        }
179    }
180
181    /// Add a command to the block
182    pub fn add_command(&mut self, command: Command) {
183        self.commands.push(command);
184    }
185
186    /// Get the overall status of the block
187    pub fn overall_status(&self) -> CommandStatus {
188        if self.commands.is_empty() {
189            return CommandStatus::Pending;
190        }
191
192        // If any command is running, block is running
193        if self
194            .commands
195            .iter()
196            .any(|c| c.status == CommandStatus::Running)
197        {
198            return CommandStatus::Running;
199        }
200
201        // If any command failed, block failed
202        if self
203            .commands
204            .iter()
205            .any(|c| c.status == CommandStatus::Failed)
206        {
207            return CommandStatus::Failed;
208        }
209
210        // If any command is pending, block is pending
211        if self
212            .commands
213            .iter()
214            .any(|c| c.status == CommandStatus::Pending)
215        {
216            return CommandStatus::Pending;
217        }
218
219        // If any command is cancelled, block is cancelled
220        if self
221            .commands
222            .iter()
223            .any(|c| c.status == CommandStatus::Cancelled)
224        {
225            return CommandStatus::Cancelled;
226        }
227
228        // All commands succeeded
229        CommandStatus::Success
230    }
231
232    /// Toggle collapsed state
233    pub fn toggle_collapsed(&mut self) {
234        self.collapsed = !self.collapsed;
235    }
236
237    /// Get visible commands
238    pub fn visible_commands(&self) -> Vec<&Command> {
239        if self.collapsed {
240            Vec::new()
241        } else {
242            self.commands.iter().collect()
243        }
244    }
245
246    /// Select next command
247    pub fn select_next_command(&mut self) {
248        match self.selected_command {
249            None => {
250                if !self.commands.is_empty() {
251                    self.selected_command = Some(0);
252                }
253            }
254            Some(idx) if idx < self.commands.len() - 1 => {
255                self.selected_command = Some(idx + 1);
256            }
257            _ => {}
258        }
259    }
260
261    /// Select previous command
262    pub fn select_prev_command(&mut self) {
263        match self.selected_command {
264            Some(idx) if idx > 0 => {
265                self.selected_command = Some(idx - 1);
266            }
267            Some(0) => {
268                self.selected_command = None;
269            }
270            _ => {}
271        }
272    }
273
274    /// Get the selected command
275    pub fn get_selected_command(&self) -> Option<&Command> {
276        self.selected_command.and_then(|idx| self.commands.get(idx))
277    }
278
279    /// Get the selected command (mutable)
280    pub fn get_selected_command_mut(&mut self) -> Option<&mut Command> {
281        let idx = self.selected_command?;
282        self.commands.get_mut(idx)
283    }
284
285    /// Get total duration of all commands
286    pub fn total_duration_secs(&self) -> u64 {
287        self.commands.iter().filter_map(|c| c.duration_secs()).sum()
288    }
289
290    /// Get the number of successful commands
291    pub fn success_count(&self) -> usize {
292        self.commands
293            .iter()
294            .filter(|c| c.status == CommandStatus::Success)
295            .count()
296    }
297
298    /// Get the number of failed commands
299    pub fn failed_count(&self) -> usize {
300        self.commands
301            .iter()
302            .filter(|c| c.status == CommandStatus::Failed)
303            .count()
304    }
305
306    /// Get the number of running commands
307    pub fn running_count(&self) -> usize {
308        self.commands
309            .iter()
310            .filter(|c| c.status == CommandStatus::Running)
311            .count()
312    }
313}
314
315/// Copy action type
316#[derive(Debug, Clone, Copy, PartialEq, Eq)]
317pub enum CopyActionType {
318    /// Copy entire block (command + output + status)
319    Block,
320    /// Copy command text only
321    Command,
322    /// Copy output only
323    Output,
324}
325
326/// Command blocks widget
327pub struct CommandBlocksWidget {
328    /// Blocks in the widget
329    pub blocks: Vec<CommandBlock>,
330    /// Selected block index
331    pub selected_block: Option<usize>,
332    /// Scroll offset
333    pub scroll: usize,
334    /// Last copy action result
335    pub last_copy_result: Option<Result<CopyActionType, String>>,
336}
337
338impl CommandBlocksWidget {
339    /// Create a new command blocks widget
340    pub fn new() -> Self {
341        Self {
342            blocks: Vec::new(),
343            selected_block: None,
344            scroll: 0,
345            last_copy_result: None,
346        }
347    }
348
349    /// Add a block
350    pub fn add_block(&mut self, block: CommandBlock) {
351        self.blocks.push(block);
352    }
353
354    /// Select next block
355    pub fn select_next_block(&mut self) {
356        match self.selected_block {
357            None => {
358                if !self.blocks.is_empty() {
359                    self.selected_block = Some(0);
360                }
361            }
362            Some(idx) if idx < self.blocks.len() - 1 => {
363                self.selected_block = Some(idx + 1);
364            }
365            _ => {}
366        }
367    }
368
369    /// Select previous block
370    pub fn select_prev_block(&mut self) {
371        match self.selected_block {
372            Some(idx) if idx > 0 => {
373                self.selected_block = Some(idx - 1);
374            }
375            Some(0) => {
376                self.selected_block = None;
377            }
378            _ => {}
379        }
380    }
381
382    /// Get the selected block
383    pub fn get_selected_block(&self) -> Option<&CommandBlock> {
384        self.selected_block.and_then(|idx| self.blocks.get(idx))
385    }
386
387    /// Get the selected block (mutable)
388    pub fn get_selected_block_mut(&mut self) -> Option<&mut CommandBlock> {
389        let idx = self.selected_block?;
390        self.blocks.get_mut(idx)
391    }
392
393    /// Toggle selected block collapsed state
394    pub fn toggle_selected_block_collapsed(&mut self) {
395        if let Some(block) = self.get_selected_block_mut() {
396            block.toggle_collapsed();
397        }
398    }
399
400    /// Clear all blocks
401    pub fn clear(&mut self) {
402        self.blocks.clear();
403        self.selected_block = None;
404        self.scroll = 0;
405    }
406
407    /// Get total number of commands across all blocks
408    pub fn total_commands(&self) -> usize {
409        self.blocks.iter().map(|b| b.commands.len()).sum()
410    }
411
412    /// Get total number of successful commands
413    pub fn total_success(&self) -> usize {
414        self.blocks.iter().map(|b| b.success_count()).sum()
415    }
416
417    /// Get total number of failed commands
418    pub fn total_failed(&self) -> usize {
419        self.blocks.iter().map(|b| b.failed_count()).sum()
420    }
421
422    /// Get total number of running commands
423    pub fn total_running(&self) -> usize {
424        self.blocks.iter().map(|b| b.running_count()).sum()
425    }
426
427    /// Copy selected block content
428    pub fn copy_selected_block(&mut self, action_type: CopyActionType) -> Result<(), String> {
429        let block = self
430            .get_selected_block()
431            .ok_or_else(|| "No block selected".to_string())?;
432
433        let cmd = block
434            .get_selected_command()
435            .ok_or_else(|| "No command selected in block".to_string())?;
436
437        match action_type {
438            CopyActionType::Block => {
439                let content = format!(
440                    "Command: {}\nStatus: {}\nOutput:\n{}",
441                    cmd.text,
442                    cmd.status.display_text(),
443                    cmd.output
444                );
445                self.copy_to_clipboard(&content)?;
446            }
447            CopyActionType::Command => {
448                self.copy_to_clipboard(&cmd.text)?;
449            }
450            CopyActionType::Output => {
451                self.copy_to_clipboard(&cmd.output)?;
452            }
453        }
454
455        self.last_copy_result = Some(Ok(action_type));
456        Ok(())
457    }
458
459    /// Copy all blocks content
460    pub fn copy_all_blocks(&mut self) -> Result<(), String> {
461        let mut content = String::new();
462
463        for (block_idx, block) in self.blocks.iter().enumerate() {
464            if block_idx > 0 {
465                content.push_str("\n\n");
466            }
467
468            content.push_str(&format!("=== Block: {} ===\n", block.title));
469
470            for cmd in &block.commands {
471                content.push_str(&format!(
472                    "Command: {}\nStatus: {}\nOutput:\n{}\n\n",
473                    cmd.text,
474                    cmd.status.display_text(),
475                    cmd.output
476                ));
477            }
478        }
479
480        self.copy_to_clipboard(&content)?;
481        self.last_copy_result = Some(Ok(CopyActionType::Block));
482        Ok(())
483    }
484
485    /// Copy block by index
486    pub fn copy_block_by_index(
487        &mut self,
488        block_idx: usize,
489        action_type: CopyActionType,
490    ) -> Result<(), String> {
491        let block = self
492            .blocks
493            .get(block_idx)
494            .ok_or_else(|| format!("Block index {} out of range", block_idx))?;
495
496        match action_type {
497            CopyActionType::Block => {
498                let mut content = String::new();
499                for cmd in &block.commands {
500                    content.push_str(&format!(
501                        "Command: {}\nStatus: {}\nOutput:\n{}\n\n",
502                        cmd.text,
503                        cmd.status.display_text(),
504                        cmd.output
505                    ));
506                }
507                self.copy_to_clipboard(&content)?;
508            }
509            CopyActionType::Command => {
510                if let Some(cmd) = block.get_selected_command() {
511                    self.copy_to_clipboard(&cmd.text)?;
512                } else {
513                    return Err("No command selected in block".to_string());
514                }
515            }
516            CopyActionType::Output => {
517                if let Some(cmd) = block.get_selected_command() {
518                    self.copy_to_clipboard(&cmd.output)?;
519                } else {
520                    return Err("No command selected in block".to_string());
521                }
522            }
523        }
524
525        self.last_copy_result = Some(Ok(action_type));
526        Ok(())
527    }
528
529    /// Internal method to copy text to clipboard
530    fn copy_to_clipboard(&self, text: &str) -> Result<(), String> {
531        // Check size limit (100 MB)
532        const MAX_SIZE: usize = 100 * 1024 * 1024;
533        if text.len() > MAX_SIZE {
534            return Err(format!(
535                "Content too large to copy: {} bytes (max: {} bytes)",
536                text.len(),
537                MAX_SIZE
538            ));
539        }
540
541        // In a real implementation, this would use the clipboard module
542        // For now, we just validate the content
543        Ok(())
544    }
545
546    /// Get the last copy result
547    pub fn get_last_copy_result(&self) -> Option<&Result<CopyActionType, String>> {
548        self.last_copy_result.as_ref()
549    }
550
551    /// Clear the last copy result
552    pub fn clear_copy_result(&mut self) {
553        self.last_copy_result = None;
554    }
555}
556
557impl Default for CommandBlocksWidget {
558    fn default() -> Self {
559        Self::new()
560    }
561}
562
563#[cfg(test)]
564mod tests {
565    use super::*;
566
567    #[test]
568    fn test_command_creation() {
569        let cmd = Command::new("echo hello");
570        assert_eq!(cmd.text, "echo hello");
571        assert_eq!(cmd.status, CommandStatus::Pending);
572        assert!(cmd.output.is_empty());
573        assert_eq!(cmd.exit_code, None);
574    }
575
576    #[test]
577    fn test_command_lifecycle() {
578        let mut cmd = Command::new("echo hello");
579        assert_eq!(cmd.status, CommandStatus::Pending);
580
581        cmd.start();
582        assert_eq!(cmd.status, CommandStatus::Running);
583        assert!(cmd.started_at.is_some());
584
585        cmd.append_output("hello\n");
586        assert_eq!(cmd.output, "hello\n");
587
588        cmd.complete(0);
589        assert_eq!(cmd.status, CommandStatus::Success);
590        assert_eq!(cmd.exit_code, Some(0));
591        assert!(cmd.finished_at.is_some());
592    }
593
594    #[test]
595    fn test_command_failure() {
596        let mut cmd = Command::new("false");
597        cmd.start();
598        cmd.complete(1);
599        assert_eq!(cmd.status, CommandStatus::Failed);
600        assert_eq!(cmd.exit_code, Some(1));
601    }
602
603    #[test]
604    fn test_command_cancellation() {
605        let mut cmd = Command::new("sleep 10");
606        cmd.start();
607        cmd.cancel();
608        assert_eq!(cmd.status, CommandStatus::Cancelled);
609        assert!(cmd.finished_at.is_some());
610    }
611
612    #[test]
613    fn test_command_block_creation() {
614        let block = CommandBlock::new("block1", "Build commands");
615        assert_eq!(block.id, "block1");
616        assert_eq!(block.title, "Build commands");
617        assert!(block.commands.is_empty());
618        assert!(!block.collapsed);
619    }
620
621    #[test]
622    fn test_command_block_add_commands() {
623        let mut block = CommandBlock::new("block1", "Build");
624        let cmd1 = Command::new("cargo build");
625        let cmd2 = Command::new("cargo test");
626
627        block.add_command(cmd1);
628        block.add_command(cmd2);
629
630        assert_eq!(block.commands.len(), 2);
631    }
632
633    #[test]
634    fn test_command_block_overall_status() {
635        let mut block = CommandBlock::new("block1", "Build");
636
637        // Empty block is pending
638        assert_eq!(block.overall_status(), CommandStatus::Pending);
639
640        // Add pending command
641        let cmd1 = Command::new("cargo build");
642        block.add_command(cmd1);
643        assert_eq!(block.overall_status(), CommandStatus::Pending);
644
645        // Start command
646        if let Some(cmd) = block.commands.get_mut(0) {
647            cmd.start();
648        }
649        assert_eq!(block.overall_status(), CommandStatus::Running);
650
651        // Complete successfully
652        if let Some(cmd) = block.commands.get_mut(0) {
653            cmd.complete(0);
654        }
655        assert_eq!(block.overall_status(), CommandStatus::Success);
656    }
657
658    #[test]
659    fn test_command_block_collapsed() {
660        let mut block = CommandBlock::new("block1", "Build");
661        let cmd = Command::new("cargo build");
662        block.add_command(cmd);
663
664        assert_eq!(block.visible_commands().len(), 1);
665
666        block.toggle_collapsed();
667        assert!(block.collapsed);
668        assert_eq!(block.visible_commands().len(), 0);
669
670        block.toggle_collapsed();
671        assert!(!block.collapsed);
672        assert_eq!(block.visible_commands().len(), 1);
673    }
674
675    #[test]
676    fn test_command_block_selection() {
677        let mut block = CommandBlock::new("block1", "Build");
678        block.add_command(Command::new("cmd1"));
679        block.add_command(Command::new("cmd2"));
680
681        assert_eq!(block.selected_command, None);
682
683        block.select_next_command();
684        assert_eq!(block.selected_command, Some(0));
685
686        block.select_next_command();
687        assert_eq!(block.selected_command, Some(1));
688
689        block.select_prev_command();
690        assert_eq!(block.selected_command, Some(0));
691
692        block.select_prev_command();
693        assert_eq!(block.selected_command, None);
694    }
695
696    #[test]
697    fn test_command_blocks_widget() {
698        let mut widget = CommandBlocksWidget::new();
699        assert!(widget.blocks.is_empty());
700
701        let block1 = CommandBlock::new("block1", "Build");
702        let block2 = CommandBlock::new("block2", "Test");
703
704        widget.add_block(block1);
705        widget.add_block(block2);
706
707        assert_eq!(widget.blocks.len(), 2);
708    }
709
710    #[test]
711    fn test_command_blocks_widget_selection() {
712        let mut widget = CommandBlocksWidget::new();
713        widget.add_block(CommandBlock::new("block1", "Build"));
714        widget.add_block(CommandBlock::new("block2", "Test"));
715
716        assert_eq!(widget.selected_block, None);
717
718        widget.select_next_block();
719        assert_eq!(widget.selected_block, Some(0));
720
721        widget.select_next_block();
722        assert_eq!(widget.selected_block, Some(1));
723
724        widget.select_prev_block();
725        assert_eq!(widget.selected_block, Some(0));
726    }
727
728    #[test]
729    fn test_command_blocks_widget_statistics() {
730        let mut widget = CommandBlocksWidget::new();
731        let mut block = CommandBlock::new("block1", "Build");
732
733        let mut cmd1 = Command::new("cmd1");
734        cmd1.complete(0);
735        block.add_command(cmd1);
736
737        let mut cmd2 = Command::new("cmd2");
738        cmd2.complete(1);
739        block.add_command(cmd2);
740
741        widget.add_block(block);
742
743        assert_eq!(widget.total_commands(), 2);
744        assert_eq!(widget.total_success(), 1);
745        assert_eq!(widget.total_failed(), 1);
746    }
747
748    #[test]
749    fn test_command_status_display() {
750        assert_eq!(CommandStatus::Pending.short_text(), "pending");
751        assert_eq!(CommandStatus::Running.short_text(), "running");
752        assert_eq!(CommandStatus::Success.short_text(), "success");
753        assert_eq!(CommandStatus::Failed.short_text(), "failed");
754        assert_eq!(CommandStatus::Cancelled.short_text(), "cancelled");
755    }
756
757    #[test]
758    fn test_copy_action_type() {
759        assert_eq!(CopyActionType::Block, CopyActionType::Block);
760        assert_eq!(CopyActionType::Command, CopyActionType::Command);
761        assert_eq!(CopyActionType::Output, CopyActionType::Output);
762        assert_ne!(CopyActionType::Block, CopyActionType::Command);
763    }
764
765    #[test]
766    fn test_command_blocks_widget_copy_selected_block() {
767        let mut widget = CommandBlocksWidget::new();
768        let mut block = CommandBlock::new("block1", "Build");
769        let mut cmd = Command::new("cargo build");
770        cmd.append_output("Compiling...\nFinished");
771        block.add_command(cmd);
772        widget.add_block(block);
773
774        widget.select_next_block();
775        if let Some(block) = widget.get_selected_block_mut() {
776            block.select_next_command();
777        }
778
779        let result = widget.copy_selected_block(CopyActionType::Command);
780        assert!(result.is_ok());
781        assert_eq!(
782            widget.get_last_copy_result(),
783            Some(&Ok(CopyActionType::Command))
784        );
785    }
786
787    #[test]
788    fn test_command_blocks_widget_copy_all_blocks() {
789        let mut widget = CommandBlocksWidget::new();
790
791        let mut block1 = CommandBlock::new("block1", "Build");
792        let mut cmd1 = Command::new("cargo build");
793        cmd1.append_output("Success");
794        block1.add_command(cmd1);
795        widget.add_block(block1);
796
797        let mut block2 = CommandBlock::new("block2", "Test");
798        let mut cmd2 = Command::new("cargo test");
799        cmd2.append_output("All tests passed");
800        block2.add_command(cmd2);
801        widget.add_block(block2);
802
803        let result = widget.copy_all_blocks();
804        assert!(result.is_ok());
805        assert_eq!(
806            widget.get_last_copy_result(),
807            Some(&Ok(CopyActionType::Block))
808        );
809    }
810
811    #[test]
812    fn test_command_blocks_widget_copy_block_by_index() {
813        let mut widget = CommandBlocksWidget::new();
814        let mut block = CommandBlock::new("block1", "Build");
815        let mut cmd = Command::new("cargo build");
816        cmd.append_output("Success");
817        block.add_command(cmd);
818        widget.add_block(block);
819
820        // Select the command in the block first
821        if let Some(block) = widget.blocks.get_mut(0) {
822            block.select_next_command();
823        }
824
825        let result = widget.copy_block_by_index(0, CopyActionType::Output);
826        assert!(result.is_ok());
827    }
828
829    #[test]
830    fn test_command_blocks_widget_copy_no_block_selected() {
831        let mut widget = CommandBlocksWidget::new();
832        let result = widget.copy_selected_block(CopyActionType::Command);
833        assert!(result.is_err());
834        assert_eq!(result.unwrap_err(), "No block selected");
835    }
836
837    #[test]
838    fn test_command_blocks_widget_copy_result_tracking() {
839        let mut widget = CommandBlocksWidget::new();
840        assert_eq!(widget.get_last_copy_result(), None);
841
842        let mut block = CommandBlock::new("block1", "Build");
843        let cmd = Command::new("cargo build");
844        block.add_command(cmd);
845        widget.add_block(block);
846
847        widget.select_next_block();
848        if let Some(block) = widget.get_selected_block_mut() {
849            block.select_next_command();
850        }
851
852        let _ = widget.copy_selected_block(CopyActionType::Command);
853        assert!(widget.get_last_copy_result().is_some());
854
855        widget.clear_copy_result();
856        assert_eq!(widget.get_last_copy_result(), None);
857    }
858
859    #[test]
860    fn test_command_blocks_widget_copy_size_limit() {
861        let mut widget = CommandBlocksWidget::new();
862        let mut block = CommandBlock::new("block1", "Build");
863        let mut cmd = Command::new("cargo build");
864
865        // Create output larger than limit
866        let large_output = "x".repeat(101 * 1024 * 1024);
867        cmd.append_output(&large_output);
868        block.add_command(cmd);
869        widget.add_block(block);
870
871        widget.select_next_block();
872        if let Some(block) = widget.get_selected_block_mut() {
873            block.select_next_command();
874        }
875
876        let result = widget.copy_selected_block(CopyActionType::Output);
877        assert!(result.is_err());
878    }
879}