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