1extern crate alloc;
16use alloc::string::String;
17use alloc::vec::Vec;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
21#[archive(check_bytes)]
22pub enum ZoneType {
23 Prompt,
25 Input,
27 Output,
29}
30
31#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
38#[archive(check_bytes)]
39pub struct SemanticZone {
40 pub id: u64,
42 pub zone_type: ZoneType,
44 pub start_row: u32,
46 pub end_row: u32,
49 pub command: Option<String>,
51 pub exit_code: Option<i32>,
53 pub started_at: u64,
55 pub duration_micros: Option<u64>,
57 pub is_complete: bool,
59}
60
61impl SemanticZone {
62 pub fn new_prompt(id: u64, start_row: u32, timestamp: u64) -> Self {
64 Self {
65 id,
66 zone_type: ZoneType::Prompt,
67 start_row,
68 end_row: start_row,
69 command: None,
70 exit_code: None,
71 started_at: timestamp,
72 duration_micros: None,
73 is_complete: false,
74 }
75 }
76
77 pub fn new_input(id: u64, start_row: u32, timestamp: u64) -> Self {
79 Self {
80 id,
81 zone_type: ZoneType::Input,
82 start_row,
83 end_row: start_row,
84 command: None,
85 exit_code: None,
86 started_at: timestamp,
87 duration_micros: None,
88 is_complete: false,
89 }
90 }
91
92 pub fn new_output(id: u64, start_row: u32, timestamp: u64) -> Self {
94 Self {
95 id,
96 zone_type: ZoneType::Output,
97 start_row,
98 end_row: start_row,
99 command: None,
100 exit_code: None,
101 started_at: timestamp,
102 duration_micros: None,
103 is_complete: false,
104 }
105 }
106
107 pub fn complete(&mut self, end_row: u32, end_timestamp: u64) {
109 self.end_row = end_row;
110 self.is_complete = true;
111 if end_timestamp >= self.started_at {
112 self.duration_micros = Some(end_timestamp - self.started_at);
113 }
114 }
115
116 pub fn set_command(&mut self, command: String) {
118 self.command = Some(command);
119 }
120
121 pub fn set_exit_code(&mut self, exit_code: i32) {
123 self.exit_code = Some(exit_code);
124 }
125
126 pub fn contains_line(&self, line: u32) -> bool {
128 line >= self.start_row && line <= self.end_row
129 }
130
131 pub fn line_count(&self) -> u32 {
133 if self.end_row >= self.start_row {
134 self.end_row - self.start_row + 1
135 } else {
136 1
137 }
138 }
139
140 pub fn is_success(&self) -> bool {
142 self.exit_code == Some(0)
143 }
144
145 pub fn is_failure(&self) -> bool {
147 matches!(self.exit_code, Some(code) if code != 0)
148 }
149
150 pub fn duration_millis(&self) -> Option<u64> {
152 self.duration_micros.map(|micros| micros / 1000)
153 }
154
155 pub fn duration_secs(&self) -> Option<f64> {
157 self.duration_micros
158 .map(|micros| micros as f64 / 1_000_000.0)
159 }
160}
161
162#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
167#[archive(check_bytes)]
168pub struct CommandBlock {
169 pub id: u64,
171 pub prompt_zone: Option<SemanticZone>,
173 pub input_zone: Option<SemanticZone>,
175 pub output_zone: Option<SemanticZone>,
177 pub start_row: u32,
179 pub end_row: u32,
181 pub started_at: u64,
183 pub duration_micros: Option<u64>,
185}
186
187impl CommandBlock {
188 pub fn new(id: u64, prompt_zone: SemanticZone) -> Self {
190 Self {
191 id,
192 start_row: prompt_zone.start_row,
193 end_row: prompt_zone.end_row,
194 started_at: prompt_zone.started_at,
195 prompt_zone: Some(prompt_zone),
196 input_zone: None,
197 output_zone: None,
198 duration_micros: None,
199 }
200 }
201
202 pub fn add_input_zone(&mut self, zone: SemanticZone) {
204 self.end_row = zone.end_row.max(self.end_row);
205 self.input_zone = Some(zone);
206 }
207
208 pub fn add_output_zone(&mut self, zone: SemanticZone) {
210 self.end_row = zone.end_row.max(self.end_row);
211
212 if zone.is_complete {
214 let end_timestamp = zone.started_at + zone.duration_micros.unwrap_or(0);
215 if end_timestamp >= self.started_at {
216 self.duration_micros = Some(end_timestamp - self.started_at);
217 }
218 }
219
220 self.output_zone = Some(zone);
221 }
222
223 pub fn command_text(&self) -> Option<&str> {
225 self.input_zone.as_ref()?.command.as_deref()
226 }
227
228 pub fn exit_code(&self) -> Option<i32> {
230 self.output_zone.as_ref()?.exit_code
231 }
232
233 pub fn is_complete(&self) -> bool {
235 self.output_zone.as_ref().map_or(false, |z| z.is_complete)
236 }
237
238 pub fn is_success(&self) -> bool {
240 self.exit_code() == Some(0)
241 }
242
243 pub fn is_failure(&self) -> bool {
245 matches!(self.exit_code(), Some(code) if code != 0)
246 }
247
248 pub fn output_bounds(&self) -> Option<(u32, u32)> {
250 self.output_zone.as_ref().map(|z| (z.start_row, z.end_row))
251 }
252
253 pub fn contains_line(&self, line: u32) -> bool {
255 line >= self.start_row && line <= self.end_row
256 }
257
258 pub fn duration_secs(&self) -> Option<f64> {
260 self.duration_micros
261 .map(|micros| micros as f64 / 1_000_000.0)
262 }
263}
264
265#[derive(Debug, Clone)]
270pub struct ZoneTracker {
271 next_zone_id: u64,
273 current_zones: Vec<SemanticZone>,
275 command_blocks: Vec<CommandBlock>,
277 max_blocks: usize,
279 current_block: Option<CommandBlock>,
281}
282
283impl ZoneTracker {
284 pub fn new(max_blocks: usize) -> Self {
286 Self {
287 next_zone_id: 1,
288 current_zones: Vec::new(),
289 command_blocks: Vec::new(),
290 max_blocks,
291 current_block: None,
292 }
293 }
294
295 fn next_id(&mut self) -> u64 {
297 let id = self.next_zone_id;
298 self.next_zone_id = self.next_zone_id.wrapping_add(1);
299 id
300 }
301
302 pub fn mark_prompt_start(&mut self, line: u32, timestamp: u64) {
304 let id = self.next_id();
305 let zone = SemanticZone::new_prompt(id, line, timestamp);
306
307 self.current_block = Some(CommandBlock::new(id, zone.clone()));
309 self.current_zones.push(zone);
310 }
311
312 pub fn mark_command_start(&mut self, line: u32, timestamp: u64) {
314 if let Some(zone) = self
316 .current_zones
317 .iter_mut()
318 .rev()
319 .find(|z| z.zone_type == ZoneType::Prompt && !z.is_complete)
320 {
321 zone.complete(line.saturating_sub(1), timestamp);
322 }
323
324 let id = self.next_id();
326 let zone = SemanticZone::new_input(id, line, timestamp);
327
328 if let Some(ref mut block) = self.current_block {
330 block.add_input_zone(zone.clone());
331 }
332
333 self.current_zones.push(zone);
334 }
335
336 pub fn mark_command_executed(&mut self, line: u32, timestamp: u64) {
338 if let Some(zone) = self
340 .current_zones
341 .iter_mut()
342 .rev()
343 .find(|z| z.zone_type == ZoneType::Input && !z.is_complete)
344 {
345 zone.complete(line.saturating_sub(1), timestamp);
346 }
347
348 let id = self.next_id();
350 let zone = SemanticZone::new_output(id, line, timestamp);
351
352 if let Some(ref mut block) = self.current_block {
354 block.add_output_zone(zone.clone());
355 }
356
357 self.current_zones.push(zone);
358 }
359
360 pub fn mark_command_finished(&mut self, line: u32, exit_code: i32, timestamp: u64) {
362 if let Some(zone) = self
364 .current_zones
365 .iter_mut()
366 .rev()
367 .find(|z| z.zone_type == ZoneType::Output && !z.is_complete)
368 {
369 zone.complete(line, timestamp);
370 zone.set_exit_code(exit_code);
371
372 if let Some(ref mut block) = self.current_block {
374 block.add_output_zone(zone.clone());
375
376 self.command_blocks.push(block.clone());
378
379 if self.command_blocks.len() > self.max_blocks {
381 self.command_blocks.remove(0);
382 }
383 }
384 }
385
386 self.current_block = None;
388 }
389
390 pub fn set_command_text(&mut self, command: String) {
392 if let Some(zone) = self
393 .current_zones
394 .iter_mut()
395 .rev()
396 .find(|z| z.zone_type == ZoneType::Input)
397 {
398 zone.set_command(command.clone());
399 }
400
401 if let Some(ref mut block) = self.current_block {
403 if let Some(ref mut input_zone) = block.input_zone {
404 input_zone.set_command(command);
405 }
406 }
407 }
408
409 pub fn zones(&self) -> &[SemanticZone] {
411 &self.current_zones
412 }
413
414 pub fn command_blocks(&self) -> &[CommandBlock] {
416 &self.command_blocks
417 }
418
419 pub fn current_block(&self) -> Option<&CommandBlock> {
421 self.current_block.as_ref()
422 }
423
424 pub fn find_block_at_line(&self, line: u32) -> Option<&CommandBlock> {
426 self.command_blocks
427 .iter()
428 .rev()
429 .find(|block| block.contains_line(line))
430 }
431
432 pub fn find_zone_at_line(&self, line: u32) -> Option<&SemanticZone> {
434 self.current_zones
435 .iter()
436 .rev()
437 .find(|zone| zone.contains_line(line))
438 }
439
440 pub fn last_output_zone(&self) -> Option<&SemanticZone> {
442 self.command_blocks
443 .iter()
444 .rev()
445 .find_map(|block| block.output_zone.as_ref())
446 }
447
448 pub fn clear(&mut self) {
450 self.current_zones.clear();
451 self.command_blocks.clear();
452 self.current_block = None;
453 }
454
455 pub fn adjust_for_scroll(&mut self, lines_scrolled: i32) {
460 if lines_scrolled == 0 {
461 return;
462 }
463
464 let adjust = |row: u32, delta: i32| -> u32 {
465 if delta < 0 {
466 row.saturating_sub(delta.abs() as u32)
467 } else {
468 row.saturating_add(delta as u32)
469 }
470 };
471
472 for zone in &mut self.current_zones {
474 zone.start_row = adjust(zone.start_row, lines_scrolled);
475 zone.end_row = adjust(zone.end_row, lines_scrolled);
476 }
477
478 for block in &mut self.command_blocks {
480 block.start_row = adjust(block.start_row, lines_scrolled);
481 block.end_row = adjust(block.end_row, lines_scrolled);
482
483 if let Some(ref mut zone) = block.prompt_zone {
484 zone.start_row = adjust(zone.start_row, lines_scrolled);
485 zone.end_row = adjust(zone.end_row, lines_scrolled);
486 }
487 if let Some(ref mut zone) = block.input_zone {
488 zone.start_row = adjust(zone.start_row, lines_scrolled);
489 zone.end_row = adjust(zone.end_row, lines_scrolled);
490 }
491 if let Some(ref mut zone) = block.output_zone {
492 zone.start_row = adjust(zone.start_row, lines_scrolled);
493 zone.end_row = adjust(zone.end_row, lines_scrolled);
494 }
495 }
496
497 if let Some(ref mut block) = self.current_block {
499 block.start_row = adjust(block.start_row, lines_scrolled);
500 block.end_row = adjust(block.end_row, lines_scrolled);
501
502 if let Some(ref mut zone) = block.prompt_zone {
503 zone.start_row = adjust(zone.start_row, lines_scrolled);
504 zone.end_row = adjust(zone.end_row, lines_scrolled);
505 }
506 if let Some(ref mut zone) = block.input_zone {
507 zone.start_row = adjust(zone.start_row, lines_scrolled);
508 zone.end_row = adjust(zone.end_row, lines_scrolled);
509 }
510 if let Some(ref mut zone) = block.output_zone {
511 zone.start_row = adjust(zone.start_row, lines_scrolled);
512 zone.end_row = adjust(zone.end_row, lines_scrolled);
513 }
514 }
515 }
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521 use alloc::string::ToString;
522
523 #[test]
524 fn test_semantic_zone_creation() {
525 let zone = SemanticZone::new_prompt(1, 10, 1000);
526 assert_eq!(zone.id, 1);
527 assert_eq!(zone.zone_type, ZoneType::Prompt);
528 assert_eq!(zone.start_row, 10);
529 assert!(!zone.is_complete);
530 }
531
532 #[test]
533 fn test_zone_completion() {
534 let mut zone = SemanticZone::new_input(1, 10, 1000);
535 zone.complete(15, 2000);
536
537 assert!(zone.is_complete);
538 assert_eq!(zone.end_row, 15);
539 assert_eq!(zone.duration_micros, Some(1000));
540 assert_eq!(zone.line_count(), 6);
541 }
542
543 #[test]
544 fn test_zone_contains_line() {
545 let mut zone = SemanticZone::new_output(1, 10, 1000);
546 zone.complete(20, 2000);
547
548 assert!(!zone.contains_line(9));
549 assert!(zone.contains_line(10));
550 assert!(zone.contains_line(15));
551 assert!(zone.contains_line(20));
552 assert!(!zone.contains_line(21));
553 }
554
555 #[test]
556 fn test_zone_tracker_prompt_flow() {
557 let mut tracker = ZoneTracker::new(100);
558
559 tracker.mark_prompt_start(0, 1000);
561 assert_eq!(tracker.zones().len(), 1);
562 assert!(tracker.current_block().is_some());
563
564 tracker.mark_command_start(1, 2000);
566 assert_eq!(tracker.zones().len(), 2);
567
568 let prompt_zone = tracker
570 .zones()
571 .iter()
572 .find(|z| z.zone_type == ZoneType::Prompt)
573 .unwrap();
574 assert!(prompt_zone.is_complete);
575 assert_eq!(prompt_zone.end_row, 0);
576
577 tracker.mark_command_executed(2, 3000);
579 assert_eq!(tracker.zones().len(), 3);
580
581 let input_zone = tracker
583 .zones()
584 .iter()
585 .find(|z| z.zone_type == ZoneType::Input)
586 .unwrap();
587 assert!(input_zone.is_complete);
588 assert_eq!(input_zone.end_row, 1);
589
590 tracker.mark_command_finished(10, 0, 4000);
592
593 let output_zone = tracker
595 .zones()
596 .iter()
597 .find(|z| z.zone_type == ZoneType::Output)
598 .unwrap();
599 assert!(output_zone.is_complete);
600 assert_eq!(output_zone.end_row, 10);
601 assert_eq!(output_zone.exit_code, Some(0));
602
603 assert_eq!(tracker.command_blocks().len(), 1);
605 let block = &tracker.command_blocks()[0];
606 assert!(block.is_complete());
607 assert!(block.is_success());
608 assert_eq!(block.duration_secs(), Some(0.003));
609 }
610
611 #[test]
612 fn test_command_block_with_failure() {
613 let mut tracker = ZoneTracker::new(100);
614
615 tracker.mark_prompt_start(0, 1000);
616 tracker.mark_command_start(1, 2000);
617 tracker.set_command_text("false".to_string());
618 tracker.mark_command_executed(2, 3000);
619 tracker.mark_command_finished(3, 1, 4000);
620
621 let block = &tracker.command_blocks()[0];
622 assert!(block.is_failure());
623 assert_eq!(block.exit_code(), Some(1));
624 assert_eq!(block.command_text(), Some("false"));
625 }
626
627 #[test]
628 fn test_zone_tracker_max_blocks() {
629 let mut tracker = ZoneTracker::new(3);
630
631 for i in 0..5u64 {
633 let base_line = (i * 10) as u32;
634 let base_time = i * 10000;
635
636 tracker.mark_prompt_start(base_line, base_time);
637 tracker.mark_command_start(base_line + 1, base_time + 1000);
638 tracker.mark_command_executed(base_line + 2, base_time + 2000);
639 tracker.mark_command_finished(base_line + 3, 0, base_time + 3000);
640 }
641
642 assert_eq!(tracker.command_blocks().len(), 3);
644
645 assert_eq!(tracker.command_blocks()[0].start_row, 20);
647 }
648
649 #[test]
650 fn test_find_zone_at_line() {
651 let mut tracker = ZoneTracker::new(100);
652
653 tracker.mark_prompt_start(10, 1000);
654 tracker.mark_command_start(11, 2000);
655 tracker.mark_command_executed(12, 3000);
656 tracker.mark_command_finished(20, 0, 4000);
657
658 let zone = tracker.find_zone_at_line(15).unwrap();
660 assert_eq!(zone.zone_type, ZoneType::Output);
661
662 let block = tracker.find_block_at_line(15).unwrap();
663 assert_eq!(block.start_row, 10);
664 }
665
666 #[test]
667 fn test_last_output_zone() {
668 let mut tracker = ZoneTracker::new(100);
669
670 tracker.mark_prompt_start(0, 1000);
672 tracker.mark_command_start(1, 2000);
673 tracker.mark_command_executed(2, 3000);
674 tracker.mark_command_finished(10, 0, 4000);
675
676 tracker.mark_prompt_start(11, 5000);
677 tracker.mark_command_start(12, 6000);
678 tracker.mark_command_executed(13, 7000);
679 tracker.mark_command_finished(20, 0, 8000);
680
681 let last_output = tracker.last_output_zone().unwrap();
683 assert_eq!(last_output.start_row, 13);
684 assert_eq!(last_output.end_row, 20);
685 }
686
687 #[test]
688 fn test_adjust_for_scroll() {
689 let mut tracker = ZoneTracker::new(100);
690
691 tracker.mark_prompt_start(10, 1000);
692 tracker.mark_command_start(11, 2000);
693 tracker.mark_command_executed(12, 3000);
694
695 tracker.adjust_for_scroll(5);
697
698 let prompt = tracker
700 .zones()
701 .iter()
702 .find(|z| z.zone_type == ZoneType::Prompt)
703 .unwrap();
704 assert_eq!(prompt.start_row, 15);
705
706 let input = tracker
707 .zones()
708 .iter()
709 .find(|z| z.zone_type == ZoneType::Input)
710 .unwrap();
711 assert_eq!(input.start_row, 16);
712
713 let output = tracker
714 .zones()
715 .iter()
716 .find(|z| z.zone_type == ZoneType::Output)
717 .unwrap();
718 assert_eq!(output.start_row, 17);
719 }
720}