From e22a51bc7e2f22b9567c9d2a050e4109914aac6f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 01:50:22 +0000 Subject: [PATCH 1/3] fix(workshop): take tapline's fix for "headers ended early" downloads Moves the tapline submodule from 1c5153d to 1b133b6, which picks CDN hosts the way SteamKit does, retries manifest fetches on other hosts and stops reusing connections the CDN has already closed. It also brings in tapline #2 (the bug pass), which haru had not picked up yet. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01JPbjRcr2kuhRi1wBhGk1ax --- tapline | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tapline b/tapline index 1c5153d..1b133b6 160000 --- a/tapline +++ b/tapline @@ -1 +1 @@ -Subproject commit 1c5153ddcded4afa913d9860922a8c23e418eb67 +Subproject commit 1b133b6dbad8b7420ff7e68dfc5b85efca337d5f From 1a865147caa69da4dcb63c68e3f73bdf6fbab554 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 02:14:05 +0000 Subject: [PATCH 2/3] fix(windows): keep haru dark in light mode, embed its icon, name monitors On Windows in light mode most of haru was unreadable: buttons, checkboxes and fields came out light grey with haru's light text on them, and headings went dark on haru's dark panels. egui keeps a style per theme and follows the system's, so the first frame swapped in its stock light style underneath haru's own panel colours. Linux rarely reports a theme, so it never showed. haru now asks for the dark theme outright, gives both slots its style, and asks for a dark title bar. - haru.exe carries its icon as a resource (embed-resource), so Explorer and the taskbar show it rather than the blank default. - Screens kirie calls DISPLAY6 are shown by the monitor's own name ("DELL U2720Q"), or "Display 6" when it has none. Commands still use kirie's name. - The control socket field's placeholder is the real default path, not a Linux one. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01DDe3iPz1un8WXmFsmkHec4 --- Cargo.lock | 337 +++++++++++++++++++++++++++++- Cargo.toml | 1 + crates/haru-apply/Cargo.toml | 2 + crates/haru-apply/src/engine.rs | 10 +- crates/haru-apply/src/kirie.rs | 8 +- crates/haru-apply/src/lib.rs | 17 ++ crates/haru-apply/src/monitors.rs | 177 ++++++++++++++++ crates/haru-apply/src/relaunch.rs | 5 +- crates/haru-ui/src/app.rs | 20 +- crates/haru-ui/src/library.rs | 9 +- crates/haru-ui/src/settings.rs | 4 +- crates/haru-ui/src/theme.rs | 15 +- crates/haru/Cargo.toml | 4 + crates/haru/build.rs | 13 ++ crates/haru/haru.rc | 3 + packaging/haru.ico | Bin 0 -> 110470 bytes 16 files changed, 584 insertions(+), 41 deletions(-) create mode 100644 crates/haru-apply/src/monitors.rs create mode 100644 crates/haru/build.rs create mode 100644 crates/haru/haru.rc create mode 100644 packaging/haru.ico 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 0000000000000000000000000000000000000000..14e9cc5b48964c469ecd2a962608f27ad373ff83 GIT binary patch literal 110470 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 V`WHVScL06X5 Date: Sat, 26 Sep 2026 02:39:49 +0000 Subject: [PATCH 3/3] chore(tapline): pin the submodule to tapline main The previous pin, 1b133b6, was tapline #3's branch head, which was cut from 1c5153d and so did not contain tapline #2 (auth token ACL, ffi event loss, SteamAppId message). 8c439f1 is tapline main after #3 merged and carries both. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01BjWixYpwMjeckvBT82WjHZ --- tapline | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tapline b/tapline index 1b133b6..8c439f1 160000 --- a/tapline +++ b/tapline @@ -1 +1 @@ -Subproject commit 1b133b6dbad8b7420ff7e68dfc5b85efca337d5f +Subproject commit 8c439f13307fe126c602bbc2074064ccbab4fc17