use crate::app::App;
pub mod adb;
pub mod app;
pub mod devtools;
pub mod effects;
pub mod event;
pub mod fastboot;
pub mod logcat;
pub mod menu;
pub mod message;
pub mod model;
pub mod theme;
pub mod update;
pub mod view;
fn print_usage() {
eprintln!("Usage: droidtui [OPTIONS]");
eprintln!();
eprintln!("Options:");
eprintln!(" --query Stream logcat as JSON lines to stdout (no TUI)");
eprintln!(" --query --last N Dump the last N logcat lines as JSON and exit");
eprintln!(" --query --level L Filter by minimum level: V, D, I, W, E, F");
eprintln!(" --query --tag TAG Filter by tag substring");
eprintln!(" --query --grep PAT Filter by message substring");
eprintln!(" --help Show this help message");
eprintln!();
eprintln!("Examples:");
eprintln!(" droidtui # Launch the TUI");
eprintln!(" droidtui --query # Stream live logcat as JSONL");
eprintln!(" droidtui --query --last 500 # Dump last 500 lines as JSONL");
eprintln!(" droidtui --query --level E # Stream only errors");
eprintln!(" droidtui --query --tag MyApp # Stream only tag matching 'MyApp'");
eprintln!(
" droidtui --query | nu -c 'lines | each {{ from json }} | where level == \"Error\"'"
);
}
fn run_query_mode(args: &[String]) -> color_eyre::Result<()> {
use adb_client::ADBServer;
use std::io::Write;
use std::net::{Ipv4Addr, SocketAddrV4};
use std::sync::mpsc;
let mut last_n: Option<usize> = None;
let mut min_level: Option<logcat::LogLevel> = None;
let mut tag_filter: Option<String> = None;
let mut grep_filter: Option<String> = None;
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--last" => {
i += 1;
if i < args.len() {
last_n = args[i].parse::<usize>().ok();
}
}
"--level" => {
i += 1;
if i < args.len() {
let ch = args[i].chars().next().unwrap_or('V');
min_level = Some(logcat::LogLevel::from_char(ch.to_ascii_uppercase()));
}
}
"--tag" => {
i += 1;
if i < args.len() {
tag_filter = Some(args[i].clone());
}
}
"--grep" => {
i += 1;
if i < args.len() {
grep_filter = Some(args[i].clone());
}
}
_ => {}
}
i += 1;
}
let min_order = min_level.map(|l| l.order()).unwrap_or(0);
let tag_lower = tag_filter.map(|t| t.to_lowercase());
let grep_lower = grep_filter.map(|g| g.to_lowercase());
let addr = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 5037);
let mut server = ADBServer::new(addr);
let devices = server
.devices()
.map_err(|e| color_eyre::eyre::eyre!("Failed to list devices (is adb running?): {}", e))?;
if devices.is_empty() {
return Err(color_eyre::eyre::eyre!(
"No devices connected. Connect a device and try again."
));
}
let serial = devices[0].identifier.clone();
eprintln!("droidtui: streaming logcat from {} as JSON lines…", serial);
if let Some(ref level) = min_level {
eprintln!(" filter: level >= {}", level.as_char());
}
if let Some(ref tag) = tag_lower {
eprintln!(" filter: tag contains \"{}\"", tag);
}
if let Some(ref pat) = grep_lower {
eprintln!(" filter: message contains \"{}\"", pat);
}
if let Some(n) = last_n {
eprintln!(" mode: last {} lines", n);
} else {
eprintln!(" mode: live stream (Ctrl+C to stop)");
}
let (tx, rx) = mpsc::sync_channel::<String>(10_000);
let serial_clone = serial.clone();
std::thread::spawn(move || {
let device_addr = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 5037);
let mut device = adb_client::ADBServerDevice::new(serial_clone, Some(device_addr));
let writer = logcat::ChannelWriter::new(tx.clone());
if let Err(e) = device.get_logs(writer) {
let _ = tx.send(format!("--- LOGCAT ERROR: {} ---", e));
}
});
let stdout = std::io::stdout();
let mut out = std::io::BufWriter::new(stdout.lock());
let mut count: usize = 0;
let running = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
let r = running.clone();
ctrlc_handler(r);
loop {
if !running.load(std::sync::atomic::Ordering::Relaxed) {
break;
}
match rx.recv_timeout(std::time::Duration::from_millis(100)) {
Ok(line) => {
let entry = logcat::LogEntry::parse(&line);
if entry.level.order() < min_order {
continue;
}
if let Some(ref tag) = tag_lower {
match &entry.tag {
Some(t) if t.to_lowercase().contains(tag) => {}
_ => continue,
}
}
if let Some(ref pat) = grep_lower {
if !entry.message.to_lowercase().contains(pat) {
continue;
}
}
if serde_json::to_writer(&mut out, &entry).is_ok() {
let _ = writeln!(out);
let _ = out.flush();
}
count += 1;
if let Some(n) = last_n {
if count >= n {
break;
}
}
}
Err(mpsc::RecvTimeoutError::Timeout) => continue,
Err(mpsc::RecvTimeoutError::Disconnected) => break,
}
}
eprintln!("droidtui: {} lines written", count);
Ok(())
}
fn ctrlc_handler(_flag: std::sync::Arc<std::sync::atomic::AtomicBool>) {
}
#[tokio::main]
async fn main() -> color_eyre::Result<()> {
let args: Vec<String> = std::env::args().collect();
if args.iter().any(|a| a == "--help" || a == "-h") {
print_usage();
return Ok(());
}
if args.iter().any(|a| a == "--query") {
color_eyre::install()?;
return run_query_mode(&args[1..]);
}
color_eyre::install()?;
let terminal = ratatui::init();
let result = App::new().run(terminal).await;
ratatui::restore();
result
}