diff --git a/Cargo.lock b/Cargo.lock index ce9dc5c..d93257f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1149,7 +1149,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -1369,7 +1369,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2055,6 +2055,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "toml", + "windows", ] [[package]] @@ -2108,7 +2109,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2117,6 +2118,27 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -2130,6 +2152,17 @@ dependencies = [ "windows-strings", ] +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.60.2" @@ -2158,6 +2191,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + [[package]] name = "windows-registry" version = "0.6.1" @@ -2271,6 +2314,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" diff --git a/Cargo.toml b/Cargo.toml index 23d5fe9..af5d8ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,9 @@ chrono = {features = ["serde"], version = "0.4"} thiserror = "2.0" clap_complete = "4.5.66" +[target.'cfg(target_os = "windows")'.dependencies] +windows = { version = "0.62.2", features = ["Win32", "Win32_System", "Win32_System_Console", "Win32_UI_WindowsAndMessaging"] } + [profile.release] lto = "thin" codegen-units = 1 diff --git a/src/cli.rs b/src/cli.rs index 9fc4a04..4920abb 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -78,6 +78,18 @@ pub struct Cli { #[arg(long, value_name = "SHELL", value_enum)] pub completions: Option, + + #[cfg(target_os = "windows")] + #[arg(long, help = "Spawn as a borderless fullscreen window")] + pub fullscreen: bool, + + #[cfg(target_os = "windows")] + #[arg( + long, + help = "Don't respawn into conhost when --fullscreen is enabled", + hide = true + )] + pub forked: bool, } pub fn extract_simulate_missing_value(err: clap::Error) -> clap::Error { diff --git a/src/config.rs b/src/config.rs index 81350b6..8424aac 100644 --- a/src/config.rs +++ b/src/config.rs @@ -176,7 +176,7 @@ impl Config { toml::Value::try_into(value).map_err(ConfigError::ParseError) } - fn get_config_path() -> Result { + pub fn get_config_path() -> Result { let config_dir = dirs::config_dir() .or_else(|| dirs::home_dir().map(|h| h.join(".config"))) .ok_or(ConfigError::NoConfigDir)?; diff --git a/src/main.rs b/src/main.rs index cdfb137..72fb2bf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,9 @@ mod render; mod scene; mod weather; +#[cfg(target_os = "windows")] +mod screensaver; + use clap::{CommandFactory, Parser}; use clap_complete::generate; use cli::Cli; @@ -38,7 +41,20 @@ async fn main() -> io::Result<()> { default_hook(info); })); - let cli = match Cli::try_parse() { + #[cfg(target_os = "windows")] + let cli = { + if screensaver::windows::is_screensaver() { + screensaver::windows::init_screensaver()?; // fysa: this function prepares a terminal + Cli::try_parse_from(screensaver::windows::normalize_args()) + } else { + Cli::try_parse() + } + }; + + #[cfg(not(target_os = "windows"))] + let cli = Cli::try_parse(); + + let cli = match cli { Ok(cli) => cli, Err(err) => { let err = cli::extract_simulate_missing_value(err); @@ -56,6 +72,15 @@ async fn main() -> io::Result<()> { return Ok(()); } + #[cfg(target_os = "windows")] + { + if cli.fullscreen && cli.forked { + screensaver::windows::make_full_screen()?; + } else if cli.fullscreen { + screensaver::windows::relaunch_in_conhost(); + } + } + let mut config = match Config::load() { Ok(config) => config, Err(e) => { @@ -201,6 +226,11 @@ async fn main() -> io::Result<()> { } }; + #[cfg(target_os = "windows")] + if cli.fullscreen { + println!("Press alt+enter twice to return the terminal to normal."); + } + renderer.cleanup()?; if let Err(e) = result { diff --git a/src/screensaver.rs b/src/screensaver.rs new file mode 100644 index 0000000..cad9588 --- /dev/null +++ b/src/screensaver.rs @@ -0,0 +1,142 @@ +//! Screensaver support + +#[cfg(target_os = "windows")] +pub mod windows { + use std::{env, process::Command}; + + use windows::Win32::Foundation::HWND; + use windows::Win32::System::Console::{ + CONSOLE_SCREEN_BUFFER_INFO, COORD, GetConsoleScreenBufferInfo, GetConsoleWindow, + GetStdHandle, STD_OUTPUT_HANDLE, SetConsoleScreenBufferSize, + }; + use windows::Win32::UI::WindowsAndMessaging::{ + GWL_STYLE, GetWindowLongW, SW_MAXIMIZE, SetWindowLongW, ShowWindow, WS_CAPTION, + WS_MAXIMIZEBOX, WS_MINIMIZEBOX, WS_SYSMENU, WS_THICKFRAME, + }; + + use crate::config::Config; + + pub fn is_screensaver() -> bool { + let binding = env::args().collect::>(); + let args = binding.first().unwrap(); + args.contains(&".scr".to_string()) + } + + pub fn normalize_args() -> Vec { + env::args() + .map(|arg| { + if let Some(rest) = arg.strip_prefix('/') { + if rest.len() == 1 && rest.chars().next().unwrap().is_ascii_alphabetic() { + match rest { + "S" | "s" | "c" => "--fullscreen".to_string(), + + _ => { + format!("-{rest}") + } + } + } else { + arg + } + } else { + arg + } + }) + .collect() + } + + /// Only useful in Windows 11, cannot run inside Windows Terminal + /// This handles all the logic for handling default CLI arguments for `.scr` files + /// Also includes logic for relaunching in conhost + pub fn init_screensaver() -> windows::core::Result<()> { + let args = normalize_args(); + + let cfg_path = Config::get_config_path().unwrap(); + + if args.len() == 1 { + println!( + "Opening config file in system text editor: {}", + cfg_path.display() + ); + Command::new("notepad.exe") + .arg(cfg_path) + .spawn() + .expect("Failed to open config file in system text editor"); + std::process::exit(1); + } + + if let Some(config) = args.get(1) + && config.contains("/c:") + { + println!("Waiting for notepad to close..."); + Command::new("notepad.exe") + .arg(cfg_path) + .output() + .expect("Failed to open config file in system text editor"); + } + + if !args.contains(&"--forked".to_string()) { + relaunch_in_conhost(); + } // Always relaunch in conhost - Best way to avoid Win Terminal in Win11 + // make_full_screen() + + Ok(()) + } + + /// Makes any console window fullscreen + /// In Windows 11 this creates some rather weird artifacts due to `Windows Terminal` + /// But it works fine in Windows 10, as a result ensure conhost.exe is the owner of the console + pub fn make_full_screen() -> windows::core::Result<()> { + unsafe { + let hwnd: HWND = GetConsoleWindow(); + + let style = GetWindowLongW(hwnd, GWL_STYLE); + + let new_style = style + & !(WS_CAPTION.0 as i32 + | WS_THICKFRAME.0 as i32 + | WS_MINIMIZEBOX.0 as i32 + | WS_MAXIMIZEBOX.0 as i32 + | WS_SYSMENU.0 as i32); + + SetWindowLongW(hwnd, GWL_STYLE, new_style); + + if !hwnd.0.is_null() { + ShowWindow(hwnd, SW_MAXIMIZE).unwrap(); + } else { + println!("No console window found."); + } + + let handle = GetStdHandle(STD_OUTPUT_HANDLE)?; + + let mut info = CONSOLE_SCREEN_BUFFER_INFO::default(); + GetConsoleScreenBufferInfo(handle, &mut info)?; + + let window_width = info.srWindow.Right - info.srWindow.Left + 1; + let window_height = info.srWindow.Bottom - info.srWindow.Top + 1; + + SetConsoleScreenBufferSize( + handle, + COORD { + X: window_width, + Y: window_height, + }, + )?; + } + + Ok(()) + } + + /// Only useful in Windows 11, cannot run inside Windows Terminal + pub fn relaunch_in_conhost() -> ! { + let mut args = normalize_args(); + + args.push("--forked".to_string()); + + Command::new("conhost") + .args(args) + .spawn() + .expect("Failed to relaunch in conhost"); + + std::process::exit(0); + } +} diff --git a/src/weather/provider/supplementary/aad.rs b/src/weather/provider/supplementary/aad.rs index 89f39b1..7f08897 100644 --- a/src/weather/provider/supplementary/aad.rs +++ b/src/weather/provider/supplementary/aad.rs @@ -190,7 +190,7 @@ struct SunData { impl SunData { fn get_time(&self) -> String { - self.time.clone().replace(" ST", "") // Unsure what ST stands for, but its not needed + self.time.clone().replace(" ST", "").replace(" DT", "") // Figured out what ST and DT mean (Standard Time & Daylight Time) } fn to_chrono_time(&self) -> Result {