extern crate alloc;
use alloc::string::String;
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
#[archive(check_bytes)]
pub enum ZoneType {
Prompt,
Input,
Output,
}
#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
#[archive(check_bytes)]
pub struct SemanticZone {
pub id: u64,
pub zone_type: ZoneType,
pub start_row: u32,
pub end_row: u32,
pub command: Option<String>,
pub exit_code: Option<i32>,
pub started_at: u64,
pub duration_micros: Option<u64>,
pub is_complete: bool,
}
impl SemanticZone {
pub fn new_prompt(id: u64, start_row: u32, timestamp: u64) -> Self {
Self {
id,
zone_type: ZoneType::Prompt,
start_row,
end_row: start_row,
command: None,
exit_code: None,
started_at: timestamp,
duration_micros: None,
is_complete: false,
}
}
pub fn new_input(id: u64, start_row: u32, timestamp: u64) -> Self {
Self {
id,
zone_type: ZoneType::Input,
start_row,
end_row: start_row,
command: None,
exit_code: None,
started_at: timestamp,
duration_micros: None,
is_complete: false,
}
}
pub fn new_output(id: u64, start_row: u32, timestamp: u64) -> Self {
Self {
id,
zone_type: ZoneType::Output,
start_row,
end_row: start_row,
command: None,
exit_code: None,
started_at: timestamp,
duration_micros: None,
is_complete: false,
}
}
pub fn complete(&mut self, end_row: u32, end_timestamp: u64) {
self.end_row = end_row;
self.is_complete = true;
if end_timestamp >= self.started_at {
self.duration_micros = Some(end_timestamp - self.started_at);
}
}
pub fn set_command(&mut self, command: String) {
self.command = Some(command);
}
pub fn set_exit_code(&mut self, exit_code: i32) {
self.exit_code = Some(exit_code);
}
pub fn contains_line(&self, line: u32) -> bool {
line >= self.start_row && line <= self.end_row
}
pub fn line_count(&self) -> u32 {
if self.end_row >= self.start_row {
self.end_row - self.start_row + 1
} else {
1
}
}
pub fn is_success(&self) -> bool {
self.exit_code == Some(0)
}
pub fn is_failure(&self) -> bool {
matches!(self.exit_code, Some(code) if code != 0)
}
pub fn duration_millis(&self) -> Option<u64> {
self.duration_micros.map(|micros| micros / 1000)
}
pub fn duration_secs(&self) -> Option<f64> {
self.duration_micros
.map(|micros| micros as f64 / 1_000_000.0)
}
}
#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
#[archive(check_bytes)]
pub struct CommandBlock {
pub id: u64,
pub prompt_zone: Option<SemanticZone>,
pub input_zone: Option<SemanticZone>,
pub output_zone: Option<SemanticZone>,
pub start_row: u32,
pub end_row: u32,
pub started_at: u64,
pub duration_micros: Option<u64>,
}
impl CommandBlock {
pub fn new(id: u64, prompt_zone: SemanticZone) -> Self {
Self {
id,
start_row: prompt_zone.start_row,
end_row: prompt_zone.end_row,
started_at: prompt_zone.started_at,
prompt_zone: Some(prompt_zone),
input_zone: None,
output_zone: None,
duration_micros: None,
}
}
pub fn add_input_zone(&mut self, zone: SemanticZone) {
self.end_row = zone.end_row.max(self.end_row);
self.input_zone = Some(zone);
}
pub fn add_output_zone(&mut self, zone: SemanticZone) {
self.end_row = zone.end_row.max(self.end_row);
if zone.is_complete {
let end_timestamp = zone.started_at + zone.duration_micros.unwrap_or(0);
if end_timestamp >= self.started_at {
self.duration_micros = Some(end_timestamp - self.started_at);
}
}
self.output_zone = Some(zone);
}
pub fn command_text(&self) -> Option<&str> {
self.input_zone.as_ref()?.command.as_deref()
}
pub fn exit_code(&self) -> Option<i32> {
self.output_zone.as_ref()?.exit_code
}
pub fn is_complete(&self) -> bool {
self.output_zone.as_ref().map_or(false, |z| z.is_complete)
}
pub fn is_success(&self) -> bool {
self.exit_code() == Some(0)
}
pub fn is_failure(&self) -> bool {
matches!(self.exit_code(), Some(code) if code != 0)
}
pub fn output_bounds(&self) -> Option<(u32, u32)> {
self.output_zone.as_ref().map(|z| (z.start_row, z.end_row))
}
pub fn contains_line(&self, line: u32) -> bool {
line >= self.start_row && line <= self.end_row
}
pub fn duration_secs(&self) -> Option<f64> {
self.duration_micros
.map(|micros| micros as f64 / 1_000_000.0)
}
}
#[derive(Debug, Clone)]
pub struct ZoneTracker {
next_zone_id: u64,
current_zones: Vec<SemanticZone>,
command_blocks: Vec<CommandBlock>,
max_blocks: usize,
current_block: Option<CommandBlock>,
}
impl ZoneTracker {
pub fn new(max_blocks: usize) -> Self {
Self {
next_zone_id: 1,
current_zones: Vec::new(),
command_blocks: Vec::new(),
max_blocks,
current_block: None,
}
}
fn next_id(&mut self) -> u64 {
let id = self.next_zone_id;
self.next_zone_id = self.next_zone_id.wrapping_add(1);
id
}
pub fn mark_prompt_start(&mut self, line: u32, timestamp: u64) {
let id = self.next_id();
let zone = SemanticZone::new_prompt(id, line, timestamp);
self.current_block = Some(CommandBlock::new(id, zone.clone()));
self.current_zones.push(zone);
}
pub fn mark_command_start(&mut self, line: u32, timestamp: u64) {
if let Some(zone) = self
.current_zones
.iter_mut()
.rev()
.find(|z| z.zone_type == ZoneType::Prompt && !z.is_complete)
{
zone.complete(line.saturating_sub(1), timestamp);
}
let id = self.next_id();
let zone = SemanticZone::new_input(id, line, timestamp);
if let Some(ref mut block) = self.current_block {
block.add_input_zone(zone.clone());
}
self.current_zones.push(zone);
}
pub fn mark_command_executed(&mut self, line: u32, timestamp: u64) {
if let Some(zone) = self
.current_zones
.iter_mut()
.rev()
.find(|z| z.zone_type == ZoneType::Input && !z.is_complete)
{
zone.complete(line.saturating_sub(1), timestamp);
}
let id = self.next_id();
let zone = SemanticZone::new_output(id, line, timestamp);
if let Some(ref mut block) = self.current_block {
block.add_output_zone(zone.clone());
}
self.current_zones.push(zone);
}
pub fn mark_command_finished(&mut self, line: u32, exit_code: i32, timestamp: u64) {
if let Some(zone) = self
.current_zones
.iter_mut()
.rev()
.find(|z| z.zone_type == ZoneType::Output && !z.is_complete)
{
zone.complete(line, timestamp);
zone.set_exit_code(exit_code);
if let Some(ref mut block) = self.current_block {
block.add_output_zone(zone.clone());
self.command_blocks.push(block.clone());
if self.command_blocks.len() > self.max_blocks {
self.command_blocks.remove(0);
}
}
}
self.current_block = None;
}
pub fn set_command_text(&mut self, command: String) {
if let Some(zone) = self
.current_zones
.iter_mut()
.rev()
.find(|z| z.zone_type == ZoneType::Input)
{
zone.set_command(command.clone());
}
if let Some(ref mut block) = self.current_block {
if let Some(ref mut input_zone) = block.input_zone {
input_zone.set_command(command);
}
}
}
pub fn zones(&self) -> &[SemanticZone] {
&self.current_zones
}
pub fn command_blocks(&self) -> &[CommandBlock] {
&self.command_blocks
}
pub fn current_block(&self) -> Option<&CommandBlock> {
self.current_block.as_ref()
}
pub fn find_block_at_line(&self, line: u32) -> Option<&CommandBlock> {
self.command_blocks
.iter()
.rev()
.find(|block| block.contains_line(line))
}
pub fn find_zone_at_line(&self, line: u32) -> Option<&SemanticZone> {
self.current_zones
.iter()
.rev()
.find(|zone| zone.contains_line(line))
}
pub fn last_output_zone(&self) -> Option<&SemanticZone> {
self.command_blocks
.iter()
.rev()
.find_map(|block| block.output_zone.as_ref())
}
pub fn clear(&mut self) {
self.current_zones.clear();
self.command_blocks.clear();
self.current_block = None;
}
pub fn adjust_for_scroll(&mut self, lines_scrolled: i32) {
if lines_scrolled == 0 {
return;
}
let adjust = |row: u32, delta: i32| -> u32 {
if delta < 0 {
row.saturating_sub(delta.abs() as u32)
} else {
row.saturating_add(delta as u32)
}
};
for zone in &mut self.current_zones {
zone.start_row = adjust(zone.start_row, lines_scrolled);
zone.end_row = adjust(zone.end_row, lines_scrolled);
}
for block in &mut self.command_blocks {
block.start_row = adjust(block.start_row, lines_scrolled);
block.end_row = adjust(block.end_row, lines_scrolled);
if let Some(ref mut zone) = block.prompt_zone {
zone.start_row = adjust(zone.start_row, lines_scrolled);
zone.end_row = adjust(zone.end_row, lines_scrolled);
}
if let Some(ref mut zone) = block.input_zone {
zone.start_row = adjust(zone.start_row, lines_scrolled);
zone.end_row = adjust(zone.end_row, lines_scrolled);
}
if let Some(ref mut zone) = block.output_zone {
zone.start_row = adjust(zone.start_row, lines_scrolled);
zone.end_row = adjust(zone.end_row, lines_scrolled);
}
}
if let Some(ref mut block) = self.current_block {
block.start_row = adjust(block.start_row, lines_scrolled);
block.end_row = adjust(block.end_row, lines_scrolled);
if let Some(ref mut zone) = block.prompt_zone {
zone.start_row = adjust(zone.start_row, lines_scrolled);
zone.end_row = adjust(zone.end_row, lines_scrolled);
}
if let Some(ref mut zone) = block.input_zone {
zone.start_row = adjust(zone.start_row, lines_scrolled);
zone.end_row = adjust(zone.end_row, lines_scrolled);
}
if let Some(ref mut zone) = block.output_zone {
zone.start_row = adjust(zone.start_row, lines_scrolled);
zone.end_row = adjust(zone.end_row, lines_scrolled);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
#[test]
fn test_semantic_zone_creation() {
let zone = SemanticZone::new_prompt(1, 10, 1000);
assert_eq!(zone.id, 1);
assert_eq!(zone.zone_type, ZoneType::Prompt);
assert_eq!(zone.start_row, 10);
assert!(!zone.is_complete);
}
#[test]
fn test_zone_completion() {
let mut zone = SemanticZone::new_input(1, 10, 1000);
zone.complete(15, 2000);
assert!(zone.is_complete);
assert_eq!(zone.end_row, 15);
assert_eq!(zone.duration_micros, Some(1000));
assert_eq!(zone.line_count(), 6);
}
#[test]
fn test_zone_contains_line() {
let mut zone = SemanticZone::new_output(1, 10, 1000);
zone.complete(20, 2000);
assert!(!zone.contains_line(9));
assert!(zone.contains_line(10));
assert!(zone.contains_line(15));
assert!(zone.contains_line(20));
assert!(!zone.contains_line(21));
}
#[test]
fn test_zone_tracker_prompt_flow() {
let mut tracker = ZoneTracker::new(100);
tracker.mark_prompt_start(0, 1000);
assert_eq!(tracker.zones().len(), 1);
assert!(tracker.current_block().is_some());
tracker.mark_command_start(1, 2000);
assert_eq!(tracker.zones().len(), 2);
let prompt_zone = tracker
.zones()
.iter()
.find(|z| z.zone_type == ZoneType::Prompt)
.unwrap();
assert!(prompt_zone.is_complete);
assert_eq!(prompt_zone.end_row, 0);
tracker.mark_command_executed(2, 3000);
assert_eq!(tracker.zones().len(), 3);
let input_zone = tracker
.zones()
.iter()
.find(|z| z.zone_type == ZoneType::Input)
.unwrap();
assert!(input_zone.is_complete);
assert_eq!(input_zone.end_row, 1);
tracker.mark_command_finished(10, 0, 4000);
let output_zone = tracker
.zones()
.iter()
.find(|z| z.zone_type == ZoneType::Output)
.unwrap();
assert!(output_zone.is_complete);
assert_eq!(output_zone.end_row, 10);
assert_eq!(output_zone.exit_code, Some(0));
assert_eq!(tracker.command_blocks().len(), 1);
let block = &tracker.command_blocks()[0];
assert!(block.is_complete());
assert!(block.is_success());
assert_eq!(block.duration_secs(), Some(0.003));
}
#[test]
fn test_command_block_with_failure() {
let mut tracker = ZoneTracker::new(100);
tracker.mark_prompt_start(0, 1000);
tracker.mark_command_start(1, 2000);
tracker.set_command_text("false".to_string());
tracker.mark_command_executed(2, 3000);
tracker.mark_command_finished(3, 1, 4000);
let block = &tracker.command_blocks()[0];
assert!(block.is_failure());
assert_eq!(block.exit_code(), Some(1));
assert_eq!(block.command_text(), Some("false"));
}
#[test]
fn test_zone_tracker_max_blocks() {
let mut tracker = ZoneTracker::new(3);
for i in 0..5u64 {
let base_line = (i * 10) as u32;
let base_time = i * 10000;
tracker.mark_prompt_start(base_line, base_time);
tracker.mark_command_start(base_line + 1, base_time + 1000);
tracker.mark_command_executed(base_line + 2, base_time + 2000);
tracker.mark_command_finished(base_line + 3, 0, base_time + 3000);
}
assert_eq!(tracker.command_blocks().len(), 3);
assert_eq!(tracker.command_blocks()[0].start_row, 20);
}
#[test]
fn test_find_zone_at_line() {
let mut tracker = ZoneTracker::new(100);
tracker.mark_prompt_start(10, 1000);
tracker.mark_command_start(11, 2000);
tracker.mark_command_executed(12, 3000);
tracker.mark_command_finished(20, 0, 4000);
let zone = tracker.find_zone_at_line(15).unwrap();
assert_eq!(zone.zone_type, ZoneType::Output);
let block = tracker.find_block_at_line(15).unwrap();
assert_eq!(block.start_row, 10);
}
#[test]
fn test_last_output_zone() {
let mut tracker = ZoneTracker::new(100);
tracker.mark_prompt_start(0, 1000);
tracker.mark_command_start(1, 2000);
tracker.mark_command_executed(2, 3000);
tracker.mark_command_finished(10, 0, 4000);
tracker.mark_prompt_start(11, 5000);
tracker.mark_command_start(12, 6000);
tracker.mark_command_executed(13, 7000);
tracker.mark_command_finished(20, 0, 8000);
let last_output = tracker.last_output_zone().unwrap();
assert_eq!(last_output.start_row, 13);
assert_eq!(last_output.end_row, 20);
}
#[test]
fn test_adjust_for_scroll() {
let mut tracker = ZoneTracker::new(100);
tracker.mark_prompt_start(10, 1000);
tracker.mark_command_start(11, 2000);
tracker.mark_command_executed(12, 3000);
tracker.adjust_for_scroll(5);
let prompt = tracker
.zones()
.iter()
.find(|z| z.zone_type == ZoneType::Prompt)
.unwrap();
assert_eq!(prompt.start_row, 15);
let input = tracker
.zones()
.iter()
.find(|z| z.zone_type == ZoneType::Input)
.unwrap();
assert_eq!(input.start_row, 16);
let output = tracker
.zones()
.iter()
.find(|z| z.zone_type == ZoneType::Output)
.unwrap();
assert_eq!(output.start_row, 17);
}
}