pub mod port;
use crate::debugger::Error;
use crate::ui::command;
use crate::ui::command::r#break::BreakpointIdentity;
use crate::ui::command::watch::WatchpointIdentity;
use crate::ui::command::{CommandError, r#break, run, watch};
use crate::ui::proto::ClientExchanger;
use crate::ui::tui::app::port::{
AsyncResponsesPort, DebuggerEventQueue, DebuggerEventsPort, LoggerPort, OutputPort, UserEvent,
};
use crate::ui::tui::components::asm::Asm;
use crate::ui::tui::components::breakpoint::Breakpoints;
use crate::ui::tui::components::control::GlobalControl;
use crate::ui::tui::components::input::{Input, InputStringType};
use crate::ui::tui::components::logs::Logs;
use crate::ui::tui::components::oracle::make_oracle_tab_window;
use crate::ui::tui::components::output::Output;
use crate::ui::tui::components::popup::{Popup, YesNoLabels};
use crate::ui::tui::components::source::Source;
use crate::ui::tui::components::status::Status;
use crate::ui::tui::components::threads::Threads;
use crate::ui::tui::components::variables::Variables;
use crate::ui::tui::utils::logger::TuiLogLine;
use crate::ui::tui::utils::tab;
use crate::ui::tui::utils::tab::TabWindow;
use anyhow::anyhow;
use chumsky::Parser;
use log::warn;
use std::borrow::Cow;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tuirealm::listener::SyncPort;
use tuirealm::props::{PropPayload, PropValue, TextSpan};
use tuirealm::ratatui::layout::Alignment;
use tuirealm::ratatui::layout::{Constraint, Direction, Layout};
use tuirealm::ratatui::style::Color;
use tuirealm::terminal::{CrosstermTerminalAdapter, TerminalBridge};
use tuirealm::{
Application, AttrValue, Attribute, EventListenerCfg, Sub, SubClause, SubEventClause, props,
};
use super::{BreakpointsAddType, ConfirmedAction, DebugeeStreamBuffer, Id, Msg};
pub struct Model {
pub app: Application<Id, Msg, UserEvent>,
pub quit: bool,
pub redraw: bool,
pub terminal: TerminalBridge<CrosstermTerminalAdapter>,
exchanger: Arc<ClientExchanger>,
tabs_layout: [Constraint; 2],
}
impl Model {
const DEFAULT_TABS_LAYOUT: [Constraint; 2] =
[Constraint::Percentage(25), Constraint::Percentage(75)];
const LEFT_TAB_FOCUS_LAYOUT: [Constraint; 2] =
[Constraint::Percentage(90), Constraint::Percentage(10)];
const RIGHT_TAB_FOCUS_LAYOUT: [Constraint; 2] =
[Constraint::Percentage(10), Constraint::Percentage(90)];
pub fn new(
output_buf: DebugeeStreamBuffer,
event_queue: DebuggerEventQueue,
client_exchanger: ClientExchanger,
log_buffer: Arc<Mutex<Vec<TuiLogLine>>>,
) -> anyhow::Result<Self> {
let exchanger = Arc::new(client_exchanger);
Ok(Self {
app: Self::init_app(output_buf, event_queue, exchanger.clone(), log_buffer)?,
quit: false,
redraw: true,
terminal: TerminalBridge::init_crossterm().expect("Cannot initialize terminal"),
exchanger,
tabs_layout: Self::DEFAULT_TABS_LAYOUT,
})
}
}
impl Model {
pub fn view(&mut self) {
_ = self.terminal.raw_mut().draw(|f| {
let input_in_focus = self.app.focus() == Some(&Id::Input);
let popup_in_focus = self.app.focus() == Some(&Id::Popup);
let mut constraints = vec![Constraint::Min(9), Constraint::Max(3)];
if input_in_focus {
constraints.push(Constraint::Max(3));
}
let main_chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints(constraints)
.split(f.area());
let tabs_rect = main_chunks[0];
let tab_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(self.tabs_layout)
.split(tabs_rect);
self.app.view(&Id::LeftTabs, f, tab_chunks[0]);
self.app.view(&Id::RightTabs, f, tab_chunks[1]);
if input_in_focus {
self.app.view(&Id::Input, f, main_chunks[1]);
self.app.view(&Id::Status, f, main_chunks[2]);
} else {
self.app.view(&Id::Status, f, main_chunks[1]);
}
if popup_in_focus {
self.app.view(&Id::Popup, f, f.area());
}
});
}
fn init_app(
output_buf: DebugeeStreamBuffer,
event_queue: DebuggerEventQueue,
exchanger: Arc<ClientExchanger>,
log_buffer: Arc<Mutex<Vec<TuiLogLine>>>,
) -> anyhow::Result<Application<Id, Msg, UserEvent>> {
let mut app: Application<Id, Msg, UserEvent> = Application::init(
EventListenerCfg::default()
.crossterm_input_listener(Duration::from_millis(20), 3)
.port(SyncPort::new(
Box::new(OutputPort::new(output_buf.data.clone())),
Duration::from_millis(10),
1,
))
.port(SyncPort::new(
Box::new(DebuggerEventsPort::new(event_queue)),
Duration::from_millis(10),
1,
))
.port(SyncPort::new(
Box::new(AsyncResponsesPort::new(exchanger.clone())),
Duration::from_millis(10),
1,
))
.port(SyncPort::new(
Box::new(LoggerPort::new(log_buffer)),
Duration::from_millis(10),
1,
))
.poll_timeout(Duration::from_millis(10))
.tick_interval(Duration::from_millis(200)),
);
let pid = exchanger
.request_sync(|dbg| dbg.process().pid())
.expect("messaging enabled at tui start");
app.mount(
Id::GlobalControl,
Box::new(GlobalControl::new(exchanger.clone(), pid)),
GlobalControl::subscriptions(),
)?;
app.mount(Id::Popup, Box::<Popup>::default(), vec![])?;
app.mount(Id::Input, Box::<Input>::default(), vec![])?;
let mb_err = exchanger
.request_sync(|dbg| run::Handler::new(dbg).handle(run::Command::DryStart))
.expect("messaging enabled at tui start");
let already_run = matches!(mb_err.err(), Some(CommandError::Handle(Error::AlreadyRun)));
let output = output_buf.data.lock().unwrap().clone();
let oracles: Vec<_> = exchanger
.request_sync(|dbg| dbg.all_oracles_arc().collect())
.expect("messaging enabled at tui start");
app.mount(
Id::Status,
Box::new(Status::new(already_run)),
Status::subscriptions(),
)?;
let mut left_tab_sub = Variables::subscriptions();
left_tab_sub.extend(Threads::subscriptions());
left_tab_sub.extend(vec![Sub::new(SubEventClause::Tick, SubClause::Always)]);
let left_tab = TabWindow::new(
"[1]",
&["🔴 Breakpoints", "🧩 Variables", "🧵 Threads"],
vec![
Box::new(Breakpoints::new(exchanger.clone())),
Box::new(Variables::new(exchanger.clone())),
Box::new(Threads::new(exchanger.clone())),
],
Some(|rewind_direction| match rewind_direction {
tuirealm::command::Direction::Left => Msg::RightTabsInFocus {
reset_to: Some(props::Direction::Right),
},
tuirealm::command::Direction::Right => Msg::RightTabsInFocus {
reset_to: Some(props::Direction::Left),
},
_ => {
unreachable!()
}
}),
);
app.mount(Id::LeftTabs, Box::new(left_tab), left_tab_sub)?;
let mut right_tab_sub = Source::subscriptions();
right_tab_sub.extend(Asm::subscriptions());
right_tab_sub.extend(Output::subscriptions());
right_tab_sub.extend(vec![Sub::new(SubEventClause::Tick, SubClause::Always)]);
let right_tab = TabWindow::new(
"[2]",
&["</> Source", "📃 Output", "🤖 Asm", "🔮 Oracles", "💾 Logs"],
vec![
Box::new(Source::new(exchanger.clone())?),
Box::new(Output::new(&output)),
Box::new(Asm::new(exchanger.clone())?),
Box::new(make_oracle_tab_window(&oracles)),
Box::<Logs>::default(),
],
Some(|rewind_direction| match rewind_direction {
tuirealm::command::Direction::Left => Msg::LeftTabsInFocus {
reset_to: Some(props::Direction::Right),
},
tuirealm::command::Direction::Right => Msg::LeftTabsInFocus {
reset_to: Some(props::Direction::Left),
},
_ => {
unreachable!()
}
}),
);
app.mount(Id::RightTabs, Box::new(right_tab), right_tab_sub)?;
app.active(&Id::LeftTabs)?;
Ok(app)
}
}
impl Model {
fn update_breakpoints(&mut self) -> anyhow::Result<()> {
Ok(self.app.attr(
&Id::LeftTabs,
Attribute::Custom("update_breakpoints"),
AttrValue::Flag(true),
)?)
}
pub fn update(&mut self, msg: Option<Msg>) -> anyhow::Result<Option<Msg>> {
if let Some(msg) = msg {
self.redraw = true;
match msg {
Msg::AppClose => {
self.exchanger.send_exit();
self.quit = true;
}
Msg::AppRunning => {
self.app.attr(
&Id::Status,
Attribute::Text,
AttrValue::Payload(PropPayload::Vec(vec![PropValue::TextSpan(
TextSpan::new("running").fg(Color::Red),
)])),
)?;
}
Msg::SwitchUI => {
self.exchanger.send_switch_ui();
self.quit = true;
}
Msg::LeftTabsInFocus { reset_to } => {
self.app.active(&Id::LeftTabs)?;
if let Some(direction) = reset_to {
self.app.attr(
&Id::LeftTabs,
TabWindow::RESET_CHOICE_ATTR,
AttrValue::Direction(direction),
)?;
}
_ = self
.app
.attr(&Id::LeftTabs, Attribute::Focus, AttrValue::Flag(true));
}
Msg::RightTabsInFocus { reset_to } => {
self.app.active(&Id::RightTabs)?;
if let Some(direction) = reset_to {
self.app.attr(
&Id::RightTabs,
TabWindow::RESET_CHOICE_ATTR,
AttrValue::Direction(direction),
)?;
}
_ = self
.app
.attr(&Id::RightTabs, Attribute::Focus, AttrValue::Flag(true));
}
Msg::BreakpointAdd(r#type) => {
if !self.exchanger.is_messaging_enabled() {
warn!(target: "tui", "trying to add breakpoint but messaging is disabled");
return Ok(None);
}
let (input_validator, input_data_type): (fn(&str) -> bool, _) = match r#type {
BreakpointsAddType::AtLine => (
|s| -> bool {
command::parser::brkpt_at_line_parser()
.parse(s)
.into_result()
.is_ok()
},
InputStringType::BreakpointAddAtLine,
),
BreakpointsAddType::AtFunction => (
|s| -> bool {
command::parser::brkpt_at_fn()
.parse(s)
.into_result()
.is_ok()
},
InputStringType::BreakpointAddAtFunction,
),
BreakpointsAddType::AtAddress => (
|s| -> bool {
command::parser::brkpt_at_addr_parser()
.parse(s)
.into_result()
.is_ok()
},
InputStringType::BreakpointAddAtAddress,
),
BreakpointsAddType::Watchpoint => (
|s| -> bool {
command::parser::watchpoint_cond()
.then(
command::parser::watchpoint_at_dqe()
.or(command::parser::watchpoint_at_address()),
)
.parse(s)
.into_result()
.is_ok()
},
InputStringType::Watchpoint,
),
};
let title = if matches!(r#type, BreakpointsAddType::Watchpoint) {
"Add watchpoint".to_string()
} else {
"Add breakpoint".to_string()
};
self.app.attr(
&Id::Input,
Attribute::InputType,
AttrValue::InputType(props::InputType::Custom(
input_validator,
|_, _| -> bool { true },
)),
)?;
self.app.attr(
&Id::Input,
Attribute::Title,
AttrValue::Title((title, Alignment::Left)),
)?;
self.app.attr(
&Id::Input,
Attribute::Custom("input_data_type"),
AttrValue::String(input_data_type.to_string()),
)?;
self.app.active(&Id::Input)?;
self.app.lock_subs();
}
Msg::Input(input) => {
let input_data_type = InputStringType::from_str(
&self
.app
.query(&Id::Input, Attribute::Custom("input_data_type"))?
.expect("infallible")
.unwrap_string(),
)
.expect("infallible");
return match input_data_type {
InputStringType::BreakpointAddAtFunction
| InputStringType::BreakpointAddAtLine
| InputStringType::BreakpointAddAtAddress => {
let identity = match input_data_type {
InputStringType::BreakpointAddAtLine => {
let file_line: Vec<_> = input.split(':').collect();
let file = file_line[0];
let line: u64 = file_line[1].parse().expect("infallible");
BreakpointIdentity::Line(file.to_string(), line)
}
InputStringType::BreakpointAddAtFunction => {
BreakpointIdentity::Function(input.trim().to_string())
}
InputStringType::BreakpointAddAtAddress => {
let input = input.trim().to_lowercase();
let hex = input
.strip_prefix("0x")
.ok_or(anyhow!("invalid hex format"))?;
let addr = usize::from_str_radix(hex, 16)
.map_err(|e| anyhow!("invalid hex format: {e}"))?;
BreakpointIdentity::Address(addr)
}
_ => unreachable!(),
};
let cmd = r#break::Command::Add(identity);
self.exchanger
.request_sync(move |dbg| -> anyhow::Result<()> {
command::r#break::Handler::new(dbg).handle(&cmd)?;
Ok(())
})
.expect("messaging enabled")?;
self.app.unlock_subs();
self.app.blur()?;
self.update_breakpoints()?;
self.app.active(&Id::LeftTabs)?;
self.app.attr(
&Id::LeftTabs,
TabWindow::ACTIVATE_TAB,
AttrValue::Flag(true),
)?;
Ok(None)
}
InputStringType::Watchpoint => {
let (cond, identity) = command::parser::watchpoint_cond()
.then(
command::parser::watchpoint_at_dqe()
.or(command::parser::watchpoint_at_address()),
)
.parse(&input)
.into_result()
.expect("infallible");
let cmd = watch::Command::Add(identity, cond);
self.exchanger
.request_sync(move |dbg| -> anyhow::Result<()> {
command::watch::Handler::new(dbg).handle(cmd)?;
Ok(())
})
.expect("messaging enabled")?;
self.app.unlock_subs();
self.app.blur()?;
self.update_breakpoints()?;
self.app.active(&Id::LeftTabs)?;
self.app.attr(
&Id::LeftTabs,
TabWindow::ACTIVATE_TAB,
AttrValue::Flag(true),
)?;
Ok(None)
}
};
}
Msg::InputCancel => {
self.app.unlock_subs();
self.app.blur()?;
self.update_breakpoints()?;
self.app.active(&Id::LeftTabs)?;
self.app.attr(
&Id::LeftTabs,
TabWindow::ACTIVATE_TAB,
AttrValue::Flag(true),
)?;
}
Msg::UpdateBreakpointList => {
self.update_breakpoints()?;
}
Msg::ShowOkPopup(title, text) => {
if let Some(title) = title {
self.app
.attr(&Id::Popup, Attribute::Title, AttrValue::String(title))?;
}
self.app
.attr(&Id::Popup, Attribute::Text, AttrValue::String(text))?;
let (ok_attr, ok_attr_val) = Popup::ok_attrs();
self.app.attr(&Id::Popup, ok_attr, ok_attr_val)?;
self.app.active(&Id::Popup)?;
}
Msg::PopupOk => {
self.app.blur()?;
}
Msg::PopupConfirmDebuggerRestart => {
self.app.attr(
&Id::Popup,
Attribute::Text,
AttrValue::String("Restart a program?".to_string()),
)?;
let (attr, attr_val) = Popup::yes_no_attrs(YesNoLabels::default());
self.app.attr(&Id::Popup, attr, attr_val)?;
let action = ConfirmedAction::Restart;
self.app.attr(
&Id::Popup,
Attribute::Custom("action"),
AttrValue::String(action.to_string()),
)?;
self.app.active(&Id::Popup)?;
}
Msg::PopupBreakpoint(brkpt) => {
let place = &brkpt.place;
let file = place
.as_ref()
.map(|p| p.file.to_string_lossy())
.unwrap_or(Cow::from("unknown"));
let line = place
.as_ref()
.map(|p| p.line_number.to_string())
.unwrap_or("unknown".to_string());
let text = format!(
"Breakpoint #{}\nAt: {:?}:{}\nAddress: {}",
brkpt.number, file, line, brkpt.addr
);
self.app
.attr(&Id::Popup, Attribute::Text, AttrValue::String(text))?;
let (attr, attr_val) = Popup::yes_no_attrs(YesNoLabels::new("OK", "Remove"));
self.app.attr(&Id::Popup, attr, attr_val)?;
self.app.attr(
&Id::Popup,
Attribute::Custom("action"),
AttrValue::String(ConfirmedAction::RemoveBreakpoint.to_string()),
)?;
self.app.active(&Id::Popup)?;
}
Msg::PopupWatchpoint(wp) => {
let dqe = if let Some(ref dqe) = wp.source_dqe {
format!("For: {dqe}\n")
} else {
String::new()
};
let text = format!(
"Watchpoint #{}\n{dqe}Address: {}\nCondition: {}, size: {}",
wp.number, wp.address, wp.condition, wp.size
);
self.app
.attr(&Id::Popup, Attribute::Text, AttrValue::String(text))?;
let (attr, attr_val) = Popup::yes_no_attrs(YesNoLabels::new("OK", "Remove"));
self.app.attr(&Id::Popup, attr, attr_val)?;
self.app.attr(
&Id::Popup,
Attribute::Custom("action"),
AttrValue::String(ConfirmedAction::RemoveWatchpoint.to_string()),
)?;
self.app.active(&Id::Popup)?;
}
Msg::PopupYes(action) => match action {
ConfirmedAction::Restart => {
self.exchanger
.request_async(|dbg| {
Ok(run::Handler::new(dbg).handle(run::Command::Restart)?)
})
.expect("messaging enabled");
self.exchanger.disable_messaging();
self.app.blur()?;
return Ok(Some(Msg::AppRunning));
}
_ => {
self.app.blur()?;
}
},
Msg::PopupNo(action) => match action {
ConfirmedAction::RemoveBreakpoint => {
let brkpt_num = self.app.state(&Id::LeftTabs)?.unwrap_one().unwrap_u32();
self.exchanger
.request_sync(move |dbg| -> anyhow::Result<()> {
let cmd =
r#break::Command::Remove(BreakpointIdentity::Number(brkpt_num));
command::r#break::Handler::new(dbg).handle(&cmd)?;
Ok(())
})
.expect("messaging enabled")?;
self.app.blur()?;
self.update_breakpoints()?;
}
ConfirmedAction::RemoveWatchpoint => {
let wp_num = self.app.state(&Id::LeftTabs)?.unwrap_one().unwrap_u32();
self.exchanger
.request_sync(move |dbg| -> anyhow::Result<()> {
let cmd =
watch::Command::Remove(WatchpointIdentity::Number(wp_num));
command::watch::Handler::new(dbg).handle(cmd)?;
Ok(())
})
.expect("messaging enabled")?;
self.app.blur()?;
self.update_breakpoints()?;
}
_ => {
self.app.blur()?;
}
},
Msg::ExpandTab(tab_id) => {
debug_assert!(tab_id == Id::RightTabs || tab_id == Id::LeftTabs);
match tab_id {
Id::RightTabs
if self.tabs_layout == Self::DEFAULT_TABS_LAYOUT
|| self.tabs_layout == Self::LEFT_TAB_FOCUS_LAYOUT =>
{
self.app.attr(
&Id::RightTabs,
TabWindow::VIEW_SIZE_ATTR,
tab::ViewSize::Expand.into(),
)?;
self.app.attr(
&Id::LeftTabs,
TabWindow::VIEW_SIZE_ATTR,
tab::ViewSize::Compacted.into(),
)?;
self.tabs_layout = Self::RIGHT_TAB_FOCUS_LAYOUT;
}
Id::LeftTabs
if self.tabs_layout == Self::DEFAULT_TABS_LAYOUT
|| self.tabs_layout == Self::RIGHT_TAB_FOCUS_LAYOUT =>
{
self.app.attr(
&Id::LeftTabs,
TabWindow::VIEW_SIZE_ATTR,
tab::ViewSize::Expand.into(),
)?;
self.app.attr(
&Id::RightTabs,
TabWindow::VIEW_SIZE_ATTR,
tab::ViewSize::Compacted.into(),
)?;
self.tabs_layout = Self::LEFT_TAB_FOCUS_LAYOUT;
}
_ => {
for id in [&Id::LeftTabs, &Id::RightTabs] {
self.app.attr(
id,
TabWindow::VIEW_SIZE_ATTR,
tab::ViewSize::Default.into(),
)?;
}
self.tabs_layout = Self::DEFAULT_TABS_LAYOUT;
}
}
}
Msg::None => {}
}
}
Ok(None)
}
}