1use crossterm::event::{Event as CEvent, KeyCode, KeyEventKind, KeyModifiers};
7use tokio::sync::mpsc;
8
9use crate::state::{
10 AppState, Focus, PackageItem, PkgbuildCheckRequest, QueryInput, types::AppMode,
11};
12
13mod global;
14mod guardrails;
16mod install;
18mod modals;
19mod mouse;
20mod preflight;
21mod recent;
23mod search;
24pub mod utils;
26
27pub use search::open_preflight_modal;
29
30pub use preflight::start_execution;
32
33pub fn try_interactive_auth_handoff() -> Result<bool, String> {
54 let tool = crate::logic::privilege::active_tool()?;
55
56 crate::app::terminal::restore_terminal()
57 .map_err(|e| format!("Failed to restore terminal for interactive auth: {e}"))?;
58
59 let auth_result = crate::logic::privilege::run_interactive_auth(tool);
60
61 if let Err(e) = crate::app::terminal::setup_terminal() {
62 tracing::error!(error = %e, "Failed to re-setup terminal after interactive auth");
63 return Err(format!("Failed to re-setup terminal: {e}"));
64 }
65
66 auth_result
67}
68
69pub fn spawn_downgrade_in_terminal(app: &mut AppState, items: &[PackageItem]) -> bool {
83 let names: Vec<String> = items.iter().map(|p| p.name.clone()).collect();
84 let joined = names.join(" ");
85
86 let tool = match crate::logic::privilege::active_tool() {
87 Ok(t) => t,
88 Err(msg) => {
89 app.modal = crate::state::Modal::Alert { message: msg };
90 return true;
91 }
92 };
93
94 let downgrade_cmd =
95 crate::logic::privilege::build_privilege_command(tool, &format!("downgrade {joined}"));
96 let cmd = if app.dry_run {
97 let quoted = crate::install::shell_single_quote(&downgrade_cmd);
98 format!("echo DRY RUN: {quoted}")
99 } else {
100 format!(
101 "if (command -v downgrade >/dev/null 2>&1) || pacman -Qi downgrade >/dev/null 2>&1; then {downgrade_cmd}; else echo 'downgrade tool not found. Install \"downgrade\" package.'; fi"
102 )
103 };
104
105 app.downgrade_list.clear();
106 app.downgrade_list_names.clear();
107 app.downgrade_state.select(None);
108
109 crate::install::spawn_shell_commands_in_terminal(&[cmd]);
110
111 app.toast_message = Some(crate::i18n::t(app, "app.toasts.downgrade_started"));
112 app.toast_expires_at = Some(std::time::Instant::now() + std::time::Duration::from_secs(3));
113
114 true
115}
116
117#[allow(clippy::too_many_arguments)]
139pub fn handle_event(
140 ev: &CEvent,
141 app: &mut AppState,
142 query_tx: &mpsc::UnboundedSender<QueryInput>,
143 details_tx: &mpsc::UnboundedSender<PackageItem>,
144 preview_tx: &mpsc::UnboundedSender<PackageItem>,
145 add_tx: &mpsc::UnboundedSender<PackageItem>,
146 pkgb_tx: &mpsc::UnboundedSender<PackageItem>,
147 comments_tx: &mpsc::UnboundedSender<String>,
148 pkgb_check_tx: &mpsc::UnboundedSender<PkgbuildCheckRequest>,
149) -> bool {
150 handle_event_with_pkgbuild_checks(
151 ev,
152 app,
153 query_tx,
154 details_tx,
155 preview_tx,
156 add_tx,
157 pkgb_tx,
158 comments_tx,
159 pkgb_check_tx,
160 )
161}
162
163#[allow(clippy::too_many_arguments)]
164pub fn handle_event_with_pkgbuild_checks(
166 ev: &CEvent,
167 app: &mut AppState,
168 query_tx: &mpsc::UnboundedSender<QueryInput>,
169 details_tx: &mpsc::UnboundedSender<PackageItem>,
170 preview_tx: &mpsc::UnboundedSender<PackageItem>,
171 add_tx: &mpsc::UnboundedSender<PackageItem>,
172 pkgb_tx: &mpsc::UnboundedSender<PackageItem>,
173 comments_tx: &mpsc::UnboundedSender<String>,
174 pkgb_check_tx: &mpsc::UnboundedSender<PkgbuildCheckRequest>,
175) -> bool {
176 if let CEvent::Key(ke) = ev {
177 if ke.kind != KeyEventKind::Press {
178 return false;
179 }
180
181 if ke.code == KeyCode::Char('t') && ke.modifiers.contains(KeyModifiers::CONTROL) {
183 tracing::debug!(
184 "[Event] Ctrl+T key event: code={:?}, mods={:?}, modal={:?}, focus={:?}",
185 ke.code,
186 ke.modifiers,
187 app.modal,
188 app.focus
189 );
190 }
191
192 if matches!(app.app_mode, AppMode::ConfigEditor)
196 && app.config_editor_state.popup.as_ref().is_some_and(|p| {
197 matches!(
198 p.kind,
199 crate::state::EditPopupKind::KeyChord { capturing: true }
200 )
201 })
202 {
203 modals::handle_config_editor_mode_key(*ke, app);
204 return false;
205 }
206
207 if let Some(should_exit) = global::handle_global_key(
210 *ke,
211 app,
212 details_tx,
213 pkgb_tx,
214 comments_tx,
215 query_tx,
216 pkgb_check_tx,
217 ) {
218 if ke.code == KeyCode::Char('t') && ke.modifiers.contains(KeyModifiers::CONTROL) {
219 tracing::debug!(
220 "[Event] Global handler returned should_exit={}",
221 should_exit
222 );
223 }
224 if should_exit {
225 return true; }
227 return false;
229 }
230
231 if ke.code == KeyCode::Char('t') && ke.modifiers.contains(KeyModifiers::CONTROL) {
233 tracing::warn!(
234 "[Event] Ctrl+T was NOT handled by global handler, continuing to other handlers"
235 );
236 }
237
238 if matches!(app.modal, crate::state::Modal::Preflight { .. }) {
240 return preflight::handle_preflight_key(*ke, app);
241 }
242
243 if modals::handle_modal_key(*ke, app, add_tx) {
245 return false;
246 }
247
248 if !matches!(app.modal, crate::state::Modal::None) {
250 return false;
251 }
252
253 if matches!(app.app_mode, AppMode::ConfigEditor) {
255 modals::handle_config_editor_mode_key(*ke, app);
256 return false;
257 }
258
259 if matches!(app.focus, Focus::Recent) {
262 let should_exit =
263 recent::handle_recent_key(*ke, app, query_tx, details_tx, preview_tx, add_tx);
264 return should_exit;
265 }
266
267 if matches!(app.focus, Focus::Install) {
269 let should_exit = install::handle_install_key(*ke, app, details_tx, preview_tx, add_tx);
270 return should_exit;
271 }
272
273 if matches!(app.focus, Focus::Search) {
275 let should_exit = search::handle_search_key(
276 *ke,
277 app,
278 query_tx,
279 details_tx,
280 add_tx,
281 preview_tx,
282 comments_tx,
283 );
284 return should_exit;
285 }
286
287 return false;
289 }
290
291 if let CEvent::Mouse(m) = ev {
293 return mouse::handle_mouse_event_with_pkgbuild_checks(
294 *m,
295 app,
296 details_tx,
297 preview_tx,
298 add_tx,
299 pkgb_tx,
300 comments_tx,
301 query_tx,
302 pkgb_check_tx,
303 );
304 }
305 false
306}
307
308#[cfg(all(test, not(target_os = "windows")))]
309mod tests {
310 use super::*;
311 use crossterm::event::{
312 Event as CEvent, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent,
313 MouseEventKind,
314 };
315 use std::fs;
316 use std::os::unix::fs::PermissionsExt;
317 use std::path::PathBuf;
318
319 #[test]
320 fn ui_options_update_system_enter_triggers_xfce4_args_shape() {
331 let _guard = crate::global_test_mutex_lock();
332 let mut dir: PathBuf = std::env::temp_dir();
334 dir.push(format!(
335 "pacsea_test_term_{}_{}",
336 std::process::id(),
337 std::time::SystemTime::now()
338 .duration_since(std::time::UNIX_EPOCH)
339 .expect("System time is before UNIX epoch")
340 .as_nanos()
341 ));
342 fs::create_dir_all(&dir).expect("create test directory");
343 let mut out_path = dir.clone();
344 out_path.push("args.txt");
345 let mut term_path = dir.clone();
346 term_path.push("xfce4-terminal");
347 let script = "#!/bin/sh\n: > \"$PACSEA_TEST_OUT\"\nfor a in \"$@\"; do printf '%s\n' \"$a\" >> \"$PACSEA_TEST_OUT\"; done\n";
348 fs::write(&term_path, script.as_bytes()).expect("Failed to write test terminal script");
349 let mut perms = fs::metadata(&term_path)
350 .expect("Failed to read test terminal script metadata")
351 .permissions();
352 perms.set_mode(0o755);
353 fs::set_permissions(&term_path, perms)
354 .expect("Failed to set test terminal script permissions");
355 let orig_path = std::env::var_os("PATH");
356 let combined_path = std::env::var("PATH").map_or_else(
358 |_| dir.display().to_string(),
359 |p| format!("{}:{p}", dir.display()),
360 );
361 unsafe {
362 std::env::set_var("PATH", combined_path);
363 std::env::set_var("PACSEA_TEST_OUT", out_path.display().to_string());
364 std::env::set_var("PACSEA_TEST_HEADLESS", "1");
365 }
366
367 let mut app = AppState::default();
368 let (qtx, _qrx) = mpsc::unbounded_channel();
369 let (dtx, _drx) = mpsc::unbounded_channel();
370 let (ptx, _prx) = mpsc::unbounded_channel();
371 let (atx, _arx) = mpsc::unbounded_channel();
372 let (pkgb_tx, _pkgb_rx) = mpsc::unbounded_channel();
373 let (pkgb_check_tx, _pkgb_check_rx) = mpsc::unbounded_channel::<PkgbuildCheckRequest>();
374 app.options_button_rect = Some((5, 5, 10, 1));
375 let click_options = CEvent::Mouse(MouseEvent {
376 kind: MouseEventKind::Down(MouseButton::Left),
377 column: 6,
378 row: 5,
379 modifiers: KeyModifiers::empty(),
380 });
381 let (comments_tx, _comments_rx) = mpsc::unbounded_channel::<String>();
382 let _ = super::handle_event(
383 &click_options,
384 &mut app,
385 &qtx,
386 &dtx,
387 &ptx,
388 &atx,
389 &pkgb_tx,
390 &comments_tx,
391 &pkgb_check_tx,
392 );
393 assert!(app.options_menu_open);
394 app.options_menu_rect = Some((5, 6, 20, 3));
395 let click_menu_update = CEvent::Mouse(MouseEvent {
396 kind: MouseEventKind::Down(MouseButton::Left),
397 column: 6,
398 row: 7,
399 modifiers: KeyModifiers::empty(),
400 });
401 let (comments_tx, _comments_rx) = mpsc::unbounded_channel::<String>();
402 let _ = super::handle_event(
403 &click_menu_update,
404 &mut app,
405 &qtx,
406 &dtx,
407 &ptx,
408 &atx,
409 &pkgb_tx,
410 &comments_tx,
411 &pkgb_check_tx,
412 );
413 let enter = CEvent::Key(KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()));
414 let (comments_tx, _comments_rx) = mpsc::unbounded_channel::<String>();
415 let _ = super::handle_event(
416 &enter,
417 &mut app,
418 &qtx,
419 &dtx,
420 &ptx,
421 &atx,
422 &pkgb_tx,
423 &comments_tx,
424 &pkgb_check_tx,
425 );
426 let mut attempts = 0;
428 while !out_path.exists() && attempts < 50 {
429 std::thread::sleep(std::time::Duration::from_millis(10));
430 attempts += 1;
431 }
432 std::thread::sleep(std::time::Duration::from_millis(100));
434 let body = fs::read_to_string(&out_path).expect("fake terminal args file written");
435 let lines: Vec<&str> = body.lines().collect();
436 let command_idx = lines.iter().rposition(|&l| l == "--command");
440 if command_idx.is_none() {
441 eprintln!(
444 "Warning: xfce4-terminal was not used (no --command found, got: {lines:?}), skipping xfce4-specific assertion"
445 );
446 unsafe {
447 if let Some(v) = orig_path {
448 std::env::set_var("PATH", v);
449 } else {
450 std::env::remove_var("PATH");
451 }
452 std::env::remove_var("PACSEA_TEST_OUT");
453 }
454 return;
455 }
456 let command_idx = command_idx.expect("command_idx should be Some after is_none() check");
457 assert!(
458 command_idx + 1 < lines.len(),
459 "--command found at index {command_idx} but no following argument. Lines: {lines:?}"
460 );
461 assert!(
462 lines[command_idx + 1].starts_with("bash -lc "),
463 "Expected argument after --command to start with 'bash -lc ', got: '{}'. All lines: {:?}",
464 lines[command_idx + 1],
465 lines
466 );
467 unsafe {
468 if let Some(v) = orig_path {
469 std::env::set_var("PATH", v);
470 } else {
471 std::env::remove_var("PATH");
472 }
473 std::env::remove_var("PACSEA_TEST_OUT");
474 }
475 }
476
477 #[test]
478 fn optional_deps_rows_reflect_installed_and_x11_and_reflector() {
489 let _guard = crate::global_test_mutex_lock();
490 let (dir, orig_path, orig_wl) = setup_test_executables();
491 let (mut app, channels) = setup_app_with_translations();
492 open_optional_deps_modal(&mut app, &channels);
493
494 verify_optional_deps_rows(&app.modal);
495 teardown_test_environment(orig_path, orig_wl, &dir);
496 }
497
498 fn setup_test_executables() -> (
508 std::path::PathBuf,
509 Option<std::ffi::OsString>,
510 Option<std::ffi::OsString>,
511 ) {
512 use std::fs;
513 use std::os::unix::fs::PermissionsExt;
514 use std::path::PathBuf;
515
516 let mut dir: PathBuf = std::env::temp_dir();
517 dir.push(format!(
518 "pacsea_test_optional_deps_{}_{}",
519 std::process::id(),
520 std::time::SystemTime::now()
521 .duration_since(std::time::UNIX_EPOCH)
522 .expect("System time is before UNIX epoch")
523 .as_nanos()
524 ));
525 let _ = fs::create_dir_all(&dir);
526
527 let make_exec = |name: &str| {
528 let mut p = dir.clone();
529 p.push(name);
530 fs::write(&p, b"#!/bin/sh\nexit 0\n").expect("Failed to write test executable stub");
531 let mut perms = fs::metadata(&p)
532 .expect("Failed to read test executable stub metadata")
533 .permissions();
534 perms.set_mode(0o755);
535 fs::set_permissions(&p, perms).expect("Failed to set test executable stub permissions");
536 };
537
538 make_exec("nvim");
539 make_exec("kitty");
540
541 let orig_path = std::env::var_os("PATH");
542 unsafe {
543 std::env::set_var("PATH", dir.display().to_string());
544 std::env::set_var("PACSEA_TEST_HEADLESS", "1");
545 };
546 let orig_wl = std::env::var_os("WAYLAND_DISPLAY");
547 unsafe { std::env::remove_var("WAYLAND_DISPLAY") };
548 (dir, orig_path, orig_wl)
549 }
550
551 type AppChannels = (
555 tokio::sync::mpsc::UnboundedSender<QueryInput>,
556 tokio::sync::mpsc::UnboundedSender<PackageItem>,
557 tokio::sync::mpsc::UnboundedSender<PackageItem>,
558 tokio::sync::mpsc::UnboundedSender<PackageItem>,
559 tokio::sync::mpsc::UnboundedSender<PackageItem>,
560 tokio::sync::mpsc::UnboundedSender<String>,
561 tokio::sync::mpsc::UnboundedSender<PkgbuildCheckRequest>,
562 );
563
564 type SetupAppResult = (AppState, AppChannels);
568
569 fn setup_app_with_translations() -> SetupAppResult {
579 use std::collections::HashMap;
580 let mut app = AppState::default();
581 let mut translations = HashMap::new();
582 translations.insert(
583 "app.optional_deps.categories.editor".to_string(),
584 "Editor".to_string(),
585 );
586 translations.insert(
587 "app.optional_deps.categories.terminal".to_string(),
588 "Terminal".to_string(),
589 );
590 translations.insert(
591 "app.optional_deps.categories.clipboard".to_string(),
592 "Clipboard".to_string(),
593 );
594 translations.insert(
595 "app.optional_deps.categories.aur_helper".to_string(),
596 "AUR Helper".to_string(),
597 );
598 translations.insert(
599 "app.optional_deps.categories.security".to_string(),
600 "Security".to_string(),
601 );
602 app.translations.clone_from(&translations);
603 app.translations_fallback = translations;
604 let (qtx, _qrx) = mpsc::unbounded_channel();
605 let (dtx, _drx) = mpsc::unbounded_channel();
606 let (ptx, _prx) = mpsc::unbounded_channel();
607 let (atx, _arx) = mpsc::unbounded_channel();
608 let (pkgb_tx, _pkgb_rx) = mpsc::unbounded_channel();
609 let (comments_tx, _comments_rx) = mpsc::unbounded_channel();
610 let (pkgb_check_tx, _pkgb_check_rx) = mpsc::unbounded_channel::<PkgbuildCheckRequest>();
611 (
612 app,
613 (qtx, dtx, ptx, atx, pkgb_tx, comments_tx, pkgb_check_tx),
614 )
615 }
616
617 fn open_optional_deps_modal(app: &mut AppState, channels: &AppChannels) {
628 app.options_button_rect = Some((5, 5, 12, 1));
629 let click_options = CEvent::Mouse(crossterm::event::MouseEvent {
630 kind: crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left),
631 column: 6,
632 row: 5,
633 modifiers: KeyModifiers::empty(),
634 });
635 let _ = super::handle_event(
636 &click_options,
637 app,
638 &channels.0,
639 &channels.1,
640 &channels.2,
641 &channels.3,
642 &channels.4,
643 &channels.5,
644 &channels.6,
645 );
646 assert!(app.options_menu_open);
647
648 let mut key_three_event =
652 crossterm::event::KeyEvent::new(KeyCode::Char('3'), KeyModifiers::empty());
653 key_three_event.kind = KeyEventKind::Press;
654 let key_three = CEvent::Key(key_three_event);
655 let _ = super::handle_event(
656 &key_three,
657 app,
658 &channels.0,
659 &channels.1,
660 &channels.2,
661 &channels.3,
662 &channels.4,
663 &channels.5,
664 &channels.6,
665 );
666 }
667
668 fn verify_optional_deps_rows(modal: &crate::state::Modal) {
678 match modal {
679 crate::state::Modal::OptionalDeps { rows, .. } => {
680 let find = |prefix: &str| rows.iter().find(|r| r.label.starts_with(prefix));
681
682 let ed = find("Editor: nvim").expect("editor row nvim");
683 assert!(ed.installed, "nvim should be marked installed");
684 assert!(!ed.selectable, "installed editor should not be selectable");
685
686 let term = find("Terminal: kitty").expect("terminal row kitty");
687 assert!(term.installed, "kitty should be marked installed");
688 assert!(
689 !term.selectable,
690 "installed terminal should not be selectable"
691 );
692
693 let clip = find("Clipboard: xclip").expect("clipboard xclip row");
694 assert!(
695 !clip.installed,
696 "xclip should not appear installed by default"
697 );
698 assert!(
699 clip.selectable,
700 "xclip should be selectable when not installed"
701 );
702 assert_eq!(clip.note.as_deref(), Some("X11"));
703
704 let mirrors = find("Mirrors: reflector").expect("reflector row");
705 assert!(
706 !mirrors.installed,
707 "reflector should not be installed by default"
708 );
709 assert!(mirrors.selectable, "reflector should be selectable");
710
711 let paru = find("AUR Helper: paru").expect("paru row");
712 assert!(!paru.installed);
713 assert!(paru.selectable);
714 let yay = find("AUR Helper: yay").expect("yay row");
715 assert!(!yay.installed);
716 assert!(yay.selectable);
717 }
718 other => panic!("Expected OptionalDeps modal, got {other:?}"),
719 }
720 }
721
722 fn teardown_test_environment(
734 orig_path: Option<std::ffi::OsString>,
735 orig_wl: Option<std::ffi::OsString>,
736 dir: &std::path::PathBuf,
737 ) {
738 unsafe {
739 if let Some(v) = orig_path {
740 std::env::set_var("PATH", v);
741 } else {
742 std::env::remove_var("PATH");
743 }
744 if let Some(v) = orig_wl {
745 std::env::set_var("WAYLAND_DISPLAY", v);
746 } else {
747 std::env::remove_var("WAYLAND_DISPLAY");
748 }
749 }
750 let _ = std::fs::remove_dir_all(dir);
751 }
752
753 #[test]
754 fn optional_deps_rows_wayland_shows_wl_clipboard() {
759 use std::collections::HashMap;
760 use std::fs;
761 use std::path::PathBuf;
762 let _guard = crate::global_test_mutex_lock();
763
764 let mut dir: PathBuf = std::env::temp_dir();
766 dir.push(format!(
767 "pacsea_test_optional_deps_wl_{}_{}",
768 std::process::id(),
769 std::time::SystemTime::now()
770 .duration_since(std::time::UNIX_EPOCH)
771 .expect("System time is before UNIX epoch")
772 .as_nanos()
773 ));
774 let _ = fs::create_dir_all(&dir);
775
776 let orig_path = std::env::var_os("PATH");
777 unsafe {
778 std::env::set_var("PATH", dir.display().to_string());
779 std::env::set_var("PACSEA_TEST_HEADLESS", "1");
780 };
781 let orig_wl = std::env::var_os("WAYLAND_DISPLAY");
782 unsafe { std::env::set_var("WAYLAND_DISPLAY", "1") };
783
784 let mut app = AppState::default();
785 let mut translations = HashMap::new();
787 translations.insert(
788 "app.optional_deps.categories.editor".to_string(),
789 "Editor".to_string(),
790 );
791 translations.insert(
792 "app.optional_deps.categories.terminal".to_string(),
793 "Terminal".to_string(),
794 );
795 translations.insert(
796 "app.optional_deps.categories.clipboard".to_string(),
797 "Clipboard".to_string(),
798 );
799 translations.insert(
800 "app.optional_deps.categories.aur_helper".to_string(),
801 "AUR Helper".to_string(),
802 );
803 translations.insert(
804 "app.optional_deps.categories.security".to_string(),
805 "Security".to_string(),
806 );
807 app.translations.clone_from(&translations);
808 app.translations_fallback = translations;
809 let (qtx, _qrx) = mpsc::unbounded_channel();
810 let (dtx, _drx) = mpsc::unbounded_channel();
811 let (ptx, _prx) = mpsc::unbounded_channel();
812 let (atx, _arx) = mpsc::unbounded_channel();
813 let (pkgb_tx, _pkgb_rx) = mpsc::unbounded_channel();
814 let (pkgb_check_tx, _pkgb_check_rx) = mpsc::unbounded_channel::<PkgbuildCheckRequest>();
815
816 app.options_button_rect = Some((5, 5, 12, 1));
818 let click_options = CEvent::Mouse(crossterm::event::MouseEvent {
819 kind: crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left),
820 column: 6,
821 row: 5,
822 modifiers: KeyModifiers::empty(),
823 });
824 let (comments_tx, _comments_rx) = mpsc::unbounded_channel::<String>();
825 let _ = super::handle_event(
826 &click_options,
827 &mut app,
828 &qtx,
829 &dtx,
830 &ptx,
831 &atx,
832 &pkgb_tx,
833 &comments_tx,
834 &pkgb_check_tx,
835 );
836 assert!(app.options_menu_open);
837
838 let mut key_three_event =
840 crossterm::event::KeyEvent::new(KeyCode::Char('3'), KeyModifiers::empty());
841 key_three_event.kind = KeyEventKind::Press;
842 let key_three = CEvent::Key(key_three_event);
843 let (comments_tx, _comments_rx) = mpsc::unbounded_channel::<String>();
844 let _ = super::handle_event(
845 &key_three,
846 &mut app,
847 &qtx,
848 &dtx,
849 &ptx,
850 &atx,
851 &pkgb_tx,
852 &comments_tx,
853 &pkgb_check_tx,
854 );
855
856 match &app.modal {
857 crate::state::Modal::OptionalDeps { rows, .. } => {
858 let clip = rows
859 .iter()
860 .find(|r| r.label.starts_with("Clipboard: wl-clipboard"))
861 .expect("wl-clipboard row");
862 assert_eq!(clip.note.as_deref(), Some("Wayland"));
863 assert!(!clip.installed);
864 assert!(clip.selectable);
865 assert!(
867 !rows.iter().any(|r| r.label.starts_with("Clipboard: xclip")),
868 "xclip should not be listed on Wayland"
869 );
870 }
871 other => panic!("Expected OptionalDeps modal, got {other:?}"),
872 }
873
874 unsafe {
876 if let Some(v) = orig_path {
877 std::env::set_var("PATH", v);
878 } else {
879 std::env::remove_var("PATH");
880 }
881 if let Some(v) = orig_wl {
882 std::env::set_var("WAYLAND_DISPLAY", v);
883 } else {
884 std::env::remove_var("WAYLAND_DISPLAY");
885 }
886 }
887 let _ = fs::remove_dir_all(&dir);
888 }
889}