playdate_device/device/
query.rs1use 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#[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 #[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(SerialNumber),
70 #[cfg_attr(not(unix), doc = "Use only on Unix.")]
74 Path(PathBuf),
75 #[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 => { },
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}