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    /// Request the event loop to suspend the editor process (SIGTSTP on Unix).
118    /// The loop tears down terminal modes, raises the signal, then re-enables
119    /// modes once the shell sends SIGCONT (e.g. via `fg`).
120    pub fn request_suspend(&mut self) {
121        self.suspend_requested = true;
122    }
123
124    /// Check if a suspend was requested, and clear the flag.
125    pub fn take_suspend_request(&mut self) -> bool {
126        let requested = self.suspend_requested;
127        self.suspend_requested = false;
128        requested
129    }
130
131    pub fn request_restart(&mut self, new_working_dir: PathBuf) {
132        tracing::info!(
133            "Restart requested with new working directory: {}",
134            new_working_dir.display()
135        );
136        self.restart_with_dir = Some(new_working_dir);
137        // Also signal quit so the event loop exits
138        self.should_quit = true;
139    }
140
141    /// Get the active theme
142    pub fn theme(&self) -> &crate::view::theme::Theme {
143        &self.theme
144    }
145
146    /// Check if the settings dialog is open and visible
147    pub fn is_settings_open(&self) -> bool {
148        self.settings_state.as_ref().is_some_and(|s| s.visible)
149    }
150
151    /// Request the editor to quit
152    pub fn quit(&mut self) {
153        // Check for unsaved buffers (all are auto-persisted when hot_exit is enabled)
154        let modified_count = self.count_modified_buffers_needing_prompt();
155        if modified_count > 0 {
156            let save_key = t!("prompt.key.save").to_string();
157            let cancel_key = t!("prompt.key.cancel").to_string();
158            let hot_exit = self.config.editor.hot_exit;
159
160            let msg = if hot_exit {
161                // With hot exit: offer save, quit-without-saving (recoverable), or cancel
162                let quit_key = t!("prompt.key.quit").to_string();
163                if modified_count == 1 {
164                    t!(
165                        "prompt.quit_modified_hot_one",
166                        save_key = save_key,
167                        quit_key = quit_key,
168                        cancel_key = cancel_key
169                    )
170                    .to_string()
171                } else {
172                    t!(
173                        "prompt.quit_modified_hot_many",
174                        count = modified_count,
175                        save_key = save_key,
176                        quit_key = quit_key,
177                        cancel_key = cancel_key
178                    )
179                    .to_string()
180                }
181            } else {
182                // Without hot exit: offer save, discard, or cancel
183                let discard_key = t!("prompt.key.discard").to_string();
184                if modified_count == 1 {
185                    t!(
186                        "prompt.quit_modified_one",
187                        save_key = save_key,
188                        discard_key = discard_key,
189                        cancel_key = cancel_key
190                    )
191                    .to_string()
192                } else {
193                    t!(
194                        "prompt.quit_modified_many",
195                        count = modified_count,
196                        save_key = save_key,
197                        discard_key = discard_key,
198                        cancel_key = cancel_key
199                    )
200                    .to_string()
201                }
202            };
203            self.start_prompt(msg, PromptType::ConfirmQuitWithModified);
204        } else {
205            self.should_quit = true;
206        }
207    }
208
209    /// Count modified buffers that would require a save prompt on quit.
210    ///
211    /// When `hot_exit` is enabled, unnamed buffers are excluded (they are
212    /// automatically recovered across sessions), but file-backed modified
213    /// buffers still trigger a prompt with a "recoverable" option.
214    /// When `auto_save_enabled` is true, file-backed buffers are excluded
215    /// (they will be saved to disk on exit).
216    fn count_modified_buffers_needing_prompt(&self) -> usize {
217        let hot_exit = self.config.editor.hot_exit;
218        let auto_save = self.config.editor.auto_save_enabled;
219
220        self.buffers
221            .iter()
222            .filter(|(buffer_id, state)| {
223                if !state.buffer.is_modified() {
224                    return false;
225                }
226                if let Some(meta) = self.buffer_metadata.get(buffer_id) {
227                    if let Some(path) = meta.file_path() {
228                        let is_unnamed = path.as_os_str().is_empty();
229                        if is_unnamed && hot_exit {
230                            return false; // unnamed buffer, auto-recovered via hot exit
231                        }
232                        if !is_unnamed && auto_save {
233                            return false; // file-backed, will be auto-saved on exit
234                        }
235                    }
236                }
237                true
238            })
239            .count()
240    }
241
242    /// Handle terminal focus gained event
243    pub fn focus_gained(&mut self) {
244        self.plugin_manager.run_hook(
245            "focus_gained",
246            crate::services::plugins::hooks::HookArgs::FocusGained {},
247        );
248    }
249
250    /// Resize all buffers to match new terminal size
251    pub fn resize(&mut self, width: u16, height: u16) {
252        // Update terminal dimensions for future buffer creation
253        self.terminal_width = width;
254        self.terminal_height = height;
255
256        // Resize all SplitViewState viewports (viewport is now owned by SplitViewState)
257        for view_state in self.split_view_states.values_mut() {
258            view_state.viewport.resize(width, height);
259        }
260
261        // Resize visible terminal PTYs to match new dimensions
262        self.resize_visible_terminals();
263
264        // Notify plugins of the resize so they can adjust layouts
265        self.plugin_manager.run_hook(
266            "resize",
267            fresh_core::hooks::HookArgs::Resize { width, height },
268        );
269    }
270}