virtual_desktop_manager/
lib.rs1extern 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
38fn 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 ::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 {
119 #[clap(required_unless_present_any(["next", "back"]))]
121 target: Option<u32>,
122
123 #[clap(long)]
125 next: bool,
126
127 #[clap(long)]
129 back: bool,
130
131 #[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
158pub fn run_gui() {
160 #[cfg(all(feature = "logging", debug_assertions))]
161 setup_logging();
162 register_panic_hook_that_writes_to_file();
163
164 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 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 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 tracing::debug!(
248 "Used COM interfaces to animate desktop switch"
249 );
250 } else {
252 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 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}