Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/app/api_levels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ impl App {
}
}
}
state.needs_render = true;
});
}

Expand Down Expand Up @@ -186,6 +187,7 @@ impl App {
api_mgmt.install_progress = Some(progress);
}
}
state.needs_render = true;
});
})
.await;
Expand All @@ -199,6 +201,7 @@ impl App {
eta_seconds: None,
});
}
state.needs_render = true;
}

tokio::time::sleep(API_INSTALLATION_COMPLETION_DELAY).await;
Expand All @@ -210,6 +213,7 @@ impl App {
api_mgmt.install_progress = None;
api_mgmt.error_message = Some(format!("Failed to install: {error}"));
}
state.needs_render = true;
} else {
let mut state = state_clone.lock().await;
if let Some(ref mut api_mgmt) = state.api_level_management {
Expand Down Expand Up @@ -248,6 +252,7 @@ impl App {
}
}
}
state.needs_render = true;
});
}
});
Expand Down Expand Up @@ -310,6 +315,7 @@ impl App {
"Failed to uninstall: {}",
last_error.unwrap_or_else(|| anyhow::anyhow!("Unknown error"))
));
state.needs_render = true;
}

{
Expand Down Expand Up @@ -337,6 +343,7 @@ impl App {
}
}
}
state.needs_render = true;
});
});
}
Expand Down
2 changes: 2 additions & 0 deletions src/app/background.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ impl App {
state.android_devices = android_devices;
state.is_loading = false;
state.mark_refreshed();
state.mark_dirty();

let should_update_details = state.active_panel == Panel::Android
&& !state.android_devices.is_empty()
Expand Down Expand Up @@ -140,6 +141,7 @@ impl App {
Ok(ios_devices) => {
let mut state = state_clone.lock().await;
state.ios_devices = ios_devices;
state.mark_dirty();

let should_update_details = state.active_panel == Panel::Ios
&& !state.ios_devices.is_empty()
Expand Down
2 changes: 2 additions & 0 deletions src/app/create_device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ impl App {
let mut state = state_clone.lock().await;
state.create_device_form.creation_status =
Some(format!("Creating device '{device_name_for_display}'..."));
state.needs_render = true;
}

let result = match active_panel {
Expand All @@ -335,6 +336,7 @@ impl App {
let mut state = state_clone.lock().await;
state.create_device_form.creation_status =
Some("Finalizing...".to_string());
state.needs_render = true;
}

match active_panel {
Expand Down
12 changes: 10 additions & 2 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ use crate::{
};
use anyhow::Result;
use crossterm::event::{self, Event as CrosstermEvent};
use crossterm::terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::sync::Arc;
use tokio::sync::Mutex;
Expand Down Expand Up @@ -204,10 +205,17 @@ impl App {
continue;
}

// Priority 2: Render UI after processing input for immediate visual feedback
// Priority 2: Render UI — only when state has changed or input was processed.
// BeginSynchronizedUpdate/EndSynchronizedUpdate (DEC PM 2026) prevents partial-frame
// flicker on supporting terminals by buffering all writes until EndSynchronizedUpdate.
{
let mut state = self.state.lock().await;
terminal.draw(|f| ui::render::draw_app(f, &mut state, &ui::Theme::dark()))?;
if events_processed > 0 || state.needs_render {
state.needs_render = false;
crossterm::queue!(terminal.backend_mut(), BeginSynchronizedUpdate)?;
terminal.draw(|f| ui::render::draw_app(f, &mut state, &ui::Theme::dark()))?;
crossterm::execute!(terminal.backend_mut(), EndSynchronizedUpdate)?;
}
}

// Priority 3: Handle background tasks (less frequently to avoid blocking input)
Expand Down
2 changes: 2 additions & 0 deletions src/app/refresh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ impl App {

state.is_loading = false;
state.mark_refreshed();
state.mark_dirty();

let need_detail_update = if let Some(ref started_name) = device_started {
match state.active_panel {
Expand Down Expand Up @@ -195,6 +196,7 @@ impl App {

state.is_loading = false;
state.mark_refreshed();
state.mark_dirty();

Ok(())
}
Expand Down
1 change: 1 addition & 0 deletions src/app/state/details.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ impl AppState {
details.device_path.is_some()
);
self.cached_device_details = Some(details);
self.needs_render = true;
}

/// Clears all cached device details.
Expand Down
6 changes: 6 additions & 0 deletions src/app/state/logs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ impl AppState {
let total_logs = self.device_logs.len();
self.log_scroll_offset = total_logs.saturating_sub(1);
}

self.needs_render = true;
}

/// Clears all device logs from memory.
Expand Down Expand Up @@ -159,6 +161,8 @@ impl AppState {
};
}
}

self.needs_render = true;
}
}

Expand All @@ -177,6 +181,8 @@ impl AppState {
};
}
}

self.needs_render = true;
}
}
}
16 changes: 16 additions & 0 deletions src/app/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ pub struct AppState {
pub ios_scroll_offset: usize,
/// API level management dialog state (when dialog is open)
pub api_level_management: Option<ApiLevelManagementState>,
/// Dirty flag: set by background tasks and state mutations, cleared after each render.
/// Prevents unconditional 125 fps redraws when nothing has changed.
pub needs_render: bool,
}

impl Default for AppState {
Expand Down Expand Up @@ -140,6 +143,7 @@ impl Default for AppState {
android_scroll_offset: 0,
ios_scroll_offset: 0,
api_level_management: None,
needs_render: true,
}
}
}
Expand All @@ -150,6 +154,12 @@ impl AppState {
Self::default()
}

/// Marks the UI as needing a redraw.
/// Call this from any background task that mutates state visible in the UI.
pub fn mark_dirty(&mut self) {
self.needs_render = true;
}

// --- Mode predicates ---

/// Returns true if the app is in normal mode.
Expand Down Expand Up @@ -224,6 +234,8 @@ impl AppState {
while self.notifications.len() > self.max_notifications {
self.notifications.pop_front();
}

self.needs_render = true;
}

/// Adds a success notification with green color.
Expand All @@ -248,7 +260,11 @@ impl AppState {

/// Removes notifications that have exceeded their auto-dismiss duration.
pub fn dismiss_expired_notifications(&mut self) {
let before = self.notifications.len();
self.notifications.retain(|n| !n.should_dismiss());
if self.notifications.len() != before {
self.needs_render = true;
}
}

/// Clears all notifications from the queue.
Expand Down
Loading