1use std::ffi::OsStr;
2
3#[derive(Clone, Debug, Default, PartialEq, Eq)]
4pub struct Status {
5 pub busy: Option<u32>,
6 pub jobs: usize,
7 pub running: Option<String>,
8 pub play: Option<String>,
9 pub paused: bool,
10}
11
12impl Status {
13 pub fn parse(json: &str) -> Result<Status, String> {
14 let value: serde_json::Value = serde_json::from_str(json).map_err(|e| e.to_string())?;
15 Ok(Status::of(&value))
16 }
17
18 pub fn of(value: &serde_json::Value) -> Status {
19 let jobs = value
20 .get("jobs")
21 .and_then(serde_json::Value::as_array)
22 .cloned()
23 .unwrap_or_default();
24 let running = jobs
25 .iter()
26 .find(|job| job.get("state").and_then(serde_json::Value::as_str) == Some("running"))
27 .map(|job| {
28 let name = job
29 .get("as")
30 .and_then(serde_json::Value::as_str)
31 .unwrap_or("?");
32 let label = job
33 .get("label")
34 .and_then(serde_json::Value::as_str)
35 .unwrap_or("");
36 format!("{name}: {label}")
37 });
38 let play = match value.get("play") {
39 Some(serde_json::Value::Object(play)) => Some(
40 play.get("name")
41 .and_then(serde_json::Value::as_str)
42 .unwrap_or("a game")
43 .to_string(),
44 ),
45 _ => None,
46 };
47 Status {
48 busy: value
49 .get("gpu_busy")
50 .and_then(serde_json::Value::as_u64)
51 .map(|busy| busy as u32),
52 jobs: jobs.len(),
53 running,
54 play,
55 paused: value
56 .get("paused")
57 .and_then(serde_json::Value::as_bool)
58 .unwrap_or(false),
59 }
60 }
61
62 pub fn line(&self) -> String {
63 let mut parts = Vec::new();
64 if let Some(busy) = self.busy {
65 parts.push(format!("gpu {busy}%"));
66 }
67 parts.push(format!("{} queued", self.jobs));
68 if let Some(play) = &self.play {
69 parts.push(format!("playing {play}"));
70 }
71 if self.paused {
72 parts.push("queue paused".to_string());
73 }
74 if let Some(running) = &self.running {
75 parts.push(running.clone());
76 }
77 parts.join(" · ")
78 }
79}
80
81pub fn status() -> Result<Status, String> {
82 pfx_editor_shell::pgpu::fetch()
83 .map(|value| Status::of(&value))
84 .ok_or_else(|| "pgpu status: no answer".to_string())
85}
86
87pub fn on_path(path: &OsStr) -> bool {
88 let program = format!("pgpu{}", std::env::consts::EXE_SUFFIX);
89 std::env::split_paths(path).any(|folder| folder.join(&program).is_file())
90}
91
92#[cfg(test)]
93mod tests {
94 use std::path::Path;
95
96 use super::*;
97
98 #[test]
99 fn the_status_line_names_the_load_the_queue_and_a_game() {
100 let json = r#"{"v":1,"paused":false,"gpu_busy":42,"play":{"pid":7,"name":"pfx edit: room.scene.toml"},
101 "jobs":[{"pid":1,"state":"running","as":"pfx","label":"cargo test"},{"pid":2,"state":"yielded","as":"manf","label":"x"}]}"#;
102 let status = Status::parse(json).unwrap();
103 assert_eq!(status.busy, Some(42));
104 assert_eq!(status.jobs, 2);
105 assert_eq!(
106 status.line(),
107 "gpu 42% · 2 queued · playing pfx edit: room.scene.toml · pfx: cargo test"
108 );
109 let idle = Status::parse(r#"{"v":1,"paused":true,"play":null,"jobs":[]}"#).unwrap();
110 assert_eq!(idle.line(), "0 queued · queue paused");
111 assert!(Status::parse("not json").is_err());
112 }
113
114 #[test]
115 fn the_hook_is_on_only_when_pgpu_is_found_on_the_path() {
116 let path = crate::tests::scene_copy("pgpu on path");
117 let folder = path.parent().unwrap().to_path_buf();
118 let empty = folder.join("empty");
119 let tools = folder.join("tools");
120 std::fs::create_dir_all(&empty).unwrap();
121 std::fs::create_dir_all(&tools).unwrap();
122 std::fs::write(
123 tools.join(format!("pgpu{}", std::env::consts::EXE_SUFFIX)),
124 "",
125 )
126 .unwrap();
127 let joined = |folders: &[&Path]| std::env::join_paths(folders).unwrap();
128 assert!(!on_path(OsStr::new("")));
129 assert!(!on_path(&joined(&[&empty])));
130 assert!(on_path(&joined(&[&empty, &tools])));
131 assert!(!on_path(&joined(&[&tools.join("pgpu")])));
132 }
133}