1use super::window::Window;
27use super::{BufferId, BufferMetadata, Editor};
28use crate::model::event::LeafId;
29use crate::services::authority::TerminalWrapper;
30use crate::services::terminal::TerminalId;
31use crate::state::EditorState;
32use crate::view::split::SplitViewState;
33use rust_i18n::t;
34use std::path::PathBuf;
35
36impl Window {
37 pub(crate) fn resolved_terminal_wrapper(&self) -> TerminalWrapper {
45 self.resources
46 .authority
47 .terminal_wrapper
48 .clone()
49 .with_user_shell_override(self.resources.config.terminal.shell.as_ref())
50 }
51
52 pub(crate) fn get_terminal_dimensions(&self) -> (u16, u16) {
56 let cols = self.terminal_width.saturating_sub(2).max(40);
57 let rows = self.terminal_height.saturating_sub(4).max(10);
58 (cols, rows)
59 }
60
61 pub fn spawn_terminal_session(
79 &mut self,
80 cwd: Option<PathBuf>,
81 persistent: bool,
82 command_override: Option<Vec<String>>,
83 ) -> Option<TerminalId> {
84 let (cols, rows) = self.get_terminal_dimensions();
85
86 let bridge = self.bridge.clone();
89 self.terminal_manager.set_async_bridge(bridge);
90
91 let working_dir = cwd.unwrap_or_else(|| self.root.clone());
92 let terminal_root = self.resources.dir_context.terminal_dir_for(&working_dir);
93 if let Err(e) = self
94 .resources
95 .authority
96 .filesystem
97 .create_dir_all(&terminal_root)
98 {
99 tracing::warn!("Failed to create terminal directory: {}", e);
100 }
101
102 let predicted_terminal_id = self.terminal_manager.next_terminal_id();
108 let name_stem = if persistent {
109 format!("fresh-terminal-{}", predicted_terminal_id.0)
110 } else {
111 let nanos = std::time::SystemTime::now()
112 .duration_since(std::time::UNIX_EPOCH)
113 .map(|d| d.as_nanos())
114 .unwrap_or(0);
115 format!("fresh-terminal-eph-{}-{}", predicted_terminal_id.0, nanos)
116 };
117 let log_path = terminal_root.join(format!("{}.log", name_stem));
118 let backing_path = terminal_root.join(format!("{}.txt", name_stem));
119 self.terminal_backing_files
120 .insert(predicted_terminal_id, backing_path.clone());
121
122 let wrapper = match command_override {
130 Some(argv) if !argv.is_empty() => {
131 let (command, args) = argv.split_first().expect("non-empty argv");
132 crate::services::authority::TerminalWrapper {
133 command: command.clone(),
134 args: args.to_vec(),
135 manages_cwd: false,
136 }
137 }
138 _ => self.resolved_terminal_wrapper(),
139 };
140 match self.terminal_manager.spawn(
141 cols,
142 rows,
143 Some(working_dir),
144 Some(log_path.clone()),
145 Some(backing_path),
146 wrapper,
147 ) {
148 Ok(terminal_id) => {
149 self.terminal_log_files.insert(terminal_id, log_path);
150 if terminal_id != predicted_terminal_id {
155 self.terminal_backing_files.remove(&predicted_terminal_id);
156 let backing_path =
157 terminal_root.join(format!("fresh-terminal-{}.txt", terminal_id.0));
158 self.terminal_backing_files
159 .insert(terminal_id, backing_path);
160 }
161 if !persistent {
162 self.ephemeral_terminals.insert(terminal_id);
163 }
164 Some(terminal_id)
165 }
166 Err(e) => {
167 self.set_status_message(
168 t!("terminal.failed_to_open", error = e.to_string()).to_string(),
169 );
170 tracing::error!("Failed to open terminal: {}", e);
171 None
172 }
173 }
174 }
175
176 pub fn create_terminal_buffer_attached(
180 &mut self,
181 terminal_id: TerminalId,
182 split_id: LeafId,
183 ) -> BufferId {
184 let buffer_id = self.alloc_buffer_id();
185 let large_file_threshold = self.resources.config.editor.large_file_threshold_bytes as usize;
186
187 let backing_file = self
190 .terminal_backing_files
191 .get(&terminal_id)
192 .cloned()
193 .unwrap_or_else(|| {
194 let root = self.resources.dir_context.terminal_dir_for(&self.root);
195 if let Err(e) = self.resources.authority.filesystem.create_dir_all(&root) {
196 tracing::warn!("Failed to create terminal directory: {}", e);
197 }
198 root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
199 });
200
201 if !self.resources.authority.filesystem.exists(&backing_file) {
205 if let Err(e) = self
206 .resources
207 .authority
208 .filesystem
209 .write_file(&backing_file, &[])
210 {
211 tracing::warn!("Failed to create terminal backing file: {}", e);
212 }
213 }
214
215 self.terminal_backing_files
216 .insert(terminal_id, backing_file.clone());
217
218 let mut state = EditorState::new_with_path(
219 large_file_threshold,
220 std::sync::Arc::clone(&self.resources.authority.filesystem),
221 backing_file.clone(),
222 );
223 state.margins.configure_for_line_numbers(false);
224 self.buffers.insert(buffer_id, state);
225
226 let metadata = BufferMetadata::virtual_buffer(
229 format!("*Terminal {}*", terminal_id.0),
230 "terminal".into(),
231 false,
232 );
233 self.buffer_metadata.insert(buffer_id, metadata);
234 self.terminal_buffers.insert(buffer_id, terminal_id);
235 self.event_logs
236 .insert(buffer_id, crate::model::event::EventLog::new());
237
238 if let Some(view_states) = self.split_view_states_mut() {
239 if let Some(view_state) = view_states.get_mut(&split_id) {
240 view_state.add_buffer(buffer_id);
241 view_state.viewport.line_wrap_enabled = false;
244 }
245 }
246
247 buffer_id
248 }
249
250 pub fn create_plugin_terminal(
272 &mut self,
273 cwd: Option<PathBuf>,
274 direction: Option<crate::model::event::SplitDirection>,
275 ratio: Option<f32>,
276 focus: bool,
277 persistent: bool,
278 command: Option<Vec<String>>,
279 title: Option<String>,
280 ) -> Result<(TerminalId, BufferId, Option<LeafId>), String> {
281 let auto_title = command.as_ref().and_then(|argv| {
287 argv.first().map(|cmd| {
288 std::path::Path::new(cmd)
289 .file_name()
290 .and_then(|os| os.to_str())
291 .unwrap_or(cmd.as_str())
292 .to_string()
293 })
294 });
295 let resolved_title = title.or(auto_title);
296 let terminal_id = self
297 .spawn_terminal_session(cwd, persistent, command)
298 .ok_or_else(|| "Failed to spawn terminal".to_string())?;
299
300 if let Some(pid) = self.terminal_manager.get(terminal_id).and_then(|h| h.pid()) {
303 let label = format!("terminal #{}", terminal_id.0);
304 self.process_groups.register(pid, label);
305 }
306
307 let active_split = self.buffers.splits().map(|(mgr, _)| mgr.active_split());
314
315 let (buffer_id, created_split_id) = if let Some(split_dir) = direction {
316 let buffer_id = self.create_terminal_buffer_detached(terminal_id);
317 match active_split {
318 Some(parent) => {
319 let split_ratio = ratio.unwrap_or(0.5);
320 let line_numbers = self.resources.config.editor.line_numbers;
321 let highlight_current_line =
322 self.resources.config.editor.highlight_current_line;
323 let rulers = self.resources.config.editor.rulers.clone();
324 let terminal_width = self.terminal_width;
325 let terminal_height = self.terminal_height;
326 let split_result = self
327 .split_manager_mut()
328 .expect("active split implies populated layout")
329 .split_active(split_dir, buffer_id, split_ratio);
330 match split_result {
331 Ok(new_split_id) => {
332 let mut view_state = SplitViewState::with_buffer(
333 terminal_width,
334 terminal_height,
335 buffer_id,
336 );
337 let _ = line_numbers;
348 let _ = highlight_current_line;
349 view_state
350 .apply_config_defaults(false, false, false, false, None, rulers);
351 view_state.viewport.line_wrap_enabled = false;
355 self.split_view_states_mut()
356 .expect("active split implies populated layout")
357 .insert(new_split_id, view_state);
358 if focus {
359 self.split_manager_mut()
360 .expect("active split implies populated layout")
361 .set_active_split(new_split_id);
362 }
363 (buffer_id, Some(new_split_id))
364 }
365 Err(e) => {
366 tracing::error!(
367 "Failed to create split for terminal: {e}; \
368 falling back to attaching to active split"
369 );
370 if let Some(view_state) = self
373 .split_view_states_mut()
374 .and_then(|m| m.get_mut(&parent))
375 {
376 view_state.add_buffer(buffer_id);
377 view_state.viewport.line_wrap_enabled = false;
378 }
379 self.set_active_buffer(buffer_id);
380 (buffer_id, None)
381 }
382 }
383 }
384 None => {
385 let manager = crate::view::split::SplitManager::new(buffer_id);
389 let active_leaf = manager.active_split();
390 let mut view_states = std::collections::HashMap::new();
391 let mut vs = SplitViewState::with_buffer(
392 self.terminal_width,
393 self.terminal_height,
394 buffer_id,
395 );
396 vs.viewport.line_wrap_enabled = false;
397 view_states.insert(active_leaf, vs);
398 self.buffers.set_splits((manager, view_states));
399 (buffer_id, Some(active_leaf))
400 }
401 }
402 } else {
403 match active_split {
404 Some(split_id) => {
405 let buffer_id = self.create_terminal_buffer_attached(terminal_id, split_id);
406 self.set_active_buffer(buffer_id);
412 (buffer_id, None)
413 }
414 None => {
415 let buffer_id = self.create_terminal_buffer_detached(terminal_id);
416 let manager = crate::view::split::SplitManager::new(buffer_id);
417 let active_leaf = manager.active_split();
418 let mut view_states = std::collections::HashMap::new();
419 let mut vs = SplitViewState::with_buffer(
420 self.terminal_width,
421 self.terminal_height,
422 buffer_id,
423 );
424 vs.viewport.line_wrap_enabled = false;
425 view_states.insert(active_leaf, vs);
426 self.buffers.set_splits((manager, view_states));
427 (buffer_id, Some(active_leaf))
428 }
429 }
430 };
431
432 if let Some(title) = resolved_title {
439 let final_name = self.disambiguate_terminal_title(&title, buffer_id);
440 if let Some(meta) = self.buffer_metadata.get_mut(&buffer_id) {
441 meta.display_name = final_name;
442 }
443 }
444
445 self.resize_visible_terminals();
446 Ok((terminal_id, buffer_id, created_split_id))
447 }
448
449 fn disambiguate_terminal_title(&self, desired: &str, for_buffer: BufferId) -> String {
458 let used: std::collections::HashSet<&str> = self
463 .terminal_buffers
464 .keys()
465 .filter(|bid| **bid != for_buffer)
466 .filter_map(|bid| {
467 self.buffer_metadata
468 .get(bid)
469 .map(|m| m.display_name.as_str())
470 })
471 .collect();
472 if !used.contains(desired) {
473 return desired.to_string();
474 }
475 for k in 2..=1024 {
478 let candidate = format!("{} ({})", desired, k);
479 if !used.contains(candidate.as_str()) {
480 return candidate;
481 }
482 }
483 format!("{} (n)", desired)
487 }
488
489 pub fn open_terminal_in_window(&mut self) -> Option<(TerminalId, BufferId)> {
501 let terminal_id = self.spawn_terminal_session(None, true, None)?;
505 let split_id = self
506 .buffers
507 .splits()
508 .map(|(mgr, _)| mgr.active_split())
509 .expect("window must have a populated split layout");
510 let buffer_id = self.create_terminal_buffer_attached(terminal_id, split_id);
511 self.set_active_buffer(buffer_id);
514 self.terminal_mode = true;
515 self.key_context = crate::input::keybindings::KeyContext::Terminal;
516 self.resize_visible_terminals();
517 Some((terminal_id, buffer_id))
518 }
519
520 pub fn create_terminal_buffer_detached(&mut self, terminal_id: TerminalId) -> BufferId {
523 let buffer_id = self.alloc_buffer_id();
524 let large_file_threshold = self.resources.config.editor.large_file_threshold_bytes as usize;
525
526 let backing_file = self
527 .terminal_backing_files
528 .get(&terminal_id)
529 .cloned()
530 .unwrap_or_else(|| {
531 let root = self.resources.dir_context.terminal_dir_for(&self.root);
532 if let Err(e) = self.resources.authority.filesystem.create_dir_all(&root) {
533 tracing::warn!("Failed to create terminal directory: {}", e);
534 }
535 root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
536 });
537
538 if !self.resources.authority.filesystem.exists(&backing_file) {
539 if let Err(e) = self
540 .resources
541 .authority
542 .filesystem
543 .write_file(&backing_file, &[])
544 {
545 tracing::warn!("Failed to create terminal backing file: {}", e);
546 }
547 }
548
549 let mut state = EditorState::new_with_path(
550 large_file_threshold,
551 std::sync::Arc::clone(&self.resources.authority.filesystem),
552 backing_file.clone(),
553 );
554 state.margins.configure_for_line_numbers(false);
555 self.buffers.insert(buffer_id, state);
556
557 let metadata = BufferMetadata::virtual_buffer(
558 format!("*Terminal {}*", terminal_id.0),
559 "terminal".into(),
560 false,
561 );
562 self.buffer_metadata.insert(buffer_id, metadata);
563 self.terminal_buffers.insert(buffer_id, terminal_id);
564 self.event_logs
565 .insert(buffer_id, crate::model::event::EventLog::new());
566
567 buffer_id
568 }
569}
570
571impl Editor {
572 pub(crate) fn resolved_terminal_wrapper(&self) -> TerminalWrapper {
576 self.active_window().resolved_terminal_wrapper()
577 }
578
579 pub(crate) fn spawn_terminal_session(&mut self) -> Option<TerminalId> {
589 self.active_window_mut()
591 .spawn_terminal_session(None, true, None)
592 }
593
594 pub fn open_terminal(&mut self) {
602 let Some((terminal_id, buffer_id)) = self.active_window_mut().open_terminal_in_window()
603 else {
604 return;
605 };
606
607 #[cfg(feature = "plugins")]
610 self.update_plugin_state_snapshot();
611 #[cfg(feature = "plugins")]
612 self.plugin_manager.read().unwrap().run_hook(
613 "buffer_activated",
614 crate::services::plugins::hooks::HookArgs::BufferActivated { buffer_id },
615 );
616
617 let exit_key = self
621 .keybindings
622 .read()
623 .unwrap()
624 .find_keybinding_for_action(
625 "terminal_escape",
626 crate::input::keybindings::KeyContext::Terminal,
627 )
628 .unwrap_or_else(|| "Ctrl+Space".to_string());
629 self.set_status_message(
630 t!("terminal.opened", id = terminal_id.0, exit_key = exit_key).to_string(),
631 );
632 tracing::info!(
633 "Opened terminal {:?} with buffer {:?}",
634 terminal_id,
635 buffer_id
636 );
637 }
638
639 pub(crate) fn create_terminal_buffer_detached(&mut self, terminal_id: TerminalId) -> BufferId {
643 self.active_window_mut()
644 .create_terminal_buffer_detached(terminal_id)
645 }
646
647 pub fn close_terminal(&mut self) {
649 let buffer_id = self.active_buffer();
650
651 if let Some(&terminal_id) = self.active_window().terminal_buffers.get(&buffer_id) {
652 self.active_window_mut().terminal_manager.close(terminal_id);
654 self.active_window_mut().terminal_buffers.remove(&buffer_id);
655 self.active_window_mut()
656 .ephemeral_terminals
657 .remove(&terminal_id);
658
659 let backing_file = self
661 .active_window_mut()
662 .terminal_backing_files
663 .remove(&terminal_id);
664 if let Some(ref path) = backing_file {
665 #[allow(clippy::let_underscore_must_use)]
667 let _ = self.authority.filesystem.remove_file(path);
668 }
669 if let Some(log_file) = self
671 .active_window_mut()
672 .terminal_log_files
673 .remove(&terminal_id)
674 {
675 if backing_file.as_ref() != Some(&log_file) {
676 #[allow(clippy::let_underscore_must_use)]
678 let _ = self.authority.filesystem.remove_file(&log_file);
679 }
680 }
681
682 self.active_window_mut().terminal_mode = false;
684 self.active_window_mut().key_context = crate::input::keybindings::KeyContext::Normal;
685
686 if let Err(e) = self.close_buffer(buffer_id) {
688 tracing::warn!("Failed to close terminal buffer: {}", e);
689 }
690
691 self.set_status_message(t!("terminal.closed", id = terminal_id.0).to_string());
692 } else {
693 self.set_status_message(t!("status.not_viewing_terminal").to_string());
694 }
695 }
696
697 pub fn handle_terminal_key(
710 &mut self,
711 code: crossterm::event::KeyCode,
712 modifiers: crossterm::event::KeyModifiers,
713 ) -> bool {
714 if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
717 match code {
718 crossterm::event::KeyCode::Char(' ')
719 | crossterm::event::KeyCode::Char(']')
720 | crossterm::event::KeyCode::Char('`') => {
721 self.active_window_mut().terminal_mode = false;
723 self.active_window_mut().key_context =
724 crate::input::keybindings::KeyContext::Normal;
725 {
726 let __b = self.active_buffer();
727 self.active_window_mut().sync_terminal_to_buffer(__b);
728 };
729 self.set_status_message(
730 "Terminal mode disabled - read only (Ctrl+Space to resume)".to_string(),
731 );
732 return true;
733 }
734 _ => {}
735 }
736 }
737
738 self.active_window_mut().send_terminal_key(code, modifiers);
740 true
741 }
742
743 pub fn enter_terminal_mode(&mut self) {
749 if self
750 .active_window()
751 .is_terminal_buffer(self.active_buffer())
752 {
753 self.active_window_mut().terminal_mode = true;
754 self.active_window_mut().key_context = crate::input::keybindings::KeyContext::Terminal;
755
756 let __buffer_id = self.active_buffer();
758 if let Some(state) = self
759 .windows
760 .get_mut(&self.active_window)
761 .map(|w| &mut w.buffers)
762 .expect("active window present")
763 .get_mut(&__buffer_id)
764 {
765 state.editing_disabled = false;
766 state.margins.configure_for_line_numbers(false);
767 }
768 let __active_split = self.split_manager().active_split();
769 if let Some(view_state) = self.split_view_states_mut().get_mut(&__active_split) {
770 view_state.viewport.line_wrap_enabled = false;
771 }
772
773 if let Some(&terminal_id) = self
775 .active_window()
776 .terminal_buffers
777 .get(&self.active_buffer())
778 {
779 if let Some(backing_path) = self
781 .active_window()
782 .terminal_backing_files
783 .get(&terminal_id)
784 {
785 if let Some(handle) = self.active_window().terminal_manager.get(terminal_id) {
786 if let Ok(state) = handle.state.lock() {
787 let truncate_pos = state.backing_file_history_end();
788 if let Err(e) = self
791 .authority
792 .filesystem
793 .set_file_length(backing_path, truncate_pos)
794 {
795 tracing::warn!("Failed to truncate terminal backing file: {}", e);
796 }
797 }
798 }
799 }
800
801 if let Some(handle) = self.active_window().terminal_manager.get(terminal_id) {
803 if let Ok(mut state) = handle.state.lock() {
804 state.scroll_to_bottom();
805 }
806 }
807 }
808
809 self.active_window_mut().resize_visible_terminals();
811
812 self.set_status_message(t!("status.terminal_mode_enabled").to_string());
813 }
814 }
815
816 pub fn get_terminal_content(
818 &self,
819 buffer_id: BufferId,
820 ) -> Option<Vec<Vec<crate::services::terminal::TerminalCell>>> {
821 let terminal_id = self.active_window().terminal_buffers.get(&buffer_id)?;
822 let handle = self.active_window().terminal_manager.get(*terminal_id)?;
823 let state = handle.state.lock().ok()?;
824
825 let (_, rows) = state.size();
826 let mut content = Vec::with_capacity(rows as usize);
827
828 for row in 0..rows {
829 content.push(state.get_line(row));
830 }
831
832 Some(content)
833 }
834}
835
836impl Window {
837 pub fn get_active_terminal_state(
839 &self,
840 ) -> Option<std::sync::MutexGuard<'_, crate::services::terminal::TerminalState>> {
841 let terminal_id = self.terminal_buffers.get(&self.active_buffer())?;
842 let handle = self.terminal_manager.get(*terminal_id)?;
843 handle.state.lock().ok()
844 }
845
846 pub fn send_terminal_input(&mut self, data: &[u8]) {
849 if let Some(&terminal_id) = self.terminal_buffers.get(&self.active_buffer()) {
850 if let Some(handle) = self.terminal_manager.get(terminal_id) {
851 handle.write(data);
852 }
853 }
854 }
855
856 pub fn send_terminal_key(
860 &mut self,
861 code: crossterm::event::KeyCode,
862 modifiers: crossterm::event::KeyModifiers,
863 ) {
864 let app_cursor = self
865 .get_active_terminal_state()
866 .map(|s| s.is_app_cursor())
867 .unwrap_or(false);
868 if let Some(bytes) =
869 crate::services::terminal::pty::key_to_pty_bytes(code, modifiers, app_cursor)
870 {
871 self.send_terminal_input(&bytes);
872 }
873 }
874
875 pub fn send_terminal_mouse(
877 &mut self,
878 col: u16,
879 row: u16,
880 kind: crate::input::handler::TerminalMouseEventKind,
881 modifiers: crossterm::event::KeyModifiers,
882 ) {
883 use crate::input::handler::TerminalMouseEventKind;
884
885 let use_sgr = self
887 .get_active_terminal_state()
888 .map(|s| s.uses_sgr_mouse())
889 .unwrap_or(true);
890
891 let uses_alt_scroll = self
893 .get_active_terminal_state()
894 .map(|s| s.uses_alternate_scroll())
895 .unwrap_or(false);
896
897 if uses_alt_scroll {
898 match kind {
899 TerminalMouseEventKind::ScrollUp => {
900 for _ in 0..3 {
901 self.send_terminal_input(b"\x1b[A");
902 }
903 return;
904 }
905 TerminalMouseEventKind::ScrollDown => {
906 for _ in 0..3 {
907 self.send_terminal_input(b"\x1b[B");
908 }
909 return;
910 }
911 _ => {}
912 }
913 }
914
915 let bytes = if use_sgr {
916 encode_sgr_mouse(col, row, kind, modifiers)
917 } else {
918 encode_x10_mouse(col, row, kind, modifiers)
919 };
920
921 if let Some(bytes) = bytes {
922 self.send_terminal_input(&bytes);
923 }
924 }
925
926 pub fn is_terminal_in_alternate_screen(&self, buffer_id: BufferId) -> bool {
929 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
930 if let Some(handle) = self.terminal_manager.get(terminal_id) {
931 if let Ok(state) = handle.state.lock() {
932 return state.is_alternate_screen();
933 }
934 }
935 }
936 false
937 }
938
939 pub fn resize_terminal(&mut self, buffer_id: BufferId, cols: u16, rows: u16) {
942 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
943 if let Some(handle) = self.terminal_manager.get_mut(terminal_id) {
944 handle.resize(cols, rows);
945 }
946 }
947 }
948
949 pub fn resize_visible_terminals(&mut self) {
953 let file_explorer_width = if self.file_explorer_visible {
955 self.file_explorer_width.to_cols(self.terminal_width)
956 } else {
957 0
958 };
959 let editor_width = self.terminal_width.saturating_sub(file_explorer_width);
960 let editor_area = ratatui::layout::Rect::new(
961 file_explorer_width,
962 1, editor_width,
964 self.terminal_height.saturating_sub(2), );
966
967 let Some((mgr, _)) = self.buffers.splits() else {
968 return;
969 };
970 let visible_buffers = mgr.get_visible_buffers(editor_area);
971
972 for (_split_id, buffer_id, split_area) in visible_buffers {
973 if self.terminal_buffers.contains_key(&buffer_id) {
974 let content_height = split_area.height.saturating_sub(2);
976 let content_width = split_area.width.saturating_sub(2);
977
978 if content_width > 0 && content_height > 0 {
979 self.resize_terminal(buffer_id, content_width, content_height);
980 }
981 }
982 }
983 }
984
985 pub fn sync_terminal_to_buffer(&mut self, buffer_id: BufferId) {
995 let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) else {
996 return;
997 };
998 let backing_file = match self.terminal_backing_files.get(&terminal_id) {
1000 Some(path) => path.clone(),
1001 None => return,
1002 };
1003
1004 if let Some(handle) = self.terminal_manager.get(terminal_id) {
1007 if let Ok(mut state) = handle.state.lock() {
1008 if let Ok(metadata) = self.resources.authority.filesystem.metadata(&backing_file) {
1011 state.set_backing_file_history_end(metadata.size);
1012 }
1013
1014 if let Ok(mut file) = self
1016 .resources
1017 .authority
1018 .filesystem
1019 .open_file_for_append(&backing_file)
1020 {
1021 use std::io::BufWriter;
1022 let mut writer = BufWriter::new(&mut *file);
1023 if let Err(e) = state.append_visible_screen(&mut writer) {
1024 tracing::error!("Failed to append visible screen to backing file: {}", e);
1025 }
1026 }
1027 }
1028 }
1029
1030 let large_file_threshold = self.resources.config.editor.large_file_threshold_bytes as usize;
1032 if let Ok(new_state) = EditorState::from_file_with_languages(
1033 &backing_file,
1034 self.terminal_width,
1035 self.terminal_height,
1036 large_file_threshold,
1037 &self.resources.grammar_registry,
1038 &self.resources.config.languages,
1039 std::sync::Arc::clone(&self.resources.authority.filesystem),
1040 ) {
1041 let total_bytes = new_state.buffer.total_bytes();
1042 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1043 *state = new_state;
1044 state.buffer.set_modified(false);
1046 }
1047 if let Some((mgr, view_states)) = self.buffers.splits_mut() {
1049 let active_split = mgr.active_split();
1050 if let Some(view_state) = view_states.get_mut(&active_split) {
1051 view_state.cursors.primary_mut().position = total_bytes;
1052 }
1053 }
1054 }
1055
1056 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1058 state.editing_disabled = true;
1059 state.margins.configure_for_line_numbers(false);
1060 }
1061
1062 let active_split = self
1065 .buffers
1066 .splits()
1067 .expect("active window must have a populated split layout")
1068 .0
1069 .active_split();
1070 self.enter_terminal_scrollback_view(buffer_id, active_split);
1071 }
1072
1073 pub fn render_terminal_splits(
1090 &self,
1091 frame: &mut ratatui::Frame,
1092 split_areas: &[(
1093 crate::model::event::LeafId,
1094 BufferId,
1095 ratatui::layout::Rect,
1096 ratatui::layout::Rect,
1097 usize,
1098 usize,
1099 )],
1100 cursor_visible_if_active: bool,
1101 ) {
1102 for (_split_id, buffer_id, content_rect, _scrollbar_rect, _thumb_start, _thumb_end) in
1103 split_areas
1104 {
1105 let Some(&terminal_id) = self.terminal_buffers.get(buffer_id) else {
1106 continue;
1107 };
1108 let is_active = *buffer_id == self.active_buffer();
1116 if is_active && !self.terminal_mode {
1117 continue;
1118 }
1119 let Some(handle) = self.terminal_manager.get(terminal_id) else {
1120 continue;
1121 };
1122 let Ok(state) = handle.state.lock() else {
1123 continue;
1124 };
1125 let cursor_pos = state.cursor_position();
1126 let cursor_visible = state.cursor_visible()
1127 && is_active
1128 && self.terminal_mode
1129 && cursor_visible_if_active;
1130 let (_, rows) = state.size();
1131 let mut content = Vec::with_capacity(rows as usize);
1132 for row in 0..rows {
1133 content.push(state.get_line(row));
1134 }
1135 frame.render_widget(ratatui::widgets::Clear, *content_rect);
1136 let theme = self.resources.theme.read().unwrap();
1137 render::render_terminal_content(
1138 &content,
1139 cursor_pos,
1140 cursor_visible,
1141 *content_rect,
1142 frame.buffer_mut(),
1143 theme.terminal_fg,
1144 theme.terminal_bg,
1145 );
1146 }
1147 }
1148}
1149
1150impl Editor {
1151 pub fn is_terminal_mode(&self) -> bool {
1153 self.active_window().terminal_mode
1154 }
1155
1156 pub fn is_in_terminal_mode_resume(&self, buffer_id: BufferId) -> bool {
1158 self.active_window()
1159 .terminal_mode_resume
1160 .contains(&buffer_id)
1161 }
1162
1163 pub fn is_keyboard_capture(&self) -> bool {
1165 self.active_window().keyboard_capture
1166 }
1167
1168 pub fn set_terminal_jump_to_end_on_output(&mut self, value: bool) {
1170 self.config_mut().terminal.jump_to_end_on_output = value;
1171 }
1172
1173 pub fn terminal_manager(&self) -> &crate::services::terminal::TerminalManager {
1177 &self
1178 .windows
1179 .get(&self.active_window)
1180 .expect("active window must exist")
1181 .terminal_manager
1182 }
1183
1184 pub fn terminal_backing_files(
1187 &self,
1188 ) -> &std::collections::HashMap<crate::services::terminal::TerminalId, std::path::PathBuf> {
1189 &self
1190 .windows
1191 .get(&self.active_window)
1192 .expect("active window must exist")
1193 .terminal_backing_files
1194 }
1195
1196 pub fn active_buffer_id(&self) -> BufferId {
1198 self.active_buffer()
1199 }
1200
1201 pub fn get_buffer_content(&self, buffer_id: BufferId) -> Option<String> {
1203 self.windows
1204 .get(&self.active_window)
1205 .map(|w| &w.buffers)
1206 .expect("active window present")
1207 .get(&buffer_id)
1208 .and_then(|state| state.buffer.to_string())
1209 }
1210
1211 pub fn get_cursor_position(&self, buffer_id: BufferId) -> Option<usize> {
1213 self.windows
1215 .get(&self.active_window)
1216 .and_then(|w| w.buffers.splits())
1217 .map(|(_, vs)| vs)
1218 .expect("active window must have a populated split layout")
1219 .values()
1220 .find_map(|vs| {
1221 if vs.keyed_states.contains_key(&buffer_id) {
1222 Some(vs.keyed_states.get(&buffer_id)?.cursors.primary().position)
1223 } else {
1224 None
1225 }
1226 })
1227 .or_else(|| {
1228 self.windows
1230 .get(&self.active_window)
1231 .and_then(|w| w.buffers.splits())
1232 .map(|(_, vs)| vs)
1233 .expect("active window must have a populated split layout")
1234 .values()
1235 .map(|vs| vs.cursors.primary().position)
1236 .next()
1237 })
1238 }
1239
1240 }
1246
1247pub mod render {
1249 use crate::services::terminal::TerminalCell;
1250 use ratatui::buffer::Buffer;
1251 use ratatui::layout::Rect;
1252 use ratatui::style::{Color, Modifier, Style};
1253
1254 pub fn render_terminal_content(
1256 content: &[Vec<TerminalCell>],
1257 cursor_pos: (u16, u16),
1258 cursor_visible: bool,
1259 area: Rect,
1260 buf: &mut Buffer,
1261 default_fg: Color,
1262 default_bg: Color,
1263 ) {
1264 buf.set_style(area, Style::default().fg(default_fg).bg(default_bg));
1269
1270 for (row_idx, row) in content.iter().enumerate() {
1271 if row_idx as u16 >= area.height {
1272 break;
1273 }
1274
1275 let y = area.y + row_idx as u16;
1276
1277 for (col_idx, cell) in row.iter().enumerate() {
1278 if col_idx as u16 >= area.width {
1279 break;
1280 }
1281
1282 let x = area.x + col_idx as u16;
1283
1284 let mut style = Style::default().fg(default_fg).bg(default_bg);
1286
1287 if let Some((r, g, b)) = cell.fg {
1289 style = style.fg(Color::Rgb(r, g, b));
1290 }
1291
1292 if let Some((r, g, b)) = cell.bg {
1293 style = style.bg(Color::Rgb(r, g, b));
1294 }
1295
1296 if cell.bold {
1298 style = style.add_modifier(Modifier::BOLD);
1299 }
1300 if cell.italic {
1301 style = style.add_modifier(Modifier::ITALIC);
1302 }
1303 if cell.underline {
1304 style = style.add_modifier(Modifier::UNDERLINED);
1305 }
1306 if cell.inverse {
1307 style = style.add_modifier(Modifier::REVERSED);
1308 }
1309
1310 if cursor_visible
1312 && row_idx as u16 == cursor_pos.1
1313 && col_idx as u16 == cursor_pos.0
1314 {
1315 style = style.add_modifier(Modifier::REVERSED);
1316 }
1317
1318 buf.set_string(x, y, cell.c.to_string(), style);
1319 }
1320 }
1321 }
1322
1323 #[cfg(test)]
1324 mod tests {
1325 use super::*;
1326 use crate::services::terminal::TerminalCell;
1327
1328 #[test]
1329 fn cells_past_pty_grid_get_theme_bg() {
1330 let area = Rect::new(0, 0, 4, 3);
1335 let mut buf = Buffer::empty(area);
1336 let row = vec![TerminalCell::default(), TerminalCell::default()];
1337 let content = vec![row.clone(), row];
1338
1339 let default_bg = Color::Rgb(0, 0, 170);
1340 let default_fg = Color::Rgb(255, 255, 85);
1341
1342 render_terminal_content(
1343 &content,
1344 (0, 0),
1345 false,
1346 area,
1347 &mut buf,
1348 default_fg,
1349 default_bg,
1350 );
1351
1352 for y in area.top()..area.bottom() {
1353 for x in area.left()..area.right() {
1354 assert_eq!(
1355 buf[(x, y)].bg,
1356 default_bg,
1357 "cell ({x}, {y}) bg should be the theme terminal_bg",
1358 );
1359 }
1360 }
1361 }
1362 }
1363}
1364
1365fn encode_sgr_mouse(
1368 col: u16,
1369 row: u16,
1370 kind: crate::input::handler::TerminalMouseEventKind,
1371 modifiers: crossterm::event::KeyModifiers,
1372) -> Option<Vec<u8>> {
1373 use crate::input::handler::{TerminalMouseButton, TerminalMouseEventKind};
1374
1375 let cx = col + 1;
1377 let cy = row + 1;
1378
1379 let (button_code, is_release) = match kind {
1381 TerminalMouseEventKind::Down(btn) => {
1382 let code = match btn {
1383 TerminalMouseButton::Left => 0,
1384 TerminalMouseButton::Middle => 1,
1385 TerminalMouseButton::Right => 2,
1386 };
1387 (code, false)
1388 }
1389 TerminalMouseEventKind::Up(btn) => {
1390 let code = match btn {
1391 TerminalMouseButton::Left => 0,
1392 TerminalMouseButton::Middle => 1,
1393 TerminalMouseButton::Right => 2,
1394 };
1395 (code, true)
1396 }
1397 TerminalMouseEventKind::Drag(btn) => {
1398 let code = match btn {
1399 TerminalMouseButton::Left => 32, TerminalMouseButton::Middle => 33, TerminalMouseButton::Right => 34, };
1403 (code, false)
1404 }
1405 TerminalMouseEventKind::Moved => (35, false), TerminalMouseEventKind::ScrollUp => (64, false),
1407 TerminalMouseEventKind::ScrollDown => (65, false),
1408 };
1409
1410 let mut cb = button_code;
1412 if modifiers.contains(crossterm::event::KeyModifiers::SHIFT) {
1413 cb += 4;
1414 }
1415 if modifiers.contains(crossterm::event::KeyModifiers::ALT) {
1416 cb += 8;
1417 }
1418 if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
1419 cb += 16;
1420 }
1421
1422 let terminator = if is_release { 'm' } else { 'M' };
1424 Some(format!("\x1b[<{};{};{}{}", cb, cx, cy, terminator).into_bytes())
1425}
1426
1427fn encode_x10_mouse(
1430 col: u16,
1431 row: u16,
1432 kind: crate::input::handler::TerminalMouseEventKind,
1433 modifiers: crossterm::event::KeyModifiers,
1434) -> Option<Vec<u8>> {
1435 use crate::input::handler::{TerminalMouseButton, TerminalMouseEventKind};
1436
1437 let cx = (col.min(222) + 1 + 32) as u8;
1440 let cy = (row.min(222) + 1 + 32) as u8;
1441
1442 let button_code: u8 = match kind {
1444 TerminalMouseEventKind::Down(btn) | TerminalMouseEventKind::Drag(btn) => match btn {
1445 TerminalMouseButton::Left => 0,
1446 TerminalMouseButton::Middle => 1,
1447 TerminalMouseButton::Right => 2,
1448 },
1449 TerminalMouseEventKind::Up(_) => 3, TerminalMouseEventKind::Moved => 3 + 32,
1451 TerminalMouseEventKind::ScrollUp => 64,
1452 TerminalMouseEventKind::ScrollDown => 65,
1453 };
1454
1455 let mut cb = button_code;
1457 if matches!(kind, TerminalMouseEventKind::Drag(_)) {
1458 cb += 32; }
1460 if modifiers.contains(crossterm::event::KeyModifiers::SHIFT) {
1461 cb += 4;
1462 }
1463 if modifiers.contains(crossterm::event::KeyModifiers::ALT) {
1464 cb += 8;
1465 }
1466 if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
1467 cb += 16;
1468 }
1469
1470 let cb = cb + 32;
1472
1473 Some(vec![0x1b, b'[', b'M', cb, cx, cy])
1474}