playdate_device/device/
query.rs

1use std::path::Path;
2use std::path::PathBuf;
3use std::str::FromStr;
4
5use super::serial::SerialNumber;
6use self::error::DeviceQueryError as Error;
7
8
9pub const DEVICE_SERIAL_ENV: &str = "PLAYDATE_SERIAL_DEVICE";
10
11
12/// Device query. Contains 4 options:
13/// - None: query all devices
14/// - Serial: query by serial number
15/// - Path: query by path/name of serial port
16/// - Com: query by COM port number (windows only)
17#[derive(Clone)]
18#[cfg_attr(feature = "clap", derive(clap::Parser))]
19#[cfg_attr(feature = "clap", command(author, version, about, long_about = None, name = "device"))]
20pub struct Query {
21	/// Serial number of usb device or absolute path to socket.
22	/// Format: 'PDUN-XNNNNNN'
23	#[cfg_attr(unix, doc = "or '/dev/cu.usbmodemPDUN_XNNNNNN(N)'.")]
24	#[cfg_attr(windows, doc = "or 'COM{X}', where {X} is a number of port, e.g.: COM3.")]
25	#[cfg_attr(feature = "clap", arg(env = DEVICE_SERIAL_ENV, name = "device"))]
26	pub value: Option<Value>,
27}
28
29impl Default for Query {
30	fn default() -> Self {
31		Self { value: std::env::var(DEVICE_SERIAL_ENV).map(|s| Value::from_str(&s).ok())
32		                                              .ok()
33		                                              .flatten() }
34	}
35}
36
37impl std::fmt::Display for Query {
38	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39		match self.value {
40			Some(ref value) => value.fmt(f),
41			None => write!(f, "None"),
42		}
43	}
44}
45
46impl std::fmt::Debug for Query {
47	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48		match self.value.as_ref() {
49			Some(value) => f.debug_tuple("Query").field(&value.to_string()).finish(),
50			None => f.debug_tuple("Query").field(&None::<()>).finish(),
51		}
52	}
53}
54
55impl std::fmt::Display for Value {
56	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57		match self {
58			Value::Serial(sn) => write!(f, "sn:{sn}"),
59			Value::Path(path) => write!(f, "serial:{}", path.display()),
60			Value::Com(port) => write!(f, "serial:COM{port}"),
61		}
62	}
63}
64
65
66#[derive(Clone, Debug)]
67pub enum Value {
68	/// Serial number of usb device.
69	Serial(SerialNumber),
70	/// Absolute path of serial/modem/telnet-socket.
71	///
72	/// In case of unmounting or installing it also can by mount-point.
73	#[cfg_attr(not(unix), doc = "Use only on Unix.")]
74	Path(PathBuf),
75	/// COM port.
76	#[cfg_attr(not(windows), doc = "Use only on Windows.")]
77	Com(u16),
78}
79
80impl FromStr for Value {
81	type Err = Error;
82
83	fn from_str(dev: &str) -> Result<Self, Self::Err> {
84		let name = dev.trim();
85		if name.is_empty() {
86			return Err(error::ParseError::from(name).into());
87		}
88
89		#[cfg(windows)]
90		match name.strip_prefix("COM").map(|s| s.parse::<u16>()) {
91			Some(Ok(com)) => return Ok(Value::Com(com)),
92			Some(Err(err)) => {
93				return Err(Error::invalid(format!("Invalid format, seems to COM port, but {err}.")));
94			},
95			None => { /* nothing there */ },
96		}
97
98		let serial = SerialNumber::try_from(name);
99		let path = Path::new(name);
100		let is_direct = path.is_absolute() && path.exists();
101
102		match serial {
103			Ok(serial) => {
104				if is_direct {
105					Ok(Value::Path(path.to_owned()))
106				} else {
107					Ok(Value::Serial(serial))
108				}
109			},
110			Err(err) => {
111				if is_direct {
112					Ok(Value::Path(path.to_owned()))
113				} else {
114					Err(err.into())
115				}
116			},
117		}
118	}
119}
120
121impl<'s> TryFrom<&'s str> for Value {
122	type Error = Error;
123	fn try_from(dev: &'s str) -> Result<Self, Self::Error> { Self::from_str(dev) }
124}
125
126impl Value {
127	pub fn to_value_string(&self) -> String {
128		match self {
129			Self::Serial(sn) => sn.to_string(),
130			Self::Path(p) => p.display().to_string(),
131			Self::Com(n) => format!("COM{n}"),
132		}
133	}
134}
135
136
137pub mod error {
138	use std::backtrace::Backtrace;
139	use std::str::FromStr;
140	use thiserror::Error;
141	use miette::Diagnostic;
142
143	pub type ParseError = <super::SerialNumber as FromStr>::Err;
144
145
146	#[derive(Error, Debug, Diagnostic)]
147	pub enum DeviceQueryError {
148		#[error(transparent)]
149		#[diagnostic(transparent)]
150		DeviceSerial {
151			#[backtrace]
152			#[from]
153			source: ParseError,
154		},
155
156		#[error("Invalid query format: {message}")]
157		Invalid {
158			message: String,
159			#[backtrace]
160			backtrace: Backtrace,
161		},
162	}
163
164	impl DeviceQueryError {
165		pub fn invalid(message: String) -> Self {
166			Self::Invalid { message,
167			                backtrace: Backtrace::capture() }
168		}
169	}
170}