tracexec_tui/
copy_popup.rs

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
209// Copyright notice for the below code:
210
211// MIT License
212
213// Copyright (c) 2023 Josh McKinney
214
215// Permission is hereby granted, free of charge, to any person obtaining a copy
216// of this software and associated documentation files (the "Software"), to deal
217// in the Software without restriction, including without limitation the rights
218// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
219// copies of the Software, and to permit persons to whom the Software is
220// furnished to do so, subject to the following conditions:
221
222// The above copyright notice and this permission notice shall be included in all
223// copies or substantial portions of the Software.
224
225// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
226// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
227// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
228// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
229// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
230// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
231// SOFTWARE.
232
233/// Create a rectangle centered in the given area.
234fn 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}