use super::*;
use crate::config::constants::ui;
use crate::core_tui::session::MouseDragTarget;
use crate::core_tui::session::render::modal_render_styles;
use crate::core_tui::session::{TranscriptLinkClickAction, inline_list, list_panel, modal};
use std::time::Instant;
impl Session {
#[cfg(test)]
pub(crate) fn process_key(&mut self, key: KeyEvent) -> Option<InlineEvent> {
events::process_key(self, key)
}
fn input_area_contains(&self, column: u16, row: u16) -> bool {
self.core.input_area().is_some_and(|area| {
row >= area.y
&& row < area.y.saturating_add(area.height)
&& column >= area.x
&& column < area.x.saturating_add(area.width)
})
}
fn bottom_panel_contains(&self, column: u16, row: u16) -> bool {
self.core.bottom_panel_area().is_some_and(|area| {
row >= area.y
&& row < area.y.saturating_add(area.height)
&& column >= area.x
&& column < area.x.saturating_add(area.width)
})
}
fn panel_row_index(
&self,
layout: &list_panel::ListPanelLayout,
column: u16,
row: u16,
) -> Option<usize> {
let area = self.core.bottom_panel_area()?;
layout.row_index(area, column, row)
}
fn handle_modal_list_result(
&mut self,
result: modal::ModalListKeyResult,
events: &UnboundedSender<InlineEvent>,
callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
) -> bool {
match result {
modal::ModalListKeyResult::NotHandled => false,
modal::ModalListKeyResult::HandledNoRedraw => true,
modal::ModalListKeyResult::Redraw => {
self.mark_dirty();
true
}
modal::ModalListKeyResult::Emit(event) => {
self.mark_dirty();
let outbound: InlineEvent = event.into();
events::emit_inline_event(&outbound, events, callback);
true
}
modal::ModalListKeyResult::Submit(event) | modal::ModalListKeyResult::Cancel(event) => {
self.close_overlay();
self.mark_dirty();
let outbound: InlineEvent = event.into();
events::emit_inline_event(&outbound, events, callback);
true
}
}
}
fn handle_link_click_action(
&mut self,
action: TranscriptLinkClickAction,
clear_drag_target: bool,
events: &UnboundedSender<InlineEvent>,
callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
) -> bool {
match action {
TranscriptLinkClickAction::Open(outbound) => {
if clear_drag_target {
self.core.mouse_drag_target = MouseDragTarget::None;
}
self.mark_dirty();
let outbound: InlineEvent = outbound.into();
events::emit_inline_event(&outbound, events, callback);
self.core.mouse_selection.clear_click_history();
true
}
TranscriptLinkClickAction::Consume => {
if clear_drag_target {
self.core.mouse_drag_target = MouseDragTarget::None;
}
self.core.mouse_selection.clear_click_history();
true
}
TranscriptLinkClickAction::Ignore => false,
}
}
fn modal_visible_index_at(&self, row: u16) -> Option<usize> {
let area = self.core.modal_list_area()?;
if row < area.y || row >= area.y.saturating_add(area.height) {
return None;
}
let styles = modal_render_styles(self);
let content_width =
area.width
.saturating_sub(inline_list::selection_padding_width() as u16) as usize;
let relative_row = usize::from(row.saturating_sub(area.y));
if let Some(wizard) = self.wizard_overlay() {
let step = wizard.steps.get(wizard.current_step)?;
let offset = step.list.list_state.offset();
let visible_indices = &step.list.visible_indices;
let mut consumed_rows = 0usize;
for (visible_index, &item_index) in visible_indices.iter().enumerate().skip(offset) {
let lines = modal::modal_list_item_lines(
&step.list,
visible_index,
item_index,
&styles,
content_width,
None,
);
let height = usize::from(inline_list::row_height(&lines));
if relative_row < consumed_rows + height {
return Some(visible_index);
}
consumed_rows += height;
if consumed_rows >= usize::from(area.height) {
break;
}
}
return None;
}
let modal = self.modal_state()?;
let list = modal.list.as_ref()?;
let offset = list.list_state.offset();
let mut consumed_rows = 0usize;
for (visible_index, &item_index) in list.visible_indices.iter().enumerate().skip(offset) {
let lines = modal::modal_list_item_lines(
list,
visible_index,
item_index,
&styles,
content_width,
None,
);
let height = usize::from(inline_list::row_height(&lines));
if relative_row < consumed_rows + height {
return Some(visible_index);
}
consumed_rows += height;
if consumed_rows >= usize::from(area.height) {
break;
}
}
None
}
fn handle_active_overlay_click(
&mut self,
mouse_event: MouseEvent,
events: &UnboundedSender<InlineEvent>,
callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
) -> bool {
let column = mouse_event.column;
let row = mouse_event.row;
let in_modal_list = self.core.modal_list_area().is_some_and(|area| {
row >= area.y
&& row < area.y.saturating_add(area.height)
&& column >= area.x
&& column < area.x.saturating_add(area.width)
});
if !in_modal_list {
return self.has_active_overlay();
}
let Some(visible_index) = self.modal_visible_index_at(row) else {
return true;
};
if let Some(wizard) = self.wizard_overlay_mut() {
let result = wizard.handle_mouse_click(visible_index);
return self.handle_modal_list_result(result, events, callback);
}
if let Some(modal) = self.modal_state_mut() {
let result = modal.handle_list_mouse_click(visible_index);
return self.handle_modal_list_result(result, events, callback);
}
true
}
fn modal_text_area_contains(&self, column: u16, row: u16) -> bool {
self.core.modal_text_areas().iter().any(|area| {
row >= area.y
&& row < area.y.saturating_add(area.height)
&& column >= area.x
&& column < area.x.saturating_add(area.width)
})
}
fn handle_active_overlay_scroll(
&mut self,
mouse_event: MouseEvent,
down: bool,
events: &UnboundedSender<InlineEvent>,
callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
) -> bool {
if !self.has_active_overlay() {
return false;
}
let column = mouse_event.column;
let row = mouse_event.row;
let in_modal_list = self.core.modal_list_area().is_some_and(|area| {
row >= area.y
&& row < area.y.saturating_add(area.height)
&& column >= area.x
&& column < area.x.saturating_add(area.width)
});
if !in_modal_list {
return true;
}
if let Some(wizard) = self.wizard_overlay_mut() {
let result = wizard.handle_mouse_scroll(down);
return self.handle_modal_list_result(result, events, callback);
}
if let Some(modal) = self.modal_state_mut() {
let result = modal.handle_list_mouse_scroll(down);
return self.handle_modal_list_result(result, events, callback);
}
true
}
fn handle_bottom_panel_scroll(&mut self, down: bool) -> bool {
if self.core.bottom_panel_area().is_none() {
return false;
}
if self.agent_palette_visible() {
let Some(palette) = self.agent_palette.as_mut() else {
return true;
};
if down {
palette.move_selection_down();
} else {
palette.move_selection_up();
}
self.mark_dirty();
return true;
}
if self.file_palette_visible() {
let Some(palette) = self.file_palette.as_mut() else {
return true;
};
if down {
palette.move_selection_down();
} else {
palette.move_selection_up();
}
self.mark_dirty();
return true;
}
if self.history_picker_visible() {
if down {
self.history_picker_state.move_down();
} else {
self.history_picker_state.move_up();
}
self.mark_dirty();
return true;
}
if self.local_agents_visible() {
let changed = if down {
self.local_agents_state.move_selection_down()
} else {
self.local_agents_state.move_selection_up()
};
if changed {
self.mark_dirty();
}
return true;
}
if slash::slash_navigation_available(self) {
if down {
slash::move_slash_selection_down(self);
} else {
slash::move_slash_selection_up(self);
}
return true;
}
false
}
fn handle_bottom_panel_click(&mut self, mouse_event: MouseEvent) -> bool {
let column = mouse_event.column;
let row = mouse_event.row;
if !self.bottom_panel_contains(column, row) {
return false;
}
if self.agent_palette_visible() {
let Some(layout) = render::agent_palette_panel_layout(self) else {
return true;
};
let bottom_area = self.core.bottom_panel_area();
let Some(palette) = self.agent_palette.as_mut() else {
return true;
};
let local_index = bottom_area.and_then(|area| layout.row_index(area, column, row));
let mut apply_name = None;
let mut should_mark_dirty = false;
if !palette.has_agents() {
return true;
}
let page_items = palette.current_page_items();
if let Some(local_index) = local_index
&& let Some((global_index, entry, selected)) = page_items.get(local_index)
{
if *selected {
apply_name = Some(entry.name.clone());
} else if palette.select_index(*global_index) {
should_mark_dirty = true;
}
}
if let Some(name) = apply_name {
self.insert_agent_reference(&name);
self.close_agent_palette();
self.mark_dirty();
} else if should_mark_dirty {
self.mark_dirty();
}
return true;
}
if self.file_palette_visible() {
let Some(layout) = render::file_palette_panel_layout(self) else {
return true;
};
let bottom_area = self.core.bottom_panel_area();
let Some(palette) = self.file_palette.as_mut() else {
return true;
};
let local_index = bottom_area.and_then(|area| layout.row_index(area, column, row));
let mut apply_path = None;
let mut should_mark_dirty = false;
if !palette.has_files() {
return true;
}
let page_items = palette.current_page_items();
if let Some(local_index) = local_index
&& let Some((global_index, entry, selected)) = page_items.get(local_index)
{
if *selected {
apply_path = Some(entry.relative_path.clone());
} else if palette.select_index(*global_index) {
should_mark_dirty = true;
}
}
if let Some(path) = apply_path {
self.insert_file_reference(&path);
self.close_file_palette();
self.mark_dirty();
} else if should_mark_dirty {
self.mark_dirty();
}
return true;
}
if self.history_picker_visible() {
let Some(layout) = render::history_picker_panel_layout(self) else {
return true;
};
if let Some(local_index) = self.panel_row_index(&layout, column, row)
&& !self.history_picker_state.matches.is_empty()
{
let actual_index = self
.history_picker_state
.scroll_offset()
.saturating_add(local_index);
if self.history_picker_state.selected_index() == Some(actual_index) {
let was_active = self.history_picker_visible();
self.history_picker_state
.accept(&mut self.core.input_manager);
self.finish_history_picker_interaction(was_active);
self.mark_dirty();
} else if self.history_picker_state.select_index(actual_index) {
self.mark_dirty();
}
}
return true;
}
if self.local_agents_visible() {
let Some(layout) = render::local_agents_panel_layout(self) else {
return true;
};
if let Some(local_index) = self.panel_row_index(&layout, column, row) {
let actual_index = self
.local_agents_state
.scroll_offset()
.saturating_add(local_index);
if self.local_agents_state.select_index(actual_index) {
self.mark_dirty();
}
}
return true;
}
if slash::slash_navigation_available(self) {
let Some(layout) = slash::slash_panel_layout(self) else {
return true;
};
if let Some(local_index) = self.panel_row_index(&layout, column, row) {
let actual_index = self
.slash_palette
.scroll_offset()
.saturating_add(local_index);
if self.slash_palette.selected_index() == Some(actual_index) {
slash::apply_selected_slash_suggestion(self);
} else {
slash::select_slash_suggestion_index(self, actual_index);
}
}
return true;
}
true
}
pub fn handle_event(
&mut self,
event: CrosstermEvent,
events: &UnboundedSender<InlineEvent>,
callback: Option<&(dyn Fn(&InlineEvent) + Send + Sync + 'static)>,
) {
match event {
CrosstermEvent::Key(key) => {
self.update_held_key_modifiers(&key);
if matches!(key.kind, KeyEventKind::Press)
&& let Some(outbound) = events::process_key(self, key)
{
events::emit_inline_event(&outbound, events, callback);
}
}
CrosstermEvent::Mouse(mouse_event) => {
if !self.core.fullscreen.interaction.mouse_capture {
return;
}
match mouse_event.kind {
MouseEventKind::Moved => {
if self
.update_transcript_file_link_hover(mouse_event.column, mouse_event.row)
{
self.mark_dirty();
}
}
MouseEventKind::ScrollDown => {
self.core.clear_pending_link_click();
self.core.mouse_selection.clear_click_history();
if !self.handle_active_overlay_scroll(mouse_event, true, events, callback)
&& !self.handle_bottom_panel_scroll(true)
{
self.scroll_line_down();
self.mark_dirty();
}
}
MouseEventKind::ScrollUp => {
self.core.clear_pending_link_click();
self.core.mouse_selection.clear_click_history();
if !self.handle_active_overlay_scroll(mouse_event, false, events, callback)
&& !self.handle_bottom_panel_scroll(false)
{
self.scroll_line_up();
self.mark_dirty();
}
}
MouseEventKind::Down(crossterm::event::MouseButton::Left) => {
self.core.clear_pending_link_click();
if self.core.queue_link_click_action(
self.transcript_file_link_click_action(
mouse_event.column,
mouse_event.row,
mouse_event.modifiers,
),
) {
self.core.mouse_selection.clear_click_history();
return;
}
if self.has_active_overlay() {
let in_modal_list = self.core.modal_list_area().is_some_and(|area| {
mouse_event.row >= area.y
&& mouse_event.row < area.y.saturating_add(area.height)
&& mouse_event.column >= area.x
&& mouse_event.column < area.x.saturating_add(area.width)
});
if self
.core
.queue_link_click_action(self.modal_link_click_action(
mouse_event.column,
mouse_event.row,
mouse_event.modifiers,
))
{
self.core.mouse_selection.clear_click_history();
return;
}
if self.modal_text_area_contains(mouse_event.column, mouse_event.row)
&& !in_modal_list
{
let is_double_click = self.core.mouse_selection.register_click(
mouse_event.column,
mouse_event.row,
Instant::now(),
);
if is_double_click {
let modal_double_click_action =
self.core.throttle_link_click_action(
self.modal_link_double_click_action(
mouse_event.column,
mouse_event.row,
),
);
if !matches!(
modal_double_click_action,
TranscriptLinkClickAction::Ignore
) {
self.core.clear_pending_link_click();
}
if self.handle_link_click_action(
modal_double_click_action,
true,
events,
callback,
) {
return;
}
}
self.core.mouse_drag_target = MouseDragTarget::ModalText;
self.core
.mouse_selection
.start_selection(mouse_event.column, mouse_event.row);
self.mark_dirty();
return;
}
}
if self.has_active_overlay()
&& self.handle_active_overlay_click(mouse_event, events, callback)
{
self.core.mouse_selection.clear_click_history();
return;
}
if self.handle_bottom_panel_click(mouse_event) {
self.core.mouse_selection.clear_click_history();
return;
}
if self.handle_input_click(mouse_event) {
self.core.mouse_drag_target = MouseDragTarget::Input;
self.core.mouse_selection.clear();
return;
}
let is_double_click = self.core.mouse_selection.register_click(
mouse_event.column,
mouse_event.row,
Instant::now(),
);
if is_double_click {
let transcript_double_click_action =
self.core.throttle_link_click_action(
self.transcript_file_link_double_click_action(
mouse_event.column,
mouse_event.row,
),
);
if !matches!(
transcript_double_click_action,
TranscriptLinkClickAction::Ignore
) {
self.core.clear_pending_link_click();
}
if self.handle_link_click_action(
transcript_double_click_action,
true,
events,
callback,
) {
return;
}
self.core.mouse_drag_target = MouseDragTarget::None;
let _ = self.handle_transcript_click(mouse_event);
if self
.core
.select_transcript_word_at(mouse_event.column, mouse_event.row)
{
self.mark_dirty();
} else {
self.core.mouse_selection.clear();
}
self.core.mouse_selection.clear_click_history();
return;
}
self.core.mouse_drag_target = MouseDragTarget::Transcript;
self.core
.mouse_selection
.start_selection(mouse_event.column, mouse_event.row);
self.mark_dirty();
self.handle_transcript_click(mouse_event);
}
MouseEventKind::Drag(crossterm::event::MouseButton::Left) => {
self.core.clear_pending_link_click();
match self.core.mouse_drag_target {
MouseDragTarget::Input => {
if let Some(cursor) = self.cursor_index_for_input_point(
mouse_event.column,
mouse_event.row,
) && self.core.input_manager.cursor() != cursor
{
self.core.input_manager.set_cursor_with_selection(cursor);
self.mark_dirty();
}
}
MouseDragTarget::Transcript => {
self.core
.mouse_selection
.update_selection(mouse_event.column, mouse_event.row);
self.mark_dirty();
}
MouseDragTarget::ModalText => {
self.core
.mouse_selection
.update_selection(mouse_event.column, mouse_event.row);
self.mark_dirty();
}
MouseDragTarget::None => {}
}
}
MouseEventKind::Up(crossterm::event::MouseButton::Left) => {
let transcript_link_action = self.core.pending_link_click_action(
self.transcript_file_link_click_action(
mouse_event.column,
mouse_event.row,
mouse_event.modifiers,
),
);
let modal_link_action =
self.core
.pending_link_click_action(self.modal_link_click_action(
mouse_event.column,
mouse_event.row,
mouse_event.modifiers,
));
match self.core.mouse_drag_target {
MouseDragTarget::Input => {
if let Some(cursor) = self.cursor_index_for_input_point(
mouse_event.column,
mouse_event.row,
) && self.core.input_manager.cursor() != cursor
{
self.core.input_manager.set_cursor_with_selection(cursor);
self.mark_dirty();
}
}
MouseDragTarget::Transcript => {
self.core
.mouse_selection
.finish_selection(mouse_event.column, mouse_event.row);
self.mark_dirty();
}
MouseDragTarget::ModalText => {
self.core
.mouse_selection
.finish_selection(mouse_event.column, mouse_event.row);
self.mark_dirty();
}
MouseDragTarget::None => {}
}
self.core.mouse_drag_target = MouseDragTarget::None;
self.core.clear_pending_link_click();
if self.handle_link_click_action(
transcript_link_action,
false,
events,
callback,
) {
return;
}
if self.handle_link_click_action(modal_link_action, false, events, callback)
{
}
}
_ => {}
}
}
CrosstermEvent::Paste(content) => {
events::handle_paste(self, &content);
}
CrosstermEvent::Resize(_, rows) => {
self.apply_view_rows(rows);
self.mark_dirty();
}
CrosstermEvent::FocusGained => {
}
CrosstermEvent::FocusLost => {
self.clear_held_key_modifiers();
}
}
}
pub(crate) fn handle_transcript_click(&mut self, mouse_event: MouseEvent) -> bool {
if !matches!(
mouse_event.kind,
MouseEventKind::Down(crossterm::event::MouseButton::Left)
) {
return false;
}
let Some(area) = self.core.transcript_area() else {
return false;
};
if mouse_event.row < area.y
|| mouse_event.row >= area.y.saturating_add(area.height)
|| mouse_event.column < area.x
|| mouse_event.column >= area.x.saturating_add(area.width)
{
return false;
}
if self.core.transcript_width == 0 || self.core.transcript_rows == 0 {
return false;
}
let row_in_view = (mouse_event.row - area.y) as usize;
if row_in_view >= self.core.transcript_rows as usize {
return false;
}
let viewport_rows = self.core.transcript_rows.max(1) as usize;
let transcript_width = self.core.transcript_width;
let padding = usize::from(ui::INLINE_TRANSCRIPT_BOTTOM_PADDING);
let effective_padding = padding.min(viewport_rows.saturating_sub(1));
let total_rows = self.total_transcript_rows(transcript_width) + effective_padding;
let (top_offset, _clamped_total_rows) =
self.prepare_transcript_scroll(total_rows, viewport_rows);
let view_top = top_offset.min(self.core.scroll_manager.max_offset());
self.core.transcript_view_top = view_top;
let clicked_row = view_top.saturating_add(row_in_view);
let expanded = self.expand_collapsed_paste_at_row(transcript_width, clicked_row);
if expanded {
self.mark_dirty();
}
expanded
}
pub(crate) fn handle_input_click(&mut self, mouse_event: MouseEvent) -> bool {
if !matches!(
mouse_event.kind,
MouseEventKind::Down(crossterm::event::MouseButton::Left)
) {
return false;
}
if !self.input_area_contains(mouse_event.column, mouse_event.row) {
return false;
}
let cursor_at_end =
self.core.input_manager.cursor() == self.core.input_manager.content().len();
if self.core.input_compact_mode()
&& cursor_at_end
&& self.input_compact_placeholder().is_some()
{
self.core.set_input_compact_mode(false);
self.mark_dirty();
return true;
}
if let Some(cursor) = self.cursor_index_for_input_point(mouse_event.column, mouse_event.row)
{
if self.core.input_manager.cursor() != cursor {
self.core.input_manager.set_cursor(cursor);
self.mark_dirty();
}
return true;
}
false
}
}