refactor: remove remote sequence CLI command

Collapse remote input to `tvctl remote key` so single-key and multi-key
usage share one public command while keeping the paced daemon path
underneath.
This commit is contained in:
44r0n7
2026-04-15 13:46:25 -04:00
parent 348c2bc9bc
commit 0095462216
5 changed files with 56 additions and 43 deletions
+1 -2
View File
@@ -221,8 +221,7 @@ tvctl
│ ├── stop │ ├── stop
│ └── refresh │ └── refresh
├── remote ├── remote
── key <key> ── key <key> [key...]
│ └── sequence <key> [key...]
├── state ├── state
├── dev ├── dev
│ ├── install <zip> │ ├── install <zip>
+1 -2
View File
@@ -171,8 +171,7 @@ tvctl app refresh Refresh app cache from TV
Send input to the TV. Send input to the TV.
``` ```
tvctl remote key <key> Send a single keypress tvctl remote key <key> [key...] Send one or more keypresses
tvctl remote sequence <key> [key...] Send a sequence of keypresses
``` ```
**Available keys:** **Available keys:**
+41 -39
View File
@@ -176,14 +176,9 @@ pub enum AppCommand {
pub enum RemoteCommand { pub enum RemoteCommand {
/// Send a single normalized key. /// Send a single normalized key.
Key { Key {
/// Key name such as `home`, `down`, or `literal:abc`. /// One or more key names such as `home`, `down`, or `literal:abc`.
key: String,
},
/// Send multiple normalized keys in order.
Sequence {
/// Key names such as `home down select`.
keys: Vec<String>, keys: Vec<String>,
/// Delay between keys in milliseconds. /// Delay between keys in milliseconds when sending more than one key.
#[arg(long, default_value_t = DEFAULT_REMOTE_SEQUENCE_DELAY_MS)] #[arg(long, default_value_t = DEFAULT_REMOTE_SEQUENCE_DELAY_MS)]
delay_ms: u64, delay_ms: u64,
}, },
@@ -442,40 +437,41 @@ async fn handle_app_command(cli: &Cli, command: AppCommand) -> Result<(), CliErr
async fn handle_remote_command(cli: &Cli, command: RemoteCommand) -> Result<(), CliError> { async fn handle_remote_command(cli: &Cli, command: RemoteCommand) -> Result<(), CliError> {
match command { match command {
RemoteCommand::Key { key } => { RemoteCommand::Key { keys, delay_ms } => {
let response = send_request(
load_socket_path().await?,
&DaemonRequest::SendKey {
device: cli.device.clone(),
key: parse_tv_key(&key)?,
},
)
.await?;
let result: ActionResult = parse_response_data(response)?;
render(cli, &result, || result.detail.clone())
}
RemoteCommand::Sequence { keys, delay_ms } => {
if keys.is_empty() { if keys.is_empty() {
return Err(CliError::new( return Err(CliError::new(
"At least one key is required for `tvctl remote sequence`.", "At least one key is required for `tvctl remote key`.",
"Pass one or more keys such as `home down select`.", "Pass one or more keys such as `home` or `home down select`.",
)); ));
} }
let parsed = keys if keys.len() == 1 {
.iter() let response = send_request(
.map(|key| parse_tv_key(key)) load_socket_path().await?,
.collect::<Result<Vec<_>, _>>()?; &DaemonRequest::SendKey {
let response = send_request( device: cli.device.clone(),
load_socket_path().await?, key: parse_tv_key(&keys[0])?,
&DaemonRequest::SendSequence { },
device: cli.device.clone(), )
keys: parsed, .await?;
delay_ms, let result: ActionResult = parse_response_data(response)?;
}, render(cli, &result, || result.detail.clone())
) } else {
.await?; let parsed = keys
let result: ActionResult = parse_response_data(response)?; .iter()
render(cli, &result, || result.detail.clone()) .map(|key| parse_tv_key(key))
.collect::<Result<Vec<_>, _>>()?;
let response = send_request(
load_socket_path().await?,
&DaemonRequest::SendSequence {
device: cli.device.clone(),
keys: parsed,
delay_ms,
},
)
.await?;
let result: ActionResult = parse_response_data(response)?;
render(cli, &result, || result.detail.clone())
}
} }
} }
} }
@@ -674,9 +670,15 @@ async fn daemon_stop(cli: &Cli) -> Result<(), CliError> {
async fn daemon_status(cli: &Cli) -> Result<(), CliError> { async fn daemon_status(cli: &Cli) -> Result<(), CliError> {
if let Some(status) = daemon_status_payload().await { if let Some(status) = daemon_status_payload().await {
return render(cli, &status, || { return render(cli, &status, || {
let http = if status.http_enabled {
format!("{}:{}", status.http_host, status.http_port)
} else {
"disabled".to_string()
};
let default_device = status.default_device.as_deref().unwrap_or("none");
format!( format!(
"tvctld is running on {} with {} known device(s).", "tvctld is running.\nPID: {}\nSocket: {}\nHTTP: {}\nKnown Devices: {}\nDefault Device: {}",
status.socket, status.device_count status.pid, status.socket, http, status.device_count, default_device
) )
}); });
} }
+7
View File
@@ -169,8 +169,15 @@ pub struct DaemonStatus {
pub socket: String, pub socket: String,
/// Whether the HTTP API is enabled. /// Whether the HTTP API is enabled.
pub http_enabled: bool, pub http_enabled: bool,
/// The HTTP bind host.
pub http_host: String,
/// The HTTP bind port.
pub http_port: u16,
/// The number of known devices. /// The number of known devices.
pub device_count: usize, pub device_count: usize,
/// The current default device, if configured.
#[serde(skip_serializing_if = "Option::is_none")]
pub default_device: Option<String>,
} }
/// Discovery results returned by the daemon. /// Discovery results returned by the daemon.
+6
View File
@@ -218,7 +218,13 @@ async fn handle_request(
pid: std::process::id(), pid: std::process::id(),
socket: guard.paths.socket_file.display().to_string(), socket: guard.paths.socket_file.display().to_string(),
http_enabled: guard.config.daemon.http_enabled, http_enabled: guard.config.daemon.http_enabled,
http_host: guard.config.daemon.http_host.clone(),
http_port: guard.config.daemon.http_port,
device_count: guard.registry.devices.len(), device_count: guard.registry.devices.len(),
default_device: guard
.registry
.default_device()
.map(|device| device.name.clone()),
}), }),
false, false,
) )