use ratatui::Frame;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use crate::app::App;
use crate::layout::PaneId;
use crate::pane::{DebugPane, DebugSection, Pane};
use crate::ui::theme;
pub fn draw(frame: &mut Frame, app: &App, pane_id: PaneId, area: Rect) {
let Some(Pane::Debug(p)) = app.panes.get(pane_id) else {
return;
};
let t = theme::cur();
let chunks = Layout::vertical([
Constraint::Length(1),
Constraint::Percentage(35),
Constraint::Percentage(35),
Constraint::Min(1),
])
.split(area);
draw_header(frame, app, chunks[0]);
draw_stack(frame, app, p, chunks[1]);
draw_variables(frame, app, p, chunks[2]);
draw_output(frame, app, p, chunks[3]);
let _ = t;
}
fn draw_header(frame: &mut Frame, app: &App, area: Rect) {
let t = theme::cur();
let (label, fg) = match app.dap.as_ref() {
None => ("no session".to_string(), t.comment),
Some(mgr) if !mgr.initialized => ("initializing…".to_string(), t.yellow),
Some(mgr) if mgr.stopped_at.is_some() => {
let reason = mgr
.stopped_at
.as_ref()
.map(|s| s.3.as_str())
.unwrap_or("stopped");
(format!("● stopped ({reason})"), t.red)
}
Some(_) => ("▶ running".to_string(), t.green),
};
let thread = app
.dap_thread
.map(|t| format!(" · thread {t}"))
.unwrap_or_default();
let line = Line::from(vec![
Span::styled(" Debug ", Style::default().fg(t.bg_dark).bg(t.cyan)),
Span::raw(" "),
Span::styled(label, Style::default().fg(fg).add_modifier(Modifier::BOLD)),
Span::styled(thread, Style::default().fg(t.comment)),
]);
frame.render_widget(
Paragraph::new(line).style(Style::default().bg(t.bg_dark)),
area,
);
}
fn draw_stack(frame: &mut Frame, app: &App, p: &DebugPane, area: Rect) {
let t = theme::cur();
let frames: &[crate::dap::StackFrame] = app
.dap
.as_ref()
.map(|m| m.stack_frames.as_slice())
.unwrap_or(&[]);
let body_h = area.height.saturating_sub(1) as usize;
let mut lines: Vec<Line> = Vec::with_capacity(body_h + 1);
let focused = p.section == DebugSection::Stack;
let title_fg = if focused { t.yellow } else { t.fg };
lines.push(Line::from(Span::styled(
" Call stack",
Style::default()
.fg(title_fg)
.bg(t.bg_dark)
.add_modifier(Modifier::BOLD),
)));
if frames.is_empty() {
lines.push(Line::from(Span::styled(
" (no frames — start a session + hit a breakpoint)",
Style::default().fg(t.comment).bg(t.bg_dark),
)));
} else {
for (i, f) in frames.iter().enumerate().skip(p.scroll).take(body_h) {
let sel = i == p.selected;
let (bg, fg, marker) = if sel && focused {
(t.cyan, t.bg_darker, "▶ ")
} else if sel {
(t.bg2, t.fg, "▶ ")
} else {
(t.bg_dark, t.fg, " ")
};
let src = f
.source
.as_ref()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "(no source)".into());
let line = format!("{marker}{}:{} {}", src, f.line, f.name);
lines.push(Line::from(Span::styled(
line,
Style::default().fg(fg).bg(bg),
)));
}
}
frame.render_widget(
Paragraph::new(lines).style(Style::default().bg(t.bg_dark)),
area,
);
}
fn draw_variables(frame: &mut Frame, app: &App, p: &DebugPane, area: Rect) {
let t = theme::cur();
let rows = app
.dap
.as_ref()
.map(|m| m.variable_rows())
.unwrap_or_default();
let mut lines: Vec<Line> = Vec::new();
let focused = p.section == DebugSection::Variables;
let title_fg = if focused { t.yellow } else { t.fg };
let watch_count = app.dap_watches.len();
let title = if watch_count > 0 {
format!(
" Variables ({watch_count} watch{}) (Tab · Enter expands · y yank · w +watch · s set)",
if watch_count == 1 { "" } else { "es" }
)
} else {
" Variables (Tab to focus · Enter expands · y yank · w +watch · s set)".to_string()
};
lines.push(Line::from(Span::styled(
title,
Style::default()
.fg(title_fg)
.bg(t.bg_dark)
.add_modifier(Modifier::BOLD),
)));
let max_w = area.width.saturating_sub(1) as usize;
for expr in &app.dap_watches {
let prefix = "👁 ";
let value = match app.dap_watch_results.get(expr) {
Some(r) if r.err.is_some() => {
format!("err: {}", r.err.as_deref().unwrap_or(""))
}
Some(r) if !r.value.is_empty() => {
if let Some(ty) = &r.ty {
format!("{} : {}", r.value, ty)
} else {
r.value.clone()
}
}
_ => "(no value)".to_string(),
};
let mut text = format!("{prefix}{expr} = {value}");
if text.chars().count() > max_w {
text = text
.chars()
.take(max_w.saturating_sub(1))
.collect::<String>()
+ "…";
}
let err = app.dap_watch_results.get(expr).and_then(|r| r.err.clone());
let fg = if err.is_some() { t.red } else { t.cyan };
lines.push(Line::from(Span::styled(
text,
Style::default().fg(fg).bg(t.bg_dark),
)));
}
if !app.dap_watches.is_empty() {
lines.push(Line::from(Span::styled(
" ──────────────",
Style::default().fg(t.comment).bg(t.bg_dark),
)));
}
let remaining = area
.height
.saturating_sub(1)
.saturating_sub(lines.len().saturating_sub(1) as u16);
let scope_body_h = remaining as usize;
if rows.is_empty() {
let hint = if app.dap.is_some() {
" (waiting for stopped state…)"
} else {
" (no session — start one with dap.run)"
};
lines.push(Line::from(Span::styled(
hint,
Style::default().fg(t.comment).bg(t.bg_dark),
)));
} else {
for (i, row) in rows
.iter()
.enumerate()
.skip(p.vars_scroll)
.take(scope_body_h)
{
let sel = i == p.vars_selected;
let (bg, fg) = if sel && focused {
(t.cyan, t.bg_darker)
} else if sel {
(t.bg2, t.fg)
} else {
(t.bg_dark, t.fg)
};
let indent = " ".repeat(row.depth);
let chevron = if row.expandable {
format!("{} ", crate::ui::expand_glyph(app, row.expanded))
} else {
" ".to_string()
};
let mut text = format!("{indent}{chevron}{}", row.label);
if !row.value.is_empty() {
text.push_str(" = ");
text.push_str(&row.value);
}
let max = area.width.saturating_sub(1) as usize;
if text.chars().count() > max {
text = text.chars().take(max.saturating_sub(1)).collect::<String>() + "…";
}
let modifier = if row.is_scope {
Modifier::BOLD
} else {
Modifier::empty()
};
lines.push(Line::from(Span::styled(
text,
Style::default().fg(fg).bg(bg).add_modifier(modifier),
)));
}
}
frame.render_widget(
Paragraph::new(lines).style(Style::default().bg(t.bg_dark)),
area,
);
}
fn draw_output(frame: &mut Frame, app: &App, p: &DebugPane, area: Rect) {
let t = theme::cur();
let body_h = area.height.saturating_sub(1) as usize;
let log = &app.dap_output_log;
let mut lines: Vec<Line> = Vec::with_capacity(body_h + 1);
lines.push(Line::from(Span::styled(
" Output",
Style::default()
.fg(t.fg)
.bg(t.bg_dark)
.add_modifier(Modifier::BOLD),
)));
let total = log.len();
let want = body_h.min(total);
let start = total.saturating_sub(want).saturating_sub(p.output_scroll);
let end = (start + want).min(total);
for (cat, text) in &log[start..end] {
let fg = match cat.as_str() {
"stderr" | "important" => t.red,
"stdout" | "console" => t.fg,
"telemetry" => t.comment,
_ => t.grey_fg,
};
let mut s = text.clone();
let max = area.width.saturating_sub(1) as usize;
if s.chars().count() > max {
s = s.chars().take(max.saturating_sub(1)).collect::<String>() + "…";
}
lines.push(Line::from(Span::styled(
s,
Style::default().fg(fg).bg(t.bg_dark),
)));
}
frame.render_widget(
Paragraph::new(lines).style(Style::default().bg(t.bg_dark)),
area,
);
}