1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
//! Clipboard history, paste special, and paste_text key handling.
use crate::app::window_state::WindowState;
use crate::terminal::ClipboardSlot;
use winit::event::{ElementState, KeyEvent};
use winit::keyboard::{Key, NamedKey};
impl WindowState {
pub(crate) fn handle_clipboard_history_keys(&mut self, event: &KeyEvent) -> bool {
// Handle Escape to close clipboard history UI
if self.overlay_ui.clipboard_history_ui.visible {
if event.state == ElementState::Pressed {
match &event.logical_key {
Key::Named(NamedKey::Escape) => {
self.overlay_ui.clipboard_history_ui.visible = false;
self.focus_state.needs_redraw = true;
return true;
}
Key::Named(NamedKey::ArrowUp) => {
self.overlay_ui.clipboard_history_ui.select_previous();
self.focus_state.needs_redraw = true;
return true;
}
Key::Named(NamedKey::ArrowDown) => {
self.overlay_ui.clipboard_history_ui.select_next();
self.focus_state.needs_redraw = true;
return true;
}
Key::Named(NamedKey::Enter) => {
// Check if Shift is held for paste special
let shift = self.input_handler.modifiers.state().shift_key();
if let Some(entry) = self.overlay_ui.clipboard_history_ui.selected_entry() {
let content = entry.content.clone();
self.overlay_ui.clipboard_history_ui.visible = false;
if shift {
// Shift+Enter: Open paste special UI with the selected content
self.overlay_ui.paste_special_ui.open(content);
log::info!("Paste special UI opened from clipboard history");
} else {
// Enter: Paste directly
self.paste_text(&content);
}
self.focus_state.needs_redraw = true;
}
return true;
}
_ => {}
}
}
// While clipboard history is visible, consume all key events
return true;
}
// Ctrl+Shift+H: Toggle clipboard history UI
if event.state == ElementState::Pressed {
let ctrl = self.input_handler.modifiers.state().control_key();
let shift = self.input_handler.modifiers.state().shift_key();
if ctrl
&& shift
&& matches!(event.logical_key, Key::Character(ref c) if c.as_str() == "h" || c.as_str() == "H")
{
self.toggle_clipboard_history();
return true;
}
}
false
}
pub(crate) fn toggle_clipboard_history(&mut self) {
// Refresh clipboard history entries from terminal before showing
// try_lock: intentional — called from keyboard handler in sync event loop.
// On miss: clipboard history UI shows stale entries. Acceptable for a UI toggle;
// the user can dismiss and re-open to get fresh entries.
if let Some(tab) = self.tab_manager.active_tab()
&& let Ok(term) = tab.terminal.try_write()
{
// Get history for all slots and merge
let mut all_entries = Vec::new();
all_entries.extend(term.get_clipboard_history(ClipboardSlot::Primary));
all_entries.extend(term.get_clipboard_history(ClipboardSlot::Clipboard));
all_entries.extend(term.get_clipboard_history(ClipboardSlot::Selection));
// Sort by timestamp (newest first)
all_entries.sort_by_key(|e| std::cmp::Reverse(e.timestamp));
self.overlay_ui
.clipboard_history_ui
.update_entries(all_entries);
}
self.overlay_ui.clipboard_history_ui.toggle();
self.focus_state.needs_redraw = true;
log::debug!(
"Clipboard history UI toggled: {}",
self.overlay_ui.clipboard_history_ui.visible
);
}
pub(crate) fn handle_paste_special_keys(&mut self, event: &KeyEvent) -> bool {
// Handle keys when paste special UI is visible
if self.overlay_ui.paste_special_ui.visible {
if event.state == ElementState::Pressed {
match &event.logical_key {
Key::Named(NamedKey::Escape) => {
self.overlay_ui.paste_special_ui.close();
self.focus_state.needs_redraw = true;
return true;
}
Key::Named(NamedKey::ArrowUp) => {
self.overlay_ui.paste_special_ui.select_previous();
self.focus_state.needs_redraw = true;
return true;
}
Key::Named(NamedKey::ArrowDown) => {
self.overlay_ui.paste_special_ui.select_next();
self.focus_state.needs_redraw = true;
return true;
}
Key::Named(NamedKey::Enter) => {
// Apply the selected transformation and paste
if let Some(result) = self.overlay_ui.paste_special_ui.apply_selected() {
self.overlay_ui.paste_special_ui.close();
self.paste_text(&result);
self.focus_state.needs_redraw = true;
}
return true;
}
_ => {}
}
}
// While paste special is visible, consume all key events
// to prevent them from going to the terminal
return true;
}
false
}
pub(crate) fn paste_text(&mut self, text: &str) {
// SEC-007: Warn when paste content contains control characters that will be stripped.
// Control characters in clipboard content (ESC, C0, C1) can inject terminal escape
// sequences. The sanitizer always strips them; this warning alerts the user that
// clipboard content was modified before pasting.
if self.config.warn_paste_control_chars
&& crate::paste_transform::paste_contains_control_chars(text)
{
log::warn!(
"Clipboard paste content contained control characters (ESC, C0, C1) that were \
stripped before pasting to prevent terminal escape sequence injection. \
This may indicate the clipboard contains crafted or binary content. \
Set `warn_paste_control_chars: false` in config to suppress this warning."
);
crate::debug_info!(
"PASTE",
"SECURITY: paste content contained control chars — stripped before PTY write \
({} chars original)",
text.len(),
);
}
// Sanitize clipboard content to strip dangerous control characters
// (escape sequences, C0/C1 controls) before sending to PTY
let text = crate::paste_transform::sanitize_paste_content(text);
// Try to paste via tmux if connected
if self.paste_via_tmux(&text) {
return; // Paste was routed through tmux
}
// Fall back to direct terminal paste
if let Some(tab) = self.tab_manager.active_tab() {
use std::sync::Arc;
let terminal_clone = Arc::clone(&tab.terminal);
let delay_ms = self.config.paste_delay_ms;
self.runtime.spawn(async move {
let term = terminal_clone.write().await;
if delay_ms > 0 && text.contains('\n') {
let _ = term.paste_with_delay(&text, delay_ms).await;
} else {
let _ = term.paste(&text);
}
log::debug!("Pasted text ({} chars)", text.len());
});
}
}
}