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_window_mut()
30                .active_custom_contexts
31                .insert(crate::types::context_keys::SESSION_MODE.to_string());
32        } else {
33            self.active_window_mut()
34                .active_custom_contexts
35                .remove(crate::types::context_keys::SESSION_MODE);
36        }
37    }
38
39    /// Check if running in session mode
40    pub fn is_session_mode(&self) -> bool {
41        self.session_mode
42    }
43
44    /// Mark that the backend does not render a hardware cursor.
45    /// When set, the renderer always draws a software cursor indicator.
46    pub fn set_software_cursor_only(&mut self, enabled: bool) {
47        self.software_cursor_only = enabled;
48    }
49
50    /// Set the session name for display in status bar.
51    ///
52    /// When a session name is set, the recovery service is reinitialized
53    /// to use a session-scoped recovery directory so each named session's
54    /// recovery data is isolated.
55    pub fn set_session_name(&mut self, name: Option<String>) {
56        if let Some(ref session_name) = name {
57            let base_recovery_dir = self.dir_context.recovery_dir();
58            let scope = crate::services::recovery::RecoveryScope::Session {
59                name: session_name.clone(),
60            };
61            let recovery_config = RecoveryConfig {
62                enabled: self.recovery_service.is_enabled(),
63                ..RecoveryConfig::default()
64            };
65            self.recovery_service =
66                RecoveryService::with_scope(recovery_config, &base_recovery_dir, &scope);
67        }
68        self.session_name = name;
69    }
70
71    /// Get the session name (for status bar display)
72    pub fn session_name(&self) -> Option<&str> {
73        self.session_name.as_deref()
74    }
75
76    /// Queue escape sequences to be sent to the client (session mode only)
77    pub fn queue_escape_sequences(&mut self, sequences: &[u8]) {
78        self.pending_escape_sequences.extend_from_slice(sequences);
79    }
80
81    /// Take pending escape sequences, clearing the queue
82    pub fn take_pending_escape_sequences(&mut self) -> Vec<u8> {
83        std::mem::take(&mut self.pending_escape_sequences)
84    }
85
86    /// Take pending clipboard data queued in session mode, clearing the request
87    pub fn take_pending_clipboard(
88        &mut self,
89    ) -> Option<crate::services::clipboard::PendingClipboard> {
90        self.clipboard.take_pending_clipboard()
91    }
92
93    /// Check if the editor should restart with a new working directory
94    pub fn should_restart(&self) -> bool {
95        self.restart_with_dir.is_some()
96    }
97
98    /// Take the restart directory, clearing the restart request
99    /// Returns the new working directory if a restart was requested
100    pub fn take_restart_dir(&mut self) -> Option<PathBuf> {
101        self.restart_with_dir.take()
102    }
103
104    /// Request the editor to restart with a new working directory
105    /// This triggers a clean shutdown and restart with the new project root
106    /// Request a full hardware terminal clear and redraw on the next frame.
107    /// Used after external commands have messed up the terminal state.
108    pub fn request_full_redraw(&mut self) {
109        self.full_redraw_requested = true;
110    }
111
112    /// Check if a full redraw was requested, and clear the flag.
113    pub fn take_full_redraw_request(&mut self) -> bool {
114        let requested = self.full_redraw_requested;
115        self.full_redraw_requested = false;
116        requested
117    }
118
119    /// Request the event loop to suspend the editor process (SIGTSTP on Unix).
120    /// The loop tears down terminal modes, raises the signal, then re-enables
121    /// modes once the shell sends SIGCONT (e.g. via `fg`).
122    pub fn request_suspend(&mut self) {
123        self.suspend_requested = true;
124    }
125
126    /// Check if a suspend was requested, and clear the flag.
127    pub fn take_suspend_request(&mut self) -> bool {
128        let requested = self.suspend_requested;
129        self.suspend_requested = false;
130        requested
131    }
132
133    pub fn request_restart(&mut self, new_working_dir: PathBuf) {
134        tracing::info!(
135            "Restart requested with new working directory: {}",
136            new_working_dir.display()
137        );
138        self.restart_with_dir = Some(new_working_dir);
139        // Also signal quit so the event loop exits
140        self.should_quit = true;
141    }
142
143    /// Get the active theme (read lock).
144    pub fn theme(&self) -> std::sync::RwLockReadGuard<'_, crate::view::theme::Theme> {
145        self.theme.read().unwrap()
146    }
147
148    /// Check if the settings dialog is open and visible
149    pub fn is_settings_open(&self) -> bool {
150        self.settings_state.as_ref().is_some_and(|s| s.visible)
151    }
152
153    /// Request the editor to quit
154    pub fn quit(&mut self) {
155        // Check for unsaved buffers (all are auto-persisted when hot_exit is enabled)
156        let modified_count = self.count_modified_buffers_needing_prompt();
157        if modified_count > 0 {
158            let save_key = t!("prompt.key.save").to_string();
159            let cancel_key = t!("prompt.key.cancel").to_string();
160            let hot_exit = self.config.editor.hot_exit;
161
162            let discard_key = t!("prompt.key.discard").to_string();
163            let msg = if hot_exit {
164                // With hot exit: offer save, discard, quit-without-saving (recoverable), or cancel
165                let quit_key = t!("prompt.key.quit").to_string();
166                if modified_count == 1 {
167                    t!(
168                        "prompt.quit_modified_hot_one",
169                        save_key = save_key,
170                        discard_key = discard_key,
171                        quit_key = quit_key,
172                        cancel_key = cancel_key
173                    )
174                    .to_string()
175                } else {
176                    t!(
177                        "prompt.quit_modified_hot_many",
178                        count = modified_count,
179                        save_key = save_key,
180                        discard_key = discard_key,
181                        quit_key = quit_key,
182                        cancel_key = cancel_key
183                    )
184                    .to_string()
185                }
186            } else {
187                // Without hot exit: offer save, discard, or cancel
188                if modified_count == 1 {
189                    t!(
190                        "prompt.quit_modified_one",
191                        save_key = save_key,
192                        discard_key = discard_key,
193                        cancel_key = cancel_key
194                    )
195                    .to_string()
196                } else {
197                    t!(
198                        "prompt.quit_modified_many",
199                        count = modified_count,
200                        save_key = save_key,
201                        discard_key = discard_key,
202                        cancel_key = cancel_key
203                    )
204                    .to_string()
205                }
206            };
207            self.start_prompt(msg, PromptType::ConfirmQuitWithModified);
208        } else {
209            self.should_quit = true;
210        }
211    }
212
213    /// Count modified buffers that would require a save prompt on quit.
214    ///
215    /// When `hot_exit` is enabled, unnamed buffers are excluded (they are
216    /// automatically recovered across sessions), but file-backed modified
217    /// buffers still trigger a prompt with a "recoverable" option.
218    /// When `auto_save_enabled` is true, file-backed buffers are excluded
219    /// (they will be saved to disk on exit).
220    fn count_modified_buffers_needing_prompt(&self) -> usize {
221        let hot_exit = self.config.editor.hot_exit;
222        let auto_save = self.config.editor.auto_save_enabled;
223
224        self.windows
225            .get(&self.active_window)
226            .map(|w| &w.buffers)
227            .expect("active window present")
228            .iter()
229            .filter(|(buffer_id, state)| {
230                if !state.buffer.is_modified() {
231                    return false;
232                }
233                if let Some(meta) = self.active_window().buffer_metadata.get(buffer_id) {
234                    if let Some(path) = meta.file_path() {
235                        let is_unnamed = path.as_os_str().is_empty();
236                        if is_unnamed && hot_exit {
237                            return false; // unnamed buffer, auto-recovered via hot exit
238                        }
239                        if !is_unnamed && auto_save {
240                            return false; // file-backed, will be auto-saved on exit
241                        }
242                    }
243                }
244                true
245            })
246            .count()
247    }
248
249    /// Handle terminal focus gained event
250    pub fn focus_gained(&mut self) {
251        self.plugin_manager.read().unwrap().run_hook(
252            "focus_gained",
253            crate::services::plugins::hooks::HookArgs::FocusGained {},
254        );
255    }
256
257    /// Resize all buffers to match new terminal size. Loops over every
258    /// `Window` so each one updates its own split viewports and visible
259    /// terminal PTYs; the plugin `resize` hook fires once for the editor
260    /// as a whole.
261    pub fn resize(&mut self, width: u16, height: u16) {
262        // Editor's canonical screen dimensions (used to seed new windows).
263        self.terminal_width = width;
264        self.terminal_height = height;
265
266        for window in self.windows.values_mut() {
267            window.resize(width, height);
268        }
269
270        // Refresh the plugin-facing snapshot BEFORE firing the
271        // resize hook. Without this, the orchestrator's resize
272        // handler reads `editor.getViewport()` from a snapshot
273        // whose `viewport.height` still reflects the pre-resize
274        // size — the one-way ratchet in `buildOpenSpec` then sees
275        // `old > old` and skips the update, leaving the picker
276        // stuck small even after a terminal-grow event. (The
277        // ratchet itself is correct; the input it consumes was
278        // stale.) Updating the snapshot here lets plugins observe
279        // the new dimensions when they react to the hook.
280        #[cfg(feature = "plugins")]
281        self.update_plugin_state_snapshot();
282
283        // Notify plugins of the resize so they can adjust layouts.
284        self.plugin_manager.read().unwrap().run_hook(
285            "resize",
286            fresh_core::hooks::HookArgs::Resize { width, height },
287        );
288
289        // If a floating widget panel is currently mounted (the
290        // Orchestrator picker, New-Session form, plugin overlays),
291        // its cached `entries` were laid out against the old screen
292        // width — re-render against the new one so column widths,
293        // side borders and embed rects all reflect the new
294        // dimensions (Bug 13). The hook above lets plugins update
295        // their spec; this rerender picks up either the updated
296        // spec or the existing spec at the new width.
297        if let Some(panel_id) = self.floating_widget_panel.as_ref().map(|f| f.panel_id) {
298            self.rerender_widget_panel(panel_id);
299        }
300    }
301}
302
303impl crate::app::window::Window {
304    /// Adopt the new terminal dimensions for this window: update the
305    /// cached `terminal_width` / `terminal_height`, resize every split
306    /// viewport, and resize any visible terminal PTYs.
307    pub fn resize(&mut self, width: u16, height: u16) {
308        self.terminal_width = width;
309        self.terminal_height = height;
310
311        if let Some(view_states) = self.split_view_states_mut() {
312            for view_state in view_states.values_mut() {
313                view_state.viewport.resize(width, height);
314            }
315        }
316
317        self.resize_visible_terminals();
318    }
319}