use crate::adb::AdbCommand;
use crate::message::{CommandResult, Message};
use crate::model::{AppState, Model};
use crate::stream::{start_stream, StreamConfig};
pub async fn update(model: &mut Model, message: Message) {
match message {
Message::MenuUp => {
model.menu.previous();
}
Message::MenuDown => {
model.menu.next();
}
Message::EnterChild => {
model.menu.enter_child_mode();
model.effects.start_fade_in();
model.effects.start_slide_in();
}
Message::ExitChild => {
model.menu.exit_child_mode();
}
Message::ExecuteCommand(command) => {
if let AdbCommand::Shell { command: cmd } = &command {
if cmd.starts_with("STREAM") {
let config = match cmd.as_str() {
"STREAM_HD" => StreamConfig {
width: 1080,
height: 1920,
bitrate: "12M".to_string(),
},
"STREAM_FAST" => StreamConfig {
width: 720,
height: 1280,
bitrate: "4M".to_string(),
},
_ => StreamConfig::default(),
};
match start_stream(config) {
Ok(stream_state) => {
model.stream_state = Some(stream_state);
model.set_result("Screen streaming started in separate window.\nClose the window or press Q in it to stop.".to_string());
model.state = AppState::ShowResult;
}
Err(e) => {
model.set_error(format!("Failed to start streaming: {}\n\nMake sure:\n- ADB is installed and in PATH\n- Device is connected\n- FFmpeg is installed", e));
model.state = AppState::ShowResult;
}
}
model.effects.start_slide_in();
return;
}
}
model.state = AppState::Loading;
model.clear_results();
model.loading_counter = 0;
model.effects.reset_slide();
let result = execute_adb_command(model, command).await;
match result {
CommandResult::Success(output) => {
model.set_result(output);
}
CommandResult::Error(error) => {
model.set_error(error);
}
}
model.state = AppState::ShowResult;
model.effects.start_slide_in();
}
Message::CommandStarted => {
model.state = AppState::Loading;
model.loading_counter = 0;
}
Message::CommandCompleted(result) => {
match result {
CommandResult::Success(output) => {
model.set_result(output);
}
CommandResult::Error(error) => {
model.set_error(error);
}
}
model.state = AppState::ShowResult;
model.effects.start_slide_in();
}
Message::ScrollUp => {
if model.scroll_position > 0 {
model.scroll_position -= 1;
}
}
Message::ScrollDown => {
if model.scroll_position + 1 < model.total_result_lines() {
model.scroll_position += 1;
}
}
Message::ScrollPageUp => {
model.scroll_position = model.scroll_position.saturating_sub(10);
}
Message::ScrollPageDown => {
let max_scroll = model.total_result_lines().saturating_sub(1);
model.scroll_position = (model.scroll_position + 10).min(max_scroll);
}
Message::ScrollToTop => {
model.scroll_position = 0;
}
Message::ScrollToBottom => {
model.scroll_position = model.total_result_lines().saturating_sub(1);
}
Message::StartStream => {
let config = StreamConfig::default();
match start_stream(config) {
Ok(stream_state) => {
model.stream_state = Some(stream_state);
model.set_result("Screen streaming started in separate window.".to_string());
model.state = AppState::ShowResult;
}
Err(e) => {
model.set_error(format!("Failed to start streaming: {}", e));
model.state = AppState::ShowResult;
}
}
model.effects.start_slide_in();
}
Message::StopStream => {
if let Some(mut stream) = model.stream_state.take() {
stream.stop();
}
model.state = AppState::Menu;
}
Message::UpdateFrame(_frame) => {
}
Message::TogglePause => {
}
Message::IncreaseRefreshRate => {
}
Message::DecreaseRefreshRate => {
}
Message::Tick => {
tick(model).await;
}
Message::Quit => {
model.running = false;
}
Message::ReturnToMenu => {
model.state = AppState::Menu;
model.clear_results();
}
Message::SkipStartup => {
model.state = AppState::Menu;
model.effects.start_slide_in();
}
}
}
async fn tick(model: &mut Model) {
let now = std::time::Instant::now();
let elapsed = now.duration_since(model.last_tick);
model.last_tick = now;
model.effects.tick(elapsed);
model.menu.tick();
if model.state == AppState::Loading {
model.loading_counter += 1;
}
if model.state == AppState::ShowResult {
model.reveal_counter += 1;
}
if model.state == AppState::Startup && model.effects.is_startup_complete() {
model.state = AppState::Menu;
}
}
async fn execute_adb_command(model: &mut Model, command: AdbCommand) -> CommandResult {
match model.adb_manager.execute(command) {
Ok(output) => CommandResult::Success(output),
Err(e) => {
let error_msg = format!("{}", e);
let enhanced_error = if error_msg.contains("Connection")
|| error_msg.contains("connection")
{
format!(
"{}\n\nTroubleshooting:\n• Make sure ADB server is running (adb start-server)\n• Check that device is connected (adb devices)\n• Verify USB debugging is enabled on device",
error_msg
)
} else if error_msg.contains("No device selected") {
format!(
"{}\n\nPlease:\n• Connect an Android device via USB\n• Enable USB debugging on the device\n• Run 'List Devices' to detect your device",
error_msg
)
} else {
error_msg
};
CommandResult::Error(enhanced_error)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_menu_navigation() {
let mut model = Model::new();
model.state = AppState::Menu;
update(&mut model, Message::MenuDown).await;
assert_eq!(model.menu.selected, 1);
update(&mut model, Message::MenuUp).await;
assert_eq!(model.menu.selected, 0);
}
#[tokio::test]
async fn test_quit() {
let mut model = Model::new();
assert!(model.running);
update(&mut model, Message::Quit).await;
assert!(!model.running);
}
#[tokio::test]
async fn test_scroll_boundaries() {
let mut model = Model::new();
model.wrapped_lines = vec!["Line 1".to_string(), "Line 2".to_string()];
model.scroll_position = 0;
update(&mut model, Message::ScrollUp).await;
assert_eq!(model.scroll_position, 0);
update(&mut model, Message::ScrollDown).await;
assert_eq!(model.scroll_position, 1);
update(&mut model, Message::ScrollDown).await;
assert_eq!(model.scroll_position, 1);
}
#[tokio::test]
async fn test_enter_exit_child_mode() {
let mut model = Model::new();
model.state = AppState::Menu;
update(&mut model, Message::EnterChild).await;
assert!(model.menu.is_in_child_mode());
update(&mut model, Message::ExitChild).await;
assert!(!model.menu.is_in_child_mode());
}
#[tokio::test]
async fn test_clear_results() {
let mut model = Model::new();
model.set_result("Test output".to_string());
assert!(model.command_result.is_some());
assert!(!model.result_lines.is_empty());
update(&mut model, Message::ReturnToMenu).await;
assert!(model.command_result.is_none());
assert!(model.result_lines.is_empty());
assert_eq!(model.state, AppState::Menu);
}
}