refactor: clean up daemon and CLI duplication

Reduce repeated adapter dispatch, CLI action rendering, and config save
flows while keeping the current Roku behavior and docs aligned with the
known secret-menu limitations.
This commit is contained in:
44r0n7
2026-04-15 15:25:49 -04:00
parent 0095462216
commit 45620b1ab5
9 changed files with 802 additions and 237 deletions
+65 -46
View File
@@ -7,7 +7,10 @@ use tokio::fs;
use uuid::Uuid;
use crate::{
adapters::{AppInfo, Device, DeviceInfo, DeviceState, TvAdapter, TvKey, roku::RokuAdapter},
adapters::{
AppInfo, Device, DeviceInfo, DeviceState, TvAdapter, TvKey,
roku::{RokuAdapter, RokuKeyMode},
},
daemon::config::TvctlConfig,
};
@@ -196,8 +199,12 @@ impl AdapterRegistry {
(!config.dev.roku_username.is_empty()).then(|| config.dev.roku_username.clone());
let password =
(!config.dev.roku_password.is_empty()).then(|| config.dev.roku_password.clone());
let key_mode = RokuKeyMode::from_config(
&config.remote.roku_key_mode,
config.remote.roku_press_duration_ms,
);
Self {
roku: RokuAdapter::with_dev_credentials(username, password),
roku: RokuAdapter::with_config(username, password, key_mode),
}
}
@@ -208,10 +215,8 @@ impl AdapterRegistry {
/// Discover candidate devices for one platform.
pub async fn discover(&self, platform: &str) -> anyhow::Result<Vec<DeviceInfo>> {
match platform {
"roku" => Ok(self.roku.discover().await?),
other => anyhow::bail!("unsupported platform '{other}'"),
}
self.with_platform(platform, |roku| Box::pin(roku.discover()))
.await
}
/// Return true when a platform is supported.
@@ -226,83 +231,97 @@ impl AdapterRegistry {
address: IpAddr,
port: Option<u16>,
) -> anyhow::Result<DeviceInfo> {
match platform {
"roku" => Ok(self
.roku
.probe_device(address, port.unwrap_or(8060))
.await?),
other => anyhow::bail!("unsupported platform '{other}'"),
}
self.with_platform(platform, |roku| {
Box::pin(roku.probe_device(address, port.unwrap_or(8060)))
})
.await
}
/// Return apps from a concrete device using its platform adapter.
pub async fn list_apps(&self, device: &Device) -> anyhow::Result<Vec<AppInfo>> {
match device.platform.as_str() {
"roku" => Ok(self.roku.list_apps(device).await?),
other => anyhow::bail!("unsupported platform '{other}'"),
}
self.with_device(device, |roku, device| Box::pin(roku.list_apps(device)))
.await
}
/// Fetch the current state for a concrete device.
pub async fn state(&self, device: &Device) -> anyhow::Result<DeviceState> {
match device.platform.as_str() {
"roku" => Ok(self.roku.state(device).await?),
other => anyhow::bail!("unsupported platform '{other}'"),
}
self.with_device(device, |roku, device| Box::pin(roku.state(device)))
.await
}
/// Launch an app on a concrete device.
pub async fn launch(&self, device: &Device, app: &str) -> anyhow::Result<()> {
match device.platform.as_str() {
"roku" => Ok(self.roku.launch(device, app).await?),
other => anyhow::bail!("unsupported platform '{other}'"),
}
let app = app.to_string();
self.with_device(device, move |roku, device| {
Box::pin(async move { roku.launch(device, &app).await })
})
.await
}
/// Stop the currently running app on a concrete device.
pub async fn stop_app(&self, device: &Device) -> anyhow::Result<()> {
match device.platform.as_str() {
"roku" => Ok(self.roku.stop_app(device).await?),
other => anyhow::bail!("unsupported platform '{other}'"),
}
self.with_device(device, |roku, device| Box::pin(roku.stop_app(device)))
.await
}
/// Send a single normalized key to a concrete device.
pub async fn key(&self, device: &Device, key: TvKey) -> anyhow::Result<()> {
match device.platform.as_str() {
"roku" => Ok(self.roku.key(device, key).await?),
other => anyhow::bail!("unsupported platform '{other}'"),
}
self.with_device(device, |roku, device| Box::pin(roku.key(device, key)))
.await
}
/// Send a normalized key sequence to a concrete device.
pub async fn sequence(&self, device: &Device, keys: Vec<TvKey>) -> anyhow::Result<()> {
match device.platform.as_str() {
"roku" => Ok(self.roku.sequence(device, keys).await?),
other => anyhow::bail!("unsupported platform '{other}'"),
}
self.with_device(device, |roku, device| Box::pin(roku.sequence(device, keys)))
.await
}
/// Install a development package on a concrete device.
pub async fn dev_install(&self, device: &Device, zip: &[u8]) -> anyhow::Result<()> {
match device.platform.as_str() {
"roku" => Ok(self.roku.dev_install(device, zip).await?),
other => anyhow::bail!("unsupported platform '{other}'"),
}
let zip = zip.to_vec();
self.with_device(device, move |roku, device| {
Box::pin(async move { roku.dev_install(device, &zip).await })
})
.await
}
/// Reload the active development package on a concrete device.
pub async fn dev_reload(&self, device: &Device) -> anyhow::Result<()> {
match device.platform.as_str() {
"roku" => Ok(self.roku.dev_reload(device).await?),
other => anyhow::bail!("unsupported platform '{other}'"),
}
self.with_device(device, |roku, device| Box::pin(roku.dev_reload(device)))
.await
}
/// Fetch development logs from a concrete device.
pub async fn dev_logs(&self, device: &Device) -> anyhow::Result<Vec<String>> {
self.with_device(device, |roku, device| Box::pin(roku.dev_logs(device)))
.await
}
async fn with_platform<T, F>(&self, platform: &str, call: F) -> anyhow::Result<T>
where
F: for<'a> FnOnce(
&'a RokuAdapter,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = crate::adapters::Result<T>> + 'a>,
>,
{
match platform {
"roku" => Ok(call(&self.roku).await?),
other => anyhow::bail!("unsupported platform '{other}'"),
}
}
async fn with_device<T, F>(&self, device: &Device, call: F) -> anyhow::Result<T>
where
F: for<'a> FnOnce(
&'a RokuAdapter,
&'a Device,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = crate::adapters::Result<T>> + 'a>,
>,
{
match device.platform.as_str() {
"roku" => Ok(self.roku.dev_logs(device).await?),
"roku" => Ok(call(&self.roku, device).await?),
other => anyhow::bail!("unsupported platform '{other}'"),
}
}