use super::*;
use ratatui::crossterm::event::KeyModifiers;
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)
}
fn dispatch_action(session: &mut Session, action: Action) -> Option<InlineEvent> {
match action {
Action::Interrupt => handle_interrupt(session),
Action::Exit => {
session.mark_dirty();
Some(InlineEvent::Exit)
}
Action::BackgroundOperation => {
session.mark_dirty();
Some(InlineEvent::BackgroundOperation)
}
Action::OpenModelPicker => {
session.mark_dirty();
Some(InlineEvent::Submit("/model".to_string()))
}
Action::ClearScreen => {
session.mark_dirty();
Some(InlineEvent::Submit("/clear".to_string()))
}
Action::ToggleMode => {
session.clear_inline_prompt_suggestion();
session.mark_dirty();
Some(InlineEvent::ToggleMode)
}
Action::ScrollPageUp => {
session.scroll_page_up();
session.mark_dirty();
Some(InlineEvent::ScrollPageUp)
}
Action::ScrollPageDown => {
session.scroll_page_down();
session.mark_dirty();
Some(InlineEvent::ScrollPageDown)
}
Action::EditQueue => {
if !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 {
None
}
}
Action::HistoryPrevious => {
if session.navigate_history_previous() {
session.mark_dirty();
Some(InlineEvent::HistoryPrevious)
} else {
None
}
}
Action::HistoryNext => {
if session.navigate_history_next() {
session.clear_inline_prompt_suggestion();
session.mark_dirty();
Some(InlineEvent::HistoryNext)
} else {
None
}
}
Action::ToggleLogs => {
session.toggle_logs();
None
}
Action::GeneratePromptSuggestion => {
if !session.input_enabled {
return None;
}
session.clear_inline_prompt_suggestion();
session.mark_dirty();
Some(InlineEvent::RequestInlinePromptSuggestion(
session.input_manager.content().to_string(),
))
}
}
}
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),
)));
}
if modal.list.is_none() {
match key.code {
KeyCode::Esc | KeyCode::Enter => {
session.close_overlay();
session.mark_dirty();
return Some(InlineEvent::Overlay(OverlayEvent::Cancelled));
}
_ => {
return None;
}
}
}
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;
}
}
if let Some(action) = session.bindings.resolve(&key) {
let is_readline_key = has_control
&& !has_command
&& !has_alt
&& matches!(
key.code,
KeyCode::Char('f')
| KeyCode::Char('F')
| KeyCode::Char('b')
| KeyCode::Char('B')
| KeyCode::Char('p')
| KeyCode::Char('P')
| KeyCode::Char('n')
| KeyCode::Char('N')
| KeyCode::Char('t')
| KeyCode::Char('T')
);
let is_alt_key = has_alt
&& !has_control
&& !has_command
&& matches!(
key.code,
KeyCode::Char('d')
| KeyCode::Char('D')
| KeyCode::Char('t')
| KeyCode::Char('T')
| KeyCode::Char('u')
| KeyCode::Char('U')
| KeyCode::Char('l')
| KeyCode::Char('L')
| KeyCode::Char('c')
| KeyCode::Char('C')
| KeyCode::Char('\\')
);
if !is_readline_key && !is_alt_key {
return dispatch_action(session, action);
}
}
match key.code {
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('g') | KeyCode::Char('G')
if has_control && !has_command && !has_alt && session.input_enabled =>
{
let draft = session.input_manager.content().to_string();
session.mark_dirty();
Some(InlineEvent::LaunchEditor { draft })
}
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('z') | KeyCode::Char('Z')
if has_control && !has_command && !has_alt && session.input_enabled =>
{
session.input_manager.undo();
session.mark_dirty();
None
}
KeyCode::Char('y') | KeyCode::Char('Y')
if has_control && !has_command && !has_alt && session.input_enabled =>
{
session.input_manager.redo();
session.mark_dirty();
None
}
KeyCode::Char('f') | KeyCode::Char('F')
if has_control && !has_command && !has_alt && session.input_enabled =>
{
session.move_right();
session.mark_dirty();
None
}
KeyCode::Char('b') | KeyCode::Char('B')
if has_control && !has_command && !has_alt && session.input_enabled =>
{
session.move_left();
session.mark_dirty();
None
}
KeyCode::Char('p') | KeyCode::Char('P')
if has_control && !has_command && !has_alt =>
{
if session.navigate_history_previous() {
session.mark_dirty();
}
None
}
KeyCode::Char('n') | KeyCode::Char('N')
if has_control && !has_command && !has_alt =>
{
if session.navigate_history_next() {
session.mark_dirty();
}
None
}
KeyCode::Char('t') | KeyCode::Char('T')
if has_control && !has_command && !has_alt && session.input_enabled =>
{
session.transpose_chars();
session.mark_dirty();
None
}
KeyCode::Char('d') | KeyCode::Char('D')
if has_alt && !has_control && !has_command && session.input_enabled =>
{
session.delete_word_forward();
session.mark_dirty();
None
}
KeyCode::Char('t') | KeyCode::Char('T')
if has_alt && !has_control && !has_command && session.input_enabled =>
{
session.transpose_words();
session.mark_dirty();
None
}
KeyCode::Char('u') | KeyCode::Char('U')
if has_alt && !has_control && !has_command && session.input_enabled =>
{
session.uppercase_word();
session.mark_dirty();
None
}
KeyCode::Char('l') | KeyCode::Char('L')
if has_alt && !has_control && !has_command && session.input_enabled =>
{
session.lowercase_word();
session.mark_dirty();
None
}
KeyCode::Char('c') | KeyCode::Char('C')
if has_alt && !has_control && !has_command && session.input_enabled =>
{
session.capitalize_word();
session.mark_dirty();
None
}
KeyCode::Char('\\')
if has_alt && !has_control && !has_command && session.input_enabled =>
{
session.delete_whitespace_around_cursor();
session.mark_dirty();
None
}
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::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(ch) => {
if !session.input_enabled {
return None;
}
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![
"─── Input ───────────────────────────────────────────────────".to_string(),
" Enter Submit Shift+Tab Auto-accept edits".to_string(),
" Tab Queue Shift+Enter Insert newline".to_string(),
" Ctrl+Enter Run now Esc Close overlay".to_string(),
" ? This help / Commands".to_string(),
"".to_string(),
"─── Readline (Emacs) ────────────────────────────────────────".to_string(),
" Ctrl+A / E Start / End of line".to_string(),
" Ctrl+F / B Forward / Backward character".to_string(),
" Ctrl+P / N Previous / Next history".to_string(),
" Alt+F / B Forward / Backward word".to_string(),
" Ctrl+T Transpose characters".to_string(),
" Alt+T Transpose words".to_string(),
" Alt+U / L / C Uppercase / Lowercase / Capitalize word".to_string(),
" Ctrl+W Delete previous word".to_string(),
" Alt+D Delete next word".to_string(),
" Ctrl+U / K Delete to start / end of line".to_string(),
" Alt+\\ Delete whitespace around cursor".to_string(),
" Ctrl+Z / Y Undo / Redo".to_string(),
" Ctrl+R / S Reverse / Forward history search".to_string(),
"".to_string(),
"─── Navigation ──────────────────────────────────────────────".to_string(),
" Ctrl+L Clear screen".to_string(),
" Ctrl+G Open in $EDITOR".to_string(),
" Ctrl+M Model picker".to_string(),
" Ctrl+I / / Toggle inline lists".to_string(),
" Ctrl+O Copy last response".to_string(),
" Alt+P Prompt suggestion".to_string(),
" Alt+O Transcript review".to_string(),
" Ctrl+Home/End Jump transcript top/bottom".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);
}