1use color_eyre::Result;
2use envx_core::{EnvVar, EnvVarManager};
3use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
4use tui_input::Input;
5use tui_input::backend::crossterm::EventHandler;
6use tui_textarea::{CursorMove, TextArea};
7
8#[derive(Debug, Clone, PartialEq)]
9pub enum Mode {
10 Normal,
11 Search,
12 Edit,
13 Add,
14 Confirm(ConfirmAction),
15 View(String), }
17
18#[derive(Debug, Clone, PartialEq)]
19pub enum ConfirmAction {
20 Delete(String),
21 Save(String, String),
22}
23
24#[derive(Debug, Clone, Copy, PartialEq)]
25pub enum EditField {
26 Name,
27 Value,
28}
29
30pub struct App {
31 pub manager: EnvVarManager,
32 pub mode: Mode,
33 pub selected_index: usize,
34 pub filtered_vars: Vec<EnvVar>,
35 pub search_input: Input,
36 pub edit_name_input: Input,
37 pub edit_value_textarea: TextArea<'static>,
38 pub active_edit_field: EditField,
39 pub status_message: Option<(String, std::time::Instant)>,
40 pub should_quit: bool,
41 pub scroll_offset: usize,
42}
43
44impl App {
45 pub fn new() -> Result<Self> {
51 let mut manager = EnvVarManager::new();
52 manager.load_all()?;
53 let vars = manager.list().into_iter().cloned().collect();
54
55 Ok(Self {
56 manager,
57 mode: Mode::Normal,
58 selected_index: 0,
59 filtered_vars: vars,
60 search_input: Input::default(),
61 edit_name_input: Input::default(),
62 edit_value_textarea: TextArea::default(),
63 active_edit_field: EditField::Name,
64 status_message: None,
65 should_quit: false,
66 scroll_offset: 0,
67 })
68 }
69
70 pub fn handle_key_event(&mut self, key: KeyEvent) -> Result<bool> {
79 match self.mode {
80 Mode::Normal => self.handle_normal_mode(key),
81 Mode::Search => Ok(self.handle_search_mode(key)),
82 Mode::Edit | Mode::Add => Ok(self.handle_edit_mode(key)),
83 Mode::Confirm(ref action) => self.handle_confirm_mode(key, action.clone()),
84 Mode::View(_) => Ok(self.handle_view_mode(key)),
85 }
86 }
87
88 fn handle_normal_mode(&mut self, key: KeyEvent) -> Result<bool> {
89 match key.code {
90 KeyCode::Char('q' | 'Q') => {
91 self.should_quit = true;
92 return Ok(true);
93 }
94 KeyCode::Char('/') => {
95 self.mode = Mode::Search;
96 self.search_input.reset();
97 }
98 KeyCode::Char('a' | 'A') => {
99 self.mode = Mode::Add;
100 self.edit_name_input.reset();
101 self.edit_value_textarea = TextArea::default();
102 self.active_edit_field = EditField::Name;
103 }
104 KeyCode::Char('e' | 'E') => {
105 if !self.filtered_vars.is_empty() {
106 let var = &self.filtered_vars[self.selected_index];
107 self.edit_name_input = Input::default().with_value(var.name.clone());
108
109 let mut textarea = TextArea::from(var.value.lines().collect::<Vec<&str>>());
111 textarea.move_cursor(CursorMove::End);
112 self.edit_value_textarea = textarea;
113
114 self.active_edit_field = EditField::Name;
115 self.mode = Mode::Edit;
116 }
117 }
118 KeyCode::Char('v' | 'V') | KeyCode::Enter => {
119 if !self.filtered_vars.is_empty() {
120 let var = &self.filtered_vars[self.selected_index];
121 self.mode = Mode::View(var.name.clone());
122 }
123 }
124 KeyCode::Char('d' | 'D') => {
125 if !self.filtered_vars.is_empty() {
126 let var_name = self.filtered_vars[self.selected_index].name.clone();
127 self.mode = Mode::Confirm(ConfirmAction::Delete(var_name));
128 }
129 }
130 KeyCode::Char('r' | 'R') => {
131 self.refresh_vars()?;
132 self.set_status("Refreshed environment variables");
133 }
134 KeyCode::Up | KeyCode::Char('k') => {
135 self.move_selection_up();
136 }
137 KeyCode::Down | KeyCode::Char('j') => {
138 self.move_selection_down();
139 }
140 KeyCode::PageUp => {
141 self.page_up();
142 }
143 KeyCode::PageDown => {
144 self.page_down();
145 }
146 KeyCode::Home => {
147 self.selected_index = 0;
148 self.scroll_offset = 0;
149 }
150 KeyCode::End => {
151 if !self.filtered_vars.is_empty() {
152 self.selected_index = self.filtered_vars.len() - 1;
153 }
154 }
155 _ => {}
156 }
157 Ok(false)
158 }
159
160 fn handle_view_mode(&mut self, key: KeyEvent) -> bool {
161 match key.code {
162 KeyCode::Esc | KeyCode::Char('q') | KeyCode::Enter => {
163 self.mode = Mode::Normal;
164 }
165 _ => {}
166 }
167 false
168 }
169
170 const fn move_selection_up(&mut self) {
171 if self.selected_index > 0 {
172 self.selected_index -= 1;
173 }
174 }
175
176 fn move_selection_down(&mut self) {
177 if !self.filtered_vars.is_empty() && self.selected_index < self.filtered_vars.len() - 1 {
178 self.selected_index += 1;
179 }
180 }
181
182 const fn page_up(&mut self) {
183 if self.selected_index >= 10 {
184 self.selected_index -= 10;
185 } else {
186 self.selected_index = 0;
187 }
188 }
189
190 fn page_down(&mut self) {
191 let max_index = if self.filtered_vars.is_empty() {
192 0
193 } else {
194 self.filtered_vars.len() - 1
195 };
196 if self.selected_index + 10 <= max_index {
197 self.selected_index += 10;
198 } else {
199 self.selected_index = max_index;
200 }
201 }
202
203 pub const fn calculate_scroll(&mut self, visible_height: usize) {
204 if self.selected_index < self.scroll_offset {
206 self.scroll_offset = self.selected_index;
207 } else if self.selected_index >= self.scroll_offset + visible_height {
208 self.scroll_offset = self.selected_index.saturating_sub(visible_height - 1);
209 }
210 }
211
212 fn handle_search_mode(&mut self, key: KeyEvent) -> bool {
213 match key.code {
214 KeyCode::Esc | KeyCode::Enter => {
215 self.mode = Mode::Normal;
216 self.apply_search();
217 }
218 _ => {
219 self.search_input.handle_event(&Event::Key(key));
220 self.apply_search();
221 }
222 }
223 false
224 }
225
226 fn handle_edit_mode(&mut self, key: KeyEvent) -> bool {
227 match key.code {
228 KeyCode::Esc => {
229 self.mode = Mode::Normal;
230 }
231 KeyCode::Tab => {
232 self.active_edit_field = match self.active_edit_field {
234 EditField::Name => EditField::Value,
235 EditField::Value => EditField::Name,
236 };
237 }
238 KeyCode::Enter if key.modifiers.contains(KeyModifiers::CONTROL) => {
239 let name = self.edit_name_input.value().to_string();
241 let value = self.edit_value_textarea.lines().join("\n");
242 if !name.is_empty() {
243 self.mode = Mode::Confirm(ConfirmAction::Save(name, value));
244 }
245 }
246 _ => {
247 match self.active_edit_field {
249 EditField::Name => {
250 self.edit_name_input.handle_event(&Event::Key(key));
251 }
252 EditField::Value => {
253 self.edit_value_textarea.input(key);
254 }
255 }
256 }
257 }
258 false
259 }
260
261 fn handle_confirm_mode(&mut self, key: KeyEvent, action: ConfirmAction) -> Result<bool> {
262 match key.code {
263 KeyCode::Char('y' | 'Y') => {
264 match action {
265 ConfirmAction::Delete(name) => match self.manager.delete(&name) {
266 Ok(()) => {
267 self.refresh_vars()?;
268 self.set_status(&format!("Deleted variable: {name}"));
269 }
270 Err(e) => {
271 self.set_status(&format!("Error deleting variable: {e}"));
272 }
273 },
274 ConfirmAction::Save(name, value) => match self.manager.set(&name, &value, false) {
275 Ok(()) => {
276 self.refresh_vars()?;
277 self.set_status(&format!("Saved variable: {name}"));
278 }
279 Err(e) => {
280 self.set_status(&format!("Error saving variable: {e}"));
281 }
282 },
283 }
284 self.mode = Mode::Normal;
285 }
286 KeyCode::Char('n' | 'N') | KeyCode::Esc => {
287 self.mode = Mode::Normal;
288 }
289 _ => {}
290 }
291 Ok(false)
292 }
293
294 pub fn tick(&mut self) {
295 if let Some((_, timestamp)) = &self.status_message {
297 if timestamp.elapsed().as_secs() > 3 {
298 self.status_message = None;
299 }
300 }
301 }
302
303 fn apply_search(&mut self) {
304 let search_term = self.search_input.value();
305 if search_term.is_empty() {
306 self.filtered_vars = self.manager.list().into_iter().cloned().collect();
307 } else {
308 self.filtered_vars = self.manager.search(search_term).into_iter().cloned().collect();
309 }
310
311 if self.selected_index >= self.filtered_vars.len() && !self.filtered_vars.is_empty() {
313 self.selected_index = self.filtered_vars.len() - 1;
314 }
315 self.scroll_offset = 0; }
317
318 fn refresh_vars(&mut self) -> Result<()> {
319 self.manager.load_all()?;
320 self.apply_search();
321 Ok(())
322 }
323
324 fn set_status(&mut self, message: &str) {
325 self.status_message = Some((message.to_string(), std::time::Instant::now()));
326 }
327}