Skip to main content

fresh/app/
lifecycle.rs

1//! Editor-lifecycle methods: quit, restart, session/detach control,
2//! focus/resize hooks, theme/settings queries, escape-sequence + clipboard
3//! piping, and the should_quit confirmation flow that walks modified buffers.
4
5use super::*;
6
7impl Editor {
8    /// Check if the editor should quit
9    pub fn should_quit(&self) -> bool {
10        self.should_quit
11    }
12
13    /// Check if the client should detach (keep server running)
14    pub fn should_detach(&self) -> bool {
15        self.should_detach
16    }
17
18    /// Clear the detach flag (after processing)
19    pub fn clear_detach(&mut self) {
20        self.should_detach = false;
21    }
22
23    /// Set session mode (use hardware cursor only, no REVERSED style for software cursor)
24    pub fn set_session_mode(&mut self, session_mode: bool) {
25        self.session_mode = session_mode;
26        self.clipboard.set_session_mode(session_mode);
27        // Also set custom context for command palette filtering
28        if session_mode {
29            self.active_custom_contexts
30                .insert(crate::types::context_keys::SESSION_MODE.to_string());
31        } else {
32            self.active_custom_contexts
33                .remove(crate::types::context_keys::SESSION_MODE);
34        }
35    }
36
37    /// Check if running in session mode
38    pub fn is_session_mode(&self) -> bool {
39        self.session_mode
40    }
41
42    /// Mark that the backend does not render a hardware cursor.
43    /// When set, the renderer always draws a software cursor indicator.
44    pub fn set_software_cursor_only(&mut self, enabled: bool) {
45        self.software_cursor_only = enabled;
46    }
47
48    /// Set the session name for display in status bar.
49    ///
50    /// When a session name is set, the recovery service is reinitialized
51    /// to use a session-scoped recovery directory so each named session's
52    /// recovery data is isolated.
53    pub fn set_session_name(&mut self, name: Option<String>) {
54        if let Some(ref session_name) = name {
55            let base_recovery_dir = self.dir_context.recovery_dir();
56            let scope = crate::services::recovery::RecoveryScope::Session {
57                name: session_name.clone(),
58            };
59            let recovery_config = RecoveryConfig {
60                enabled: self.recovery_service.is_enabled(),
61                ..RecoveryConfig::default()
62            };
63            self.recovery_service =
64                RecoveryService::with_scope(recovery_config, &base_recovery_dir, &scope);
65        }
66        self.session_name = name;
67    }
68
69    /// Get the session name (for status bar display)
70    pub fn session_name(&self) -> Option<&str> {
71        self.session_name.as_deref()
72    }
73
74    /// Queue escape sequences to be sent to the client (session mode only)
75    pub fn queue_escape_sequences(&mut self, sequences: &[u8]) {
76        self.pending_escape_sequences.extend_from_slice(sequences);
77    }
78
79    /// Take pending escape sequences, clearing the queue
80    pub fn take_pending_escape_sequences(&mut self) -> Vec<u8> {
81        std::mem::take(&mut self.pending_escape_sequences)
82    }
83
84    /// Take pending clipboard data queued in session mode, clearing the request
85    pub fn take_pending_clipboard(
86        &mut self,
87    ) -> Option<crate::services::clipboard::PendingClipboard> {
88        self.clipboard.take_pending_clipboard()
89    }
90
91    /// Check if the editor should restart with a new working directory
92    pub fn should_restart(&self) -> bool {
93        self.restart_with_dir.is_some()
94    }
95
96    /// Take the restart directory, clearing the restart request
97    /// Returns the new working directory if a restart was requested
98    pub fn take_restart_dir(&mut self) -> Option<PathBuf> {
99        self.restart_with_dir.take()
100    }
101
102    /// Request the editor to restart with a new working directory
103    /// This triggers a clean shutdown and restart with the new project root
104    /// Request a full hardware terminal clear and redraw on the next frame.
105    /// Used after external commands have messed up the terminal state.
106    pub fn request_full_redraw(&mut self) {
107        self.full_redraw_requested = true;
108    }
109
110    /// Check if a full redraw was requested, and clear the flag.
111    pub fn take_full_redraw_request(&mut self) -> bool {
112        let requested = self.full_redraw_requested;
113        self.full_redraw_requested = false;
114        requested
115    }
116
117    pub fn request_restart(&mut self, new_working_dir: PathBuf) {
118        tracing::info!(
119            "Restart requested with new working directory: {}",
120            new_working_dir.display()
121        );
122        self.restart_with_dir = Some(new_working_dir);
123        // Also signal quit so the event loop exits
124        self.should_quit = true;
125    }
126
127    /// Get the active theme
128    pub fn theme(&self) -> &crate::view::theme::Theme {
129        &self.theme
130    }
131
132    /// Check if the settings dialog is open and visible
133    pub fn is_settings_open(&self) -> bool {
134        self.settings_state.as_ref().is_some_and(|s| s.visible)
135    }
136
137    /// Request the editor to quit
138    pub fn quit(&mut self) {
139        // Check for unsaved buffers (all are auto-persisted when hot_exit is enabled)
140        let modified_count = self.count_modified_buffers_needing_prompt();
141        if modified_count > 0 {
142            let save_key = t!("prompt.key.save").to_string();
143            let cancel_key = t!("prompt.key.cancel").to_string();
144            let hot_exit = self.config.editor.hot_exit;
145
146            let msg = if hot_exit {
147                // With hot exit: offer save, quit-without-saving (recoverable), or cancel
148                let quit_key = t!("prompt.key.quit").to_string();
149                if modified_count == 1 {
150                    t!(
151                        "prompt.quit_modified_hot_one",
152                        save_key = save_key,
153                        quit_key = quit_key,
154                        cancel_key = cancel_key
155                    )
156                    .to_string()
157                } else {
158                    t!(
159                        "prompt.quit_modified_hot_many",
160                        count = modified_count,
161                        save_key = save_key,
162                        quit_key = quit_key,
163                        cancel_key = cancel_key
164                    )
165                    .to_string()
166                }
167            } else {
168                // Without hot exit: offer save, discard, or cancel
169                let discard_key = t!("prompt.key.discard").to_string();
170                if modified_count == 1 {
171                    t!(
172                        "prompt.quit_modified_one",
173                        save_key = save_key,
174                        discard_key = discard_key,
175                        cancel_key = cancel_key
176                    )
177                    .to_string()
178                } else {
179                    t!(
180                        "prompt.quit_modified_many",
181                        count = modified_count,
182                        save_key = save_key,
183                        discard_key = discard_key,
184                        cancel_key = cancel_key
185                    )
186                    .to_string()
187                }
188            };
189            self.start_prompt(msg, PromptType::ConfirmQuitWithModified);
190        } else {
191            self.should_quit = true;
192        }
193    }
194
195    /// Count modified buffers that would require a save prompt on quit.
196    ///
197    /// When `hot_exit` is enabled, unnamed buffers are excluded (they are
198    /// automatically recovered across sessions), but file-backed modified
199    /// buffers still trigger a prompt with a "recoverable" option.
200    /// When `auto_save_enabled` is true, file-backed buffers are excluded
201    /// (they will be saved to disk on exit).
202    fn count_modified_buffers_needing_prompt(&self) -> usize {
203        let hot_exit = self.config.editor.hot_exit;
204        let auto_save = self.config.editor.auto_save_enabled;
205
206        self.buffers
207            .iter()
208            .filter(|(buffer_id, state)| {
209                if !state.buffer.is_modified() {
210                    return false;
211                }
212                if let Some(meta) = self.buffer_metadata.get(buffer_id) {
213                    if let Some(path) = meta.file_path() {
214                        let is_unnamed = path.as_os_str().is_empty();
215                        if is_unnamed && hot_exit {
216                            return false; // unnamed buffer, auto-recovered via hot exit
217                        }
218                        if !is_unnamed && auto_save {
219                            return false; // file-backed, will be auto-saved on exit
220                        }
221                    }
222                }
223                true
224            })
225            .count()
226    }
227
228    /// Handle terminal focus gained event
229    pub fn focus_gained(&mut self) {
230        self.plugin_manager.run_hook(
231            "focus_gained",
232            crate::services::plugins::hooks::HookArgs::FocusGained,
233        );
234    }
235
236    /// Resize all buffers to match new terminal size
237    pub fn resize(&mut self, width: u16, height: u16) {
238        // Update terminal dimensions for future buffer creation
239        self.terminal_width = width;
240        self.terminal_height = height;
241
242        // Resize all SplitViewState viewports (viewport is now owned by SplitViewState)
243        for view_state in self.split_view_states.values_mut() {
244            view_state.viewport.resize(width, height);
245        }
246
247        // Resize visible terminal PTYs to match new dimensions
248        self.resize_visible_terminals();
249
250        // Notify plugins of the resize so they can adjust layouts
251        self.plugin_manager.run_hook(
252            "resize",
253            fresh_core::hooks::HookArgs::Resize { width, height },
254        );
255    }
256}