use std::collections::VecDeque;
use crate::cell::Cell;
use crate::event::TermEvent;
use crate::grid::Grid;
use crate::serialize::{MarkerId, MarkerKind, MarkerPosition};
use super::{
CommandLine, CommandRecord, MAX_COMMAND_TEXT, MAX_MARKERS, Marker, MarkerEntry, MarkerIndex,
Term,
};
impl Term {
fn primary_grid(&self) -> &Grid {
if self.on_alt {
&self.alt_grid
} else {
&self.grid
}
}
pub(super) fn markers(&self) -> &VecDeque<Marker> {
if self.on_alt {
&self.alt_markers
} else {
&self.normal_markers
}
}
fn markers_mut(&mut self) -> &mut VecDeque<Marker> {
if self.on_alt {
&mut self.alt_markers
} else {
&mut self.normal_markers
}
}
pub fn add_marker(&mut self, row: usize) -> MarkerId {
let line = self.viewport_to_abs(row, 0).line;
self.push_marker(line, 0, MarkerKind::Plain)
}
fn push_marker(&mut self, line: usize, col: usize, kind: MarkerKind) -> MarkerId {
let id = MarkerId(self.next_marker_id);
self.next_marker_id += 1;
let mut disposed = Vec::new();
let markers = self.markers_mut();
while markers.len() >= MAX_MARKERS {
let Some(m) = markers.pop_front() else {
break;
};
disposed.push(m.id);
}
markers.push_back(Marker {
id,
line,
col,
kind,
command: None,
});
for id in disposed {
self.events.push(TermEvent::MarkerDisposed(id));
}
self.events.push(TermEvent::MarkerCreated {
id,
line: line as u32,
kind,
evicted_total: self.evicted_total,
epoch: self.marker_epoch,
});
id
}
pub(super) fn add_command_mark(&mut self, kind: MarkerKind) {
if self.on_alt {
return;
}
let line = self.scrollback.len() + self.cursor.row;
let col = self.cursor.col + usize::from(self.cursor.pending_wrap);
self.push_marker(line, col, kind);
match kind {
MarkerKind::OutputStart => self.capture_command_text(line, col),
MarkerKind::CommandFinished(exit) => self.attach_exit(exit),
_ => {}
}
}
fn capture_command_text(&mut self, c_line: usize, c_col: usize) {
let Some((b_line, b_col)) = self.open_command_start() else {
return;
};
let grid = self.primary_grid();
let (b_line, b_col) = self.command_start(grid, b_line, b_col, c_line);
let mut text = self.extract_lines(grid, b_line, b_col, c_line, c_col);
if text.chars().count() > MAX_COMMAND_TEXT {
let end = text
.char_indices()
.nth(MAX_COMMAND_TEXT)
.map_or(text.len(), |(i, _)| i);
text.truncate(end);
}
if let Some(m) = self.normal_markers.back_mut() {
m.command = Some(Box::new(CommandRecord {
text: text.into_boxed_str(),
exit: None,
}));
}
}
fn open_command_start(&self) -> Option<(usize, usize)> {
self.normal_markers
.iter()
.rev()
.skip(1)
.find_map(|m| match m.kind {
MarkerKind::CommandStart => Some(Some((m.line, m.col))),
MarkerKind::OutputStart => Some(None),
_ => None,
})
.flatten()
}
fn attach_exit(&mut self, exit: Option<i32>) {
let open = self
.normal_markers
.iter_mut()
.rev()
.find_map(|m| match m.kind {
MarkerKind::OutputStart => Some(Some(m)),
MarkerKind::CommandStart => Some(None),
_ => None,
})
.flatten();
if let Some(rec) = open.and_then(|m| m.command.as_mut())
&& rec.exit.is_none()
{
rec.exit = exit;
}
}
pub(super) fn dispose_markers_on_row(&mut self, row: usize) {
let line = self.scrollback.len() + row;
let mut disposed = Vec::new();
self.markers_mut().retain(|m| {
if m.line == line {
disposed.push(m.id);
false
} else {
true
}
});
for id in disposed {
self.events.push(TermEvent::MarkerDisposed(id));
}
}
pub fn command_marks(&self) -> Vec<(MarkerId, usize, MarkerKind)> {
self.normal_markers
.iter()
.filter(|m| m.kind != MarkerKind::Plain)
.map(|m| (m.id, m.line, m.kind))
.collect()
}
pub fn command_lines(&self) -> Vec<CommandLine> {
let mut out: Vec<CommandLine> = Vec::new();
let mut pending: Option<(usize, usize)> = None;
for m in &self.normal_markers {
match m.kind {
MarkerKind::CommandStart => pending = Some((m.line, m.col)),
MarkerKind::OutputStart => {
if let Some((b_line, b_col)) = pending.take() {
let Some(rec) = m.command.as_deref() else {
continue;
};
let (b_line, _) =
self.command_start(self.primary_grid(), b_line, b_col, m.line);
out.push(CommandLine {
line: self.doc_line_of(self.primary_grid(), b_line),
command: rec.text.to_string(),
exit: rec.exit,
});
}
}
MarkerKind::CommandFinished(_) => {
}
MarkerKind::Plain | MarkerKind::PromptStart => {}
}
}
out
}
fn command_start(&self, grid: &Grid, line: usize, col: usize, end: usize) -> (usize, usize) {
let (mut line, mut col) = (line, col);
while line < end && !self.row_in(grid, line).is_wrapped() {
let cells = self.line_in(grid, line);
if col < cells.len() && !cells[col..].iter().all(Cell::is_blank) {
break;
}
line += 1;
col = 0;
}
(line, col)
}
fn doc_line_of(&self, grid: &Grid, abs: usize) -> usize {
(0..abs)
.filter(|&l| !self.row_in(grid, l).is_wrapped())
.count()
}
pub fn remove_marker(&mut self, id: MarkerId) {
let before = self.normal_markers.len() + self.alt_markers.len();
self.normal_markers.retain(|m| m.id != id);
self.alt_markers.retain(|m| m.id != id);
if self.normal_markers.len() + self.alt_markers.len() != before {
self.events.push(TermEvent::MarkerDisposed(id));
}
}
pub fn marker_index(&self) -> MarkerIndex {
MarkerIndex {
markers: self
.markers()
.iter()
.map(|m| MarkerEntry {
id: m.id,
line: m.line as u32,
kind: m.kind,
})
.collect(),
evicted_total: self.evicted_total,
epoch: self.marker_epoch,
}
}
pub(super) fn bump_marker_epoch(&mut self) {
self.marker_epoch = self.marker_epoch.wrapping_add(1);
}
pub(super) fn markers_shift_below_margin(&mut self, from: usize) {
let mut moved = false;
for m in &mut self.normal_markers {
if m.line >= from {
m.line += 1;
moved = true;
}
}
if moved {
self.bump_marker_epoch();
}
}
pub(super) fn markers_evict_oldest(&mut self) {
let mut disposed = Vec::new();
self.normal_markers.retain_mut(|m| {
if m.line == 0 {
disposed.push(m.id);
false
} else {
m.line -= 1;
true
}
});
for id in disposed {
self.events.push(TermEvent::MarkerDisposed(id));
}
}
pub(super) fn markers_rotate_region(&mut self, top: usize, bottom: usize, up: bool) {
let mut disposed = Vec::new();
let mut moved = false;
self.markers_mut().retain_mut(|m| {
if m.line < top || m.line > bottom {
return true; }
let dropped_edge = if up { top } else { bottom };
if m.line == dropped_edge {
disposed.push(m.id);
false
} else {
m.line = if up { m.line - 1 } else { m.line + 1 };
moved = true;
true
}
});
for id in disposed {
self.events.push(TermEvent::MarkerDisposed(id));
}
if moved {
self.bump_marker_epoch();
}
}
pub(super) fn marker_positions(&self) -> Vec<MarkerPosition> {
let top = self.scrollback.len() - self.display_offset;
let rows = self.grid.rows();
self.markers()
.iter()
.filter_map(|m| {
let row = m.line.checked_sub(top)?;
(row < rows).then_some(MarkerPosition {
id: m.id,
row,
kind: m.kind,
})
})
.collect()
}
}