diff --git a/Cargo.lock b/Cargo.lock index f988312..c99a69e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -574,6 +574,27 @@ dependencies = [ "objc2 0.6.4", ] +[[package]] +name = "display-info" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e0aca670967c2528799e316f9f97913efcc034867614d55681dd41a1c2f7830" +dependencies = [ + "fxhash", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "scopeguard", + "smithay-client-toolkit 0.20.0", + "thiserror 2.0.20", + "widestring", + "windows 0.62.2", + "xcb", +] + [[package]] name = "displaydoc" version = "0.2.7" @@ -734,6 +755,20 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "embed-resource" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml", + "vswhom", + "winreg", +] + [[package]] name = "epaint" version = "0.29.1" @@ -876,6 +911,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1013,7 +1057,7 @@ dependencies = [ "presser", "thiserror 1.0.69", "winapi", - "windows", + "windows 0.52.0", ] [[package]] @@ -1042,6 +1086,7 @@ version = "0.1.0" dependencies = [ "eframe", "egui", + "embed-resource", "haru-core", "haru-ui", "image", @@ -1052,6 +1097,7 @@ dependencies = [ name = "haru-apply" version = "0.1.0" dependencies = [ + "display-info", "haru-core", "serde_json", "sha2", @@ -1746,10 +1792,10 @@ dependencies = [ "block2 0.5.1", "libc", "objc2 0.5.2", - "objc2-core-data", - "objc2-core-image", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", "objc2-foundation 0.2.2", - "objc2-quartz-core", + "objc2-quartz-core 0.2.2", ] [[package]] @@ -1760,9 +1806,17 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.13.1", "block2 0.6.2", + "libc", "objc2 0.6.4", + "objc2-cloud-kit 0.3.2", + "objc2-core-data 0.3.2", + "objc2-core-foundation", "objc2-core-graphics", + "objc2-core-image 0.3.2", + "objc2-core-text", + "objc2-core-video", "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", ] [[package]] @@ -1778,6 +1832,17 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-contacts" version = "0.2.2" @@ -1801,6 +1866,17 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" @@ -1808,7 +1884,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.13.1", + "block2 0.6.2", "dispatch2", + "libc", "objc2 0.6.4", ] @@ -1819,10 +1897,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ "bitflags 2.13.1", + "block2 0.6.2", "dispatch2", + "libc", "objc2 0.6.4", "objc2-core-foundation", "objc2-io-surface", + "objc2-metal 0.3.2", ] [[package]] @@ -1834,7 +1915,17 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", - "objc2-metal", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", ] [[package]] @@ -1849,6 +1940,31 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", +] + [[package]] name = "objc2-encode" version = "4.1.0" @@ -1875,6 +1991,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.13.1", + "block2 0.6.2", + "libc", "objc2 0.6.4", "objc2-core-foundation", ] @@ -1914,6 +2032,17 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-quartz-core" version = "0.2.2" @@ -1924,7 +2053,18 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", - "objc2-metal", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", + "objc2-foundation 0.3.2", ] [[package]] @@ -1946,13 +2086,13 @@ dependencies = [ "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-image", + "objc2-cloud-kit 0.2.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", "objc2-core-location", "objc2-foundation 0.2.2", "objc2-link-presentation", - "objc2-quartz-core", + "objc2-quartz-core 0.2.2", "objc2-symbols", "objc2-uniform-type-identifiers", "objc2-user-notifications", @@ -2523,6 +2663,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "sha1" version = "0.10.7" @@ -3059,6 +3208,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "1.1.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -3089,6 +3253,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + [[package]] name = "tracing" version = "0.1.44" @@ -3213,6 +3383,26 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -3634,10 +3824,31 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ - "windows-core", + "windows-core 0.52.0", "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core 0.62.2", + "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 0.62.2", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -3647,12 +3858,86 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "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 0.62.2", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "windows-link" 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 0.62.2", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -3722,6 +4007,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.52.6" @@ -3878,6 +4172,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + [[package]] name = "wit-bindgen" version = "0.46.0" @@ -3922,6 +4226,17 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" +[[package]] +name = "xcb" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6c2ad15e0e922856ee89afe862b8992334bbe7953adad56cd1199358cb30566" +dependencies = [ + "bitflags 2.13.1", + "libc", + "quick-xml", +] + [[package]] name = "xcursor" version = "0.3.11" diff --git a/Cargo.toml b/Cargo.toml index ba5b7cc..c2ca831 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ image = { version = "0.25", default-features = false, features = [ ] } qrcode = { version = "0.14", default-features = false } rfd = { version = "0.15", default-features = false } +display-info = "0.5" serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["rt-multi-thread", "sync"] } diff --git a/crates/haru-apply/Cargo.toml b/crates/haru-apply/Cargo.toml index c5be437..e893421 100644 --- a/crates/haru-apply/Cargo.toml +++ b/crates/haru-apply/Cargo.toml @@ -15,6 +15,8 @@ ureq = { workspace = true } [target.'cfg(windows)'.dependencies] uds_windows = { workspace = true } +# The monitor's own name for a screen kirie calls DISPLAY6. +display-info = { workspace = true } [lints] workspace = true diff --git a/crates/haru-apply/src/engine.rs b/crates/haru-apply/src/engine.rs index 8586679..1683462 100644 --- a/crates/haru-apply/src/engine.rs +++ b/crates/haru-apply/src/engine.rs @@ -4,7 +4,7 @@ use std::sync::{Arc, Mutex}; use std::time::Duration; use crate::launch::Plan; -use crate::{Backend, Screen, install, launch}; +use crate::{Backend, Screen, install, launch, monitors}; const POLL: Duration = Duration::from_millis(900); @@ -218,11 +218,12 @@ fn start( fn poll(engine: &dyn Backend, shared: &Arc>) { let available = engine.available(); - let screens = if available { + let mut screens = if available { engine.screens().unwrap_or_default() } else { Vec::new() }; + monitors::label(&mut screens); let pid = launch::pid(); let binary = install::installed(); let connectors = if cfg!(target_os = "linux") || screens.is_empty() { @@ -272,10 +273,7 @@ mod tests { if cfg!(target_os = "linux") { return; } - let engine = Reporting(vec![Screen { - name: "Built-in Retina Display".to_owned(), - current: None, - }]); + let engine = Reporting(vec![Screen::new("Built-in Retina Display", None)]); let shared = Arc::new(Mutex::new(Snapshot::default())); poll(&engine, &shared); let seen = shared.lock().map(|held| held.clone()).unwrap_or_default(); diff --git a/crates/haru-apply/src/kirie.rs b/crates/haru-apply/src/kirie.rs index e2f16e4..134441c 100644 --- a/crates/haru-apply/src/kirie.rs +++ b/crates/haru-apply/src/kirie.rs @@ -66,10 +66,10 @@ impl Backend for Kirie { .filter_map(|line| { let rest = line.strip_prefix("screen=")?; let (name, background) = rest.split_once(" bg=")?; - Some(Screen { - name: name.to_owned(), - current: (!background.is_empty()).then(|| PathBuf::from(background)), - }) + Some(Screen::new( + name, + (!background.is_empty()).then(|| PathBuf::from(background)), + )) }) .collect()) } diff --git a/crates/haru-apply/src/lib.rs b/crates/haru-apply/src/lib.rs index 83684fa..dbda850 100644 --- a/crates/haru-apply/src/lib.rs +++ b/crates/haru-apply/src/lib.rs @@ -6,6 +6,7 @@ pub mod engine; pub mod install; mod kirie; pub mod launch; +mod monitors; mod offscreen; mod relaunch; mod socket; @@ -27,10 +28,26 @@ pub use stream::{Frame, Preview as PreviewStream}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Screen { + /// What the renderer calls it, and what commands to it must use. pub name: String, + /// What to show a person: the monitor's own name where the system has + /// one, since Windows' `DISPLAY6` says nothing about which screen it is. + pub label: String, pub current: Option, } +impl Screen { + #[must_use] + pub fn new(name: impl Into, current: Option) -> Self { + let name = name.into(); + Self { + label: name.clone(), + name, + current, + } + } +} + pub trait Backend: Send + Sync { fn name(&self) -> &'static str; diff --git a/crates/haru-apply/src/monitors.rs b/crates/haru-apply/src/monitors.rs new file mode 100644 index 0000000..7a500dc --- /dev/null +++ b/crates/haru-apply/src/monitors.rs @@ -0,0 +1,177 @@ +//! Names a person recognises for the screens the renderer reports. +//! +//! kirie names a Windows screen after its GDI device, `DISPLAY6`, which is +//! neither the number Windows' own settings show nor anything printed on the +//! monitor. Wayland and X11 connector names (`DP-1`) stay as they are. + +use crate::Screen; + +#[cfg(windows)] +pub(crate) fn label(screens: &mut [Screen]) { + use std::collections::HashMap; + use std::sync::Mutex; + + /// The screens last asked about, and what they were called. + type Asked = (Vec, HashMap); + + // Asked again only when the set of screens changes, not on every poll. + static KNOWN: Mutex> = Mutex::new(None); + + let names: Vec = screens.iter().map(|screen| screen.name.clone()).collect(); + let Ok(mut known) = KNOWN.lock() else { + return; + }; + let fresh = known + .as_ref() + .is_none_or(|(asked_for, _)| *asked_for != names); + if fresh { + let monitors = display_info::DisplayInfo::all() + .unwrap_or_default() + .into_iter() + .map(|display| Monitor { + device: device_tail(&display.name).to_owned(), + model: display.friendly_name, + builtin: display.is_builtin, + }) + .collect::>(); + *known = Some((names.clone(), labels(&names, &monitors))); + } + if let Some((_, labels)) = known.as_ref() { + for screen in screens { + if let Some(label) = labels.get(&screen.name) { + screen.label.clone_from(label); + } + } + } +} + +#[cfg(not(windows))] +pub(crate) fn label(_screens: &mut [Screen]) {} + +#[cfg_attr(not(windows), allow(dead_code))] +struct Monitor { + /// `DISPLAY6`, from `\\.\DISPLAY6`, which is how kirie names it. + device: String, + /// The EDID name, `DELL U2720Q`; empty or a placeholder when unknown. + model: String, + builtin: bool, +} + +#[cfg_attr(not(windows), allow(dead_code))] +fn device_tail(device: &str) -> &str { + device.rsplit('\\').next().unwrap_or(device) +} + +/// `DISPLAY6` becomes `Display 6`, the way Windows writes it. +#[cfg_attr(not(windows), allow(dead_code))] +fn spelled(device: &str) -> String { + match device.strip_prefix("DISPLAY") { + Some(number) if !number.is_empty() && number.chars().all(|c| c.is_ascii_digit()) => { + format!("Display {number}") + } + _ => device.to_owned(), + } +} + +#[cfg_attr(not(windows), allow(dead_code))] +fn labels(names: &[String], monitors: &[Monitor]) -> std::collections::HashMap { + let model = |name: &str| { + let monitor = monitors + .iter() + .find(|monitor| monitor.device.eq_ignore_ascii_case(name))?; + let model = monitor.model.trim(); + if !model.is_empty() && !model.starts_with("Unknown Display") { + Some(model.to_owned()) + } else if monitor.builtin { + Some("Built-in display".to_owned()) + } else { + None + } + }; + + let found: Vec<(String, Option)> = names + .iter() + .map(|name| (name.clone(), model(name))) + .collect(); + found + .iter() + .map(|(name, model)| { + let label = match model { + // Two of the same monitor need the number to tell them apart. + Some(model) + if found + .iter() + .filter(|(_, other)| other.as_ref() == Some(model)) + .count() + > 1 => + { + format!("{model} ({})", spelled(name)) + } + Some(model) => model.clone(), + None => spelled(name), + }; + (name.clone(), label) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn monitor(device: &str, model: &str, builtin: bool) -> Monitor { + Monitor { + device: device_tail(device).to_owned(), + model: model.to_owned(), + builtin, + } + } + + fn names(names: &[&str]) -> Vec { + names.iter().map(|name| (*name).to_owned()).collect() + } + + #[test] + fn a_monitor_is_called_by_its_model() { + let found = labels( + &names(&["DISPLAY6"]), + &[monitor(r"\\.\DISPLAY6", "DELL U2720Q", false)], + ); + assert_eq!( + found.get("DISPLAY6").map(String::as_str), + Some("DELL U2720Q") + ); + } + + #[test] + fn two_of_the_same_model_keep_their_numbers() { + let found = labels( + &names(&["DISPLAY1", "DISPLAY2"]), + &[ + monitor(r"\\.\DISPLAY1", "LG ULTRAGEAR", false), + monitor(r"\\.\DISPLAY2", "LG ULTRAGEAR", false), + ], + ); + assert_eq!( + found.get("DISPLAY2").map(String::as_str), + Some("LG ULTRAGEAR (Display 2)") + ); + } + + #[test] + fn an_unnamed_monitor_is_spelled_like_windows_does() { + let found = labels( + &names(&["DISPLAY3", "DISPLAY4", "desktop"]), + &[ + monitor(r"\\.\DISPLAY3", "", true), + monitor(r"\\.\DISPLAY4", "Unknown Display 65537", false), + ], + ); + assert_eq!( + found.get("DISPLAY3").map(String::as_str), + Some("Built-in display") + ); + assert_eq!(found.get("DISPLAY4").map(String::as_str), Some("Display 4")); + assert_eq!(found.get("desktop").map(String::as_str), Some("desktop")); + } +} diff --git a/crates/haru-apply/src/relaunch.rs b/crates/haru-apply/src/relaunch.rs index 4bb9d19..7dc1cb1 100644 --- a/crates/haru-apply/src/relaunch.rs +++ b/crates/haru-apply/src/relaunch.rs @@ -69,10 +69,7 @@ impl Backend for Relaunch { .and_then(|showing| showing.clone()) .filter(|_| launch::running()) }); - Ok(vec![Screen { - name: DESKTOP.to_owned(), - current, - }]) + Ok(vec![Screen::new(DESKTOP, current)]) } fn apply(&self, screen: &str, dir: &Path) -> Result<(), String> { diff --git a/crates/haru-ui/src/app.rs b/crates/haru-ui/src/app.rs index 0afa6a5..fc8ed18 100644 --- a/crates/haru-ui/src/app.rs +++ b/crates/haru-ui/src/app.rs @@ -336,10 +336,16 @@ impl Haru { self.restart_prompt(ui); if !screens.is_empty() { ui.add_space(10.0); - let chosen = self - .library - .target() - .map_or_else(|| "Screen".to_owned(), str::to_owned); + let chosen = self.library.target().map_or_else( + || "Screen".to_owned(), + |target| { + screens + .iter() + .find(|screen| screen.name == target) + .map_or(target, |screen| screen.label.as_str()) + .to_owned() + }, + ); egui::ComboBox::from_id_salt("screen") .selected_text(chosen) .width(160.0) @@ -351,8 +357,10 @@ impl Haru { .as_ref() .and_then(|dir| self.library.title_of(dir)) .unwrap_or_else(|| "nothing".to_owned()); - let response = ui - .selectable_label(picked, format!("{} · {showing}", screen.name)); + let response = ui.selectable_label( + picked, + format!("{} · {showing}", screen.label), + ); if response.clicked() { self.library.set_target(screen.name.clone()); } diff --git a/crates/haru-ui/src/library.rs b/crates/haru-ui/src/library.rs index 1ca762f..45e61b3 100644 --- a/crates/haru-ui/src/library.rs +++ b/crates/haru-ui/src/library.rs @@ -141,10 +141,7 @@ impl Library { self.screens = seen.screens; for name in seen.connectors { if !self.screens.iter().any(|screen| screen.name == name) { - self.screens.push(Screen { - name, - current: None, - }); + self.screens.push(Screen::new(name, None)); } } if self @@ -346,9 +343,9 @@ impl Library { for screen in &self.screens { let here = screen.current.as_deref() == Some(&item.dir); let label = if here { - format!("On {}", screen.name) + format!("On {}", screen.label) } else { - format!("Put up on {}", screen.name) + format!("Put up on {}", screen.label) }; if ui.add_enabled(!here, egui::Button::new(label)).clicked() { apply = Some((screen.name.clone(), item.dir.clone())); diff --git a/crates/haru-ui/src/settings.rs b/crates/haru-ui/src/settings.rs index 868fd30..e4bb746 100644 --- a/crates/haru-ui/src/settings.rs +++ b/crates/haru-ui/src/settings.rs @@ -484,7 +484,7 @@ impl Settings { if ui .add( egui::TextEdit::singleline(&mut self.socket) - .hint_text("/run/user/1000/lwe.sock") + .hint_text(haru_apply::default_socket().display().to_string()) .desired_width(420.0), ) .lost_focus() @@ -508,7 +508,7 @@ impl Settings { let names: Vec<&str> = engine .screens .iter() - .map(|screen| screen.name.as_str()) + .map(|screen| screen.label.as_str()) .collect(); if !names.is_empty() { ui.add_space(2.0); diff --git a/crates/haru-ui/src/theme.rs b/crates/haru-ui/src/theme.rs index 75ea038..17e3f30 100644 --- a/crates/haru-ui/src/theme.rs +++ b/crates/haru-ui/src/theme.rs @@ -74,7 +74,15 @@ fn load_fonts(ctx: &egui::Context) { pub fn apply(ctx: &egui::Context) { load_fonts(ctx); - let mut style = (*ctx.style()).clone(); + // haru is dark only. egui keeps a separate style per theme and, by + // default, follows the system's: on Windows in light mode it switched to + // its stock light style on the first frame, which painted light buttons, + // checkboxes and fields with haru's light text on them, and dark headings + // on haru's dark panels. Linux rarely reports a theme, so it never showed. + ctx.set_theme(egui::Theme::Dark); + ctx.send_viewport_cmd(egui::ViewportCommand::SetTheme(egui::SystemTheme::Dark)); + + let mut style = (*ctx.style_of(egui::Theme::Dark)).clone(); let visuals = &mut style.visuals; *visuals = egui::Visuals::dark(); @@ -133,7 +141,10 @@ pub fn apply(ctx: &egui::Context) { style.spacing.scroll.bar_width = 8.0; style.spacing.combo_height = 360.0; - ctx.set_style(style); + // The light slot gets the same style, so nothing can bring egui's back. + let style = std::sync::Arc::new(style); + ctx.set_style_of(egui::Theme::Dark, style.clone()); + ctx.set_style_of(egui::Theme::Light, style); } pub fn card(rounding: f32) -> egui::Frame { diff --git a/crates/haru/Cargo.toml b/crates/haru/Cargo.toml index 80a55bc..96bd48c 100644 --- a/crates/haru/Cargo.toml +++ b/crates/haru/Cargo.toml @@ -11,6 +11,10 @@ rust-version.workspace = true name = "haru" path = "src/main.rs" +[build-dependencies] +# Puts the icon into haru.exe, where Explorer and the taskbar look for it. +embed-resource = "3" + [dependencies] eframe = { workspace = true } egui = { workspace = true } diff --git a/crates/haru/build.rs b/crates/haru/build.rs new file mode 100644 index 0000000..4b96926 --- /dev/null +++ b/crates/haru/build.rs @@ -0,0 +1,13 @@ +fn main() { + println!("cargo:rerun-if-changed=haru.rc"); + println!("cargo:rerun-if-changed=../../packaging/haru.ico"); + + // Only does anything when building for Windows. Required rather than + // optional, so a build machine without a resource compiler fails instead + // of quietly shipping an exe with no icon. + if let Err(error) = embed_resource::compile("haru.rc", embed_resource::NONE).manifest_required() + { + println!("cargo:warning=could not embed haru's icon: {error}"); + std::process::exit(1); + } +} diff --git a/crates/haru/haru.rc b/crates/haru/haru.rc new file mode 100644 index 0000000..50a3e92 --- /dev/null +++ b/crates/haru/haru.rc @@ -0,0 +1,3 @@ +// The first icon in the executable is the one Explorer, the taskbar and the +// Start menu show. The window's own icon is set separately, at run time. +1 ICON "../../packaging/haru.ico" diff --git a/packaging/haru.ico b/packaging/haru.ico new file mode 100644 index 0000000..14e9cc5 Binary files /dev/null and b/packaging/haru.ico differ diff --git a/tapline b/tapline index 1c5153d..8c439f1 160000 --- a/tapline +++ b/tapline @@ -1 +1 @@ -Subproject commit 1c5153ddcded4afa913d9860922a8c23e418eb67 +Subproject commit 8c439f13307fe126c602bbc2074064ccbab4fc17