use ratatui::layout::Constraint;
use ratatui::prelude::Stylize;
use ratatui::widgets::{Block, Borders, Scrollbar, ScrollbarOrientation, Table};
use ratatui::{
Frame,
layout::{Layout, Rect},
style::Style,
widgets::Row,
};
use super::{BLUE, ORANGE, effective_mode, pane_block};
use crate::{Mode, State};
pub fn draw_symbols(state: &mut State, f: &mut Frame, area: Rect) {
if state.symbols_viewing_asm {
let horizontal =
Layout::horizontal([Constraint::Percentage(30), Constraint::Percentage(70)]);
let [left_panel, right_panel] = horizontal.areas(area);
draw_symbol_list(state, f, left_panel, true);
draw_symbol_asm(state, f, right_panel);
} else {
if state.symbols_search_active {
let vertical = Layout::vertical([Constraint::Fill(1), Constraint::Length(3)]);
let [list_area, search_area] = vertical.areas(area);
draw_symbol_list(state, f, list_area, false);
draw_search_input(state, f, search_area);
} else {
draw_symbol_list(state, f, area, false);
}
}
}
fn draw_symbol_list(state: &mut State, f: &mut Frame, area: Rect, viewing_asm: bool) {
let context = if !state.symbols_search_input.value().is_empty() {
Some(format!("filter: \"{}\"", state.symbols_search_input.value()))
} else {
None
};
let hints = if viewing_asm {
""
} else if state.symbols_search_active {
"⏎/Esc finish"
} else {
"/ search r refresh ⏎ disasm"
};
let active = matches!(effective_mode(state), Mode::OnlySymbols) && !viewing_asm;
let block = pane_block("Symbols", context, hints, active);
let header = Row::new(["Address", "Name"]).style(Style::new().fg(BLUE).bold());
let mut rows = vec![];
let filtered_symbols = state.get_filtered_symbols();
for (list_index, (_original_index, sym)) in filtered_symbols.iter().enumerate() {
let mut row = Row::new([format!("0x{:016x}", sym.address), sym.name.clone()]);
if list_index == state.symbols_selected {
row = row.style(Style::new().fg(ORANGE).bold());
}
rows.push(row);
}
let len = rows.len();
let max = area.height.saturating_sub(2);
state.symbols_viewport_height = max;
state.symbols_scroll.set_max_scroll(len.saturating_sub(max as usize));
let skip = state.symbols_scroll.scroll;
let rows: Vec<Row> = rows.into_iter().skip(skip).take(max as usize).collect();
let widths = [Constraint::Length(18), Constraint::Fill(1)];
let table = Table::new(rows, widths).header(header).block(block);
f.render_widget(table, area);
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight),
area,
&mut state.symbols_scroll.state,
);
}
fn draw_symbol_asm(state: &mut State, f: &mut Frame, area: Rect) {
let context =
if state.symbol_asm_name.is_empty() { None } else { Some(state.symbol_asm_name.clone()) };
let active = matches!(effective_mode(state), Mode::OnlySymbols);
let block = pane_block("Disassembly", context, "Esc back", active);
let header = Row::new(["Address", "Instruction"]).style(Style::new().fg(BLUE).bold());
let mut rows = vec![];
for asm in &state.symbol_asm {
let row = Row::new([format!("0x{:016x}", asm.address), asm.inst.clone()]);
rows.push(row);
}
let len = rows.len();
let max = area.height.saturating_sub(2);
state.symbol_asm_scroll.set_max_scroll(len.saturating_sub(max as usize));
let skip = state.symbol_asm_scroll.scroll;
let rows: Vec<Row> = rows.into_iter().skip(skip).take(max as usize).collect();
let widths = [Constraint::Length(18), Constraint::Fill(1)];
let table = Table::new(rows, widths).header(header).block(block);
f.render_widget(table, area);
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight),
area,
&mut state.symbol_asm_scroll.state,
);
}
fn draw_search_input(state: &State, f: &mut Frame, area: Rect) {
use ratatui::widgets::Paragraph;
let search_text = state.symbols_search_input.value();
let block = Block::default().borders(Borders::ALL).title("Search (fuzzy)".fg(ORANGE));
let width = area.width.saturating_sub(2) as usize;
let scroll = state.symbols_search_input.visual_scroll(width);
let paragraph = Paragraph::new(search_text).block(block).scroll((0, scroll as u16));
f.render_widget(paragraph, area);
let cursor_pos = state.symbols_search_input.visual_cursor();
f.set_cursor_position((area.x + 1 + (cursor_pos.saturating_sub(scroll)) as u16, area.y + 1));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Args, PtrSize, Symbol};
use ratatui::Terminal;
use ratatui::backend::TestBackend;
fn create_test_state() -> State {
let args = Args {
gdb_path: None,
remote: None,
ptr_size: PtrSize::Size64,
cmds: None,
log_path: None,
};
State::new(args)
}
fn create_test_symbols() -> Vec<Symbol> {
vec![
Symbol { address: 0x401000, name: "main".to_string(), needs_address_resolution: false },
Symbol { address: 0x401100, name: "foo".to_string(), needs_address_resolution: false },
Symbol { address: 0x401200, name: "bar".to_string(), needs_address_resolution: false },
]
}
#[test]
fn test_draw_symbols_empty() {
let mut state = create_test_state();
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let area = f.area();
draw_symbols(&mut state, f, area);
})
.unwrap();
}
#[test]
fn test_draw_symbols_with_data() {
let mut state = create_test_state();
state.symbols = create_test_symbols();
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let area = f.area();
draw_symbols(&mut state, f, area);
})
.unwrap();
}
#[test]
fn test_draw_symbols_viewing_asm() {
let mut state = create_test_state();
state.symbols = create_test_symbols();
state.symbols_viewing_asm = true;
state.symbol_asm = vec![crate::mi::Asm {
address: 0x401000,
inst: "push rbp".to_string(),
offset: 0,
func_name: Some("main".to_string()),
}];
let backend = TestBackend::new(120, 24);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let area = f.area();
draw_symbols(&mut state, f, area);
})
.unwrap();
}
#[test]
fn test_draw_symbols_search_active() {
let mut state = create_test_state();
state.symbols = create_test_symbols();
state.symbols_search_active = true;
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let area = f.area();
draw_symbols(&mut state, f, area);
})
.unwrap();
}
#[test]
fn test_draw_symbols_with_selection() {
let mut state = create_test_state();
state.symbols = create_test_symbols();
state.symbols_selected = 1;
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let area = f.area();
draw_symbols(&mut state, f, area);
})
.unwrap();
assert_eq!(state.symbols_selected, 1);
}
#[test]
fn test_draw_symbols_with_scroll() {
let mut state = create_test_state();
state.symbols = (0..100)
.map(|i| Symbol {
address: 0x400000 + (i * 0x10),
name: format!("func_{i}"),
needs_address_resolution: false,
})
.collect();
state.symbols_scroll.scroll = 10;
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let area = f.area();
draw_symbols(&mut state, f, area);
})
.unwrap();
assert_eq!(state.symbols_scroll.scroll, 10);
}
#[test]
fn test_draw_symbols_asm_scroll() {
let mut state = create_test_state();
state.symbols = create_test_symbols();
state.symbols_viewing_asm = true;
state.symbol_asm = (0..100)
.map(|i| crate::mi::Asm {
address: 0x401000 + i,
inst: format!("instruction_{i}"),
offset: i,
func_name: Some("main".to_string()),
})
.collect();
state.symbol_asm_scroll.scroll = 5;
let backend = TestBackend::new(120, 24);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let area = f.area();
draw_symbols(&mut state, f, area);
})
.unwrap();
assert_eq!(state.symbol_asm_scroll.scroll, 5);
}
#[test]
fn test_draw_symbols_with_filter() {
let mut state = create_test_state();
state.symbols = create_test_symbols();
state.symbols_search_input = tui_input::Input::new("ma".to_string());
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let area = f.area();
draw_symbols(&mut state, f, area);
})
.unwrap();
}
}