Repository navigation
Installer: show supported devices that are plugged in #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| //! Finds the supported devices that are plugged in, so the installer can say | ||
| //! what OpenMouse will work with before anything is installed. | ||
| //! | ||
| //! Detection lives in the app itself, so rather than duplicate it the | ||
| //! installer writes the embedded app to a temporary folder and runs it with | ||
| //! `--list-supported <file>` (see `gpui/src/supported_devices.rs`), which lists | ||
| //! the devices exactly as the Overview page would and exits. | ||
|
|
||
| use std::time::{Duration, Instant}; | ||
|
|
||
| use serde::Deserialize; | ||
|
|
||
| use crate::payload; | ||
|
|
||
| /// How long to wait for the app before giving up on the list. | ||
| const TIMEOUT: Duration = Duration::from_secs(25); | ||
| /// Mirrors `gpui/src/supported_devices.rs`'s `ARG`. | ||
| const LIST_ARG: &str = "--list-supported"; | ||
|
|
||
| #[derive(Debug, Clone, Deserialize)] | ||
| pub struct Device { | ||
| pub name: String, | ||
| pub brand: String, | ||
| } | ||
|
|
||
| /// Blocking; run it off the UI thread. | ||
| pub fn scan() -> Result<Vec<Device>, String> { | ||
| let dir = std::env::temp_dir().join(format!("openmouse-scan-{}", std::process::id())); | ||
| std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?; | ||
| let result = run(&dir); | ||
| let _ = std::fs::remove_dir_all(&dir); | ||
| result | ||
| } | ||
|
|
||
| fn run(dir: &std::path::Path) -> Result<Vec<Device>, String> { | ||
| let exe = dir.join(payload::APP_EXE_NAME); | ||
| let out = dir.join("devices.json"); | ||
| std::fs::write(&exe, payload::APP_EXE).map_err(|e| e.to_string())?; | ||
|
|
||
| let mut command = std::process::Command::new(&exe); | ||
| command | ||
| .arg(LIST_ARG) | ||
| .arg(&out) | ||
| .stdin(std::process::Stdio::null()) | ||
| .stdout(std::process::Stdio::null()) | ||
| .stderr(std::process::Stdio::null()); | ||
| #[cfg(windows)] | ||
| { | ||
| use std::os::windows::process::CommandExt; | ||
| const CREATE_NO_WINDOW: u32 = 0x0800_0000; | ||
| command.creation_flags(CREATE_NO_WINDOW); | ||
| } | ||
| let mut child = command.spawn().map_err(|e| e.to_string())?; | ||
|
|
||
| let started = Instant::now(); | ||
| loop { | ||
| match child.try_wait().map_err(|e| e.to_string())? { | ||
| Some(_) => break, | ||
| None if started.elapsed() > TIMEOUT => { | ||
| let _ = child.kill(); | ||
| let _ = child.wait(); | ||
| return Err("timed out".to_string()); | ||
| } | ||
| None => std::thread::sleep(Duration::from_millis(100)), | ||
| } | ||
| } | ||
|
|
||
| let text = std::fs::read_to_string(&out).map_err(|e| e.to_string())?; | ||
| serde_json::from_str(&text).map_err(|e| e.to_string()) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| pub mod colors; | ||
| pub mod devices; | ||
| pub mod paths; | ||
| pub mod payload; | ||
| pub mod registry; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| //! `openmouse-gpui --list-supported <file>`: writes the supported devices that | ||
| //! are plugged in right now to `<file>` as JSON and exits, without opening a | ||
| //! window. The installer runs this to show what it will work with; it is the | ||
| //! same detection the Overview page uses, so the two always agree. | ||
| //! | ||
| //! A file rather than stdout, because the release build has no console. | ||
|
|
||
| use serde::Serialize; | ||
|
|
||
| pub const ARG: &str = "--list-supported"; | ||
|
|
||
| #[derive(Serialize)] | ||
| struct Entry { | ||
| name: String, | ||
| brand: String, | ||
| } | ||
|
|
||
| /// The path after [`ARG`], when the app was started that way. | ||
| pub fn requested_path() -> Option<std::path::PathBuf> { | ||
| let mut args = std::env::args().skip(1); | ||
| while let Some(arg) = args.next() { | ||
| if arg == ARG { | ||
| return args.next().map(Into::into); | ||
| } | ||
| } | ||
| None | ||
| } | ||
|
|
||
| /// Lists the devices and writes them to `path`. Writes `[]` if the device | ||
| /// runtime cannot be reached, so a caller always finds a readable file. | ||
| pub fn write(path: &std::path::Path) { | ||
| let service = crate::hid::HidService::start(); | ||
| let entries: Vec<Entry> = match futures::executor::block_on(service.list_candidates()) { | ||
| Ok(Ok(candidates)) => candidates | ||
| .into_iter() | ||
| .map(|candidate| Entry { | ||
| name: candidate.info.product_string, | ||
| brand: candidate.brands.into_iter().next().unwrap_or_default(), | ||
| }) | ||
| .collect(), | ||
| _ => Vec::new(), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Keep detection failure distinct from an empty scan. If 🤖 Prompt for AI Agents |
||
| }; | ||
| let text = serde_json::to_string(&entries).unwrap_or_else(|_| "[]".to_string()); | ||
| let _ = std::fs::write(path, text); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Medium
src/supported_devices.rs:41When
list_candidates()fails,write()converts the failure into[], so the installer treats an unavailable device runtime as a successful scan and displays “None plugged in right now” instead ofUnavailable. Preserve the failure separately—for example, exit nonzero without writing a success file and have the installer check the exit status.🚀 Reply "fix it for me" or copy this AI Prompt for your agent: