Skip to main content

par_term/
settings_window.rs

1//! Separate settings window for the terminal emulator.
2//!
3//! This module provides a standalone window for the settings UI,
4//! allowing users to configure the terminal while viewing terminal content.
5
6use crate::settings_ui::{SettingsUI, UpdateCheckResult};
7use anyhow::{Context, Result};
8use par_term_config::Config;
9
10// Re-export SettingsWindowAction so the rest of the crate can use it via
11// `crate::settings_window::SettingsWindowAction` as before.
12pub use crate::settings_ui::SettingsWindowAction;
13use std::sync::Arc;
14use wgpu::SurfaceError;
15use winit::event::WindowEvent;
16use winit::event_loop::ActiveEventLoop;
17use winit::keyboard::{Key, NamedKey};
18use winit::window::{Window, WindowId};
19
20/// Manages a separate settings window with its own egui context and wgpu renderer
21pub struct SettingsWindow {
22    /// The winit window
23    window: Arc<Window>,
24    /// Window ID for event routing
25    window_id: WindowId,
26    /// wgpu surface
27    surface: wgpu::Surface<'static>,
28    /// wgpu device
29    device: Arc<wgpu::Device>,
30    /// wgpu queue
31    queue: Arc<wgpu::Queue>,
32    /// Surface configuration
33    surface_config: wgpu::SurfaceConfiguration,
34    /// egui context
35    egui_ctx: egui::Context,
36    /// egui-winit state
37    egui_state: egui_winit::State,
38    /// egui-wgpu renderer
39    egui_renderer: egui_wgpu::Renderer,
40    /// Settings UI component
41    pub settings_ui: SettingsUI,
42    /// Whether the window is ready for rendering
43    ready: bool,
44    /// Flag to indicate window should close
45    should_close: bool,
46    /// Pending paste text from menu accelerator (injected into egui next frame)
47    pending_paste: Option<String>,
48    /// Pending egui events from menu accelerators (Copy, Cut, SelectAll)
49    pending_events: Vec<egui::Event>,
50}
51
52impl SettingsWindow {
53    /// Create a new settings window
54    pub async fn new(
55        event_loop: &ActiveEventLoop,
56        config: Config,
57        supported_vsync_modes: Vec<crate::config::VsyncMode>,
58    ) -> Result<Self> {
59        // Create the window
60        let window_attrs = Window::default_attributes()
61            .with_title("Settings")
62            .with_inner_size(winit::dpi::LogicalSize::new(700, 800))
63            .with_min_inner_size(winit::dpi::LogicalSize::new(500, 400))
64            .with_resizable(true);
65
66        let window = Arc::new(event_loop.create_window(window_attrs)?);
67        let window_id = window.id();
68        let size = window.inner_size();
69
70        // Create wgpu instance
71        // Platform-specific backend selection for better VM compatibility
72        #[cfg(target_os = "windows")]
73        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
74            backends: wgpu::Backends::DX12,
75            ..Default::default()
76        });
77        #[cfg(target_os = "macos")]
78        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
79            backends: wgpu::Backends::all(),
80            ..Default::default()
81        });
82        #[cfg(target_os = "linux")]
83        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
84            backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
85            ..Default::default()
86        });
87
88        // Create surface
89        let surface = instance.create_surface(window.clone())?;
90
91        // Request adapter
92        let adapter = instance
93            .request_adapter(&wgpu::RequestAdapterOptions {
94                power_preference: wgpu::PowerPreference::LowPower,
95                compatible_surface: Some(&surface),
96                force_fallback_adapter: false,
97            })
98            .await
99            .context("Failed to find suitable GPU adapter")?;
100
101        // Request device
102        let (device, queue) = adapter
103            .request_device(&wgpu::DeviceDescriptor::default())
104            .await?;
105
106        let device = Arc::new(device);
107        let queue = Arc::new(queue);
108
109        // Configure surface
110        let surface_caps = surface.get_capabilities(&adapter);
111        let surface_format = surface_caps
112            .formats
113            .iter()
114            .find(|f| f.is_srgb())
115            .copied()
116            .unwrap_or(surface_caps.formats[0]);
117
118        // Select alpha mode for window transparency (consistent with main window)
119        let alpha_mode = if surface_caps
120            .alpha_modes
121            .contains(&wgpu::CompositeAlphaMode::PreMultiplied)
122        {
123            wgpu::CompositeAlphaMode::PreMultiplied
124        } else if surface_caps
125            .alpha_modes
126            .contains(&wgpu::CompositeAlphaMode::PostMultiplied)
127        {
128            wgpu::CompositeAlphaMode::PostMultiplied
129        } else if surface_caps
130            .alpha_modes
131            .contains(&wgpu::CompositeAlphaMode::Auto)
132        {
133            wgpu::CompositeAlphaMode::Auto
134        } else {
135            surface_caps.alpha_modes[0]
136        };
137
138        let surface_config = wgpu::SurfaceConfiguration {
139            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
140            format: surface_format,
141            width: size.width.max(1),
142            height: size.height.max(1),
143            present_mode: wgpu::PresentMode::AutoVsync,
144            alpha_mode,
145            view_formats: vec![],
146            desired_maximum_frame_latency: 2,
147        };
148        surface.configure(&device, &surface_config);
149
150        // Initialize egui
151        let scale_factor = window.scale_factor() as f32;
152        let egui_ctx = egui::Context::default();
153        let egui_state = egui_winit::State::new(
154            egui_ctx.clone(),
155            egui::ViewportId::ROOT,
156            &window,
157            Some(scale_factor),
158            None,
159            None,
160        );
161
162        // Create egui renderer
163        let egui_renderer = egui_wgpu::Renderer::new(
164            &device,
165            surface_format,
166            egui_wgpu::RendererOptions {
167                msaa_samples: 1,
168                depth_stencil_format: None,
169                dithering: false,
170                predictable_texture_filtering: false,
171            },
172        );
173
174        // Create settings UI
175        let mut settings_ui = SettingsUI::new(config);
176        settings_ui.visible = true; // Always visible in settings window
177        settings_ui.update_supported_vsync_modes(supported_vsync_modes);
178
179        Ok(Self {
180            window,
181            window_id,
182            surface,
183            device,
184            queue,
185            surface_config,
186            egui_ctx,
187            egui_state,
188            egui_renderer,
189            settings_ui,
190            ready: true,
191            should_close: false,
192            pending_paste: None,
193            pending_events: Vec::new(),
194        })
195    }
196
197    /// Get the window ID
198    pub fn window_id(&self) -> WindowId {
199        self.window_id
200    }
201
202    /// Check if the window should close
203    pub fn should_close(&self) -> bool {
204        self.should_close
205    }
206
207    /// Queue a paste event for the next egui frame.
208    ///
209    /// Used when the macOS menu accelerator intercepts Cmd+V before
210    /// the keypress reaches egui.
211    pub fn inject_paste(&mut self, text: String) {
212        self.pending_paste = Some(text);
213        self.window.request_redraw();
214    }
215
216    /// Queue an egui event for the next frame.
217    ///
218    /// Used when menu accelerators intercept Cmd+C, Cmd+X, Cmd+A, etc.
219    /// before egui sees them.
220    pub fn inject_event(&mut self, event: egui::Event) {
221        self.pending_events.push(event);
222        self.window.request_redraw();
223    }
224
225    /// Update the config in the settings UI
226    pub fn update_config(&mut self, config: Config) {
227        self.settings_ui.update_config(config);
228    }
229
230    /// Force-update the config, bypassing the `has_changes` guard.
231    /// Used when the ACP agent changes config — must propagate even if
232    /// the user has pending edits in the settings window.
233    pub fn force_update_config(&mut self, config: Config) {
234        self.settings_ui.force_update_config(config);
235    }
236
237    /// Set a shader compilation error message
238    pub fn set_shader_error(&mut self, error: Option<String>) {
239        self.settings_ui.set_shader_error(error);
240    }
241
242    /// Set a cursor shader compilation error message
243    pub fn set_cursor_shader_error(&mut self, error: Option<String>) {
244        self.settings_ui.set_cursor_shader_error(error);
245    }
246
247    /// Clear shader error
248    pub fn clear_shader_error(&mut self) {
249        self.settings_ui.clear_shader_error();
250    }
251
252    /// Clear cursor shader error
253    pub fn clear_cursor_shader_error(&mut self) {
254        self.settings_ui.clear_cursor_shader_error();
255    }
256
257    /// Sync shader enabled states from external source (e.g., keybinding toggle)
258    /// This prevents the settings window from overwriting externally toggled states
259    pub fn sync_shader_states(&mut self, custom_shader_enabled: bool, cursor_shader_enabled: bool) {
260        self.settings_ui.config.custom_shader_enabled = custom_shader_enabled;
261        self.settings_ui.config.cursor_shader_enabled = cursor_shader_enabled;
262    }
263
264    /// Handle a window event
265    pub fn handle_window_event(&mut self, event: WindowEvent) -> SettingsWindowAction {
266        // Let egui handle the event
267        let event_response = self.egui_state.on_window_event(&self.window, &event);
268
269        match event {
270            WindowEvent::CloseRequested => {
271                self.should_close = true;
272                return SettingsWindowAction::Close;
273            }
274
275            WindowEvent::Resized(new_size) => {
276                if new_size.width > 0 && new_size.height > 0 {
277                    self.surface_config.width = new_size.width;
278                    self.surface_config.height = new_size.height;
279                    self.surface.configure(&self.device, &self.surface_config);
280                    self.window.request_redraw();
281                }
282            }
283
284            WindowEvent::KeyboardInput { event, .. } => {
285                // Handle Escape to close window (if egui didn't consume it)
286                if !event_response.consumed
287                    && event.state.is_pressed()
288                    && matches!(event.logical_key, Key::Named(NamedKey::Escape))
289                {
290                    // Only close if no shader editor is open
291                    if !self.settings_ui.shader_editor_visible
292                        && !self.settings_ui.cursor_shader_editor_visible
293                    {
294                        self.should_close = true;
295                        return SettingsWindowAction::Close;
296                    }
297                }
298            }
299
300            WindowEvent::RedrawRequested => {
301                return self.render();
302            }
303
304            _ => {}
305        }
306
307        // Request redraw if egui needs it
308        if event_response.repaint {
309            self.window.request_redraw();
310        }
311
312        SettingsWindowAction::None
313    }
314
315    /// Render the settings window
316    fn render(&mut self) -> SettingsWindowAction {
317        if !self.ready {
318            return SettingsWindowAction::None;
319        }
320
321        // Get surface texture
322        let output = match self.surface.get_current_texture() {
323            Ok(output) => output,
324            Err(SurfaceError::Lost | SurfaceError::Outdated) => {
325                self.surface.configure(&self.device, &self.surface_config);
326                return SettingsWindowAction::None;
327            }
328            Err(SurfaceError::Timeout) => {
329                log::warn!("Settings window surface timeout");
330                return SettingsWindowAction::None;
331            }
332            Err(e) => {
333                log::error!("Settings window surface error: {:?}", e);
334                return SettingsWindowAction::None;
335            }
336        };
337
338        let view = output
339            .texture
340            .create_view(&wgpu::TextureViewDescriptor::default());
341
342        // Track settings results
343        let mut config_to_save = None;
344        let mut config_for_live = None;
345        let mut shader_apply = None;
346        let mut cursor_shader_apply = None;
347
348        // Run egui
349        let mut raw_input = self.egui_state.take_egui_input(&self.window);
350
351        // Inject pending events from menu accelerators (Cmd+V/C/X/A intercepted by muda)
352        if let Some(text) = self.pending_paste.take() {
353            raw_input.events.push(egui::Event::Paste(text));
354        }
355        raw_input.events.append(&mut self.pending_events);
356
357        let egui_output = self.egui_ctx.run(raw_input, |ctx| {
358            // Show the settings UI as a panel (not a nested window) and capture results
359            let (save, live, shader, cursor_shader) = self.settings_ui.show_as_panel(ctx);
360            config_to_save = save;
361            config_for_live = live;
362            shader_apply = shader;
363            cursor_shader_apply = cursor_shader;
364        });
365
366        // Handle platform output (clipboard, cursor)
367        // Manually handle clipboard copy as a fallback for macOS menu accelerator issues.
368        // In egui 0.33, copy commands are in platform_output.commands as OutputCommand::CopyText.
369        for cmd in &egui_output.platform_output.commands {
370            match cmd {
371                egui::OutputCommand::CopyText(text) => {
372                    if let Ok(mut clipboard) = arboard::Clipboard::new()
373                        && let Err(e) = clipboard.set_text(text)
374                    {
375                        log::warn!("Settings window: failed to copy to clipboard: {}", e);
376                    }
377                }
378                egui::OutputCommand::CopyImage(_) => {}
379                _ => {}
380            }
381        }
382        self.egui_state
383            .handle_platform_output(&self.window, egui_output.platform_output.clone());
384
385        // Tessellate shapes
386        let paint_jobs = self
387            .egui_ctx
388            .tessellate(egui_output.shapes, self.egui_ctx.pixels_per_point());
389
390        // Upload egui textures
391        for (id, delta) in &egui_output.textures_delta.set {
392            self.egui_renderer
393                .update_texture(&self.device, &self.queue, *id, delta);
394        }
395
396        // Create command encoder
397        let mut encoder = self
398            .device
399            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
400                label: Some("Settings Window Encoder"),
401            });
402
403        // Screen descriptor
404        let screen_descriptor = egui_wgpu::ScreenDescriptor {
405            size_in_pixels: [self.surface_config.width, self.surface_config.height],
406            pixels_per_point: self.window.scale_factor() as f32,
407        };
408
409        // Update buffers
410        self.egui_renderer.update_buffers(
411            &self.device,
412            &self.queue,
413            &mut encoder,
414            &paint_jobs,
415            &screen_descriptor,
416        );
417
418        // Render pass
419        {
420            let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
421                label: Some("Settings Window Render Pass"),
422                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
423                    view: &view,
424                    resolve_target: None,
425                    ops: wgpu::Operations {
426                        load: wgpu::LoadOp::Clear(wgpu::Color {
427                            r: 0.094,
428                            g: 0.094,
429                            b: 0.094,
430                            a: 1.0,
431                        }),
432                        store: wgpu::StoreOp::Store,
433                    },
434                    depth_slice: None,
435                })],
436                depth_stencil_attachment: None,
437                timestamp_writes: None,
438                occlusion_query_set: None,
439            });
440
441            // Convert to 'static lifetime as required by egui_renderer.render()
442            let mut render_pass = render_pass.forget_lifetime();
443
444            self.egui_renderer
445                .render(&mut render_pass, &paint_jobs, &screen_descriptor);
446        } // render_pass dropped here
447
448        // Submit
449        self.queue.submit(std::iter::once(encoder.finish()));
450        output.present();
451
452        // Free textures
453        for id in &egui_output.textures_delta.free {
454            self.egui_renderer.free_texture(id);
455        }
456
457        // Check for test notification request
458        if self.settings_ui.take_test_notification_request() {
459            return SettingsWindowAction::TestNotification;
460        }
461
462        // Check for profile save request
463        if let Some(profiles) = self.settings_ui.take_profile_save_request() {
464            self.window.request_redraw();
465            return SettingsWindowAction::SaveProfiles(profiles);
466        }
467
468        // Check for profile open request
469        if let Some(id) = self.settings_ui.take_profile_open_request() {
470            self.window.request_redraw();
471            return SettingsWindowAction::OpenProfile(id);
472        }
473
474        // Check for open log file request
475        if self.settings_ui.open_log_requested {
476            self.settings_ui.open_log_requested = false;
477            return SettingsWindowAction::OpenLogFile;
478        }
479
480        // Check for update check request
481        if self.settings_ui.check_now_requested {
482            self.settings_ui.check_now_requested = false;
483            self.window.request_redraw();
484            return SettingsWindowAction::ForceUpdateCheck;
485        }
486
487        // Check for update install request
488        if self.settings_ui.update_install_requested {
489            self.settings_ui.update_install_requested = false;
490            // Extract the version from the last_update_result
491            if let Some(UpdateCheckResult::UpdateAvailable(ref info)) =
492                self.settings_ui.last_update_result
493            {
494                let version = info
495                    .version
496                    .strip_prefix('v')
497                    .unwrap_or(&info.version)
498                    .to_string();
499                self.settings_ui
500                    .start_self_update_with(version.clone(), |v| {
501                        crate::self_updater::perform_update(v).map(|r| {
502                            crate::settings_ui::UpdateResult {
503                                old_version: r.old_version,
504                                new_version: r.new_version,
505                                install_path: r.install_path.display().to_string(),
506                                needs_restart: r.needs_restart,
507                            }
508                        })
509                    });
510                self.window.request_redraw();
511                return SettingsWindowAction::InstallUpdate(version);
512            }
513        }
514
515        // Poll for update install completion
516        self.settings_ui.poll_update_install_status();
517
518        // Check for coprocess start/stop actions
519        if let Some((index, start)) = self.settings_ui.pending_coprocess_actions.pop() {
520            log::info!(
521                "Settings window: popped coprocess action index={} start={}",
522                index,
523                start
524            );
525            // Request another redraw to process remaining actions (if any) and config changes
526            self.window.request_redraw();
527            return if start {
528                SettingsWindowAction::StartCoprocess(index)
529            } else {
530                SettingsWindowAction::StopCoprocess(index)
531            };
532        }
533
534        // Check for script start/stop actions
535        if let Some((index, start)) = self.settings_ui.pending_script_actions.pop() {
536            crate::debug_info!(
537                "SCRIPT",
538                "Settings window: popped script action index={} start={}",
539                index,
540                start
541            );
542            self.window.request_redraw();
543            return if start {
544                SettingsWindowAction::StartScript(index)
545            } else {
546                SettingsWindowAction::StopScript(index)
547            };
548        }
549
550        // Check for arrangement actions
551        if let Some(action) = self.settings_ui.pending_arrangement_actions.pop() {
552            self.window.request_redraw();
553            return action;
554        }
555
556        // Check for identify panes request
557        if self.settings_ui.identify_panes_requested {
558            self.settings_ui.identify_panes_requested = false;
559            self.window.request_redraw();
560            return SettingsWindowAction::IdentifyPanes;
561        }
562
563        // Check for shell integration install/uninstall actions
564        if let Some(action) = self.settings_ui.shell_integration_action.take() {
565            self.window.request_redraw();
566            return match action {
567                crate::settings_ui::integrations_tab::ShellIntegrationAction::Install => {
568                    SettingsWindowAction::InstallShellIntegration
569                }
570                crate::settings_ui::integrations_tab::ShellIntegrationAction::Uninstall => {
571                    SettingsWindowAction::UninstallShellIntegration
572                }
573            };
574        }
575
576        // Determine action based on settings UI results
577        if let Some(config) = config_to_save {
578            return SettingsWindowAction::SaveConfig(config);
579        }
580        if let Some(shader_result) = shader_apply {
581            return SettingsWindowAction::ApplyShader(shader_result);
582        }
583        if let Some(cursor_shader_result) = cursor_shader_apply {
584            return SettingsWindowAction::ApplyCursorShader(cursor_shader_result);
585        }
586        if let Some(config) = config_for_live {
587            return SettingsWindowAction::ApplyConfig(config);
588        }
589
590        SettingsWindowAction::None
591    }
592
593    /// Request a redraw
594    pub fn request_redraw(&self) {
595        self.window.request_redraw();
596    }
597
598    /// Bring the window to the front and focus it
599    pub fn focus(&self) {
600        self.window.focus_window();
601        self.window.request_redraw();
602    }
603
604    /// Check if the settings window currently has focus.
605    pub fn is_focused(&self) -> bool {
606        self.window.has_focus()
607    }
608}