Skip to main content

lazytables/commands/
basic.rs

1// FilePath: src/commands/basic.rs
2
3use super::{Command, CommandAction, CommandCategory, CommandContext, CommandId, CommandResult};
4use crate::core::error::Result;
5
6/// Quit command - exits the application
7pub struct QuitCommand;
8
9impl Command for QuitCommand {
10    fn execute(&self, _context: &mut CommandContext) -> Result<CommandResult> {
11        Ok(CommandResult::Action(CommandAction::Quit))
12    }
13
14    fn description(&self) -> &str {
15        "Quit the application"
16    }
17
18    fn id(&self) -> CommandId {
19        CommandId::Quit
20    }
21
22    fn shortcut(&self) -> Option<String> {
23        Some("q".to_string())
24    }
25
26    fn category(&self) -> CommandCategory {
27        CommandCategory::General
28    }
29}
30
31/// Force quit command - exits without confirmation
32pub struct ForceQuitCommand;
33
34impl Command for ForceQuitCommand {
35    fn execute(&self, _context: &mut CommandContext) -> Result<CommandResult> {
36        Ok(CommandResult::Action(CommandAction::Quit))
37    }
38
39    fn description(&self) -> &str {
40        "Force quit the application without confirmation"
41    }
42
43    fn id(&self) -> CommandId {
44        CommandId::ForceQuit
45    }
46
47    fn shortcut(&self) -> Option<String> {
48        Some("Q".to_string())
49    }
50
51    fn category(&self) -> CommandCategory {
52        CommandCategory::General
53    }
54}
55
56/// Help command - shows help overlay
57pub struct HelpCommand;
58
59impl Command for HelpCommand {
60    fn execute(&self, context: &mut CommandContext) -> Result<CommandResult> {
61        use crate::app::state::HelpMode;
62        use crate::state::view::OverlayView;
63
64        // Set help mode based on current pane
65        context.state.ui.help_mode = match context.state.ui.focused_pane {
66            crate::app::FocusedPane::Connections => HelpMode::Connections,
67            crate::app::FocusedPane::Tables => HelpMode::Tables,
68            crate::app::FocusedPane::Details => HelpMode::Details,
69            crate::app::FocusedPane::TabularOutput => HelpMode::TabularOutput,
70            crate::app::FocusedPane::QueryWindow => HelpMode::QueryWindow,
71            crate::app::FocusedPane::SqlFiles => HelpMode::SqlFiles,
72        };
73
74        // Set current view to Help overlay to enable proper event routing
75        context.state.ui.show_overlay(OverlayView::Help);
76
77        // Reset help modal navigation state
78        context.state.ui.reset_help_modal_state();
79
80        Ok(CommandResult::SuccessWithMessage(
81            "Help overlay opened".to_string(),
82        ))
83    }
84
85    fn description(&self) -> &str {
86        "Show context-aware help"
87    }
88
89    fn id(&self) -> CommandId {
90        CommandId::Help
91    }
92
93    fn shortcut(&self) -> Option<String> {
94        Some("?".to_string())
95    }
96
97    fn category(&self) -> CommandCategory {
98        CommandCategory::General
99    }
100}
101
102/// Toggle help command
103pub struct ToggleHelpCommand;
104
105impl Command for ToggleHelpCommand {
106    fn execute(&self, context: &mut CommandContext) -> Result<CommandResult> {
107        use crate::app::state::HelpMode;
108
109        if context.state.ui.help_mode != HelpMode::None {
110            // Close help modal - reset both help_mode and current_view
111            context.state.ui.help_mode = HelpMode::None;
112            context.state.ui.return_to_main();
113            Ok(CommandResult::SuccessWithMessage(
114                "Help overlay closed".to_string(),
115            ))
116        } else {
117            // Delegate to HelpCommand
118            HelpCommand.execute(context)
119        }
120    }
121
122    fn description(&self) -> &str {
123        "Toggle help overlay"
124    }
125
126    fn id(&self) -> CommandId {
127        CommandId::ToggleHelp
128    }
129
130    fn category(&self) -> CommandCategory {
131        CommandCategory::General
132    }
133}
134
135/// Save command - saves current content
136pub struct SaveCommand;
137
138impl Command for SaveCommand {
139    fn execute(&self, context: &mut CommandContext) -> Result<CommandResult> {
140        // Check what needs to be saved based on current focus
141        match context.state.ui.focused_pane {
142            crate::app::FocusedPane::QueryWindow => {
143                // Save current query
144                if !context.state.query_content.is_empty() {
145                    let query = &context.state.query_content;
146                    // Generate filename with timestamp
147                    let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
148                    let filename = format!("query_{timestamp}.sql");
149
150                    // Save to sql_files directory
151                    let sql_dir = dirs::home_dir()
152                        .unwrap_or_else(|| std::path::PathBuf::from("."))
153                        .join(".lazytables")
154                        .join("sql_files");
155
156                    let filepath = sql_dir.join(&filename);
157
158                    // Use async file I/O with block_on (Command trait doesn't support async)
159                    // TODO: Move to background task with event notification for truly non-blocking operation
160                    tokio::runtime::Handle::current().block_on(async {
161                        // Ensure directory exists
162                        crate::io::async_fs::create_dir_all(&sql_dir).await?;
163                        // Write file
164                        crate::io::async_fs::write(&filepath, query).await
165                    })?;
166
167                    // Add success toast
168                    context
169                        .state
170                        .toast_manager
171                        .success(format!("Query saved to {filename}"));
172
173                    Ok(CommandResult::SuccessWithMessage(format!(
174                        "Query saved to {}",
175                        filepath.display()
176                    )))
177                } else {
178                    Ok(CommandResult::Error("No query to save".to_string()))
179                }
180            }
181            crate::app::FocusedPane::TabularOutput => {
182                // Export results to CSV
183                // TODO: Check if results exist once query results are implemented
184                Ok(CommandResult::Error(
185                    "Export not yet implemented".to_string(),
186                ))
187            }
188            _ => Ok(CommandResult::Error(
189                "Nothing to save in current pane".to_string(),
190            )),
191        }
192    }
193
194    fn description(&self) -> &str {
195        "Save current content"
196    }
197
198    fn id(&self) -> CommandId {
199        CommandId::Save
200    }
201
202    fn shortcut(&self) -> Option<String> {
203        Some("Ctrl+s".to_string())
204    }
205
206    fn category(&self) -> CommandCategory {
207        CommandCategory::File
208    }
209
210    fn can_execute(&self, context: &CommandContext) -> bool {
211        // Can save if in query window with content or results pane with data
212        match context.state.ui.focused_pane {
213            crate::app::FocusedPane::QueryWindow => !context.state.query_content.is_empty(),
214            crate::app::FocusedPane::TabularOutput => {
215                // TODO: Check if results exist once query results are implemented
216                false
217            }
218            _ => false,
219        }
220    }
221}
222
223/// Save As command - saves with a specific filename
224pub struct SaveAsCommand;
225
226impl Command for SaveAsCommand {
227    fn execute(&self, context: &mut CommandContext) -> Result<CommandResult> {
228        // This would typically open a file dialog or prompt
229        // For now, delegate to SaveCommand
230        SaveCommand.execute(context)
231    }
232
233    fn description(&self) -> &str {
234        "Save current content with a specific name"
235    }
236
237    fn id(&self) -> CommandId {
238        CommandId::SaveAs
239    }
240
241    fn shortcut(&self) -> Option<String> {
242        Some("Ctrl+Shift+s".to_string())
243    }
244
245    fn category(&self) -> CommandCategory {
246        CommandCategory::File
247    }
248}
249
250/// Open command - opens a file
251pub struct OpenCommand;
252
253impl Command for OpenCommand {
254    fn execute(&self, context: &mut CommandContext) -> Result<CommandResult> {
255        match context.state.ui.focused_pane {
256            crate::app::FocusedPane::SqlFiles => {
257                // Load selected SQL file
258                {
259                    let selected = context.state.ui.selected_sql_file;
260                    let sql_dir = dirs::home_dir()
261                        .unwrap_or_else(|| std::path::PathBuf::from("."))
262                        .join(".lazytables")
263                        .join("sql_files");
264
265                    // Use async file I/O with block_on (Command trait doesn't support async)
266                    // TODO: Move to background task with event notification for truly non-blocking operation
267                    let files = tokio::runtime::Handle::current().block_on(async {
268                        let entries = crate::io::async_fs::read_dir(&sql_dir).await?;
269                        let filtered: Vec<_> = entries
270                            .into_iter()
271                            .filter(|entry| {
272                                entry
273                                    .path()
274                                    .extension()
275                                    .and_then(|ext| ext.to_str())
276                                    .map(|ext| ext == "sql")
277                                    .unwrap_or(false)
278                            })
279                            .collect();
280                        Ok::<_, crate::core::error::LazyTablesError>(filtered)
281                    })?;
282
283                    if selected < files.len() {
284                        let content = tokio::runtime::Handle::current().block_on(async {
285                            crate::io::async_fs::read_to_string(files[selected].path()).await
286                        })?;
287                        context.state.query_content = content;
288                        context.state.ui.current_sql_file =
289                            Some(files[selected].file_name().to_string_lossy().to_string());
290
291                        Ok(CommandResult::SuccessWithMessage(format!(
292                            "Loaded {}",
293                            files[selected].file_name().to_string_lossy()
294                        )))
295                    } else {
296                        Ok(CommandResult::Error("Invalid file selection".to_string()))
297                    }
298                }
299            }
300            _ => Ok(CommandResult::Error(
301                "Navigate to SQL Files pane to open a file".to_string(),
302            )),
303        }
304    }
305
306    fn description(&self) -> &str {
307        "Open a file"
308    }
309
310    fn id(&self) -> CommandId {
311        CommandId::Open
312    }
313
314    fn shortcut(&self) -> Option<String> {
315        Some("Ctrl+o".to_string())
316    }
317
318    fn category(&self) -> CommandCategory {
319        CommandCategory::File
320    }
321}