1use std::{
2 cmp::min,
3 collections::BTreeMap,
4 sync::{
5 Arc,
6 LazyLock,
7 },
8};
9
10use crossterm::event::{
11 KeyCode,
12 KeyEvent,
13 KeyModifiers,
14};
15use ratatui::{
16 buffer::Buffer,
17 layout::{
18 Alignment,
19 Rect,
20 },
21 style::{
22 Color,
23 Modifier,
24 Style,
25 },
26 text::Span,
27 widgets::{
28 Block,
29 Borders,
30 Clear,
31 HighlightSpacing,
32 List,
33 ListState,
34 StatefulWidget,
35 StatefulWidgetRef,
36 Widget,
37 },
38};
39use tracexec_core::event::TracerEventDetails;
40
41use super::help::help_item;
42use crate::action::{
43 Action,
44 CopyTarget,
45 SupportedShell::Bash,
46};
47
48#[derive(Debug, Clone)]
49pub struct CopyPopup;
50
51#[derive(Debug, Clone)]
52pub struct CopyPopupState {
53 pub event: Arc<TracerEventDetails>,
54 pub state: ListState,
55 pub available_targets: Vec<char>,
56}
57
58static KEY_MAP: LazyLock<BTreeMap<char, (&'static str, &'static str)>> = LazyLock::new(|| {
59 [
60 ('c', ("(C)ommand line", "Cmdline")),
61 (
62 'o',
63 ("C(o)mmand line with full env", "Cmdline with full env"),
64 ),
65 ('s', ("Command line with (S)tdio", "Cmdline with stdio")),
66 (
67 'f',
68 ("Command line with (F)ile descriptors", "Cmdline with Fds"),
69 ),
70 ('e', ("(E)nvironment variables", "Env")),
71 ('d', ("(D)iff of environment variables", "Diff of Env")),
72 ('a', ("(A)rguments", "Argv")),
73 ('n', ("File(N)ame", "Filename")),
74 ('r', ("Syscall (R)esult", "Result")),
75 ('l', ("Current (L)ine", "Line")),
76 ]
77 .into_iter()
78 .collect()
79});
80
81impl CopyPopupState {
82 pub fn new(event: Arc<TracerEventDetails>) -> Self {
83 let mut state = ListState::default();
84 state.select(Some(0));
85 let available_targets = if let TracerEventDetails::Exec(_) = &event.as_ref() {
86 KEY_MAP.keys().copied().collect()
87 } else {
88 vec!['l']
89 };
90 Self {
91 event,
92 state,
93 available_targets,
94 }
95 }
96
97 pub fn next(&mut self) {
98 self.state.select(Some(
99 (self.state.selected().unwrap() + 1).min(self.available_targets.len() - 1),
100 ))
101 }
102
103 pub fn prev(&mut self) {
104 self
105 .state
106 .select(Some(self.state.selected().unwrap().saturating_sub(1)))
107 }
108
109 pub fn selected(&self) -> CopyTarget {
110 let id = self.state.selected().unwrap_or(0);
111 let key = self.available_targets[id];
112 match key {
113 'c' => CopyTarget::Commandline(Bash),
114 'o' => CopyTarget::CommandlineWithFullEnv(Bash),
115 's' => CopyTarget::CommandlineWithStdio(Bash),
116 'f' => CopyTarget::CommandlineWithFds(Bash),
117 'e' => CopyTarget::Env,
118 'd' => CopyTarget::EnvDiff,
119 'a' => CopyTarget::Argv,
120 'n' => CopyTarget::Filename,
121 'r' => CopyTarget::SyscallResult,
122 'l' => CopyTarget::Line,
123 _ => unreachable!(),
124 }
125 }
126
127 pub fn select_by_key(&mut self, key: char) -> Option<CopyTarget> {
128 if let Some(id) = self.available_targets.iter().position(|&k| k == key) {
129 self.state.select(Some(id));
130 Some(self.selected())
131 } else {
132 None
133 }
134 }
135
136 pub fn help_items(&self) -> impl Iterator<Item = Span<'_>> {
137 self.available_targets.iter().flat_map(|&key| {
138 help_item!(
139 key.to_ascii_uppercase().to_string(),
140 KEY_MAP.get(&key).unwrap().1
141 )
142 })
143 }
144
145 pub fn handle_key_event(&mut self, ke: KeyEvent) -> color_eyre::Result<Option<Action>> {
146 if ke.modifiers == KeyModifiers::NONE {
147 match ke.code {
148 KeyCode::Char('q') => {
149 return Ok(Some(Action::CancelCurrentPopup));
150 }
151 KeyCode::Down | KeyCode::Char('j') => {
152 self.next();
153 }
154 KeyCode::Up | KeyCode::Char('k') => {
155 self.prev();
156 }
157 KeyCode::Enter => {
158 return Ok(Some(Action::CopyToClipboard {
159 event: self.event.clone(),
160 target: self.selected(),
161 }));
162 }
163 KeyCode::Char(c) => {
164 if let Some(target) = self.select_by_key(c) {
165 return Ok(Some(Action::CopyToClipboard {
166 event: self.event.clone(),
167 target,
168 }));
169 }
170 }
171 _ => {}
172 }
173 }
174 Ok(None)
175 }
176}
177
178impl StatefulWidgetRef for CopyPopup {
179 fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut CopyPopupState) {
180 let list = List::from_iter(
181 state
182 .available_targets
183 .iter()
184 .map(|&key| KEY_MAP.get(&key).unwrap().0),
185 )
186 .block(
187 Block::default()
188 .title("Copy")
189 .title_alignment(Alignment::Center)
190 .borders(Borders::ALL)
191 .border_style(Style::default().fg(Color::LightGreen)),
192 )
193 .highlight_style(
194 Style::default()
195 .add_modifier(Modifier::BOLD)
196 .add_modifier(Modifier::REVERSED)
197 .fg(Color::Cyan),
198 )
199 .highlight_symbol(">")
200 .highlight_spacing(HighlightSpacing::Always);
201 let popup_area = centered_popup_rect(38, list.len() as u16, area);
202 Clear.render(popup_area, buf);
203 StatefulWidget::render(&list, popup_area, buf, &mut state.state);
204 }
205
206 type State = CopyPopupState;
207}
208
209fn centered_popup_rect(width: u16, height: u16, area: Rect) -> Rect {
235 let height = height.saturating_add(2).min(area.height);
236 let width = width.saturating_add(2).min(area.width);
237 Rect {
238 x: area.width.saturating_sub(width) / 2,
239 y: area.height.saturating_sub(height) / 2,
240 width: min(width, area.width),
241 height: min(height, area.height),
242 }
243}