use miden_assembly_syntax::diagnostics::{IntoDiagnostic, Report};
use ratatui::{
crossterm::event::{KeyCode, KeyEvent},
prelude::*,
};
use tokio::sync::mpsc::UnboundedSender;
use crate::{
debug::{BreakpointType, ReadMemoryExpr},
ui::{
action::Action,
pages::Page,
panes::{
Pane, breakpoints::BreakpointsPane, disasm::DisassemblyPane,
source_code::SourceCodePane, stack::OperandStackPane, stacktrace::StackTracePane,
},
state::{InputMode, State},
tui::EventResponse,
},
};
#[derive(Default)]
pub struct Home {
command_tx: Option<UnboundedSender<Action>>,
panes: Vec<Box<dyn Pane>>,
focused_pane_index: usize,
fullscreen_pane_index: Option<usize>,
}
impl Home {
pub fn new() -> Result<Self, Report> {
let focused_border_style = Style::default().fg(Color::LightGreen);
Ok(Self {
command_tx: None,
panes: vec![
Box::new(SourceCodePane::new(true, focused_border_style)),
Box::new(DisassemblyPane::new(false, focused_border_style)),
Box::new(StackTracePane::new(false, focused_border_style)),
Box::new(OperandStackPane::new(false, focused_border_style)),
Box::new(BreakpointsPane::new(false, focused_border_style)),
],
focused_pane_index: 0,
fullscreen_pane_index: None,
})
}
}
impl Page for Home {
fn init(&mut self, state: &State) -> Result<(), Report> {
for pane in self.panes.iter_mut() {
pane.init(state)?;
}
Ok(())
}
fn focus(&mut self) -> Result<(), Report> {
if let Some(command_tx) = &self.command_tx {
const ARROW: &str = symbols::scrollbar::HORIZONTAL.end;
let status_line =
format!("[l,h {ARROW} pane movement] [: {ARROW} commands] [q {ARROW} quit]");
command_tx.send(Action::StatusLine(status_line)).into_diagnostic()?;
}
Ok(())
}
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<(), Report> {
self.command_tx = Some(tx);
Ok(())
}
fn update(&mut self, action: Action, state: &mut State) -> Result<Option<Action>, Report> {
let mut actions: Vec<Option<Action>> = vec![];
match action {
Action::Tick => {}
Action::FocusNext => {
let next_index = self.focused_pane_index.saturating_add(1) % self.panes.len();
if let Some(pane) = self.panes.get_mut(self.focused_pane_index) {
actions.push(pane.update(Action::UnFocus, state)?);
}
self.focused_pane_index = next_index;
if let Some(pane) = self.panes.get_mut(self.focused_pane_index) {
actions.push(pane.update(Action::Focus, state)?);
}
}
Action::FocusPrev => {
let prev_index =
self.focused_pane_index.saturating_add(self.panes.len() - 1) % self.panes.len();
if let Some(pane) = self.panes.get_mut(self.focused_pane_index) {
actions.push(pane.update(Action::UnFocus, state)?);
}
self.focused_pane_index = prev_index;
if let Some(pane) = self.panes.get_mut(self.focused_pane_index) {
actions.push(pane.update(Action::Focus, state)?);
}
}
Action::Update => {
for pane in self.panes.iter_mut() {
actions.push(pane.update(action.clone(), state)?);
}
}
Action::ToggleFullScreen => {
self.fullscreen_pane_index =
self.fullscreen_pane_index.map_or(Some(self.focused_pane_index), |_| None);
}
Action::FocusFooter(..) => {
if let Some(pane) = self.panes.get_mut(self.focused_pane_index) {
actions.push(pane.update(Action::UnFocus, state)?);
}
}
Action::FooterResult(cmd, Some(args)) if cmd.eq(":") => {
if let Some(pane) = self.panes.get_mut(self.focused_pane_index) {
pane.update(Action::Focus, state)?;
}
match args.split_once(' ') {
Some((cmd, rest)) => match cmd.trim() {
"b" | "break" | "breakpoint" => match rest.parse::<BreakpointType>() {
Ok(ty) => {
state.create_breakpoint(ty);
actions.push(Some(Action::TimedStatusLine(
"breakpoint created".to_string(),
1,
)));
}
Err(err) => {
actions.push(Some(Action::TimedStatusLine(err, 5)));
}
},
"r" | "read" => match rest.parse::<ReadMemoryExpr>() {
Ok(expr) => match state.read_memory(&expr) {
Ok(result) => actions.push(Some(Action::StatusLine(result))),
Err(err) => actions.push(Some(Action::TimedStatusLine(err, 5))),
},
Err(err) => actions.push(Some(Action::TimedStatusLine(err, 5))),
},
"vars" | "variables" | "locals" => {
let show_all = rest.trim() == "all";
let result = state.format_variables(show_all);
actions.push(Some(Action::StatusLine(result)));
}
_ => {
log::debug!("unknown command with arguments: '{cmd} {args}'");
actions.push(Some(Action::TimedStatusLine("unknown command".into(), 1)))
}
},
None => match args.trim() {
"q" | "quit" => actions.push(Some(Action::Quit)),
"r" | "reload" | "restart" => {
actions.push(Some(Action::Reload));
}
"nl" | "next-line" | "nextline" => {
if state.stopped && !state.executor().stopped {
state.create_breakpoint(BreakpointType::NextLine);
state.stopped = false;
actions.push(Some(Action::Continue));
} else if state.executor().stopped {
actions.push(Some(Action::TimedStatusLine(
"program has terminated, cannot continue".to_string(),
3,
)));
}
}
"debug" => {
actions.push(Some(Action::ShowDebug));
}
"vars" | "variables" | "locals" => {
let result = state.format_variables(false);
actions.push(Some(Action::StatusLine(result)));
}
"p" | "proc" | "where" => {
let live = state
.executor()
.current_asmop
.as_ref()
.map(|op| op.context_name().to_string());
let frame = state
.executor()
.callstack
.current_frame()
.and_then(|f| f.procedure(""))
.map(|p| p.to_string());
let msg = match (live, frame) {
(Some(l), Some(f)) if l == f => {
format!("proc: {l}")
}
(Some(l), Some(f)) => {
format!("proc (live): {l} / (frame): {f}")
}
(Some(l), None) => format!("proc (live): {l}"),
(None, Some(f)) => format!("proc (frame): {f}"),
(None, None) => "proc: <unknown>".to_string(),
};
actions.push(Some(Action::StatusLine(msg)));
}
invalid => {
log::debug!("unknown command: '{invalid}'");
actions.push(Some(Action::TimedStatusLine("unknown command".into(), 1)))
}
},
}
}
Action::FooterResult(_cmd, None) => {
if let Some(pane) = self.panes.get_mut(self.focused_pane_index) {
actions.push(pane.update(Action::Focus, state)?);
}
}
Action::Continue => {
#[cfg(feature = "dap")]
if state.debug_mode == crate::ui::state::DebugMode::Remote {
self.step_remote(state, &mut actions)?;
if let Some(tx) = &mut self.command_tx {
actions.into_iter().flatten().for_each(|action| {
tx.send(action).ok();
});
}
return Ok(None);
}
if state.executor().stopped {
actions.push(Some(Action::TimedStatusLine(
"program has terminated — use :r to restart".into(),
3,
)));
return Ok(None);
}
let start_cycle = state.executor().cycle;
let start_asmop = state.executor().current_asmop.clone();
let start_proc = state.current_procedure();
let start_line_loc = state.current_display_location();
let mut previous_proc = state.current_procedure();
let mut pending_called_breakpoints = Vec::new();
let mut breakpoints = core::mem::take(&mut state.breakpoints);
state.breakpoints_hit.clear();
state.stopped = false;
let stopped = loop {
if state.executor().stopped {
break true;
}
let mut consume_most_recent_finish = false;
match state.executor_mut().step() {
Ok(Some(exited)) if exited.should_break_on_exit() => {
consume_most_recent_finish = true;
}
Ok(_) => (),
Err(err) => {
state.set_execution_failed(err);
break true;
}
}
if breakpoints.is_empty() {
continue;
}
let (_op, is_op_boundary, proc, loc, line_loc) = {
let op = state.executor().current_op;
let is_boundary = state
.executor()
.current_asmop
.as_ref()
.map(|_info| true)
.unwrap_or(false);
let loc = state.current_location();
let line_loc = state.current_display_location();
let proc = state.current_procedure();
(op, is_boundary, proc, loc, line_loc)
};
let current_cycle = state.executor().cycle;
let cycles_stepped = current_cycle - start_cycle;
breakpoints.retain_mut(|bp| {
if let Some(n) = bp.cycles_to_skip(current_cycle) {
if cycles_stepped >= n {
let retained = !bp.is_one_shot();
if retained {
state.breakpoints_hit.push(bp.clone());
} else {
state.breakpoints_hit.push(core::mem::take(bp));
}
return retained;
} else {
return true;
}
}
if cycles_stepped > 0
&& is_op_boundary
&& matches!(&bp.ty, BreakpointType::Next)
&& state.executor().current_asmop != start_asmop
{
state.breakpoints_hit.push(core::mem::take(bp));
return false;
}
if cycles_stepped > 0
&& is_op_boundary
&& matches!(&bp.ty, BreakpointType::NextLine)
{
let has_source_context = start_line_loc.is_some() || line_loc.is_some();
let reached_next = if has_source_context {
State::is_next_source_line(
start_proc.as_deref(),
start_line_loc.as_ref(),
proc.as_deref(),
line_loc.as_ref(),
)
} else {
state.executor().current_asmop != start_asmop
};
if reached_next {
state.breakpoints_hit.push(core::mem::take(bp));
return false;
}
}
if let Some(loc) = loc.as_ref()
&& bp.should_break_at(loc)
{
let retained = !bp.is_one_shot();
if retained {
state.breakpoints_hit.push(bp.clone());
} else {
state.breakpoints_hit.push(core::mem::take(bp));
}
return retained;
}
if matches!(&bp.ty, BreakpointType::Called(_))
&& let Some(proc) = proc.as_deref()
{
let matched = bp.should_break_in(proc);
if !matched {
pending_called_breakpoints.retain(|id| *id != bp.id);
return true;
}
let was_matched = previous_proc
.as_deref()
.is_some_and(|previous| bp.should_break_in(previous));
let matched_at_start = start_proc
.as_deref()
.is_some_and(|start| bp.should_break_in(start));
let pending = pending_called_breakpoints.contains(&bp.id);
let entered_matching_proc = !was_matched && !matched_at_start;
if entered_matching_proc
&& state.executor().procedure_has_debug_vars(proc)
&& state.executor().last_debug_var_count == 0
{
if !pending {
pending_called_breakpoints.push(bp.id);
}
return true;
}
if entered_matching_proc
|| (pending && state.executor().last_debug_var_count > 0)
{
pending_called_breakpoints.retain(|id| *id != bp.id);
let retained = !bp.is_one_shot();
if retained {
state.breakpoints_hit.push(bp.clone());
} else {
state.breakpoints_hit.push(core::mem::take(bp));
}
return retained;
}
}
true
});
if consume_most_recent_finish
&& let Some(id) = breakpoints.iter().rev().find_map(|bp| {
if matches!(bp.ty, BreakpointType::Finish) {
Some(bp.id)
} else {
None
}
})
{
breakpoints.retain(|bp| bp.id != id);
break true;
}
if !state.breakpoints_hit.is_empty() {
break true;
}
previous_proc = proc;
};
state.breakpoints = breakpoints;
state.stopped = stopped;
if stopped && state.executor().stopped {
if let Some(err) = state.execution_failed() {
actions.push(Some(Action::StatusLine(err.to_string())));
} else {
actions.push(Some(Action::StatusLine(
"program terminated successfully".to_string(),
)));
}
}
for pane in self.panes.iter_mut() {
actions.push(pane.update(Action::Update, state)?);
}
}
Action::Reload => match state.reload() {
Ok(_) => {
for pane in self.panes.iter_mut() {
actions.push(pane.update(Action::Reload, state)?);
}
}
Err(err) => {
actions.push(Some(Action::TimedStatusLine(err.to_string(), 5)));
}
},
_ => {
if let Some(pane) = self.panes.get_mut(self.focused_pane_index) {
actions.push(pane.update(action, state)?);
}
}
}
if let Some(tx) = &mut self.command_tx {
actions.into_iter().flatten().for_each(|action| {
tx.send(action).ok();
});
}
Ok(None)
}
fn handle_key_events(
&mut self,
key: KeyEvent,
state: &mut State,
) -> Result<Option<EventResponse<Action>>, Report> {
match state.input_mode {
InputMode::Normal => {
let response = match key.code {
KeyCode::Right | KeyCode::Char('l') | KeyCode::Char('L') => {
EventResponse::Stop(Action::FocusNext)
}
KeyCode::Left | KeyCode::Char('h') | KeyCode::Char('H') => {
EventResponse::Stop(Action::FocusPrev)
}
KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => {
EventResponse::Stop(Action::Down)
}
KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => {
EventResponse::Stop(Action::Up)
}
KeyCode::Char('g') | KeyCode::Char('G') => EventResponse::Stop(Action::Go),
KeyCode::Backspace | KeyCode::Char('b') | KeyCode::Char('B') => {
EventResponse::Stop(Action::Back)
}
KeyCode::Char('f') | KeyCode::Char('F') => {
EventResponse::Stop(Action::ToggleFullScreen)
}
KeyCode::Char(c) if ('1'..='9').contains(&c) => {
EventResponse::Stop(Action::Tab(c.to_digit(10).unwrap_or(0) - 1))
}
KeyCode::Char(']') => EventResponse::Stop(Action::TabNext),
KeyCode::Char('[') => EventResponse::Stop(Action::TabPrev),
KeyCode::Char(':') => {
EventResponse::Stop(Action::FocusFooter(":".into(), None))
}
KeyCode::Char('q') => EventResponse::Stop(Action::Quit),
KeyCode::Char('e') => {
state.create_breakpoint(BreakpointType::Finish);
state.stopped = false;
EventResponse::Stop(Action::Continue)
}
KeyCode::Char('s') if state.stopped && !state.executor().stopped => {
state.create_breakpoint(BreakpointType::Step);
state.stopped = false;
EventResponse::Stop(Action::Continue)
}
KeyCode::Char('n') if state.stopped && !state.executor().stopped => {
state.create_breakpoint(BreakpointType::Next);
state.stopped = false;
EventResponse::Stop(Action::Continue)
}
KeyCode::Char('c') if state.stopped && !state.executor().stopped => {
state.stopped = false;
EventResponse::Stop(Action::Continue)
}
KeyCode::Char('c' | 's' | 'n') if state.stopped && state.executor().stopped => {
EventResponse::Stop(Action::TimedStatusLine(
"program has terminated, cannot continue".to_string(),
3,
))
}
KeyCode::Char('d') => EventResponse::Stop(Action::Delete),
_ => {
return Ok(None);
}
};
Ok(Some(response))
}
InputMode::Insert => Ok(None),
InputMode::Command => Ok(None),
}
}
fn draw(&mut self, frame: &mut Frame<'_>, area: Rect, state: &State) -> Result<(), Report> {
if let Some(fullscreen_pane_index) = self.fullscreen_pane_index {
self.panes[fullscreen_pane_index].draw(frame, area, state)?;
} else {
let outer_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Fill(3), Constraint::Fill(1)])
.split(area);
let left_panes = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
self.panes[0].height_constraint(),
self.panes[1].height_constraint(),
self.panes[2].height_constraint(),
])
.split(outer_layout[0]);
let right_panes = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
self.panes[3].height_constraint(),
self.panes[4].height_constraint(),
])
.split(outer_layout[1]);
self.panes[0].draw(frame, left_panes[0], state)?;
self.panes[1].draw(frame, left_panes[1], state)?;
self.panes[2].draw(frame, left_panes[2], state)?;
self.panes[3].draw(frame, right_panes[0], state)?;
self.panes[4].draw(frame, right_panes[1], state)?;
}
Ok(())
}
}
#[cfg(feature = "dap")]
impl Home {
fn step_remote(
&mut self,
state: &mut State,
actions: &mut Vec<Option<Action>>,
) -> Result<(), Report> {
use crate::exec::DapStopReason;
let result = state.step_remote();
match result {
Ok(DapStopReason::Stopped(_)) => state.stopped = true,
Ok(DapStopReason::Terminated) => {
state.executor_mut().stopped = true;
state.stopped = true;
actions.push(Some(Action::StatusLine("program terminated successfully".into())));
}
Ok(DapStopReason::Restarting) => {
state.stopped = true;
actions.push(Some(Action::StatusLine(
"server signaled Phase 2 restart during step".into(),
)));
}
Err(e) => {
state.executor_mut().stopped = true;
state.stopped = true;
actions.push(Some(Action::StatusLine(format!("DAP error: {e}"))));
}
}
for pane in self.panes.iter_mut() {
actions.push(pane.update(Action::Update, state)?);
}
Ok(())
}
}