use bmux_keyboard::KeyStroke;
use bmux_tui_components::text_input::{TextInputControl, TextInputOutcome};
use super::app::{BmuxApp, KeyActivationOutcome, composer_policy};
use super::keymap::{BmuxAction, BmuxKeyMap, BmuxScope};
const TRANSCRIPT_SCROLL_ROWS: usize = 3;
const TRANSCRIPT_PAGE_ROWS: usize = 10;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum KeyRequest {
#[default]
None,
Submit {
placement: bcode_ipc::PromptPlacement,
},
Interrupt,
CycleAgent,
CycleThinkingEffort,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct KeyOutcome {
pub redraw: bool,
pub request: KeyRequest,
}
pub fn handle_key(app: &mut BmuxApp, keymap: &BmuxKeyMap, stroke: KeyStroke) -> KeyOutcome {
if let Some(binding) = keymap.binding_for_key(BmuxScope::Chat, stroke) {
match app.activate_key_binding(BmuxScope::Chat, &binding) {
KeyActivationOutcome::Activated(action) => {
if let Some(outcome) = handle_chat_action(app, Some(action)) {
return outcome;
}
}
KeyActivationOutcome::Pending => {
return KeyOutcome {
redraw: true,
request: KeyRequest::None,
};
}
}
}
if let Some(motion) = keymap.editor_selection_motion_for_key(stroke) {
app.clear_pending_key_activation();
app.extend_composer_selection(motion);
return KeyOutcome {
redraw: true,
request: KeyRequest::None,
};
}
if let Some(command) = keymap.editor_command_for_key(stroke) {
app.clear_pending_key_activation();
app.reset_input_history_navigation();
app.composer_mut().apply_command(command);
app.wake_cursor();
return KeyOutcome {
redraw: true,
request: KeyRequest::None,
};
}
let Some(text) = bcode_plugin_sdk::tui::keymap::controls::LiteralText::from_stroke(stroke)
.filter(|_| {
keymap.action("chat", stroke).is_none()
&& !keymap.registry().is_overridden_key("chat", stroke)
})
else {
return KeyOutcome::default();
};
let outcome = TextInputControl::new(&composer_policy()).handle_key_with_history_action(
app.composer_state_mut(),
text.native_stroke(),
None,
);
match outcome {
TextInputOutcome::Submitted => {
app.clear_pending_key_activation();
submit(app, bcode_ipc::PromptPlacement::Steering)
}
TextInputOutcome::Edited => {
app.clear_pending_key_activation();
app.reset_input_history_navigation();
app.wake_cursor();
KeyOutcome {
redraw: true,
request: KeyRequest::None,
}
}
TextInputOutcome::Ignored
| TextInputOutcome::Redraw
| TextInputOutcome::EdgeUp
| TextInputOutcome::EdgeDown => KeyOutcome::default(),
}
}
fn handle_composer_history(
app: &mut BmuxApp,
action: bmux_tui_components::text_input::TextInputHistoryAction,
) -> KeyOutcome {
let result = TextInputControl::new(&composer_policy()).handle_key_with_history_action(
app.composer_state_mut(),
KeyStroke::simple(bmux_keyboard::KeyCode::Escape),
Some(action),
);
if result == TextInputOutcome::Edited {
app.reset_input_history_navigation();
app.wake_cursor();
}
KeyOutcome {
redraw: result != TextInputOutcome::Ignored,
request: KeyRequest::None,
}
}
fn exit_or_clear_composer(app: &mut BmuxApp) -> KeyOutcome {
if app.composer().is_empty() {
app.request_exit();
} else {
app.reset_input_history_navigation();
app.composer_mut().clear();
app.set_status("input cleared; press exit again to quit".to_owned());
app.wake_cursor();
}
KeyOutcome {
redraw: true,
request: KeyRequest::None,
}
}
pub fn handle_chat_action(app: &mut BmuxApp, action: Option<BmuxAction>) -> Option<KeyOutcome> {
use bmux_tui_components::text_input::TextInputHistoryAction;
let outcome = match action? {
BmuxAction::EditorUndo => handle_composer_history(app, TextInputHistoryAction::Undo),
BmuxAction::EditorRedo => handle_composer_history(app, TextInputHistoryAction::Redo),
BmuxAction::AppExit => exit_or_clear_composer(app),
BmuxAction::AppInterrupt => KeyOutcome {
redraw: true,
request: KeyRequest::Interrupt,
},
BmuxAction::InputSubmitSteering => submit(app, bcode_ipc::PromptPlacement::Steering),
BmuxAction::InputSubmitFollowUp => submit(app, bcode_ipc::PromptPlacement::FollowUp),
BmuxAction::AgentCycle => KeyOutcome {
redraw: true,
request: KeyRequest::CycleAgent,
},
BmuxAction::ThinkingEffortCycle => KeyOutcome {
redraw: true,
request: KeyRequest::CycleThinkingEffort,
},
BmuxAction::DiffViewerLayoutCycle => diff_viewer_layout_action(app),
BmuxAction::MarkdownFocusNext
| BmuxAction::MarkdownFocusPrevious
| BmuxAction::MarkdownActivate
| BmuxAction::MarkdownCopyDestination => markdown_action(app, action?),
BmuxAction::InputHistoryPrevious => history_previous(app),
BmuxAction::InputHistoryNext => history_next(app),
BmuxAction::TranscriptPageUp => KeyOutcome {
redraw: app.scroll_transcript_up(TRANSCRIPT_PAGE_ROWS),
request: KeyRequest::None,
},
BmuxAction::TranscriptPageDown => KeyOutcome {
redraw: app.scroll_transcript_down(TRANSCRIPT_PAGE_ROWS),
request: KeyRequest::None,
},
BmuxAction::TranscriptTop => KeyOutcome {
redraw: app.scroll_transcript_up(usize::MAX / 2),
request: KeyRequest::None,
},
BmuxAction::TranscriptBottom => KeyOutcome {
redraw: app.scroll_transcript_to_bottom(),
request: KeyRequest::None,
},
BmuxAction::TranscriptLineUp => KeyOutcome {
redraw: app.scroll_transcript_up(TRANSCRIPT_SCROLL_ROWS),
request: KeyRequest::None,
},
BmuxAction::TranscriptLineDown => KeyOutcome {
redraw: app.scroll_transcript_down(TRANSCRIPT_SCROLL_ROWS),
request: KeyRequest::None,
},
BmuxAction::ClipboardPasteImage
| BmuxAction::TranscriptCopySelection
| BmuxAction::TranscriptClearSelection
| BmuxAction::CommandPaletteOpen
| BmuxAction::SessionSearchOpen
| BmuxAction::PermissionApprove
| BmuxAction::PermissionDeny
| BmuxAction::SelectUp
| BmuxAction::SelectDown
| BmuxAction::SelectConfirm
| BmuxAction::SelectCancel
| BmuxAction::SessionNew
| BmuxAction::SessionRename
| BmuxAction::SessionDelete
| BmuxAction::InputNewLine
| BmuxAction::EditorMoveLeft
| BmuxAction::EditorMoveRight
| BmuxAction::EditorMoveWordLeft
| BmuxAction::EditorMoveWordRight
| BmuxAction::EditorMoveStart
| BmuxAction::EditorMoveEnd
| BmuxAction::EditorSelectLeft
| BmuxAction::EditorSelectRight
| BmuxAction::EditorSelectWordLeft
| BmuxAction::EditorSelectWordRight
| BmuxAction::EditorSelectUp
| BmuxAction::EditorSelectDown
| BmuxAction::EditorDeleteBackward
| BmuxAction::EditorDeleteForward
| BmuxAction::EditorDeleteWordBackward
| BmuxAction::EditorDeleteWordForward
| BmuxAction::EditorDeleteToStart
| BmuxAction::EditorDeleteToEnd
| BmuxAction::InteractionFocusActive
| BmuxAction::SkillInvoke
| BmuxAction::SkillActivate
| BmuxAction::SkillDeactivate
| BmuxAction::SkillHelp => return None,
};
Some(outcome)
}
fn diff_viewer_layout_action(app: &mut BmuxApp) -> KeyOutcome {
app.cycle_diff_viewer_layout();
KeyOutcome {
redraw: true,
request: KeyRequest::None,
}
}
fn markdown_action(app: &mut BmuxApp, action: BmuxAction) -> KeyOutcome {
let redraw = match action {
BmuxAction::MarkdownFocusNext => app.move_markdown_focus(false),
BmuxAction::MarkdownFocusPrevious => app.move_markdown_focus(true),
BmuxAction::MarkdownActivate => app.activate_focused_markdown_contribution(),
BmuxAction::MarkdownCopyDestination => app.copy_focused_markdown_destination(),
_ => false,
};
KeyOutcome {
redraw,
request: KeyRequest::None,
}
}
fn history_previous(app: &mut BmuxApp) -> KeyOutcome {
KeyOutcome {
redraw: if app.input_history_navigation_active() {
app.move_composer_visual_up_preserving_history() || app.previous_input_history()
} else {
app.move_composer_visual_up() || app.previous_input_history()
},
request: KeyRequest::None,
}
}
fn history_next(app: &mut BmuxApp) -> KeyOutcome {
KeyOutcome {
redraw: if app.input_history_navigation_active() {
app.move_composer_visual_down_preserving_history() || app.next_input_history()
} else {
app.move_composer_visual_down() || app.next_input_history()
},
request: KeyRequest::None,
}
}
fn submit(app: &mut BmuxApp, placement: bcode_ipc::PromptPlacement) -> KeyOutcome {
app.stage_submission();
app.wake_cursor();
KeyOutcome {
redraw: true,
request: KeyRequest::Submit { placement },
}
}