const MAX_OSC: usize = 8192;
#[derive(PartialEq, Eq, Clone, Copy)]
enum State {
Ground,
Esc,
Osc,
OscEsc,
}
#[derive(Debug, PartialEq, Eq)]
pub enum CmdEvent {
Start,
End {
command: Option<String>,
cwd: Option<String>,
exit: Option<i32>,
},
}
pub struct Scanner {
state: State,
buf: Vec<u8>,
cwd: Option<String>,
command: Option<String>,
}
impl Default for Scanner {
fn default() -> Self {
Scanner { state: State::Ground, buf: Vec::new(), cwd: None, command: None }
}
}
impl Scanner {
pub fn new() -> Self {
Self::default()
}
pub fn feed(&mut self, bytes: &[u8]) -> Vec<CmdEvent> {
let mut out = Vec::new();
for &b in bytes {
match self.state {
State::Ground => {
if b == 0x1b {
self.state = State::Esc;
}
}
State::Esc => {
self.state = if b == b']' {
self.buf.clear();
State::Osc
} else {
State::Ground };
}
State::Osc => {
if b == 0x07 {
self.finish(&mut out);
self.state = State::Ground;
} else if b == 0x1b {
self.state = State::OscEsc;
} else if self.buf.len() < MAX_OSC {
self.buf.push(b);
} else {
self.buf.clear();
self.state = State::Ground; }
}
State::OscEsc => {
if b == b'\\' {
self.finish(&mut out);
}
self.state = State::Ground;
}
}
}
out
}
fn finish(&mut self, out: &mut Vec<CmdEvent>) {
let s = String::from_utf8_lossy(&std::mem::take(&mut self.buf)).into_owned();
let (num, rest) = match s.split_once(';') {
Some((n, r)) => (n, r),
None => (s.as_str(), ""),
};
match num {
"133" => {
let code = rest.split(';').next().unwrap_or("");
match code {
"C" => out.push(CmdEvent::Start),
"D" => {
let exit = rest
.split(';')
.nth(1)
.and_then(|x| x.trim().parse::<i32>().ok());
out.push(CmdEvent::End {
command: self.command.take(),
cwd: self.cwd.clone(),
exit,
});
}
_ => {} }
}
"633" => {
if let Some(cmd) = rest.strip_prefix("E;") {
self.command = Some(cmd.to_string());
}
}
"7" => {
if let Some(after) = rest.strip_prefix("file://") {
if let Some(i) = after.find('/') {
self.cwd = Some(after[i..].to_string());
}
}
}
_ => {}
}
}
}
pub fn to_ledger_entry(
session: &str,
surface: &str,
command: Option<String>,
cwd: Option<String>,
exit: Option<i32>,
dur_secs: Option<u64>,
) -> Option<crate::worklog::WorkEntry> {
let failed = exit.map(|c| c != 0).unwrap_or(false);
let long = dur_secs.map(|d| d >= crate::briefing::GOODNEWS_SECS).unwrap_or(false);
if !failed && !long {
return None;
}
Some(crate::worklog::WorkEntry {
ts: crate::worklog::now_secs(),
session: session.to_string(),
tab: surface.to_string(),
verdict: String::new(),
failed,
dur_secs,
cwd: cwd.unwrap_or_default(),
command: command.map(|c| crate::retrieval::redact(&c)),
exit,
error_excerpt: None,
})
}