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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::{
connection::add_connection,
tui::{AppState, TextMode},
};
pub async fn handle_form(event: KeyEvent, state: &mut AppState) -> color_eyre::Result<()> {
if state.form.is_none() {
return Ok(());
}
// Ctrl+S always submits regardless of mode or field.
if event.modifiers.contains(KeyModifiers::CONTROL) && event.code == KeyCode::Char('s') {
submit_form(state).await?;
return Ok(());
}
let is_text = state.form.as_ref().unwrap().focused_field().is_text();
let text_mode = state.form.as_ref().unwrap().text_mode.clone();
match event.code {
// ── Universal ───────────────────────────────────────────────────────────────
// Esc: exit Insert → Normal; in Normal → close the form.
KeyCode::Esc => {
if is_text && text_mode == TextMode::Insert {
state.form.as_mut().unwrap().enter_normal();
} else {
state.form = None;
state.overlay = None;
}
}
// Tab / Enter — advance field (always resets to Insert on arrival).
KeyCode::Tab | KeyCode::Enter => {
state.form.as_mut().unwrap().focus_next();
}
// Shift+Tab — go back.
KeyCode::BackTab => {
state.form.as_mut().unwrap().focus_prev();
}
// ── Text field keys ────────────────────────────────────────────────────────
// Up / Down — field navigation, always, regardless of mode.
KeyCode::Down => {
state.form.as_mut().unwrap().focus_next();
}
KeyCode::Up => {
state.form.as_mut().unwrap().focus_prev();
}
// Left / Right: cursor movement on text fields; cycle on selectors.
KeyCode::Left => {
let form = state.form.as_mut().unwrap();
if is_text {
form.cursor_left();
} else {
form.cycle_left();
}
}
KeyCode::Right => {
let form = state.form.as_mut().unwrap();
if is_text {
form.cursor_right();
} else {
form.cycle_right();
}
}
// Backspace: only delete in Insert mode.
KeyCode::Backspace if is_text && text_mode == TextMode::Insert => {
state.form.as_mut().unwrap().delete_before_cursor();
}
// Space: insert on text fields (Insert mode) or toggle selectors.
KeyCode::Char(' ') => {
let form = state.form.as_mut().unwrap();
if is_text && text_mode == TextMode::Insert {
form.insert_char(' ');
} else if !is_text {
form.toggle_focused();
}
}
// q in Normal mode — close the form without saving.
KeyCode::Char('q') if text_mode == TextMode::Normal => {
state.form = None;
state.overlay = None;
}
// Character input.
KeyCode::Char(c) => {
let form = state.form.as_mut().unwrap();
if is_text {
match text_mode {
TextMode::Insert => form.insert_char(c),
TextMode::Normal => match c {
'i' => form.enter_insert_before(),
'a' => form.enter_insert_after(),
'I' => form.enter_insert_at_start(),
'A' => form.enter_insert_at_end(),
'h' => form.cursor_left(),
'l' => form.cursor_right(),
'j' => form.focus_next(),
'k' => form.focus_prev(),
'0' => form.cursor_to_start(),
'$' => form.cursor_to_end(),
'x' => form.delete_at_cursor(),
_ => {}
},
}
} else {
// Selector / toggle fields: h/l cycle, j/k navigate in Normal mode.
if text_mode == TextMode::Normal {
match c {
'h' => form.cycle_left(),
'l' => form.cycle_right(),
'j' => form.focus_next(),
'k' => form.focus_prev(),
_ => {}
}
}
}
}
_ => {}
}
Ok(())
}
async fn submit_form(state: &mut AppState) -> color_eyre::Result<()> {
let Some(ref form) = state.form else {
return Ok(());
};
// Validate before hitting the network.
if let Err(e) = form.validate() {
state.cmdline.set_error(e);
return Ok(());
}
let name = form.name.trim().to_string();
let engine = form.engine.clone();
let source = match form.build_source() {
Ok(s) => s,
Err(e) => {
state.cmdline.set_error(e);
return Ok(());
}
};
// add_connection tests the connection before persisting.
match add_connection(name, source, engine).await {
Ok(db) => {
state.connections.push(db);
state.selected_connection = state.connections.len() - 1;
state.form = None;
state.overlay = None;
}
Err(e) => {
state.cmdline.set_error(e.to_string());
}
}
Ok(())
}