1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
|
//! Top-level AtuinAi component that translates key events into AiTuiEvents.
//!
//! This component wraps the entire view and handles key events that bubble up
//! from child components (or aren't consumed by them). It maps raw key events
//! to semantic `AiTuiEvent` variants based on the current `AppMode`.
use std::sync::mpsc;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use eye_declare::{Elements, EventResult, Hooks, component, props};
use crate::tui::events::AiTuiEvent;
use crate::tui::state::AppMode;
/// Top-level wrapper component for the AI TUI.
///
/// Props carry the current mode so `handle_event` can translate keys
/// into the right `AiTuiEvent`. Children are rendered via slot children.
#[props]
pub(crate) struct AtuinAi {
pub mode: AppMode,
pub has_command: bool,
pub is_input_blank: bool,
pub pending_confirmation: bool,
}
#[derive(Default)]
pub struct AtuinAiState {
tx: Option<mpsc::Sender<AiTuiEvent>>,
}
#[component(props = AtuinAi, state = AtuinAiState, children = Elements)]
fn atuin_ai(
_props: &AtuinAi,
_state: &AtuinAiState,
hooks: &mut Hooks<AtuinAi, AtuinAiState>,
children: Elements,
) -> Elements {
hooks.use_context::<mpsc::Sender<AiTuiEvent>>(|tx, _, state| {
state.tx = tx.cloned();
});
hooks.use_event_capture(move |event, props, state| {
let Event::Key(KeyEvent {
code,
kind: KeyEventKind::Press,
modifiers,
..
}) = event
else {
return EventResult::Ignored;
};
let Some(ref tx) = state.read().tx else {
return EventResult::Ignored;
};
// Ctrl+C always exits
if modifiers.contains(KeyModifiers::CONTROL) && *code == KeyCode::Char('c') {
let _ = tx.send(AiTuiEvent::Exit);
return EventResult::Consumed;
}
match props.mode {
AppMode::Input => match code {
KeyCode::Esc => {
if props.pending_confirmation {
let _ = tx.send(AiTuiEvent::CancelConfirmation);
return EventResult::Consumed;
}
let _ = tx.send(AiTuiEvent::Exit);
EventResult::Consumed
}
KeyCode::Tab => {
if props.has_command && props.is_input_blank {
let _ = tx.send(AiTuiEvent::InsertCommand);
return EventResult::Consumed;
}
EventResult::Ignored
}
KeyCode::Enter => {
if props.has_command && props.is_input_blank {
let _ = tx.send(AiTuiEvent::ExecuteCommand);
return EventResult::Consumed;
}
EventResult::Ignored
}
_ => EventResult::Ignored,
},
AppMode::Generating | AppMode::Streaming => match code {
KeyCode::Esc => {
let _ = tx.send(AiTuiEvent::CancelGeneration);
EventResult::Consumed
}
_ => EventResult::Ignored,
},
AppMode::Error => match code {
KeyCode::Esc => {
let _ = tx.send(AiTuiEvent::Exit);
EventResult::Consumed
}
KeyCode::Enter | KeyCode::Char('r') => {
let _ = tx.send(AiTuiEvent::Retry);
EventResult::Consumed
}
_ => EventResult::Ignored,
},
}
});
children
}
|