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}