Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions crates/cli/src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use ratatui::{

use super::{
state::{Panel, TuiState},
widgets::{controls, timeline},
widgets::{controls, inspector, timeline},
};

/// Restore the terminal on normal exit, I/O errors, and panic unwinding.
Expand Down Expand Up @@ -129,6 +129,10 @@ fn render(frame: &mut Frame<'_>, state: &TuiState) {
state.scroll_offset,
state.selected_panel == Panel::Timeline,
);
let detail_sections = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(6), Constraint::Min(0)])
.split(panels[1]);
let details = state.entries.get(state.scroll_offset).map_or_else(
|| "No recorded invocations. [q] quits.".to_owned(),
|entry| {
Expand All @@ -145,8 +149,15 @@ fn render(frame: &mut Frame<'_>, state: &TuiState) {
frame.render_widget(
Paragraph::new(details)
.wrap(Wrap { trim: true })
.block(panel("Inspector", state.selected_panel == Panel::Inspector)),
panels[1],
.block(panel("Invocation", state.selected_panel == Panel::Inspector)),
detail_sections[0],
);
inspector::render_inspector(
frame,
detail_sections[1],
&state.state_diff,
state.selected_invocation.as_ref(),
state.scroll_offset,
);
controls::render(frame, sections[2], state);
}
Expand Down Expand Up @@ -178,9 +189,23 @@ mod tests {
assert_eq!(state.scroll_offset, 0);
press(&mut state, KeyCode::Char('s'));
assert_eq!(state.entries[state.scroll_offset].function, "child");
assert_eq!(
state
.selected_invocation
.as_ref()
.map(|invocation| invocation.function_name.as_str()),
Some("child")
);
press(&mut state, KeyCode::Up);
press(&mut state, KeyCode::Char('o'));
assert_eq!(state.entries[state.scroll_offset].function, "sibling");
assert_eq!(
state
.selected_invocation
.as_ref()
.map(|invocation| invocation.function_name.as_str()),
Some("sibling")
);
press(&mut state, KeyCode::Char('j'));
assert_eq!(state.scroll_offset, 2);
assert_eq!(state.replay_status(), "Replay: End 3/3");
Expand Down
41 changes: 39 additions & 2 deletions crates/cli/src/tui/state.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use grat_core::types::trace::{ContractInvocation, ExecutionTrace, ResourceProfile};
use grat_core::types::trace::{ContractInvocation, ExecutionTrace, ResourceProfile, StateDiff};

pub struct TuiState {
pub tx_hash: String,
pub network: String,
pub ledger_sequence: u32,
pub selected_panel: Panel,
pub scroll_offset: usize,
pub selected_invocation: Option<ContractInvocation>,
pub state_diff: StateDiff,
pub entries: Vec<TraceEntry>,
pub invocations: Vec<ContractInvocation>,
pub resources: ResourceProfile,
Expand All @@ -20,8 +22,9 @@ pub struct TraceEntry {
pub is_error: bool,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Panel {
#[default]
Timeline,
Inspector,
Controls,
Expand All @@ -31,12 +34,15 @@ impl TuiState {
pub fn new(trace: ExecutionTrace) -> Self {
let mut entries = Vec::new();
append_invocations(&trace.invocations, 0, &mut entries);
let selected_invocation = invocation_at(&trace.invocations, 0).cloned();
Self {
tx_hash: trace.tx_hash,
network: trace.network,
ledger_sequence: trace.ledger_sequence,
selected_panel: Panel::Timeline,
scroll_offset: 0,
selected_invocation,
state_diff: trace.state_diff,
entries,
invocations: trace.invocations,
resources: trace.resource_profile,
Expand All @@ -56,10 +62,12 @@ impl TuiState {
.scroll_offset
.saturating_add(1)
.min(self.entries.len().saturating_sub(1));
self.update_selected_invocation();
}

pub fn previous(&mut self) {
self.scroll_offset = self.scroll_offset.saturating_sub(1);
self.update_selected_invocation();
}

/// Advance past descendants of the selected recorded invocation.
Expand All @@ -74,6 +82,11 @@ impl TuiState {
.skip(self.scroll_offset + 1)
.find(|(_, entry)| entry.depth <= current.depth)
.map_or(self.entries.len().saturating_sub(1), |(index, _)| index);
self.update_selected_invocation();
}

fn update_selected_invocation(&mut self) {
self.selected_invocation = invocation_at(&self.invocations, self.scroll_offset).cloned();
}

pub fn can_step(&self) -> bool {
Expand All @@ -94,6 +107,30 @@ impl TuiState {
}
}

fn invocation_at(
invocations: &[ContractInvocation],
target: usize,
) -> Option<&ContractInvocation> {
fn visit<'a>(
invocations: &'a [ContractInvocation],
target: &mut usize,
) -> Option<&'a ContractInvocation> {
for invocation in invocations {
if *target == 0 {
return Some(invocation);
}
*target -= 1;
if let Some(found) = visit(&invocation.sub_invocations, target) {
return Some(found);
}
}
None
}

let mut target = target;
visit(invocations, &mut target)
}

fn append_invocations(
invocations: &[ContractInvocation],
depth: usize,
Expand Down
Loading