use std::path::PathBuf;
use clap::{Args, Subcommand, ValueEnum};
use light_fw_cli_core::devtools::{
DEFAULT_PORT, DevtoolsRequest, parse_attr, save_png, send_request,
};
use serde_json::Value;
#[derive(Args, Debug)]
pub struct DevtoolsArgs {
#[command(subcommand)]
pub cmd: DevtoolsCmd,
}
#[derive(Subcommand, Debug)]
pub enum DevtoolsCmd {
Tree(Shell),
Layout(Shell),
Find(FindArgs),
Click(ClickArgs),
Type(TypeArgs),
Key(KeyArgs),
State(Shell),
Shot(ShotArgs),
Session(SessionArgs),
}
#[derive(Args, Debug)]
pub struct Shell {
#[arg(long, default_value_t = DEFAULT_PORT)]
port: u16,
#[arg(long, default_value_t = false)]
json: bool,
}
#[derive(Args, Debug)]
pub struct FindArgs {
#[arg(long, default_value_t = DEFAULT_PORT)]
port: u16,
#[arg(long, default_value_t = false)]
json: bool,
#[arg(long)]
tag: Option<String>,
#[arg(long)]
text: Option<String>,
#[arg(long, num_args(0..))]
attr: Vec<String>,
}
#[derive(Args, Debug)]
pub struct ClickArgs {
#[arg(long, default_value_t = DEFAULT_PORT)]
port: u16,
#[arg(long, default_value_t = false)]
json: bool,
#[arg(long, required = true)]
key: String,
}
#[derive(Args, Debug)]
pub struct TypeArgs {
#[arg(long, default_value_t = DEFAULT_PORT)]
port: u16,
#[arg(long, default_value_t = false)]
json: bool,
#[arg(long, required = true)]
key: String,
#[arg(required = true)]
text: Vec<String>,
}
#[derive(Args, Debug)]
pub struct KeyArgs {
#[arg(long, default_value_t = DEFAULT_PORT)]
port: u16,
#[arg(long, default_value_t = false)]
json: bool,
#[arg(long, required = true)]
key: String,
#[arg(required = true, value_enum)]
named: NamedKey,
}
#[derive(Clone, Copy, ValueEnum, Debug)]
enum NamedKey {
Enter,
Tab,
Escape,
}
#[derive(Args, Debug)]
pub struct ShotArgs {
#[arg(long, default_value_t = DEFAULT_PORT)]
port: u16,
#[arg(long, default_value_t = false)]
json: bool,
#[arg(long)]
path: Option<PathBuf>,
}
#[derive(Args, Debug)]
pub struct SessionArgs {
#[arg(value_enum)]
action: SessionAction,
}
#[derive(Clone, Copy, ValueEnum, Debug)]
enum SessionAction {
Start,
Stop,
}
pub fn run(args: DevtoolsArgs) -> anyhow::Result<i32> {
let cwd = std::env::current_dir()?;
if let DevtoolsCmd::Session(s) = &args.cmd {
match s.action {
SessionAction::Start => {
let id = light_fw_cli_core::session::start_session(&cwd)?;
println!("Session started: .light/agent-sessions/{id}");
}
SessionAction::Stop => {
let cur = light_fw_cli_core::session::active_session_id(&cwd)
.ok_or_else(|| anyhow::anyhow!("no active session"))?;
let summary = light_fw_cli_core::session::stop_session(&cwd, &cur)?;
println!(
"Session stopped. Summary: {:?}",
summary.strip_prefix(&cwd)?
);
}
}
return Ok(0);
}
let (port, json, req) = build_request(&args.cmd)?;
let raw = match send_request(port, &req) {
Ok(r) => r,
Err(e) => {
log_error(&cwd, &req, &e.to_string())?;
return Err(anyhow::Error::from(e));
}
};
if req.cmd == "shot" {
if !light_fw_cli_core::devtools::is_screenshot_response(&raw) {
log_action(&cwd, &req, &raw)?;
eprintln!("{}", light_fw_cli_core::devtools::shot_error_message(&raw));
return Ok(1);
}
if let Some(rel) = light_fw_cli_core::session::save_screenshot(&cwd, &raw, "shot")? {
log_shot_with_screenshot(&cwd, &rel, &raw)?;
} else {
log_action(&cwd, &req, &raw)?;
}
if let Some(path) = &req.save_path {
let p = save_png(&raw, path)?;
println!("{}", p.display());
} else {
let b64 = raw.strip_prefix("base64:").unwrap_or(&raw);
print!("{b64}");
use std::io::Write;
let _ = std::io::stdout().flush();
}
return Ok(0);
}
log_action(&cwd, &req, &raw)?;
print_result(&raw, json);
Ok(0)
}
fn build_request(cmd: &DevtoolsCmd) -> anyhow::Result<(u16, bool, DevtoolsRequest)> {
Ok(match cmd {
DevtoolsCmd::Tree(s) => (s.port, s.json, simple("tree")),
DevtoolsCmd::Layout(s) => (s.port, s.json, simple("layout")),
DevtoolsCmd::State(s) => (
s.port,
s.json,
DevtoolsRequest {
cmd: "state".into(),
body: None,
save_path: None,
json: s.json,
},
),
DevtoolsCmd::Find(f) => {
let mut obj = serde_json::Map::new();
if let Some(t) = &f.tag {
obj.insert("tag".into(), Value::String(t.clone()));
}
if let Some(t) = &f.text {
obj.insert("text".into(), Value::String(t.clone()));
}
if !f.attr.is_empty() {
let mut attrs = serde_json::Map::new();
for a in &f.attr {
let (k, v) = parse_attr(a)?;
attrs.insert(k, Value::String(v));
}
obj.insert("attr".into(), Value::Object(attrs));
}
(
f.port,
f.json,
DevtoolsRequest {
cmd: "find".into(),
body: Some(Value::Object(obj).to_string()),
save_path: None,
json: f.json,
},
)
}
DevtoolsCmd::Click(c) => (
c.port,
c.json,
DevtoolsRequest {
cmd: "click".into(),
body: Some(format!("click {}", c.key)),
save_path: None,
json: c.json,
},
),
DevtoolsCmd::Type(t) => {
let text = t.text.join(" ");
(
t.port,
t.json,
DevtoolsRequest {
cmd: "type".into(),
body: Some(format!("type {} {text}", t.key)),
save_path: None,
json: t.json,
},
)
}
DevtoolsCmd::Key(k) => {
let name = match k.named {
NamedKey::Enter => "Enter",
NamedKey::Tab => "Tab",
NamedKey::Escape => "Escape",
};
(
k.port,
k.json,
DevtoolsRequest {
cmd: "key".into(),
body: Some(format!("key {} {name}", k.key)),
save_path: None,
json: k.json,
},
)
}
DevtoolsCmd::Shot(s) => (
s.port,
s.json,
DevtoolsRequest {
cmd: "shot".into(),
body: None,
save_path: s.path.clone(),
json: s.json,
},
),
DevtoolsCmd::Session(_) => unreachable!("handled earlier"),
})
}
fn simple(cmd: &str) -> DevtoolsRequest {
DevtoolsRequest {
cmd: cmd.to_owned(),
body: None,
save_path: None,
json: false,
}
}
fn print_result(raw: &str, json: bool) {
if json {
println!("{raw}");
return;
}
match raw {
"ok" | "null" | "timeout" => println!("{raw}"),
s if s.starts_with("error") => eprintln!("{s}"),
s => {
if let Ok(v) = serde_json::from_str::<Value>(s) {
match serde_json::to_string_pretty(&v) {
Ok(p) => println!("{p}"),
Err(_) => println!("{s}"),
}
} else {
println!("{s}");
}
}
}
}
fn log_action(cwd: &std::path::Path, req: &DevtoolsRequest, raw: &str) -> anyhow::Result<()> {
use light_fw_cli_core::session;
let Some(_id) = session::active_session_id(cwd) else {
return Ok(());
};
let args_val = match req.body.as_deref() {
None => Value::Null,
Some(b) if req.cmd == "find" => {
serde_json::from_str::<Value>(b).unwrap_or(Value::String(b.to_owned()))
}
Some(b) => Value::String(b.to_owned()),
};
let result_val = if raw.starts_with("base64:") {
Value::String("<png>".into())
} else {
serde_json::from_str::<Value>(raw).unwrap_or(Value::String(raw.to_owned()))
};
session::record_action(cwd, &req.cmd, &args_val, &result_val, None)?;
Ok(())
}
fn log_shot_with_screenshot(cwd: &std::path::Path, rel: &str, raw: &str) -> anyhow::Result<()> {
use light_fw_cli_core::session;
let result_val = Value::String("<png>".into());
session::record_action(cwd, "shot", &Value::Null, &result_val, Some(rel))?;
let _ = raw;
Ok(())
}
fn log_error(cwd: &std::path::Path, req: &DevtoolsRequest, err: &str) -> anyhow::Result<()> {
use light_fw_cli_core::session;
let args_val = match req.body.as_deref() {
None => Value::Null,
Some(b) if req.cmd == "find" => {
serde_json::from_str::<Value>(b).unwrap_or(Value::String(b.to_owned()))
}
Some(b) => Value::String(b.to_owned()),
};
let result_val = Value::String(format!("error: {err}"));
session::record_action(cwd, &req.cmd, &args_val, &result_val, None)?;
Ok(())
}