virtual_desktop_manager/
lib.rs

1//! This library implements various ideas for helping with Windows Virtual
2//! Desktops. For similar projects check out [Virtual Desktop Manager · Issue
3//! #343 ·
4//! microsoft/PowerToys](https://github.com/microsoft/PowerToys/issues/343).
5
6// Note: can't do this renaming in Cargo.toml since the derive macros rely on
7// the package name being `native_windows_gui`.
8extern crate native_windows_derive as nwd;
9extern crate native_windows_gui as nwg;
10
11#[cfg(feature = "auto_start")]
12mod auto_start;
13pub mod block_on;
14#[cfg(feature = "admin_startup")]
15mod change_elevation;
16mod config_window;
17pub mod dynamic_gui;
18mod invisible_window;
19pub mod nwg_ext;
20mod quick_switch;
21mod settings;
22mod tray;
23mod tray_icons;
24pub mod vd;
25mod window_filter;
26pub mod window_info;
27#[cfg(all(feature = "logging", debug_assertions))]
28mod wm_msg_to_string;
29mod tray_plugins {
30    pub mod apply_filters;
31    pub mod desktop_events;
32    pub mod desktop_events_dynamic;
33    pub mod hotkeys;
34    pub mod menus;
35    pub mod panic_notifier;
36}
37
38/// Get a reference to the executable's embedded icon.
39fn exe_icon() -> Option<std::rc::Rc<nwg::Icon>> {
40    use std::{cell::OnceCell, rc::Rc};
41
42    thread_local! {
43        static CACHE: OnceCell<Option<Rc<nwg::Icon>>> = const { OnceCell::new() };
44    }
45    CACHE.with(|cache| {
46        cache
47            .get_or_init(|| {
48                nwg::EmbedResource::load(None)
49                    .unwrap()
50                    .icon(1, None)
51                    .map(Rc::new)
52            })
53            .as_ref()
54            .cloned()
55    })
56}
57
58#[cfg(all(feature = "logging", debug_assertions))]
59fn setup_logging() {
60    // Set the global logger for the `log` crate:
61    ::tracing_log::LogTracer::init().expect("setting global logger");
62
63    let my_subscriber = ::tracing_subscriber::fmt::SubscriberBuilder::default()
64        .pretty()
65        .with_ansi(std::io::IsTerminal::is_terminal(&std::io::stdout()))
66        .with_max_level(tracing::Level::TRACE)
67        .finish();
68    tracing::subscriber::set_global_default(my_subscriber).expect("setting tracing default failed");
69
70    tracing::debug!("Configured global logger");
71
72    let prev = std::panic::take_hook();
73    std::panic::set_hook(Box::new(move |info| {
74        prev(info);
75        tracing::error!("Panic: {}", info);
76    }));
77}
78
79fn register_panic_hook_that_writes_to_file() {
80    static CREATED_LOG: std::sync::Mutex<bool> = std::sync::Mutex::new(false);
81    let prev = std::panic::take_hook();
82    std::panic::set_hook(Box::new(move |info| {
83        prev(info);
84        let Ok(exe_path) = std::env::current_exe() else {
85            return;
86        };
87        let log_file = exe_path.with_extension("panic-log.txt");
88        let Ok(mut created_log_guard) = CREATED_LOG.lock() else {
89            return;
90        };
91        let mut open_options = std::fs::OpenOptions::new();
92        let has_previous_panic = *created_log_guard;
93        if has_previous_panic {
94            open_options.create(true).append(true);
95        } else {
96            open_options.create(true).write(true).truncate(true);
97        }
98        let Ok(mut file) = open_options.open(log_file) else {
99            return;
100        };
101        *created_log_guard = true;
102
103        use std::io::Write;
104        let _ = write!(
105            file,
106            "{}{}",
107            if has_previous_panic { "\n\n\n\n" } else { "" },
108            info
109        );
110    }));
111}
112
113#[cfg(feature = "cli_commands")]
114#[derive(clap::Parser, Debug)]
115#[command(version, about)]
116enum Args {
117    /// Switch to another virtual desktop.
118    Switch {
119        /// The index of the desktop to switch to.
120        #[clap(required_unless_present_any(["next", "back"]))]
121        target: Option<u32>,
122
123        /// Switch to the desktop with an index one more than the current one.
124        #[clap(long)]
125        next: bool,
126
127        /// Switch to the desktop with an index one less than the current one.
128        #[clap(long)]
129        back: bool,
130
131        /// Smooth desktop switching using an animation instead of instant
132        /// switch.
133        #[clap(long)]
134        smooth: bool,
135    },
136}
137
138fn desktop_event_plugin() -> Box<dyn tray::TrayPlugin> {
139    #[cfg(feature = "winvd_dynamic")]
140    {
141        if vd::has_loaded_dynamic_library_successfully() {
142            tracing::info!("Using dynamic library to get virtual desktop events");
143            return Box::<tray_plugins::desktop_events_dynamic::DynamicVirtualDesktopEventManager>::default(
144            );
145        }
146    }
147    #[cfg(feature = "winvd_static")]
148    {
149        tracing::info!("Using static library to get virtual desktop events");
150        return Box::<tray_plugins::desktop_events::VirtualDesktopEventManager>::default();
151    }
152    #[allow(unreachable_code)]
153    {
154        panic!("Could not listen to virtual desktop events since no dynamic library was loaded");
155    }
156}
157
158/// Start the GUI main loop and show the tray icon.
159pub fn run_gui() {
160    #[cfg(all(feature = "logging", debug_assertions))]
161    setup_logging();
162    register_panic_hook_that_writes_to_file();
163
164    // Safety: "VirtualDesktopAccessor.dll" is well-behaved if it exists.
165    if let Err(e) = unsafe { vd::load_dynamic_library() } {
166        if nwg::init().is_ok() {
167            nwg::error_message(
168                "VirtualDesktopManager - Failed to load dynamic library",
169                &e.to_string(),
170            );
171        }
172        std::process::exit(3);
173    }
174
175    #[cfg(feature = "cli_commands")]
176    if let Some(cmd) = std::env::args().nth(1) {
177        use clap::{Parser, Subcommand};
178
179        if Args::has_subcommand(&cmd) || cmd.contains("help") {
180            let args = Args::try_parse().unwrap_or_else(|e| {
181                if nwg::init().is_ok() {
182                    nwg::error_message(
183                        "Virtual Desktop Manager - Invalid CLI arguments",
184                        &format!("{e}"),
185                    );
186                }
187                std::process::exit(2);
188            });
189            std::thread::Builder::new()
190                .name("CLI Command Executor".to_owned())
191                .spawn(move || {
192                    struct ExitGuard;
193                    impl Drop for ExitGuard {
194                        fn drop(&mut self) {
195                            std::process::exit(1);
196                        }
197                    }
198                    let _exit_guard = ExitGuard;
199
200                    // Old .dll files might not call `CoInitialize` and then not work,
201                    // so to be safe we make sure to do that:
202                    if let Err(e) = unsafe { windows::Win32::System::Com::CoInitialize(None) }.ok()
203                    {
204                        tracing::warn!(
205                            error = e.to_string(),
206                            "Failed to call CoInitialize on CLI Command Executor thread"
207                        );
208                    }
209
210                    match args {
211                        Args::Switch {
212                            target,
213                            next,
214                            back,
215                            smooth,
216                        } => {
217                            let target = if let Some(target) = target {
218                                // Ensure WinVD is initialized (should not be needed anymore since we manually call CoInitialize):
219                                let _ = vd::get_current_desktop();
220                                target
221                            } else if next {
222                                let count =
223                                    vd::get_desktop_count().expect("Failed to get desktop count");
224                                let current = vd::get_current_desktop()
225                                    .expect("Failed to get current desktop");
226                                let index = current
227                                    .get_index()
228                                    .expect("Failed to get index of current desktop");
229                                (index + 1).min(count - 1)
230                            } else if back {
231                                let current = vd::get_current_desktop()
232                                    .expect("Failed to get current desktop");
233                                let index: u32 = current
234                                    .get_index()
235                                    .expect("Failed to get index of current desktop");
236                                index.saturating_sub(1)
237                            } else {
238                                unreachable!("Clap should ensure a switch target is specified");
239                            };
240                            tracing::event!(
241                                tracing::Level::INFO,
242                                "Switching to desktop index {target}"
243                            );
244                            if smooth {
245                                if vd::switch_desktop_with_animation(vd::Desktop::from(target)).is_ok() {
246                                    // Windows 11!
247                                    tracing::debug!(
248                                        "Used COM interfaces to animate desktop switch"
249                                    );
250                                    // TODO: maybe try to force windows to refocus the last used window?
251                                } else {
252                                    // Likely Windows 10 which doesn't have a dedicated API for
253                                    // changing desktop with animation, instead we use an invisible
254                                    // window as a workaround.
255                                    nwg::init().expect("Failed to init Native Windows GUI");
256                                    invisible_window::switch_desktop_with_invisible_window(
257                                        vd::get_desktop(target),
258                                        None,
259                                    )
260                                    .expect("Failed to smoothly switch desktop");
261                                }
262                            } else {
263                                vd::switch_desktop(vd::Desktop::from(target))
264                                    .expect("Failed to switch to target desktop");
265                            }
266                        }
267                    }
268                    std::process::exit(0);
269                })
270                .expect("Failed to spawn background thread to work on CLI command");
271
272            // Start GUI event loop ASAP to prevent spinner next to mouse cursor:
273            match nwg::init() {
274                Ok(()) => {
275                    nwg::dispatch_thread_events();
276                }
277                Err(e) => {
278                    tracing::error!(error = ?e, "Failed to initialize gui");
279                }
280            }
281            loop {
282                std::thread::park();
283            }
284        }
285    }
286
287    let settings_plugin = Box::new(settings::UiSettingsPlugin::with_save_path_next_to_exe());
288
289    #[cfg(feature = "admin_startup")]
290    {
291        let mut admin = change_elevation::AdminRestart;
292        admin.handle_startup();
293        if settings_plugin.get().request_admin_at_startup {
294            if let Err(e) = change_elevation::set_elevation(&mut admin, true) {
295                tracing::error!("Failed to request admin rights: {e}");
296            }
297        }
298    }
299
300    nwg::init().expect("Failed to init Native Windows GUI");
301    nwg::Font::set_global_family("Segoe UI").expect("Failed to set default font");
302    let _ui = tray::SystemTray::new(vec![
303        Box::<tray_plugins::panic_notifier::PanicNotifier>::default(),
304        Box::<tray_plugins::apply_filters::ApplyFilters>::default(),
305        settings_plugin,
306        #[cfg(feature = "global_hotkey")]
307        Box::<tray_plugins::hotkeys::HotKeyPlugin>::default(),
308        #[cfg(feature = "auto_start")]
309        Box::<auto_start::AutoStartPlugin>::default(),
310        desktop_event_plugin(),
311        Box::<invisible_window::SmoothDesktopSwitcher>::default(),
312        Box::<tray_plugins::menus::OpenSubmenuPlugin>::default(),
313        Box::<tray_plugins::menus::TopMenuItems>::default(),
314        Box::<tray_plugins::menus::BackspaceAsEscapeAlias>::default(),
315        Box::<tray_plugins::menus::QuickSwitchTopMenu>::default(),
316        Box::<tray_plugins::menus::QuickSwitchMenuUiAdapter>::default(),
317        Box::<tray_plugins::menus::FlatSwitchMenu>::default(),
318        Box::<tray_plugins::menus::BottomMenuItems>::default(),
319        Box::<config_window::ConfigWindow>::default(),
320    ])
321    .build_ui()
322    .expect("Failed to build UI");
323    nwg::dispatch_thread_events();
324}