use super::{EditorAction, KeyAction, handle_editor_key};
use crate::{
core::keybindings,
modes::interactive::{
app::App,
giff::{GiffView, Pane},
},
};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
use ratatui::layout::Rect;
pub(super) fn handle_key(st: &mut App, key: &KeyEvent) -> KeyAction {
if st.giff.as_ref().is_some_and(GiffView::is_input_active) {
if key.code == KeyCode::Esc && key.modifiers.is_empty() {
if let Some(view) = st.giff.as_mut() {
view.set_input_active(false);
}
st.dirty = true;
return KeyAction::Continue;
}
if keybindings::get_global().matches(key, "app.editor.external") {
return KeyAction::Continue;
}
if keybindings::get_global().matches(key, "tui.input.submit")
&& !key.modifiers.contains(KeyModifiers::SHIFT)
&& !st.editor.is_empty()
{
st.close_giff();
}
return match handle_editor_key(st, key) {
EditorAction::Quit => KeyAction::Quit,
EditorAction::Continue | EditorAction::OpenExternal(_) => KeyAction::Continue,
};
}
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
st.close_giff();
return KeyAction::Continue;
}
let help_open = st.giff.as_ref().is_some_and(GiffView::is_help_open);
if help_open {
if matches!(
key.code,
KeyCode::Esc | KeyCode::Char('?') | KeyCode::Char('q')
) {
if let Some(view) = st.giff.as_mut() {
view.close_help();
}
st.dirty = true;
}
return KeyAction::Continue;
}
match key.code {
KeyCode::Char('q') => st.close_giff(),
KeyCode::Char('i') => {
if let Some(view) = st.giff.as_mut() {
view.set_input_active(true);
}
st.dirty = true;
}
KeyCode::Char('?') => {
if let Some(view) = st.giff.as_mut() {
view.toggle_help();
}
st.dirty = true;
}
_ => {
let now_ms = st.accel_epoch.elapsed().as_millis() as u64;
if let Some(view) = st.giff.as_mut()
&& navigate(view, key, now_ms)
{
st.dirty = true;
}
}
}
KeyAction::Continue
}
fn navigate(view: &mut GiffView, key: &KeyEvent, now_ms: u64) -> bool {
let in_file_list = view.focused_pane() == Pane::FileList;
let page = view.page_rows().max(1) as isize;
match key.code {
KeyCode::Char('j') | KeyCode::Down => {
let step = view.nav_step(1, now_ms);
if in_file_list {
view.move_file(step);
} else {
view.scroll_by(step);
}
true
}
KeyCode::Char('k') | KeyCode::Up => {
let step = view.nav_step(-1, now_ms);
if in_file_list {
view.move_file(-step);
} else {
view.scroll_by(-step);
}
true
}
KeyCode::PageDown => {
if in_file_list {
view.move_file(page);
} else {
view.page(true);
}
true
}
KeyCode::PageUp => {
if in_file_list {
view.move_file(-page);
} else {
view.page(false);
}
true
}
KeyCode::Home => {
if in_file_list {
view.select_edge(false);
} else {
view.scroll_to_top();
}
true
}
KeyCode::End => {
if in_file_list {
view.select_edge(true);
} else {
view.scroll_to_bottom();
}
true
}
KeyCode::Tab => {
view.toggle_focus();
true
}
KeyCode::Char('h') | KeyCode::Left => {
view.focus(Pane::FileList);
true
}
KeyCode::Char('l') | KeyCode::Right => {
view.focus(Pane::DiffContent);
true
}
KeyCode::Char('u') => {
view.toggle_view();
true
}
_ => {
view.reset_nav_repeat();
false
}
}
}
fn rect_contains(area: Rect, column: u16, row: u16) -> bool {
column >= area.x
&& column < area.x.saturating_add(area.width)
&& row >= area.y
&& row < area.y.saturating_add(area.height)
}
impl App {
fn close_giff(&mut self) {
if let Some(view) = self.giff.take() {
self.giff_restore = view.into_restore();
}
self.full_redraw = true;
self.dirty = true;
}
pub(super) fn giff_wheel(&mut self, mouse: &MouseEvent) {
if self.giff.as_ref().is_some_and(|view| view.is_help_open()) {
return;
}
let lines = self.wheel_lines(mouse);
let delta = if mouse.kind == MouseEventKind::ScrollDown {
lines as isize
} else {
-(lines as isize)
};
let Some(view) = self.giff.as_mut() else {
return;
};
if view
.file_list_area()
.is_some_and(|area| rect_contains(area, mouse.column, mouse.row))
{
view.move_file(delta);
} else {
view.scroll_by(delta);
}
self.dirty = true;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::modes::interactive::giff::{FileChanges, GiffRestore};
use crossterm::event::Event;
use std::{cell::RefCell, rc::Rc};
#[test]
fn giff_view_input_and_quit_keys() {
let _dir = crate::test_support::AgentDirGuard::temp();
let shared = Rc::new(RefCell::new(App::new()));
let view = {
let st = shared.borrow();
GiffView::new(
FileChanges::new(),
"HEAD".to_string(),
"Working Tree".to_string(),
&st.theme,
&GiffRestore::default(),
)
};
shared.borrow_mut().giff = Some(view);
let press = |shared: &Rc<RefCell<App>>, code: KeyCode| {
crate::modes::interactive::handlers::handle_event(
shared,
Event::Key(KeyEvent::new(code, KeyModifiers::NONE)),
);
};
press(&shared, KeyCode::Char('i'));
assert!(shared.borrow().giff.as_ref().unwrap().is_input_active());
assert_eq!(shared.borrow().editor.text(), "");
press(&shared, KeyCode::Char('h'));
press(&shared, KeyCode::Char('i'));
assert_eq!(shared.borrow().editor.text(), "hi");
press(&shared, KeyCode::Esc);
assert!(!shared.borrow().giff.as_ref().unwrap().is_input_active());
assert!(shared.borrow().giff.is_some(), "Esc 不退出视图");
assert_eq!(shared.borrow().editor.text(), "hi");
press(&shared, KeyCode::Char('q'));
assert!(shared.borrow().giff.is_none());
assert!(shared.borrow().full_redraw);
}
}