use std::{
cmp::min,
collections::BTreeMap,
sync::{Arc, LazyLock},
};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::{
buffer::Buffer,
layout::{Alignment::Center, Rect},
style::{Color, Modifier, Style},
text::Span,
widgets::{Block, Borders, Clear, HighlightSpacing, List, ListState, StatefulWidgetRef, Widget},
};
use crate::{
action::{Action, CopyTarget, SupportedShell::Bash},
event::TracerEventDetails,
};
use super::help::help_item;
#[derive(Debug, Clone)]
pub struct CopyPopup;
#[derive(Debug, Clone)]
pub struct CopyPopupState {
pub event: Arc<TracerEventDetails>,
pub state: ListState,
pub available_targets: Vec<char>,
}
static KEY_MAP: LazyLock<BTreeMap<char, (&'static str, &'static str)>> = LazyLock::new(|| {
[
('c', ("(C)ommand line", "Cmdline")),
('s', ("Command line with (S)tdio", "Cmdline with stdio")),
(
'f',
("Command line with (F)ile descriptors", "Cmdline with Fds"),
),
('e', ("(E)nvironment variables", "Env")),
('d', ("(D)iff of environment variables", "Diff of Env")),
('a', ("(A)rguments", "Argv")),
('n', ("File(N)ame", "Filename")),
('r', ("Syscall (R)esult", "Result")),
('l', ("Current (L)ine", "Line")),
]
.into_iter()
.collect()
});
impl CopyPopupState {
pub fn new(event: Arc<TracerEventDetails>) -> Self {
let mut state = ListState::default();
state.select(Some(0));
let available_targets = if let TracerEventDetails::Exec(_) = &event.as_ref() {
KEY_MAP.keys().copied().collect()
} else {
vec!['l']
};
Self {
event,
state,
available_targets,
}
}
pub fn next(&mut self) {
self.state.select(Some(
(self.state.selected().unwrap() + 1).min(self.available_targets.len() - 1),
))
}
pub fn prev(&mut self) {
self
.state
.select(Some(self.state.selected().unwrap().saturating_sub(1)))
}
pub fn selected(&self) -> CopyTarget {
let id = self.state.selected().unwrap_or(0);
let key = self.available_targets[id];
match key {
'c' => CopyTarget::Commandline(Bash),
's' => CopyTarget::CommandlineWithStdio(Bash),
'f' => CopyTarget::CommandlineWithFds(Bash),
'e' => CopyTarget::Env,
'd' => CopyTarget::EnvDiff,
'a' => CopyTarget::Argv,
'n' => CopyTarget::Filename,
'r' => CopyTarget::SyscallResult,
'l' => CopyTarget::Line,
_ => unreachable!(),
}
}
pub fn select_by_key(&mut self, key: char) -> Option<CopyTarget> {
if let Some(id) = self.available_targets.iter().position(|&k| k == key) {
self.state.select(Some(id));
Some(self.selected())
} else {
None
}
}
pub fn help_items(&self) -> impl Iterator<Item = Span> {
self.available_targets.iter().flat_map(|&key| {
help_item!(
key.to_ascii_uppercase().to_string(),
KEY_MAP.get(&key).unwrap().1
)
})
}
pub fn handle_key_event(&mut self, ke: KeyEvent) -> color_eyre::Result<Option<Action>> {
if ke.modifiers == KeyModifiers::NONE {
match ke.code {
KeyCode::Char('q') => {
return Ok(Some(Action::CancelCurrentPopup));
}
KeyCode::Down | KeyCode::Char('j') => {
self.next();
}
KeyCode::Up | KeyCode::Char('k') => {
self.prev();
}
KeyCode::Enter => {
return Ok(Some(Action::CopyToClipboard {
event: self.event.clone(),
target: self.selected(),
}));
}
KeyCode::Char(c) => {
if let Some(target) = self.select_by_key(c) {
return Ok(Some(Action::CopyToClipboard {
event: self.event.clone(),
target,
}));
}
}
_ => {}
}
}
Ok(None)
}
}
impl StatefulWidgetRef for CopyPopup {
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut CopyPopupState) {
let list = List::from_iter(
state
.available_targets
.iter()
.map(|&key| KEY_MAP.get(&key).unwrap().0),
)
.block(
Block::default()
.title("Copy")
.title_alignment(Center)
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::LightGreen)),
)
.highlight_style(
Style::default()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::REVERSED)
.fg(Color::Cyan),
)
.highlight_symbol(">")
.highlight_spacing(HighlightSpacing::Always);
let popup_area = centered_popup_rect(38, list.len() as u16, area);
Clear.render(popup_area, buf);
StatefulWidgetRef::render_ref(&list, popup_area, buf, &mut state.state);
}
type State = CopyPopupState;
}
fn centered_popup_rect(width: u16, height: u16, area: Rect) -> Rect {
let height = height.saturating_add(2).min(area.height);
let width = width.saturating_add(2).min(area.width);
Rect {
x: area.width.saturating_sub(width) / 2,
y: area.height.saturating_sub(height) / 2,
width: min(width, area.width),
height: min(height, area.height),
}
}