use super::*;
use ratatui::crossterm::event::{KeyEventKind, KeyModifiers, MouseEvent, MouseEventKind};
use std::time::Instant;
use super::super::types::{OverlayEvent, OverlaySubmission};
use crate::ui::tui::session::modal::{ModalKeyModifiers, ModalListKeyResult};
pub(super) fn handle_paste(session: &mut Session, content: &str) {
if session.input_enabled {
session.insert_paste_text(content);
session.mark_dirty();
} else if let Some(modal) = session.modal_state_mut()
&& let (Some(list), Some(search)) = (modal.list.as_mut(), modal.search.as_mut())
{
search.insert(content);
list.apply_search(&search.query);
session.mark_dirty();
} else if let Some(wizard) = session.wizard_overlay_mut()
&& let Some(search) = wizard.search.as_mut()
{
search.insert(content);
if let Some(step) = wizard.steps.get_mut(wizard.current_step) {
step.list.apply_search(&search.query);
}
session.mark_dirty();
}
}
fn copy_selected_input_if_requested(
session: &mut Session,
key: &KeyEvent,
has_command: bool,
) -> bool {
let is_copy_shortcut = if has_command {
matches!(key.code, KeyCode::Char('c') | KeyCode::Char('C'))
} else {
match key.code {
KeyCode::Char('c') | KeyCode::Char('C') => {
key.modifiers.contains(KeyModifiers::CONTROL)
}
KeyCode::Char('\u{3}') => true,
_ => false,
}
};
if !is_copy_shortcut {
return false;
}
if session.copy_input_selection_to_clipboard() {
session.mark_dirty();
return true;
}
false
}
fn handle_interrupt(session: &mut Session) -> Option<InlineEvent> {
if session.mouse_selection.has_selection {
session.mouse_selection.request_copy();
session.mark_dirty();
return None;
}
if session.has_active_overlay() {
session.close_overlay();
}
session.mark_dirty();
Some(InlineEvent::Interrupt)
}
#[allow(dead_code)]
pub(super) fn handle_event(
session: &mut Session,
event: CrosstermEvent,
events: &UnboundedSender<InlineEvent>,
callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
) {
match event {
CrosstermEvent::Key(key) => {
if matches!(key.kind, KeyEventKind::Press)
&& let Some(outbound) = process_key(session, key)
{
session.emit_inline_event(&outbound, events, callback);
}
}
CrosstermEvent::Mouse(MouseEvent {
kind, column, row, ..
}) => {
if !session.fullscreen.interaction.mouse_capture {
return;
}
match kind {
MouseEventKind::ScrollDown => {
session.mouse_selection.clear_click_history();
session.scroll_line_down();
session.mark_dirty();
}
MouseEventKind::ScrollUp => {
session.mouse_selection.clear_click_history();
session.scroll_line_up();
session.mark_dirty();
}
MouseEventKind::Down(crossterm::event::MouseButton::Left) => {
if session
.mouse_selection
.register_click(column, row, Instant::now())
{
let _ = session.select_transcript_word_at(column, row);
session.mouse_selection.clear_click_history();
} else {
session.mouse_selection.start_selection(column, row);
}
session.mark_dirty();
}
MouseEventKind::Drag(crossterm::event::MouseButton::Left) => {
session.mouse_selection.update_selection(column, row);
session.mark_dirty();
}
MouseEventKind::Up(crossterm::event::MouseButton::Left) => {
session.mouse_selection.finish_selection(column, row);
session.mark_dirty();
}
_ => {}
}
}
CrosstermEvent::Paste(content) => {
handle_paste(session, &content);
}
CrosstermEvent::Resize(_, rows) => {
session.apply_view_rows(rows);
session.mark_dirty();
}
CrosstermEvent::FocusGained => {
}
CrosstermEvent::FocusLost => {
}
}
}
pub(super) fn process_key(session: &mut Session, key: KeyEvent) -> Option<InlineEvent> {
let modifiers = key.modifiers;
let has_control = modifiers.contains(KeyModifiers::CONTROL);
let has_shift = modifiers.contains(KeyModifiers::SHIFT);
let raw_alt = modifiers.contains(KeyModifiers::ALT);
let raw_meta = modifiers.contains(KeyModifiers::META);
let has_super = modifiers.contains(KeyModifiers::SUPER);
let has_command = has_super || raw_meta;
let has_alt = raw_alt && !has_command;
if copy_selected_input_if_requested(session, &key, has_command) {
return None;
}
if let Some(modal) = session.modal_state_mut() {
let modal_modifiers = ModalKeyModifiers {
control: has_control,
alt: has_alt,
command: has_command,
};
if let Some(action) = modal.hotkey_action(&key, modal_modifiers) {
session.close_overlay();
session.mark_dirty();
return Some(InlineEvent::Overlay(OverlayEvent::Submitted(
OverlaySubmission::Hotkey(action),
)));
}
let result = modal.handle_list_key_event(&key, modal_modifiers);
match result {
ModalListKeyResult::Redraw => {
session.mark_dirty();
return None;
}
ModalListKeyResult::Emit(event) => {
session.mark_dirty();
return Some(event);
}
ModalListKeyResult::HandledNoRedraw => {
return None;
}
ModalListKeyResult::Submit(event) | ModalListKeyResult::Cancel(event) => {
session.close_overlay();
return Some(event);
}
ModalListKeyResult::NotHandled => {}
}
}
if let Some(wizard) = session.wizard_overlay_mut() {
let result = wizard.handle_key_event(
&key,
ModalKeyModifiers {
control: has_control,
alt: has_alt,
command: has_command,
},
);
match result {
ModalListKeyResult::Redraw => {
session.mark_dirty();
return None;
}
ModalListKeyResult::Emit(event) => {
session.mark_dirty();
return Some(event);
}
ModalListKeyResult::HandledNoRedraw => {
return None;
}
ModalListKeyResult::Submit(event) => {
session.close_overlay();
return Some(event);
}
ModalListKeyResult::Cancel(event) => {
session.close_overlay();
return Some(event);
}
ModalListKeyResult::NotHandled => {}
}
}
if session.handle_vim_key(&key) {
return None;
}
if session.reverse_search_state.active {
let history = session.input_manager.history_texts();
let handled = reverse_search::handle_reverse_search_key(
&key,
&mut session.reverse_search_state,
&mut session.input_manager,
&history,
);
if handled {
session.mark_dirty();
return None;
}
}
match key.code {
KeyCode::Char('c') | KeyCode::Char('C') if has_control => handle_interrupt(session),
KeyCode::Char('\u{3}') => handle_interrupt(session),
KeyCode::Char('d') if has_control => {
session.mark_dirty();
Some(InlineEvent::Exit)
}
KeyCode::Char('b') if has_control => {
session.mark_dirty();
Some(InlineEvent::BackgroundOperation)
}
KeyCode::Char('a') | KeyCode::Char('A') if has_control && !has_command && !has_alt => {
if session.input_enabled {
session.move_to_start();
session.mark_dirty();
}
None
}
KeyCode::Char('e') | KeyCode::Char('E') if has_control && !has_command && !has_alt => {
if !session.input_enabled {
None
} else if session.input_manager.content().is_empty() {
session.mark_dirty();
Some(InlineEvent::LaunchEditor)
} else {
session.move_to_end();
session.mark_dirty();
None
}
}
KeyCode::Char('w') | KeyCode::Char('W') if has_control && !has_command && !has_alt => {
if session.input_enabled {
session.delete_word_backward();
session.mark_dirty();
}
None
}
KeyCode::Char('u') | KeyCode::Char('U') if has_control && !has_command && !has_alt => {
if session.input_enabled {
session.delete_to_start_of_line();
session.mark_dirty();
}
None
}
KeyCode::Char('k') | KeyCode::Char('K') if has_control && !has_command && !has_alt => {
if session.input_enabled {
session.delete_to_end_of_line();
session.mark_dirty();
}
None
}
KeyCode::Char('j') if has_control => {
session.insert_char('\n');
session.mark_dirty();
None
}
KeyCode::Char('l') | KeyCode::Char('L') if has_control => {
session.mark_dirty();
Some(InlineEvent::Submit("/clear".to_string()))
}
KeyCode::BackTab => {
session.clear_inline_prompt_suggestion();
session.mark_dirty();
Some(InlineEvent::ToggleMode)
}
KeyCode::Esc => {
if session.has_active_overlay() {
session.close_overlay();
None
} else {
let is_double_escape = session.input_manager.check_escape_double_tap();
let active_pty_count = session.active_pty_session_count();
let has_running_activity = session.is_running_activity();
if has_running_activity || active_pty_count > 0 {
session.mark_dirty();
if is_double_escape {
Some(InlineEvent::Exit)
} else {
Some(InlineEvent::Interrupt)
}
} else if is_double_escape && !has_running_activity {
session.mark_dirty();
Some(InlineEvent::Submit("/rewind".to_string()))
} else if !session.input_manager.content().is_empty() {
command::clear_input(session);
session.mark_dirty();
None
} else {
session.mark_dirty();
Some(InlineEvent::Cancel)
}
}
}
KeyCode::PageUp => {
session.scroll_page_up();
session.mark_dirty();
Some(InlineEvent::ScrollPageUp)
}
KeyCode::PageDown => {
session.scroll_page_down();
session.mark_dirty();
Some(InlineEvent::ScrollPageDown)
}
KeyCode::Up => {
let edit_queue_modifier = has_alt || (raw_meta && !has_super);
if !terminal_capabilities::queued_input_edit_uses_shift_left()
&& edit_queue_modifier
&& !session.queued_inputs.is_empty()
{
if let Some(latest) = session.pop_latest_queued_input() {
session.clear_inline_prompt_suggestion();
session.input_manager.set_content(latest);
session.input_compact_mode = session.input_compact_placeholder().is_some();
session.scroll_manager.set_offset(0);
}
session.mark_dirty();
Some(InlineEvent::EditQueue)
} else if session.navigate_history_previous() {
session.mark_dirty();
Some(InlineEvent::HistoryPrevious)
} else {
None
}
}
KeyCode::Down => {
if session.navigate_history_next() {
session.clear_inline_prompt_suggestion();
session.mark_dirty();
Some(InlineEvent::HistoryNext)
} else {
None
}
}
KeyCode::Enter => {
if !session.input_enabled {
return None;
}
if !has_control
&& !has_shift
&& !has_alt
&& session.input_manager.content().trim().is_empty()
&& session.active_pty_session_count() > 0
{
session.mark_dirty();
return Some(InlineEvent::Submit("/jobs".to_string()));
}
if !has_control && session.input_manager.content().ends_with('\\') {
let mut content = session.input_manager.content().to_string();
content.pop(); content.push('\n');
session.input_manager.set_content(content);
session.mark_dirty();
return None;
}
if has_control {
let Some(submitted) = take_submitted_input(session) else {
session.mark_dirty();
return if session.is_running_activity() {
None
} else {
Some(InlineEvent::ProcessLatestQueued)
};
};
session.mark_dirty();
return if session.is_running_activity() {
Some(InlineEvent::Steer(submitted))
} else {
Some(InlineEvent::Submit(submitted))
};
}
if has_shift || has_alt {
session.insert_char('\n');
session.mark_dirty();
return None;
}
let Some(submitted) = take_submitted_input(session) else {
session.mark_dirty();
return None;
};
session.mark_dirty();
if session.is_running_activity() {
session.push_queued_input(submitted.clone());
Some(InlineEvent::QueueSubmit(submitted))
} else {
Some(InlineEvent::Submit(submitted))
}
}
KeyCode::Tab => {
if !session.input_enabled {
return None;
}
if session.accept_inline_prompt_suggestion() {
return None;
}
let Some(submitted) = take_submitted_input(session) else {
session.mark_dirty();
return None;
};
session.push_queued_input(submitted.clone());
session.mark_dirty();
Some(InlineEvent::QueueSubmit(submitted))
}
KeyCode::Backspace => {
if session.input_enabled {
if has_alt {
session.delete_word_backward();
} else if has_command {
session.delete_to_start_of_line();
} else {
session.delete_char();
}
session.mark_dirty();
}
None
}
KeyCode::Delete => {
if session.input_enabled {
if has_alt {
session.delete_word_backward();
} else if has_command {
session.delete_to_end_of_line();
} else {
session.delete_char_forward();
}
session.mark_dirty();
}
None
}
KeyCode::Left => {
if session.input_enabled {
let tmux_queue_edit = has_shift
&& !has_control
&& !has_command
&& !has_alt
&& terminal_capabilities::queued_input_edit_uses_shift_left()
&& !session.queued_inputs.is_empty();
if tmux_queue_edit {
if let Some(latest) = session.pop_latest_queued_input() {
session.clear_inline_prompt_suggestion();
session.input_manager.set_content(latest);
session.input_compact_mode = session.input_compact_placeholder().is_some();
session.scroll_manager.set_offset(0);
}
session.mark_dirty();
return Some(InlineEvent::EditQueue);
}
session.clear_inline_prompt_suggestion();
if has_shift && has_command {
session.select_to_start();
} else if has_shift {
session.select_left();
} else if has_command {
session.move_to_start();
} else if has_alt {
session.move_left_word();
} else {
session.move_left();
}
session.mark_dirty();
}
None
}
KeyCode::Right => {
if session.input_enabled {
session.clear_inline_prompt_suggestion();
if has_shift && has_command {
session.select_to_end();
} else if has_shift {
session.select_right();
} else if has_command {
session.move_to_end();
} else if has_alt {
session.move_right_word();
} else {
session.move_right();
}
session.mark_dirty();
}
None }
KeyCode::Home => {
if session.input_enabled {
session.clear_inline_prompt_suggestion();
if has_shift {
session.select_to_start();
} else {
session.move_to_start();
}
session.mark_dirty();
}
None
}
KeyCode::End => {
if session.input_enabled {
session.clear_inline_prompt_suggestion();
if has_shift {
session.select_to_end();
} else {
session.move_to_end();
}
session.mark_dirty();
}
None
}
KeyCode::Char('t') | KeyCode::Char('T') if has_control => {
session.toggle_logs();
None
}
KeyCode::Char(ch) => {
if !session.input_enabled {
return None;
}
if has_alt && matches!(ch, 'p' | 'P') {
session.clear_inline_prompt_suggestion();
session.mark_dirty();
return Some(InlineEvent::RequestInlinePromptSuggestion(
session.input_manager.content().to_string(),
));
}
if ch == '?'
&& !has_control
&& !has_alt
&& !has_command
&& session.input_manager.content().is_empty()
{
session.show_modal("Keyboard Shortcuts".to_string(), quick_help_lines(), None);
return None;
}
if ch == '\t' {
if session.accept_inline_prompt_suggestion() {
return None;
}
let Some(submitted) = take_submitted_input(session) else {
session.mark_dirty();
return None;
};
session.push_queued_input(submitted.clone());
session.mark_dirty();
return Some(InlineEvent::QueueSubmit(submitted));
}
if has_command {
match ch {
'a' | 'A' => {
session.move_to_start();
session.mark_dirty();
return None;
}
'e' | 'E' => {
session.move_to_end();
session.mark_dirty();
return None;
}
_ => {}
}
}
if has_alt {
match ch {
'b' | 'B' => {
session.move_left_word();
session.mark_dirty();
}
'f' | 'F' => {
session.move_right_word();
session.mark_dirty();
}
_ => {}
}
return None;
}
if !has_control {
session.insert_char(ch);
session.mark_dirty();
}
None
}
_ => None,
}
}
fn quick_help_lines() -> Vec<String> {
vec![
"Enter queues; Tab queues or accepts an inline suggestion.".to_string(),
"Alt+P: Generate an inline prompt suggestion.".to_string(),
"Ctrl+Enter: Run now while idle, or steer the active task.".to_string(),
"Shift+Enter: Insert a newline.".to_string(),
"/config: Toggle Vim-style prompt editing via Editor mode.".to_string(),
"Ctrl+A / Ctrl+E: Move to start/end of line.".to_string(),
"Ctrl+W: Delete previous word.".to_string(),
"Ctrl+U / Ctrl+K: Delete to start/end of line.".to_string(),
"Ctrl+I or Ctrl+/: Toggle inline lists.".to_string(),
"Alt+Left / Alt+Right: Move by word.".to_string(),
"Ctrl+Z (Unix): Suspend VT Code; use `fg` to resume.".to_string(),
"Esc: Close this overlay.".to_string(),
]
}
fn take_submitted_input(session: &mut Session) -> Option<String> {
let submitted = session.input_manager.content().to_owned();
let submitted_entry = session.input_manager.current_history_entry();
clear_submitted_input(session);
if submitted.trim().is_empty() {
return None;
}
session.remember_submitted_input(submitted_entry);
Some(submitted)
}
fn clear_submitted_input(session: &mut Session) {
session.input_manager.clear();
session.clear_suggested_prompt_state();
session.clear_inline_prompt_suggestion();
session.input_compact_mode = false;
session.scroll_manager.set_offset(0);
}
#[inline]
pub(super) fn emit_inline_event(
event: &InlineEvent,
events: &UnboundedSender<InlineEvent>,
callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
) {
if let Some(cb) = callback {
cb(event);
}
let _ = events.send(event.clone());
}
#[inline]
#[allow(dead_code)]
pub(super) fn handle_scroll_down(
session: &mut Session,
events: &UnboundedSender<InlineEvent>,
callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
) {
session.scroll_line_down();
session.mark_dirty();
emit_inline_event(&InlineEvent::ScrollLineDown, events, callback);
}
#[inline]
#[allow(dead_code)]
pub(super) fn handle_scroll_up(
session: &mut Session,
events: &UnboundedSender<InlineEvent>,
callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
) {
session.scroll_line_up();
session.mark_dirty();
emit_inline_event(&InlineEvent::ScrollLineUp, events, callback);
}