use std::sync::Arc;
use indexmap::IndexMap;
use nix::sys::signal;
use ratatui::{
layout::Alignment::Right,
prelude::{Buffer, Rect},
style::{Color, Modifier, Style},
text::Line,
widgets::{
HighlightSpacing, List, ListItem, ListState, Scrollbar, ScrollbarOrientation, ScrollbarState,
StatefulWidget, StatefulWidgetRef, Widget,
},
};
use crate::{
cli::args::ModifierArgs,
event::{
EventStatus, ProcessStateUpdate, ProcessStateUpdateEvent, RuntimeModifier, TracerEventDetails,
},
proc::BaselineInfo,
ptrace::Signal,
tracer::state::ProcessExit,
};
use super::{
event_line::EventLine,
partial_line::PartialLine,
query::{Query, QueryResult},
theme::THEME,
};
pub struct Event {
pub details: Arc<TracerEventDetails>,
pub status: Option<EventStatus>,
}
impl Event {
fn to_event_line(&self, list: &EventList) -> EventLine {
self.details.to_event_line(
&list.baseline,
false,
&list.modifier_args,
list.runtime_modifier(),
self.status,
true,
)
}
}
pub struct EventList {
state: ListState,
events: Vec<Event>,
event_lines: Vec<EventLine>,
window: (usize, usize),
list_cache: List<'static>,
should_refresh_list_cache: bool,
nr_items_in_window: usize,
horizontal_offset: usize,
inner_width: u16,
max_width: usize,
pub max_window_len: usize,
pub baseline: Arc<BaselineInfo>,
follow: bool,
pub modifier_args: ModifierArgs,
rt_modifier: RuntimeModifier,
query: Option<Query>,
query_result: Option<QueryResult>,
}
impl EventList {
pub fn new(baseline: Arc<BaselineInfo>, follow: bool, modifier_args: ModifierArgs) -> Self {
Self {
state: ListState::default(),
events: vec![],
event_lines: vec![],
window: (0, 0),
nr_items_in_window: 0,
horizontal_offset: 0,
inner_width: 0,
max_width: 0,
max_window_len: 0,
baseline,
follow,
should_refresh_list_cache: true,
list_cache: List::default(),
modifier_args,
rt_modifier: Default::default(),
query: None,
query_result: None,
}
}
pub fn runtime_modifier(&self) -> RuntimeModifier {
self.rt_modifier
}
pub fn is_env_in_cmdline(&self) -> bool {
self.rt_modifier.show_env
}
pub fn is_cwd_in_cmdline(&self) -> bool {
self.rt_modifier.show_cwd
}
pub fn is_following(&self) -> bool {
self.follow
}
pub fn toggle_follow(&mut self) {
self.follow = !self.follow;
}
pub fn stop_follow(&mut self) {
self.follow = false;
}
pub fn toggle_env_display(&mut self) {
self.rt_modifier.show_env = !self.rt_modifier.show_env;
for line in &mut self.event_lines {
if let Some(mask) = &mut line.env_mask {
mask.toggle(&mut line.line);
}
}
self.should_refresh_list_cache = true;
self.search();
}
pub fn toggle_cwd_display(&mut self) {
self.rt_modifier.show_cwd = !self.rt_modifier.show_cwd;
for line in &mut self.event_lines {
if let Some(mask) = &mut line.cwd_mask {
mask.toggle(&mut line.line);
}
}
self.should_refresh_list_cache = true;
self.search();
}
pub fn selection_index(&self) -> Option<usize> {
self.state.selected().map(|i| self.window.0 + i)
}
pub fn selection(&self) -> Option<&Event> {
self.selection_index().map(|i| &self.events[i])
}
pub fn set_window(&mut self, window: (usize, usize)) {
self.window = window;
self.should_refresh_list_cache = true;
}
pub fn get_window(&self) -> (usize, usize) {
self.window
}
pub fn window(items: &[EventLine], window: (usize, usize)) -> &[EventLine] {
&items[window.0..window.1.min(items.len())]
}
pub fn statistics(&self) -> Line {
let id = self.selection_index().unwrap_or(0);
Line::raw(format!(
"{}/{}──",
(id + 1).min(self.events.len()),
self.events.len()
))
.alignment(Right)
}
pub fn len(&self) -> usize {
self.events.len()
}
}
impl Widget for &mut EventList {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
self.inner_width = area.width - 2; let mut max_len = area.width as usize - 1;
let events_in_window = EventList::window(&self.event_lines, self.window);
self.nr_items_in_window = events_in_window.len();
if self.should_refresh_list_cache {
self.should_refresh_list_cache = false;
tracing::debug!("Refreshing list cache");
let items = self
.event_lines
.iter()
.enumerate()
.skip(self.window.0)
.take(self.window.1 - self.window.0)
.map(|(i, full_line)| {
max_len = max_len.max(full_line.line.width());
let highlighted = self
.query_result
.as_ref()
.is_some_and(|query_result| query_result.indices.contains_key(&i));
let mut base = full_line
.line
.clone()
.substring(self.horizontal_offset, area.width);
if highlighted {
base = base.style(THEME.search_match);
}
ListItem::from(base)
});
let list = List::new(items)
.highlight_style(
Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::DarkGray),
)
.highlight_symbol("➡️")
.highlight_spacing(HighlightSpacing::Always);
self.max_width = max_len;
self.list_cache = list;
}
StatefulWidgetRef::render_ref(&self.list_cache, area, buf, &mut self.state);
if self.max_width + 1 > area.width as usize {
let scrollbar = Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol("■");
let scrollbar_area = Rect {
x: area.x,
y: area.y + area.height,
width: area.width,
height: 1,
};
scrollbar.render(
scrollbar_area,
buf,
&mut ScrollbarState::new(self.max_width + 1 - area.width as usize)
.position(self.horizontal_offset),
);
}
if self.events.len() > area.height as usize {
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight);
let scrollbar_area = Rect {
x: area.x + area.width,
y: area.y,
width: 1,
height: area.height,
};
scrollbar.render(
scrollbar_area,
buf,
&mut ScrollbarState::new(self.events.len() - area.height as usize)
.position(self.window.0 + self.state.selected().unwrap_or(0)),
);
}
if let Some(query_result) = self.query_result.as_ref() {
let statistics = query_result.statistics();
let statistics_len = statistics.width();
if statistics_len > buf.area().width as usize {
return;
}
let statistics_area = Rect {
x: buf.area().right().saturating_sub(statistics_len as u16),
y: 1,
width: statistics_len as u16,
height: 1,
};
statistics.render(statistics_area, buf);
}
}
}
impl EventList {
pub fn set_query(&mut self, query: Option<Query>) {
if query.is_some() {
self.query = query;
self.search();
} else {
self.query = None;
self.query_result = None;
self.should_refresh_list_cache = true;
}
}
pub fn search(&mut self) {
let Some(query) = self.query.as_ref() else {
return;
};
let mut indices = IndexMap::new();
let searched_len = self.events.len();
for (i, evt) in self.event_lines.iter().enumerate() {
if query.matches(evt) {
indices.insert(i, 0);
}
}
let mut result = QueryResult {
indices,
searched_len,
selection: None,
};
result.next_result();
let selection = result.selection();
self.query_result = Some(result);
self.should_refresh_list_cache = true;
self.scroll_to(selection);
}
pub fn incremental_search(&mut self) {
let Some(query) = self.query.as_ref() else {
return;
};
let Some(existing_result) = self.query_result.as_mut() else {
self.search();
return;
};
let mut modified = false;
for (i, evt) in self
.event_lines
.iter()
.enumerate()
.skip(existing_result.searched_len)
{
if query.matches(evt) {
existing_result.indices.insert(i, 0);
modified = true;
}
}
existing_result.searched_len = self.event_lines.len();
if modified {
self.should_refresh_list_cache = true;
}
}
pub fn next_match(&mut self) {
if let Some(query_result) = self.query_result.as_mut() {
query_result.next_result();
let selection = query_result.selection();
self.scroll_to(selection);
self.stop_follow();
}
}
pub fn prev_match(&mut self) {
if let Some(query_result) = self.query_result.as_mut() {
query_result.prev_result();
let selection = query_result.selection();
self.scroll_to(selection);
self.stop_follow();
}
}
}
impl EventList {
pub fn push(&mut self, event: impl Into<Arc<TracerEventDetails>>) {
let event = event.into();
let event = Event {
status: match event.as_ref() {
TracerEventDetails::NewChild { .. } => Some(EventStatus::ProcessRunning),
TracerEventDetails::Exec(exec) => {
match exec.result {
0 => Some(EventStatus::ProcessRunning),
-2 => Some(EventStatus::ExecENOENT), _ => Some(EventStatus::ExecFailure),
}
}
_ => None,
},
details: event,
};
self.event_lines.push(event.to_event_line(self));
self.events.push(event);
self.incremental_search();
if (self.window.0..self.window.1).contains(&(self.events.len() - 1)) {
self.should_refresh_list_cache = true;
}
}
pub fn update(&mut self, update: ProcessStateUpdateEvent) {
for i in update.ids {
let i = i as usize;
if let TracerEventDetails::Exec(exec) = self.events[i].details.as_ref() {
if exec.result != 0 {
continue;
}
}
self.events[i].status = match update.update {
ProcessStateUpdate::Exit(ProcessExit::Code(0)) => Some(EventStatus::ProcessExitedNormally),
ProcessStateUpdate::Exit(ProcessExit::Code(c)) => {
Some(EventStatus::ProcessExitedAbnormally(c))
}
ProcessStateUpdate::Exit(ProcessExit::Signal(Signal::Standard(signal::SIGTERM))) => {
Some(EventStatus::ProcessTerminated)
}
ProcessStateUpdate::Exit(ProcessExit::Signal(Signal::Standard(signal::SIGKILL))) => {
Some(EventStatus::ProcessKilled)
}
ProcessStateUpdate::Exit(ProcessExit::Signal(Signal::Standard(signal::SIGINT))) => {
Some(EventStatus::ProcessInterrupted)
}
ProcessStateUpdate::Exit(ProcessExit::Signal(Signal::Standard(signal::SIGSEGV))) => {
Some(EventStatus::ProcessSegfault)
}
ProcessStateUpdate::Exit(ProcessExit::Signal(Signal::Standard(signal::SIGABRT))) => {
Some(EventStatus::ProcessAborted)
}
ProcessStateUpdate::Exit(ProcessExit::Signal(Signal::Standard(signal::SIGILL))) => {
Some(EventStatus::ProcessIllegalInstruction)
}
ProcessStateUpdate::Exit(ProcessExit::Signal(s)) => Some(EventStatus::ProcessSignaled(s)),
ProcessStateUpdate::BreakPointHit { .. } => Some(EventStatus::ProcessPaused),
ProcessStateUpdate::Resumed => Some(EventStatus::ProcessRunning),
ProcessStateUpdate::Detached { .. } => Some(EventStatus::ProcessDetached),
_ => unimplemented!(),
};
self.event_lines[i] = self.events[i].to_event_line(self);
if self.window.0 <= i && i < self.window.1 {
self.should_refresh_list_cache = true;
}
}
}
pub fn rebuild_lines(&mut self) {
self.event_lines = self
.events
.iter()
.map(|evt| evt.to_event_line(self))
.collect();
self.should_refresh_list_cache = true;
}
}
impl EventList {
fn scroll_to(&mut self, index: Option<usize>) {
let Some(index) = index else {
return;
};
if index < self.window.0 {
self.window.0 = index;
self.window.1 = self.window.0 + self.max_window_len;
self.should_refresh_list_cache = true;
self.state.select(Some(0));
} else if index >= self.window.1 {
self.window.0 = index.min(self.events.len().saturating_sub(self.max_window_len));
self.window.1 = self.window.0 + self.max_window_len;
self.should_refresh_list_cache = true;
self.state.select(Some(index - self.window.0));
} else {
self.state.select(Some(index - self.window.0));
}
}
#[allow(dead_code)]
fn last_item_in_window_absolute(&self) -> Option<usize> {
if self.events.is_empty() {
return None;
}
Some(
self
.window
.1
.saturating_sub(1)
.min(self.events.len().saturating_sub(1)),
)
}
fn last_item_in_window_relative(&self) -> Option<usize> {
if !self.events.is_empty() {
Some(
self
.window
.1
.min(self.events.len())
.saturating_sub(self.window.0)
.saturating_sub(1),
)
} else {
None
}
}
fn select_last(&mut self) {
if !self.events.is_empty() {
self.state.select(self.last_item_in_window_relative());
}
}
fn select_first(&mut self) {
if !self.events.is_empty() {
self.state.select(Some(0));
}
}
pub fn next_window(&mut self) -> bool {
if self.events.is_empty() {
return false;
}
if self.window.1 < self.events.len() {
self.window.0 += 1;
self.window.1 += 1;
self.should_refresh_list_cache = true;
true
} else {
false
}
}
pub fn previous_window(&mut self) -> bool {
if self.window.0 > 0 {
self.window.0 -= 1;
self.window.1 -= 1;
self.should_refresh_list_cache = true;
true
} else {
false
}
}
pub fn next(&mut self) {
let i = match self.state.selected() {
Some(i) => Some(
if i >= self.window.1 - self.window.0 - 1 {
self.next_window();
i
} else {
i + 1
}
.min(self.nr_items_in_window.saturating_sub(1)),
),
None => {
if !self.events.is_empty() {
Some(0)
} else {
None
}
}
};
self.state.select(i);
}
pub fn previous(&mut self) {
let i = match self.state.selected() {
Some(i) => Some(if i == 0 {
self.previous_window();
i
} else {
i - 1
}),
None => {
if !self.events.is_empty() {
Some(0)
} else {
None
}
}
};
self.state.select(i);
}
pub fn page_down(&mut self) {
if self.window.1 + self.max_window_len <= self.events.len() {
self.window.0 += self.max_window_len;
self.window.1 += self.max_window_len;
self.should_refresh_list_cache = true;
} else {
let old_window = self.window;
self.window.0 = self.events.len().saturating_sub(self.max_window_len);
self.window.1 = self.window.0 + self.max_window_len;
self.should_refresh_list_cache = old_window != self.window;
}
self.state.select(self.last_item_in_window_relative());
}
pub fn page_up(&mut self) {
if self.window.0 >= self.max_window_len {
self.window.0 -= self.max_window_len;
self.window.1 -= self.max_window_len;
self.should_refresh_list_cache = true;
} else {
let old_window = self.window;
self.window.0 = 0;
self.window.1 = self.window.0 + self.max_window_len;
self.should_refresh_list_cache = old_window != self.window;
}
self.select_first();
}
pub fn page_left(&mut self) {
let old_offset = self.horizontal_offset;
self.horizontal_offset = self
.horizontal_offset
.saturating_sub(self.inner_width as usize);
if self.horizontal_offset != old_offset {
self.should_refresh_list_cache = true;
}
}
pub fn page_right(&mut self) {
let old_offset = self.horizontal_offset;
self.horizontal_offset = (self.horizontal_offset + self.inner_width as usize)
.min(self.max_width.saturating_sub(self.inner_width as usize));
if self.horizontal_offset != old_offset {
self.should_refresh_list_cache = true;
}
}
pub fn scroll_left(&mut self) {
if self.horizontal_offset > 0 {
self.horizontal_offset = self.horizontal_offset.saturating_sub(1);
self.should_refresh_list_cache = true;
tracing::trace!(
"scroll_left: should_refresh_list_cache = {}",
self.should_refresh_list_cache
);
}
}
pub fn scroll_right(&mut self) {
let new_offset =
(self.horizontal_offset + 1).min(self.max_width.saturating_sub(self.inner_width as usize));
if new_offset != self.horizontal_offset {
self.horizontal_offset = new_offset;
self.should_refresh_list_cache = true;
tracing::trace!(
"scroll_right: should_refresh_list_cache = {}",
self.should_refresh_list_cache
);
}
}
pub fn scroll_to_top(&mut self) {
let old_window = self.window;
self.window.0 = 0;
self.window.1 = self.max_window_len;
self.should_refresh_list_cache = old_window != self.window;
self.select_first();
}
pub fn scroll_to_bottom(&mut self) {
if self.events.is_empty() {
return;
}
let old_window = self.window;
self.window.0 = self.events.len().saturating_sub(self.max_window_len);
self.window.1 = self.window.0 + self.max_window_len;
self.select_last();
self.should_refresh_list_cache = old_window != self.window;
}
pub fn scroll_to_start(&mut self) {
if self.horizontal_offset > 0 {
self.horizontal_offset = 0;
self.should_refresh_list_cache = true;
tracing::trace!(
"scroll_to_start: should_refresh_list_cache = {}",
self.should_refresh_list_cache
);
}
}
pub fn scroll_to_end(&mut self) {
let new_offset = self.max_width.saturating_sub(self.inner_width as usize);
if self.horizontal_offset < new_offset {
self.horizontal_offset = new_offset;
self.should_refresh_list_cache = true;
tracing::trace!(
"scroll_to_end: should_refresh_list_cache = {}",
self.should_refresh_list_cache
);
}
}
}