use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
use super::serial::SerialNumber;
use self::error::DeviceQueryError as Error;
pub const DEVICE_SERIAL_ENV: &str = "PLAYDATE_SERIAL_DEVICE";
#[derive(Clone)]
#[cfg_attr(feature = "clap", derive(clap::Parser))]
#[cfg_attr(feature = "clap", command(author, version, about, long_about = None, name = "device"))]
pub struct Query {
#[cfg_attr(unix, doc = "or '/dev/cu.usbmodemPDUN_XNNNNNN(N)'.")]
#[cfg_attr(windows, doc = "or 'COM{X}', where {X} is a number of port, e.g.: COM3.")]
#[cfg_attr(feature = "clap", arg(env = DEVICE_SERIAL_ENV, name = "device"))]
pub value: Option<Value>,
}
impl Default for Query {
fn default() -> Self {
Self { value: std::env::var(DEVICE_SERIAL_ENV).map(|s| Value::from_str(&s).ok())
.ok()
.flatten() }
}
}
impl std::fmt::Display for Query {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.value {
Some(ref value) => value.fmt(f),
None => write!(f, "None"),
}
}
}
impl std::fmt::Debug for Query {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.value.as_ref() {
Some(value) => f.debug_tuple("Query").field(&value.to_string()).finish(),
None => f.debug_tuple("Query").field(&None::<()>).finish(),
}
}
}
impl std::fmt::Display for Value {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Value::Serial(sn) => write!(f, "sn:{sn}"),
Value::Path(path) => write!(f, "serial:{}", path.display()),
Value::Com(port) => write!(f, "serial:COM{port}"),
}
}
}
#[derive(Clone, Debug)]
pub enum Value {
Serial(SerialNumber),
#[cfg_attr(not(unix), doc = "Use only on Unix.")]
Path(PathBuf),
#[cfg_attr(not(windows), doc = "Use only on Windows.")]
Com(u16),
}
impl FromStr for Value {
type Err = Error;
fn from_str(dev: &str) -> Result<Self, Self::Err> {
let name = dev.trim();
if name.is_empty() {
return Err(error::ParseError::from(name).into());
}
#[cfg(windows)]
match name.strip_prefix("COM").map(|s| s.parse::<u16>()) {
Some(Ok(com)) => return Ok(Value::Com(com)),
Some(Err(err)) => {
return Err(Error::invalid(format!("Invalid format, seems to COM port, but {err}.")));
},
None => { },
}
let serial = SerialNumber::try_from(name);
let path = Path::new(name);
let is_direct = path.is_absolute() && path.exists();
match serial {
Ok(serial) => {
if is_direct {
Ok(Value::Path(path.to_owned()))
} else {
Ok(Value::Serial(serial))
}
},
Err(err) => {
if is_direct {
Ok(Value::Path(path.to_owned()))
} else {
Err(err.into())
}
},
}
}
}
impl<'s> TryFrom<&'s str> for Value {
type Error = Error;
fn try_from(dev: &'s str) -> Result<Self, Self::Error> { Self::from_str(dev) }
}
impl Value {
pub fn to_value_string(&self) -> String {
match self {
Self::Serial(sn) => sn.to_string(),
Self::Path(p) => p.display().to_string(),
Self::Com(n) => format!("COM{n}"),
}
}
}
pub mod error {
use std::backtrace::Backtrace;
use std::str::FromStr;
use thiserror::Error;
use miette::Diagnostic;
pub type ParseError = <super::SerialNumber as FromStr>::Err;
#[derive(Error, Debug, Diagnostic)]
pub enum DeviceQueryError {
#[error(transparent)]
#[diagnostic(transparent)]
DeviceSerial {
#[backtrace]
#[from]
source: ParseError,
},
#[error("Invalid query format: {message}")]
Invalid {
message: String,
#[backtrace]
backtrace: Backtrace,
},
}
impl DeviceQueryError {
pub fn invalid(message: String) -> Self {
Self::Invalid { message,
backtrace: Backtrace::capture() }
}
}
}