fix: Only re-draw the screen if data/layout has changed

This commit is contained in:
Jack Wills
2025-02-21 16:40:34 +00:00
parent 53625e67cb
commit bfc295c50e
9 changed files with 184 additions and 49 deletions
+55 -3
View File
@@ -13,7 +13,7 @@ mod container_state;
use crate::{
app_error::AppError,
config::Config,
ui::{log_sanitizer, GuiState, Status},
ui::{log_sanitizer, GuiState, Redraw, Status},
ENTRY_POINT,
};
pub use container_state::*;
@@ -122,7 +122,9 @@ pub struct AppData {
error: Option<AppError>,
filter: Filter,
hidden_containers: Vec<ContainerItem>,
redraw: Arc<Redraw>,
sorted_by: Option<(Header, SortedOrder)>,
current_sorted_id: Vec<ContainerId>,
pub config: Config,
}
@@ -134,18 +136,22 @@ pub struct AppData {
pub error: Option<AppError>,
pub filter: Filter,
pub hidden_containers: Vec<ContainerItem>,
pub current_sorted_id: Vec<ContainerId>,
pub redraw: Arc<Redraw>,
pub sorted_by: Option<(Header, SortedOrder)>,
}
impl AppData {
/// Generate a default app_state
pub fn default(config: Config) -> Self {
pub fn new(config: Config, redraw: &Arc<Redraw>) -> Self {
Self {
config,
containers: StatefulList::new(vec![]),
current_sorted_id: vec![],
error: None,
filter: Filter::new(),
hidden_containers: vec![],
redraw: Arc::clone(redraw),
sorted_by: None,
}
}
@@ -186,6 +192,7 @@ impl AppData {
/// sets the state to start if any filtering has occurred
/// Also search in the "hidden" vec for items and insert back into the main containers vec
fn filter_containers(&mut self) {
self.redraw.set_true();
let pre_len = self.get_container_len();
if !self.hidden_containers.is_empty() {
@@ -289,6 +296,7 @@ impl AppData {
/// Remove the sorted header & order, and sort by default - created datetime
pub fn reset_sorted(&mut self) {
self.set_sorted(None);
self.redraw.set_true();
}
/// Sort containers based on a given header, if headings match, and already ascending, remove sorting
@@ -309,10 +317,19 @@ impl AppData {
self.sorted_by
}
/// Get a vec of the containers ID's
fn get_current_ids(&self) -> Vec<ContainerId> {
self.containers
.items
.iter()
.map(|i| i.id.clone())
.collect::<Vec<_>>()
}
/// Sort the containers vec, based on a heading (and if clash, then by name), either ascending or descending,
/// If not sort set, then sort by created time
pub fn sort_containers(&mut self) {
if let Some((head, ord)) = self.sorted_by {
let pre_order = self.get_current_ids();
let sort_closure = |a: &ContainerItem, b: &ContainerItem| -> std::cmp::Ordering {
let item_ord = match ord {
SortedOrder::Asc => (a, b),
@@ -372,13 +389,19 @@ impl AppData {
.then_with(|| item_ord.0.id.cmp(&item_ord.1.id)),
}
};
self.containers.items.sort_by(sort_closure);
} else {
if pre_order != self.get_current_ids() {
self.redraw.set_true();
}
} else if self.current_sorted_id != self.get_current_ids() {
self.containers.items.sort_by(|a, b| {
a.created
.cmp(&b.created)
.then_with(|| a.name.get().cmp(b.name.get()))
});
self.redraw.set_true();
self.current_sorted_id = self.get_current_ids();
}
}
@@ -414,21 +437,25 @@ impl AppData {
/// Select the first container
pub fn containers_start(&mut self) {
self.containers.start();
self.redraw.set_true();
}
/// select the last container
pub fn containers_end(&mut self) {
self.containers.end();
self.redraw.set_true();
}
/// Select the next container
pub fn containers_next(&mut self) {
self.containers.next();
self.redraw.set_true();
}
/// select the previous container
pub fn containers_previous(&mut self) {
self.containers.previous();
self.redraw.set_true();
}
/// Get ListState of containers
@@ -521,6 +548,11 @@ impl AppData {
self.get_selected_container().map(|i| i.id.clone())
}
/// Check if a given ID matches the currently selected container
pub fn is_selected_container(&self, id: &ContainerId) -> bool {
self.get_selected_container().is_some_and(|i| &i.id == id)
}
/// Get the Id and State for the currently selected container - used by the exec check method
pub fn get_selected_container_id_state_name(&self) -> Option<(ContainerId, State, String)> {
self.get_selected_container()
@@ -545,6 +577,7 @@ impl AppData {
pub fn docker_controls_next(&mut self) {
if let Some(i) = self.get_mut_selected_container() {
i.docker_controls.next();
self.redraw.set_true();
}
}
@@ -552,6 +585,7 @@ impl AppData {
pub fn docker_controls_previous(&mut self) {
if let Some(i) = self.get_mut_selected_container() {
i.docker_controls.previous();
self.redraw.set_true();
}
}
@@ -559,6 +593,7 @@ impl AppData {
pub fn docker_controls_start(&mut self) {
if let Some(i) = self.get_mut_selected_container() {
i.docker_controls.start();
self.redraw.set_true();
}
}
@@ -566,6 +601,7 @@ impl AppData {
pub fn docker_controls_end(&mut self) {
if let Some(i) = self.get_mut_selected_container() {
i.docker_controls.end();
self.redraw.set_true();
}
}
@@ -603,6 +639,7 @@ impl AppData {
pub fn log_next(&mut self) {
if let Some(i) = self.get_mut_selected_container() {
i.logs.next();
self.redraw.set_true();
}
}
@@ -610,6 +647,7 @@ impl AppData {
pub fn log_previous(&mut self) {
if let Some(i) = self.get_mut_selected_container() {
i.logs.previous();
self.redraw.set_true();
}
}
@@ -617,6 +655,7 @@ impl AppData {
pub fn log_end(&mut self) {
if let Some(i) = self.get_mut_selected_container() {
i.logs.end();
self.redraw.set_true();
}
}
@@ -624,6 +663,7 @@ impl AppData {
pub fn log_start(&mut self) {
if let Some(i) = self.get_mut_selected_container() {
i.logs.start();
self.redraw.set_true();
}
}
@@ -664,12 +704,14 @@ impl AppData {
/// Remove single app_state error
pub fn remove_error(&mut self) {
self.error = None;
self.redraw.set_true();
}
/// Insert single app_state error
pub fn set_error(&mut self, error: AppError, gui_state: &Arc<Mutex<GuiState>>, status: Status) {
gui_state.lock().status_push(status);
self.error = Some(error);
self.redraw.set_true();
}
/// Check if the selected container is a dockerised version of oxker
@@ -758,6 +800,9 @@ impl AppData {
container.tx.update(tx);
container.mem_limit.update(mem_limit);
}
if self.is_selected_container(id) {
self.redraw.set_true();
}
self.sort_containers();
}
@@ -793,6 +838,9 @@ impl AppData {
// Check is some, else can cause out of bounds error, if containers get removed before a docker update
if self.containers.items.get(index).is_some() {
self.containers.items.remove(index);
if self.is_selected_container(id) {
self.redraw.set_true();
}
}
}
}
@@ -872,6 +920,7 @@ impl AppData {
}
}
}
// self.redraw.set_true("update_containers");
}
}
@@ -919,6 +968,9 @@ impl AppData {
container.logs.end();
}
}
if self.is_selected_container(id) {
self.redraw.set_true();
}
}
}
}
+6 -6
View File
@@ -18,7 +18,7 @@ mod parse_config_file;
pub struct Config {
pub app_colors: AppColors,
pub color_logs: bool,
pub docker_interval: u32,
pub docker_interval_ms: u32,
pub gui: bool,
pub host: Option<String>,
pub in_container: bool,
@@ -38,7 +38,7 @@ impl From<&Args> for Config {
Self {
app_colors: AppColors::new(),
color_logs: args.color,
docker_interval: args.docker_interval,
docker_interval_ms: args.docker_interval,
gui: !args.gui,
host: args.host.clone(),
in_container: Self::check_if_in_container(),
@@ -60,7 +60,7 @@ impl From<ConfigFile> for Config {
Self {
app_colors: AppColors::from(config_file.colors),
color_logs: config_file.color_logs.unwrap_or(false),
docker_interval: config_file.docker_interval.unwrap_or(1000),
docker_interval_ms: config_file.docker_interval.unwrap_or(1000),
gui: config_file.gui.unwrap_or(true),
host: config_file.host,
in_container: Self::check_if_in_container(),
@@ -129,7 +129,7 @@ impl Config {
/// make sure color_logs and raw_logs can't clash
fn merge_args(mut self, config_from_cli: Self) -> Self {
self.color_logs = config_from_cli.color_logs;
self.docker_interval = config_from_cli.docker_interval;
self.docker_interval_ms = config_from_cli.docker_interval_ms;
self.gui = config_from_cli.gui;
self.raw_logs = config_from_cli.raw_logs;
self.show_self = config_from_cli.show_self;
@@ -137,8 +137,8 @@ impl Config {
self.show_timestamp = config_from_cli.show_timestamp;
self.use_cli = config_from_cli.use_cli;
if config_from_cli.docker_interval < 1000 {
self.docker_interval = 1000;
if config_from_cli.docker_interval_ms < 1000 {
self.docker_interval_ms = 1000;
}
if let Some(host) = config_from_cli.host {
+2 -1
View File
@@ -400,7 +400,8 @@ impl DockerData {
/// Send an update message every x ms, where x is the args.docker_interval
fn heartbeat(config: &Config, docker_tx: Sender<DockerMessage>) {
let update_duration = std::time::Duration::from_millis(u64::from(config.docker_interval));
let update_duration =
std::time::Duration::from_millis(u64::from(config.docker_interval_ms));
let mut now = std::time::Instant::now();
tokio::spawn(async move {
loop {
+1 -1
View File
@@ -625,7 +625,7 @@ impl InputHandler {
self.gui_state.lock().status_push(Status::Help);
}
self.gui_state.lock().get_intersect_panel(mouse_point);
self.gui_state.lock().check_panel_intersect(mouse_point);
}
_ => (),
}
+12 -6
View File
@@ -23,7 +23,7 @@ mod exec;
mod input_handler;
mod ui;
use ui::{GuiState, Status, Ui};
use ui::{GuiState, Redraw, Status, Ui};
use crate::docker_data::DockerMessage;
@@ -98,9 +98,10 @@ fn handler_init(
async fn main() {
setup_tracing();
let config = config::Config::new();
let redraw = Arc::new(Redraw::new());
let app_data = Arc::new(Mutex::new(AppData::default(config.clone())));
let gui_state = Arc::new(Mutex::new(GuiState::default()));
let app_data = Arc::new(Mutex::new(AppData::new(config.clone(), &redraw)));
let gui_state = Arc::new(Mutex::new(GuiState::new(&redraw)));
let is_running = Arc::new(AtomicBool::new(true));
let (docker_tx, docker_rx) = tokio::sync::mpsc::channel(32);
@@ -109,7 +110,7 @@ async fn main() {
if config.gui {
let (input_tx, input_rx) = tokio::sync::mpsc::channel(32);
handler_init(&app_data, &docker_tx, &gui_state, input_rx, &is_running);
Ui::start(app_data, gui_state, input_tx, is_running).await;
Ui::start(app_data, gui_state, input_tx, is_running, redraw).await;
} else {
info!("in debug mode\n");
let mut now = std::time::Instant::now();
@@ -120,7 +121,7 @@ async fn main() {
error!("{}", err);
process::exit(1);
}
if let Some(Ok(to_sleep)) = u128::from(config.docker_interval)
if let Some(Ok(to_sleep)) = u128::from(config.docker_interval_ms)
.checked_sub(now.elapsed().as_millis())
.map(u64::try_from)
{
@@ -148,6 +149,8 @@ async fn main() {
#[allow(clippy::unwrap_used)]
mod tests {
use std::sync::Arc;
use bollard::service::{ContainerSummary, Port};
use crate::{
@@ -156,13 +159,14 @@ mod tests {
RunningState, State, StatefulList,
},
config::{AppColors, Config, Keymap},
ui::Redraw,
};
/// Default test config, has timestamps turned off
pub fn gen_config() -> Config {
Config {
color_logs: false,
docker_interval: 1000,
docker_interval_ms: 1000,
gui: true,
host: None,
show_std_err: false,
@@ -200,8 +204,10 @@ mod tests {
AppData {
containers: StatefulList::new(containers.to_vec()),
hidden_containers: vec![],
current_sorted_id: vec![],
error: None,
sorted_by: None,
redraw: Arc::new(Redraw::new()),
filter: Filter::new(),
config: gen_config(),
}
+3 -2
View File
@@ -123,7 +123,7 @@ pub mod tests {
use crate::{
app_data::{AppData, ContainerId, ContainerImage, ContainerName, ContainerPorts},
tests::{gen_appdata, gen_containers},
ui::{draw_frame, GuiState},
ui::{draw_frame, GuiState, Redraw},
};
use super::FrameData;
@@ -194,7 +194,8 @@ pub mod tests {
app_data.containers_start();
}
let gui_state = GuiState::default();
let redraw = Arc::new(Redraw::new());
let gui_state = GuiState::new(&redraw);
let app_data = Arc::new(Mutex::new(app_data));
let gui_state = Arc::new(Mutex::new(gui_state));
+36 -9
View File
@@ -13,6 +13,8 @@ use crate::{
exec::ExecMode,
};
use super::Redraw;
#[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq)]
pub enum SelectablePanel {
#[default]
@@ -171,22 +173,40 @@ pub enum Status {
}
/// Global gui_state, stored in an Arc<Mutex>
#[derive(Debug, Default)]
#[derive(Debug)]
pub struct GuiState {
delete_container: Option<ContainerId>,
exec_mode: Option<ExecMode>,
loading_handle: Option<JoinHandle<()>>,
loading_index: u8,
loading_set: HashSet<Uuid>,
intersect_delete: HashMap<DeleteButton, Rect>,
intersect_heading: HashMap<Header, Rect>,
intersect_help: Option<Rect>,
intersect_panel: HashMap<SelectablePanel, Rect>,
loading_handle: Option<JoinHandle<()>>,
loading_index: u8,
loading_set: HashSet<Uuid>,
redraw: Arc<Redraw>,
selected_panel: SelectablePanel,
status: HashSet<Status>,
pub info_box_text: Option<(String, Instant)>,
}
impl GuiState {
pub fn new(redraw: &Arc<Redraw>) -> Self {
Self {
delete_container: None,
exec_mode: None,
info_box_text: None,
intersect_delete: HashMap::new(),
intersect_heading: HashMap::new(),
intersect_help: None,
intersect_panel: HashMap::new(),
loading_handle: None,
loading_index: 0,
loading_set: HashSet::new(),
redraw: Arc::clone(redraw),
selected_panel: SelectablePanel::default(),
status: HashSet::new(),
}
}
/// Clear panels hash map, so on resize can fix the sizes for mouse clicks
pub fn clear_area_map(&mut self) {
self.intersect_panel.clear();
@@ -198,7 +218,7 @@ impl GuiState {
}
/// Check if a given Rect (a clicked area of 1x1), interacts with any known panels
pub fn get_intersect_panel(&mut self, rect: Rect) {
pub fn check_panel_intersect(&mut self, rect: Rect) {
if let Some(data) = self
.intersect_panel
.iter()
@@ -207,6 +227,7 @@ impl GuiState {
.first()
{
self.selected_panel = *data.0;
self.redraw.set_true();
}
}
@@ -299,6 +320,7 @@ impl GuiState {
}
_ => (),
}
self.redraw.set_true();
}
/// Inset the ExecMode into self, and set the Status as exec
@@ -307,6 +329,7 @@ impl GuiState {
pub fn set_exec_mode(&mut self, mode: ExecMode) {
self.exec_mode = Some(mode);
self.status.insert(Status::Exec);
self.redraw.set_true();
}
pub fn get_exec_mode(&self) -> Option<ExecMode> {
@@ -316,22 +339,22 @@ impl GuiState {
/// Insert a gui_status into the current gui_status HashSet
/// If the status is Exec, it won't get inserted, set_exec_mode() should be used instead
pub fn status_push(&mut self, status: Status) {
match status {
Status::Exec => (),
_ => {
if status != Status::Exec {
self.status.insert(status);
}
self.redraw.set_true();
}
}
/// Change to next selectable panel
pub fn next_panel(&mut self) {
self.selected_panel = self.selected_panel.next();
self.redraw.set_true();
}
/// Change to previous selectable panel
pub fn previous_panel(&mut self) {
self.selected_panel = self.selected_panel.prev();
self.redraw.set_true();
}
/// Insert a new loading_uuid into HashSet, and advance the loading_index by one frame, or reset to 0 if at end of array
@@ -342,6 +365,7 @@ impl GuiState {
self.loading_index += 1;
}
self.loading_set.insert(uuid);
self.redraw.set_true();
}
pub fn is_loading(&self) -> bool {
@@ -374,6 +398,7 @@ impl GuiState {
/// Stop the loading_spin function, and reset gui loading status
pub fn stop_loading_animation(&mut self, loading_uuid: Uuid) {
self.loading_set.remove(&loading_uuid);
self.redraw.set_true();
if self.loading_set.is_empty() {
self.loading_index = 0;
if let Some(h) = &self.loading_handle {
@@ -386,10 +411,12 @@ impl GuiState {
/// Set info box content
pub fn set_info_box(&mut self, text: &str) {
self.info_box_text = Some((text.to_owned(), std::time::Instant::now()));
self.redraw.set_true();
}
/// Remove info box content
pub fn reset_info_box(&mut self) {
self.info_box_text = None;
self.redraw.set_true();
}
}
+29 -6
View File
@@ -23,6 +23,8 @@ use tracing::error;
mod color_match;
mod draw_blocks;
mod gui_state;
mod redraw;
pub use redraw::Redraw;
pub use self::color_match::*;
pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status};
@@ -37,16 +39,19 @@ use crate::{
input_handler::InputMessages,
};
const POLL_RATE: Duration = std::time::Duration::from_millis(100);
const POLL_RATE: Duration = std::time::Duration::from_millis(50);
// could have a render struct, which takes in poll rate, and docker
pub struct Ui {
app_data: Arc<Mutex<AppData>>,
cursor_position: Position,
gui_state: Arc<Mutex<GuiState>>,
input_tx: Sender<InputMessages>,
is_running: Arc<AtomicBool>,
now: Instant,
redraw: Arc<Redraw>,
terminal: Terminal<CrosstermBackend<Stdout>>,
cursor_position: Position,
}
impl Ui {
@@ -68,6 +73,7 @@ impl Ui {
gui_state: Arc<Mutex<GuiState>>,
input_tx: Sender<InputMessages>,
is_running: Arc<AtomicBool>,
redraw: Arc<Redraw>,
) {
if let Ok(mut terminal) = Self::setup_terminal() {
let cursor_position = terminal.get_cursor_position().unwrap_or_default();
@@ -78,6 +84,7 @@ impl Ui {
input_tx,
is_running,
now: Instant::now(),
redraw,
terminal,
};
if let Err(e) = ui.draw_ui().await {
@@ -126,18 +133,18 @@ impl Ui {
let mut seconds = 5;
let colors = self.app_data.lock().config.app_colors;
let keymap = self.app_data.lock().config.keymap.clone();
let mut render = true;
let mut redraw = true;
loop {
if self.now.elapsed() >= std::time::Duration::from_secs(1) {
seconds -= 1;
self.now = Instant::now();
render = true;
redraw = true;
if seconds < 1 {
break;
}
}
if render
if redraw
&& self
.terminal
.draw(|f| {
@@ -153,7 +160,7 @@ impl Ui {
{
return Err(AppError::Terminal);
}
render = false;
redraw = false;
std::thread::sleep(POLL_RATE);
}
Ok(())
@@ -178,12 +185,27 @@ impl Ui {
self.gui_state.lock().status_del(Status::Exec);
}
/// Use the previously redrawn time, the current time, the docker_interval, and the redraw struct, to calculate
/// if the screen should be redrawn or not
fn should_redraw(&self, previous: &mut Instant, docker_interval_ms: u128) -> bool {
let result = self.redraw.swap() || previous.elapsed().as_millis() >= docker_interval_ms;
if result {
*previous = std::time::Instant::now();
}
result
}
/// The loop for drawing the main UI to the terminal
async fn gui_loop(&mut self) -> Result<(), AppError> {
let colors = self.app_data.lock().config.app_colors;
let keymap = self.app_data.lock().config.keymap.clone();
let docker_interval_ms = u128::from(self.app_data.lock().config.docker_interval_ms);
let mut drawn_at = std::time::Instant::now();
while self.is_running.load(Ordering::SeqCst) {
if self.should_redraw(&mut drawn_at, docker_interval_ms) {
let fd = FrameData::from(&*self);
let exec = fd.status.contains(&Status::Exec);
if exec {
self.exec().await;
@@ -198,6 +220,7 @@ impl Ui {
{
return Err(AppError::Terminal);
}
}
if crossterm::event::poll(POLL_RATE).unwrap_or(false) {
if let Ok(event) = event::read() {
+25
View File
@@ -0,0 +1,25 @@
use std::sync::atomic::{AtomicBool, Ordering};
#[derive(Debug)]
pub struct Redraw(AtomicBool);
impl Redraw {
pub const fn new() -> Self {
Self(AtomicBool::new(true))
}
pub fn set_true(&self) {
self.0.store(true, Ordering::SeqCst);
}
/// Return the value of the self, and set to false
pub fn swap(&self) -> bool {
match self
.0
.compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
{
Ok(previous_value) => previous_value,
Err(current_value) => current_value,
}
}
}