use crate::{elf::Info, tui::state::State};
use ansi_to_tui::IntoText;
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Margin, Position, Rect},
style::{Color, Modifier, Style, Stylize},
text::{Line, Span, Text},
widgets::{
Block, Borders, Cell, Clear, Paragraph, Row, Scrollbar, ScrollbarOrientation,
ScrollbarState, Table, TableState, Tabs, Wrap,
},
Frame,
};
use tui_big_text::{BigTextBuilder, PixelSize};
use tui_input::Input;
use tui_popup::Popup;
use unicode_width::UnicodeWidthStr;
pub const MAIN_TABS: &[&str] = Tab::get_headers();
const STRINGS_HEADERS: &[&str] = &["Location", "String"];
const LIST_LIMIT: usize = 100;
pub const ELF_INFO_TABS: &[Info] = &[
Info::ProgramHeaders,
Info::SectionHeaders,
Info::Symbols,
Info::DynamicSymbols,
Info::Dynamics,
Info::Relocations,
];
#[derive(Clone, Copy, Debug, Eq, PartialEq, Default, clap::ValueEnum)]
pub enum Tab {
#[default]
General = 0,
StaticAnalysis = 1,
DynamicAnalysis = 2,
Strings = 3,
Hexdump = 4,
}
impl Tab {
const fn get_headers() -> &'static [&'static str] {
&["General", "Static", "Dynamic", "Strings", "Hexdump"]
}
}
impl From<usize> for Tab {
fn from(v: usize) -> Self {
match v {
0 => Self::General,
1 => Self::StaticAnalysis,
2 => Self::DynamicAnalysis,
3 => Self::Strings,
4 => Self::Hexdump,
_ => Self::default(),
}
}
}
pub fn render(state: &mut State, frame: &mut Frame) {
if !state.logo.is_rendered && state.analyzer.files.len() == 1 {
let area = frame.area();
let (logo_width, logo_height) = state.logo.get_size();
if logo_width < area.width && logo_height < area.height {
frame.render_widget(
&state.logo,
Rect::new(
area.width / 2 - logo_width / 2,
area.height / 2 - logo_height / 2,
logo_width,
logo_height,
),
);
state.logo.is_rendered = state.logo.init_time.elapsed().as_millis() > 500;
return;
}
}
let chunks = Layout::new(
Direction::Vertical,
[Constraint::Length(3), Constraint::Min(0)],
)
.direction(Direction::Vertical)
.margin(1)
.split(frame.area());
{
frame.render_widget(
Block::bordered()
.title(vec![
"|".fg(Color::Rgb(100, 100, 100)),
env!("CARGO_PKG_NAME").bold(),
"-".fg(Color::Rgb(100, 100, 100)),
env!("CARGO_PKG_VERSION").into(),
"|".fg(Color::Rgb(100, 100, 100)),
])
.title_alignment(Alignment::Center),
chunks[0],
);
let chunks = Layout::new(
Direction::Horizontal,
[Constraint::Percentage(50), Constraint::Percentage(50)],
)
.margin(1)
.split(chunks[0]);
let tabs = Tabs::new(MAIN_TABS.iter().map(|v| Line::from(*v)))
.select(state.tab as usize)
.style(Style::default().fg(Color::Cyan))
.highlight_style(
Style::default()
.add_modifier(Modifier::BOLD)
.fg(state.accent_color),
);
frame.render_widget(tabs, chunks[0]);
let mut files = Vec::new();
for (i, file) in state.analyzer.files.iter().enumerate() {
if i != state.analyzer.files.len() - 1 {
files.push(
file.file_name()
.map(|v| v.to_string_lossy().to_string())
.unwrap_or_else(|| "?".to_string())
.italic(),
);
files.push("→ ".fg(Color::Rgb(100, 100, 100)));
} else {
files.push(file.to_string_lossy().to_string().italic());
}
}
files.push(" ".into());
frame.render_widget(
Paragraph::new(Line::from(files)).alignment(Alignment::Right),
chunks[1],
)
}
match state.tab {
Tab::General => {
render_general_info(state, frame, chunks[1]);
}
Tab::StaticAnalysis => {
render_static_analysis(state, frame, chunks[1]);
}
Tab::DynamicAnalysis => {
render_dynamic_analysis(state, frame, chunks[1]);
}
Tab::Strings => {
render_strings(state, frame, chunks[1]);
}
Tab::Hexdump => {
{
let chunks = Layout::vertical([Constraint::Percentage(100), Constraint::Min(1)])
.split(chunks[1]);
state.analyzer.heh.render_frame(frame, chunks[0]);
}
frame.render_widget(Block::new().borders(Borders::BOTTOM), chunks[1])
}
}
render_key_bindings(state, frame, chunks[1]);
}
pub fn render_key_bindings(state: &mut State, frame: &mut Frame, rect: Rect) {
let chunks = Layout::vertical([Constraint::Percentage(100), Constraint::Min(1)]).split(rect);
let key_bindings = state.get_key_bindings();
let line = Line::from(
key_bindings
.iter()
.enumerate()
.flat_map(|(i, (keys, desc))| {
vec![
"[".fg(Color::Rgb(100, 100, 100)),
keys.yellow(),
"→ ".fg(Color::Rgb(100, 100, 100)),
Span::from(*desc),
"]".fg(Color::Rgb(100, 100, 100)),
if i != key_bindings.len() - 1 { " " } else { "" }.into(),
]
})
.collect::<Vec<Span>>(),
);
if line.width() as u16 > chunks[1].width.saturating_sub(25)
&& get_input_line(state).width() != 0
&& (state.tab != Tab::StaticAnalysis || state.tab != Tab::Hexdump)
{
return;
}
frame.render_widget(Paragraph::new(line.alignment(Alignment::Center)), chunks[1]);
}
pub fn render_general_info(state: &mut State, frame: &mut Frame, rect: Rect) {
let selected_index = state.list.state.selected().unwrap_or_default();
let items_len = state.list.items.len();
frame.render_widget(Block::bordered(), rect);
let area = Layout::new(
Direction::Vertical,
[
Constraint::Percentage(5),
Constraint::Length(7),
Constraint::Percentage(100),
],
)
.margin(1)
.split(rect);
let banner = BigTextBuilder::default()
.pixel_size(PixelSize::Sextant)
.lines([format!("{}.", env!("CARGO_PKG_NAME")).into()])
.build();
let banner_width = 34;
let banner_area = Layout::new(
Direction::Horizontal,
[
Constraint::Length((area[1].width.checked_sub(banner_width)).unwrap_or_default() / 2),
Constraint::Min(banner_width),
Constraint::Length((area[1].width.checked_sub(banner_width)).unwrap_or_default() / 2),
],
)
.split(area[1]);
frame.render_widget(banner, banner_area[1]);
frame.render_widget(
Paragraph::new(Text::from(vec![
Line::default(),
Line::default(),
Line::default(),
Line::from(vec![
"Analyze ELF binaries ".fg(state.accent_color),
"like a boss.".yellow().italic(),
]),
Line::from(
ratatui::symbols::line::HORIZONTAL
.repeat(33)
.fg(Color::Rgb(100, 100, 100)),
),
Line::from(env!("CARGO_PKG_REPOSITORY").italic()),
Line::from(vec![
"[".fg(Color::Rgb(100, 100, 100)),
"with ".into(),
"♥".cyan(),
" by ".into(),
"@orhun".cyan(),
"]".fg(Color::Rgb(100, 100, 100)),
]),
]))
.centered(),
banner_area[1],
);
let lines = if cfg!(target_os = "windows") {
vec![
Line::from("This feature is not implemented!"),
Line::from("See <https://github.com/orhun/binsider/issues/35>"),
]
} else {
vec![
Line::from(vec![
"Size".cyan(),
Span::raw(": ").fg(Color::Rgb(100, 100, 100)),
state.analyzer.file.size.to_string().fg(state.accent_color),
]),
Line::from(vec![
" ".into(),
"Blocks".cyan(),
Span::raw(": ").fg(Color::Rgb(100, 100, 100)),
state
.analyzer
.file
.blocks
.to_string()
.fg(state.accent_color),
" ".into(),
]),
Line::from(vec![
"Block Size".cyan(),
Span::raw(": ").fg(Color::Rgb(100, 100, 100)),
state
.analyzer
.file
.block_size
.to_string()
.fg(state.accent_color),
]),
Line::from(vec![
"Device".cyan(),
Span::raw(": ").fg(Color::Rgb(100, 100, 100)),
state.analyzer.file.links.to_string().fg(state.accent_color),
]),
Line::from(vec![
"Inode".cyan(),
Span::raw(": ").fg(Color::Rgb(100, 100, 100)),
state.analyzer.file.inode.to_string().fg(state.accent_color),
]),
Line::from(vec![
"Links".cyan(),
Span::raw(": ").fg(Color::Rgb(100, 100, 100)),
state.analyzer.file.links.to_string().fg(state.accent_color),
]),
Line::from(vec![
"Access".cyan(),
Span::raw(": ").fg(Color::Rgb(100, 100, 100)),
state
.analyzer
.file
.access
.mode
.to_string()
.fg(state.accent_color),
]),
Line::from(vec![
"Uid".cyan(),
Span::raw(": ").fg(Color::Rgb(100, 100, 100)),
state
.analyzer
.file
.access
.uid
.to_string()
.fg(state.accent_color),
]),
Line::from(vec![
"Gid".cyan(),
Span::raw(": ").fg(Color::Rgb(100, 100, 100)),
state
.analyzer
.file
.access
.gid
.to_string()
.fg(state.accent_color),
]),
Line::from(vec![
"Access".cyan(),
Span::raw(": ").fg(Color::Rgb(100, 100, 100)),
state
.analyzer
.file
.date
.access
.to_string()
.fg(state.accent_color),
]),
Line::from(vec![
"Modify".cyan(),
Span::raw(": ").fg(Color::Rgb(100, 100, 100)),
state
.analyzer
.file
.date
.modify
.to_string()
.fg(state.accent_color),
]),
Line::from(vec![
"Change".cyan(),
Span::raw(": ").fg(Color::Rgb(100, 100, 100)),
state
.analyzer
.file
.date
.change
.to_string()
.fg(state.accent_color),
]),
Line::from(vec![
"Birth".cyan(),
Span::raw(": ").fg(Color::Rgb(100, 100, 100)),
state
.analyzer
.file
.date
.birth
.to_string()
.fg(state.accent_color),
]),
]
};
let info_width = lines.iter().map(|v| v.width()).max().unwrap_or_default() as u16 + 2;
let rect = area[2].inner(Margin {
horizontal: 0,
vertical: 1,
});
let area = Layout::new(
Direction::Vertical,
if state.list.items.is_empty() {
vec![Constraint::Max(lines.len() as u16 + 2)]
} else if (lines.len() as u16).saturating_sub(2) < rect.height / 2 {
vec![
Constraint::Min(lines.len() as u16 + 2),
Constraint::Percentage(100),
]
} else {
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
},
)
.split(rect);
let info_area = Layout::new(
Direction::Horizontal,
[
Constraint::Length((area[0].width.checked_sub(info_width)).unwrap_or_default() / 2),
Constraint::Min(info_width),
Constraint::Length((area[0].width.checked_sub(info_width)).unwrap_or_default() / 2),
],
)
.split(area[0])[1];
let max_height = lines.len().saturating_sub(info_area.height as usize);
if max_height + 2 < state.general_scroll_index {
state.general_scroll_index = max_height + 2;
}
frame.render_widget(
Paragraph::new(lines)
.block(
Block::bordered()
.title(Line::from(vec![
"|".fg(Color::Rgb(100, 100, 100)),
"File".cyan(),
Span::raw(": ").fg(Color::Rgb(100, 100, 100)),
state
.analyzer
.file
.name
.to_string()
.fg(state.accent_color)
.bold(),
"|".fg(Color::Rgb(100, 100, 100)),
]))
.title_alignment(Alignment::Center)
.border_style(Style::default().fg(Color::Rgb(100, 100, 100))),
)
.scroll((state.general_scroll_index as u16, 0))
.wrap(Wrap { trim: true }),
info_area,
);
frame.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("↑"))
.end_symbol(Some("↓")),
info_area.inner(Margin {
vertical: 1,
horizontal: 0,
}),
&mut ScrollbarState::new(max_height).position(state.general_scroll_index),
);
if state.list.items.is_empty() {
return;
}
let max_row_width = state
.list
.items
.iter()
.map(|v| v.join(" ").len())
.max()
.unwrap_or_default() as u16
+ 5;
let table_area = Layout::new(
Direction::Horizontal,
[
Constraint::Length((area[1].width.checked_sub(max_row_width)).unwrap_or_default() / 2),
Constraint::Min(max_row_width),
Constraint::Length((area[1].width.checked_sub(max_row_width)).unwrap_or_default() / 2),
],
)
.split(area[1]);
let table_area = Layout::new(
Direction::Vertical,
[
Constraint::Min(state.list.items.len() as u16 + 3),
Constraint::Percentage(100),
],
)
.split(table_area[1])[0];
let items = state
.list
.items
.clone()
.into_iter()
.map(Row::new)
.collect::<Vec<Row>>();
frame.render_stateful_widget(
Table::new(
items.clone(),
&[
Constraint::Min(
state
.list
.items
.iter()
.map(|v| v[0].len())
.max()
.unwrap_or_default() as u16
+ 1,
),
Constraint::Percentage(100),
],
)
.header(Row::new(vec!["Library".bold(), "Path".bold()]))
.block(
Block::bordered()
.title(vec![
"|".fg(Color::Rgb(100, 100, 100)),
"Dependencies".fg(state.accent_color).bold(),
"|".fg(Color::Rgb(100, 100, 100)),
])
.title_alignment(Alignment::Center)
.border_style(Style::default().fg(Color::Rgb(100, 100, 100)))
.title_bottom(
if items_len != 0 {
Line::from(vec![
"|".fg(Color::Rgb(100, 100, 100)),
format!("{}/{}", selected_index.saturating_add(1), items_len)
.fg(state.accent_color)
.bold(),
"|".fg(Color::Rgb(100, 100, 100)),
])
} else {
Line::default()
}
.right_aligned(),
),
)
.row_highlight_style(Style::default().fg(Color::Green)),
table_area,
&mut state.list.state,
);
frame.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("↑"))
.end_symbol(Some("↓")),
table_area.inner(Margin {
vertical: 1,
horizontal: 0,
}),
&mut ScrollbarState::new(items.len())
.position(state.list.state.selected().unwrap_or_default()),
);
}
pub fn render_static_analysis(state: &mut State, frame: &mut Frame, rect: Rect) {
let headers: Vec<Line> = state
.analyzer
.elf
.info(&Info::FileHeaders)
.items()
.iter()
.map(|items| {
Line::from(vec![
Span::styled(items[0].to_string(), Style::default().fg(Color::Cyan)),
Span::raw(": ").fg(Color::Rgb(100, 100, 100)),
Span::styled(
items[1].to_string(),
Style::default().fg(state.accent_color),
),
])
})
.collect();
let mut notes = Vec::new();
for note in state.analyzer.elf.notes.inner.iter() {
notes.push(Line::from(vec![
"Notes in ".cyan(),
note.name.to_string().cyan().italic(),
]));
notes.push(Line::from(
note.header
.iter()
.map(|v| Span::raw(format!("{v} ")))
.collect::<Vec<Span>>(),
));
notes.push(Line::from(
note.text
.iter()
.map(|v| Span::raw(format!("{v} ")))
.collect::<Vec<Span>>(),
));
}
frame.render_widget(Block::bordered(), rect);
let chunks = Layout::new(
Direction::Vertical,
[Constraint::Percentage(50), Constraint::Percentage(50)],
)
.margin(1)
.split(rect);
{
let chunks = Layout::new(
Direction::Horizontal,
[Constraint::Percentage(50), Constraint::Percentage(50)],
)
.split(chunks[0]);
let max_height = headers.len().saturating_sub(chunks[0].height as usize);
if max_height + 3 < state.headers_scroll_index {
state.headers_scroll_index = max_height + 3;
}
frame.render_widget(
Paragraph::new(headers)
.block(
Block::bordered()
.title(vec![
"|".fg(Color::Rgb(100, 100, 100)),
"File Headers".fg(state.accent_color).bold(),
"|".fg(Color::Rgb(100, 100, 100)),
])
.border_style({
if state.block_index == 0 {
Style::default().fg(state.accent_color).bold()
} else {
Style::default().fg(Color::Rgb(100, 100, 100))
}
}),
)
.scroll((state.headers_scroll_index as u16, 0))
.wrap(Wrap { trim: true }),
chunks[0],
);
frame.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("↑"))
.end_symbol(Some("↓")),
chunks[0].inner(Margin {
vertical: 1,
horizontal: 0,
}),
&mut ScrollbarState::new(max_height).position(state.headers_scroll_index),
);
let max_height = notes.len().saturating_sub(chunks[0].height as usize);
if max_height + 2 < state.notes_scroll_index {
state.notes_scroll_index = max_height + 2;
}
frame.render_widget(
Paragraph::new(notes)
.block(
Block::bordered()
.title(vec![
"|".fg(Color::Rgb(100, 100, 100)),
"Notes".fg(state.accent_color).bold(),
"|".fg(Color::Rgb(100, 100, 100)),
])
.border_style(Style::default().fg(if state.block_index == 1 {
Color::Yellow
} else {
Color::Rgb(100, 100, 100)
})),
)
.scroll((state.notes_scroll_index as u16, 0))
.wrap(Wrap { trim: true }),
chunks[1],
);
frame.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("↑"))
.end_symbol(Some("↓")),
chunks[1].inner(Margin {
vertical: 1,
horizontal: 0,
}),
&mut ScrollbarState::new(max_height).position(state.notes_scroll_index),
);
}
{
let area = chunks[1];
let chunks = Layout::new(
Direction::Vertical,
[Constraint::Length(1), Constraint::Percentage(100)],
)
.split(area);
let tabs = Tabs::new(MAIN_TABS.iter().map(|v| Line::from(*v)))
.select(state.tab as usize)
.highlight_style(
Style::default()
.add_modifier(Modifier::BOLD)
.fg(state.accent_color),
);
frame.render_widget(tabs, chunks[0]);
let selected_index = state.list.state.selected().unwrap_or_default();
let items_len = state.list.items.len();
let page = selected_index / LIST_LIMIT;
let headers = ELF_INFO_TABS[state.info_index].headers();
let mut table_state = TableState::default();
table_state.select(Some(selected_index % LIST_LIMIT));
let max_row_width = (area.width as usize / headers.len()).saturating_sub(2);
let items = state
.list
.items
.iter()
.skip(page * LIST_LIMIT)
.take(LIST_LIMIT)
.map(|items| {
Row::new(items.iter().enumerate().map(|(i, value)| {
Cell::from(Line::from(if value.width() > max_row_width && i == 0 {
let mut spans = highlight_search_result(
value.chars().take(max_row_width).collect::<String>().into(),
&state.input,
);
spans.push("…".fg(Color::Rgb(100, 100, 100)));
spans
} else {
highlight_search_result(value.to_string().into(), &state.input)
}))
}))
});
frame.render_stateful_widget(
Table::new(
items,
&[Constraint::Percentage(
(100 / headers.len()).try_into().unwrap_or_default(),
)]
.repeat(headers.len()),
)
.header(Row::new(
headers.to_vec().iter().map(|v| Cell::from((*v).bold())),
))
.block(
Block::bordered()
.border_style(Style::default().fg(if state.block_index == 2 {
Color::Yellow
} else {
Color::Rgb(100, 100, 100)
}))
.title_bottom(
if items_len != 0 {
Line::from(vec![
"|".fg(Color::Rgb(100, 100, 100)),
format!("{}/{}", selected_index.saturating_add(1), items_len)
.fg(state.accent_color)
.bold(),
"|".fg(Color::Rgb(100, 100, 100)),
])
} else {
Line::default()
}
.right_aligned(),
)
.title_bottom(get_input_line(state)),
)
.row_highlight_style(Style::default().fg(Color::Green)),
area,
&mut table_state,
);
render_cursor(state, area, frame);
frame.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("↑"))
.end_symbol(Some("↓")),
area.inner(Margin {
vertical: 1,
horizontal: 0,
}),
&mut ScrollbarState::new(items_len).position(selected_index),
);
let chunks = Layout::new(
Direction::Horizontal,
[
Constraint::Length(1),
Constraint::Length(
ELF_INFO_TABS
.iter()
.map(|v| v.title().len() as u16)
.sum::<u16>()
+ ((ELF_INFO_TABS.len() as u16 - 1) * 3)
+ 2,
),
Constraint::Percentage(100),
],
)
.split(chunks[0]);
frame.render_widget(Clear, chunks[1]);
let tabs = Tabs::new(ELF_INFO_TABS.iter().map(|v| Line::from(v.title())))
.select(state.info_index)
.style(Style::default().fg(Color::Cyan))
.highlight_style(
Style::default()
.add_modifier(Modifier::BOLD)
.fg(state.accent_color),
);
frame.render_widget(tabs, chunks[1]);
render_details(state, rect, frame);
}
}
pub fn render_strings(state: &mut State, frame: &mut Frame, rect: Rect) {
let selected_index = state.list.state.selected().unwrap_or_default();
let items_len = state.list.items.len();
let page = selected_index / LIST_LIMIT;
let items = state
.list
.items
.iter()
.skip(page * LIST_LIMIT)
.take(LIST_LIMIT);
let left_padding = items
.clone()
.next_back()
.cloned()
.unwrap_or_default()
.first()
.map(|v| v.len())
.unwrap_or_default()
+ 1;
if !state.strings_loaded {
frame.render_widget(Block::bordered(), rect);
frame.render_widget(
Paragraph::new("Loading...".italic()).alignment(Alignment::Center),
rect.inner(Margin {
vertical: 1,
horizontal: 0,
}),
);
return;
}
let mut list_state = TableState::default();
list_state.select(Some(selected_index % LIST_LIMIT));
let max_row_width = rect.width.saturating_sub(4) as usize;
frame.render_stateful_widget(
Table::new(
items.map(|items| {
Row::new(vec![Cell::from({
let index = format!("{:>p$}", items[0], p = left_padding);
let value = items[1].to_string();
let mut spans = vec![index.clone().cyan(), " ".into()];
if index.width() + value.width() > max_row_width {
spans.extend(highlight_search_result(
value
.chars()
.map(|c| if c.is_whitespace() { ' ' } else { c })
.take(max_row_width.saturating_sub(index.width()))
.collect::<String>()
.into(),
&state.input,
));
spans.push("…".fg(Color::Rgb(100, 100, 100)));
} else {
spans.extend(highlight_search_result(
value
.chars()
.map(|c| if c.is_whitespace() { ' ' } else { c })
.collect::<String>()
.into(),
&state.input,
))
}
Line::from(spans)
})])
}),
&[Constraint::Percentage(100)],
)
.header(Row::new(vec![
format!(" {}", STRINGS_HEADERS.join(" ")).bold()
]))
.block(
Block::bordered()
.title_top(
Line::from(vec![
"|".fg(Color::Rgb(100, 100, 100)),
format!("Min length: {}", state.analyzer.strings_len)
.fg(state.accent_color)
.bold(),
"|".fg(Color::Rgb(100, 100, 100)),
])
.right_aligned(),
)
.title_bottom(
if items_len != 0 {
Line::from(vec![
"|".fg(Color::Rgb(100, 100, 100)),
format!("{}/{}", selected_index.saturating_add(1), items_len)
.fg(state.accent_color)
.bold(),
"|".fg(Color::Rgb(100, 100, 100)),
])
} else {
Line::default()
}
.right_aligned(),
)
.title_bottom(get_input_line(state)),
)
.row_highlight_style(Style::default().fg(Color::Green).bold()),
rect,
&mut list_state,
);
render_cursor(state, rect, frame);
render_details(state, rect, frame);
frame.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("↑"))
.end_symbol(Some("↓")),
rect.inner(Margin {
vertical: 1,
horizontal: 0,
}),
&mut ScrollbarState::new(items_len).position(selected_index),
);
}
fn render_cursor(state: &mut State<'_>, area: Rect, frame: &mut Frame<'_>) {
if state.input_mode {
let (x, y) = (
area.x
+ Input::default()
.with_value(format!("search: {}", state.input.value()))
.visual_cursor() as u16
+ 2,
area.bottom().saturating_sub(1),
);
frame.render_widget(
Clear,
Rect {
x,
y,
width: 1,
height: 1,
},
);
frame.set_cursor_position(Position::new(x, y));
}
}
fn render_details(state: &mut State<'_>, area: Rect, frame: &mut Frame<'_>) {
if state.show_details {
let headers;
match state.tab {
Tab::StaticAnalysis => {
headers = ELF_INFO_TABS[state.info_index].headers();
}
Tab::Strings => {
headers = STRINGS_HEADERS;
}
_ => {
unimplemented!()
}
}
let max_row_width = (area.width - 2) / 2;
let items = state.list.selected().cloned().unwrap_or_default();
let lines: Vec<Line> = items
.iter()
.enumerate()
.flat_map(|(i, v)| {
let mut lines = Vec::new();
if v.width() as u16 > max_row_width {
lines.extend(
textwrap::wrap(v, textwrap::Options::new(max_row_width as usize))
.into_iter()
.enumerate()
.map(|(x, v)| {
if x == 0 {
Line::from(vec![
Span::styled(
headers[i].to_string(),
Style::default().fg(Color::Cyan),
),
Span::raw(": ").fg(Color::Rgb(100, 100, 100)),
v.to_string().into(),
])
} else {
Line::from(v.to_string())
}
}),
)
} else {
lines.push(Line::from(vec![
Span::styled(headers[i].to_string(), Style::default().fg(Color::Cyan)),
Span::raw(": ").fg(Color::Rgb(100, 100, 100)),
Span::styled(v, Style::default().fg(state.accent_color)),
]));
}
lines
})
.collect();
let popup = Popup::new(Text::from(lines)).title(Line::from(vec![
"|".fg(Color::Rgb(100, 100, 100)),
"Details".fg(state.accent_color).bold(),
"|".fg(Color::Rgb(100, 100, 100)),
]));
frame.render_widget(&popup, area);
}
}
pub fn render_dynamic_analysis(state: &mut State, frame: &mut Frame, rect: Rect) {
if !state.system_calls_loaded {
let lines = if cfg!(feature = "dynamic-analysis") {
vec![Line::from(vec![
"Press ".into(),
"Enter".yellow(),
" to run the executable.".into(),
])]
} else {
vec![
Line::from(vec![
"You need to enable the ".into(),
"\"dynamic-analysis\"".yellow(),
" feature at build time to use this functionality!".into(),
]),
Line::from(vec![
"(This is currently only supported on ".into(),
"Linux".cyan(),
")".into(),
]),
]
};
frame.render_widget(
Paragraph::new(Text::from(lines))
.block(Block::bordered())
.alignment(Alignment::Center),
rect,
);
} else {
let max_height = state
.analyzer
.system_calls
.len()
.saturating_sub(rect.height as usize)
+ 2;
if max_height < state.dynamic_scroll_index {
state.dynamic_scroll_index = max_height;
}
frame.render_widget(
Paragraph::new(
state
.analyzer
.system_calls
.clone()
.into_iter()
.map(|line| highlight_search_result(line, &state.input).into())
.collect::<Vec<Line>>(),
)
.block(
Block::bordered()
.title(vec![
"|".fg(Color::Rgb(100, 100, 100)),
"System Calls".fg(state.accent_color).bold(),
"|".fg(Color::Rgb(100, 100, 100)),
])
.title_bottom(
Line::from(vec![
"|".fg(Color::Rgb(100, 100, 100)),
"Total: ".into(),
state
.analyzer
.system_calls
.len()
.to_string()
.fg(state.accent_color)
.bold(),
"|".fg(Color::Rgb(100, 100, 100)),
])
.right_aligned(),
)
.title_bottom(get_input_line(state)),
)
.scroll((state.dynamic_scroll_index as u16, 0)),
rect,
);
render_cursor(state, rect, frame);
frame.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("↑"))
.end_symbol(Some("↓")),
rect.inner(Margin {
vertical: 1,
horizontal: 0,
}),
&mut ScrollbarState::new(max_height).position(state.dynamic_scroll_index),
);
if state.show_details && !state.analyzer.tracer.summary.is_empty() {
let summary = state
.analyzer
.tracer
.summary
.into_text()
.unwrap_or_else(|_| Text::from("ANSI error occurred"))
.into_iter()
.filter(|v| v.width() != 0)
.collect::<Vec<Line>>();
let popup = Popup::new(Text::from(summary)).title(Line::from(vec![
"|".fg(Color::Rgb(100, 100, 100)),
"Details".fg(state.accent_color).bold(),
"|".fg(Color::Rgb(100, 100, 100)),
]));
frame.render_widget(&popup, rect);
}
}
}
fn get_input_line<'a>(state: &'a State) -> Line<'a> {
if !state.input.value().is_empty() || state.input_mode {
Line::from(vec![
"|".fg(Color::Rgb(100, 100, 100)),
"search: ".yellow(),
state.input.value().fg(state.accent_color),
if state.input_mode { " " } else { "" }.into(),
"|".fg(Color::Rgb(100, 100, 100)),
])
} else {
Line::default()
}
}
fn highlight_search_result<'a>(line: Line<'a>, input: &'a Input) -> Vec<Span<'a>> {
let line_str = line.to_string();
if line_str.contains(input.value()) && !input.value().is_empty() {
let splits = line_str.split(input.value());
let chunks = splits.into_iter().map(|c| Span::from(c.to_owned()));
let pattern = Span::styled(
input.value(),
Style::new().bg(Color::Yellow).fg(Color::Black),
);
itertools::intersperse(chunks, pattern).collect::<Vec<Span>>()
} else {
line.spans.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_highlight_search_string() {
let line: Line = "onetwothree".into();
let query = Input::new("two".into());
let highlighted = highlight_search_result(line, &query);
assert_eq!(
vec![
Span::raw("one"),
Span::styled("two", Style::new().bg(Color::Yellow).fg(Color::Black)),
Span::raw("three")
],
highlighted
);
}
#[test]
#[ignore]
fn test_highlight_search_line() {
let line: Line = vec![
Span::raw("one"),
Span::styled("two", Style::new().bg(Color::Blue).fg(Color::Black)),
Span::raw("three"),
]
.into();
let query = Input::new("one".into());
let highlighted = highlight_search_result(line, &query);
assert_eq!(
vec![
Span::styled("one", Style::new().bg(Color::Yellow).fg(Color::Black)),
Span::styled("two", Style::new().bg(Color::Blue).fg(Color::Black)),
Span::raw("three")
],
highlighted
);
}
}