diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5dca4c6..0cdbe41 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -17,7 +17,7 @@ "seccomp=unconfined" ], - "postCreateCommand": "cargo install cross typos-cli", + "postCreateCommand": "rustup target add x86_64-unknown-linux-musl && cargo install cross typos-cli", "mounts": [ "source=/etc/timezone,target=/etc/timezone,type=bind,readonly" diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a6618f..a6f88c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +### Chores ++ dependencies updated, [aac3ef2b1def3345d749d813d9b76020d6b5e5ca], [4723be7fb2eb101024bb9d5a514e2c6cc51eb6f6], [c69ab4f7c3b873f25ea46958add37be78d23e9cf], [ba6437862dae0f422660a602aeabd6217d023fac] ++ dev container install x86 musl toolchain, [e650034d50f01a7598876d4f2887df691700e06a] + +### Docs ++ typos removed, [23ad9a5fb3cacf3fb8cb70c65ca9133ed9949e45], [cebb975cb82f653407ec801fd8c726ca6ed68289], [fdc67c9249a239bac97a78b20c9378472865209c] ++ comments improved, [ec962295a8789ff8010604e974969bf618ea7108] + +### Features ++ Mouse capture is now more specific, should have substantial performance impact, two to four time reduction in cpu usage when mouse is moved, especially on low end machines (i.e. raspberry pi), as well as fixing intermittent mouse events output bug, [0a1b53111627206cc7436589e5b7212e1b72edb8], [93f7c07f708885f8870da5dfb6d57c62f93c9c78], [c74f6c1179b5f62989eb74f395a56b43a8781b03] ++ Improve the styling of the help information popup, [28de74b866f07c8543e46be3cab929eff28953fd] ++ use checked_sub & checked_div for bounds checks, [72279e26ae996353c95a75527f704bac1e4bcf4d] + +### Refactors ++ Dead code removed, [b8f5792d1865d3a398cd7f23aa9473a55dc6ea44] ++ improve the get_width function, [04c26fe8fc7c79506921b9cff42825b1ee132737] ++ Place ui methods into a Ui struct, [3437df59884f084624031fceb34ea3012a8e2251] ++ get_horizotal/vertical constraints into single method, [e8f5cf9c6f8cd5f807a05fb61e31d7cd1426486f] ++ docker update_everything variables, [074cb957f274675a468f08fecb1c43ff7453217d] + # v0.2.3 ### 2023-02-04 diff --git a/Cargo.lock b/Cargo.lock index 87f1446..b290830 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,9 +131,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.1.6" +version = "4.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0b0588d44d4d63a87dbd75c136c166bbfd9a86a31cb89e09906521c7d3f5e3" +checksum = "c3d7ae14b20b94cb02149ed21a86c423859cbe18dc7ed69845cace50e52b40a5" dependencies = [ "bitflags", "clap_derive", @@ -148,9 +148,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.1.0" +version = "4.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "684a277d672e91966334af371f1a7b5833f9aa00b07c84e92fbce95e00208ce8" +checksum = "44bec8e5c9d09e439c4335b1af0abaab56dcf3b94999a936e1bb47b9134288f0" dependencies = [ "heck", "proc-macro-error", @@ -370,9 +370,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" +checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d" dependencies = [ "bytes", "fnv", @@ -543,7 +543,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" dependencies = [ "libc", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] @@ -555,7 +555,7 @@ dependencies = [ "hermit-abi 0.3.1", "io-lifetimes", "rustix", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] @@ -634,7 +634,7 @@ dependencies = [ "libc", "log", "wasi", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] @@ -732,7 +732,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] @@ -871,7 +871,7 @@ dependencies = [ "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] @@ -1125,9 +1125,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.25.0" +version = "1.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" +checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64" dependencies = [ "autocfg", "bytes", @@ -1140,7 +1140,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.42.0", + "windows-sys", ] [[package]] @@ -1427,21 +1427,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - [[package]] name = "windows-sys" version = "0.45.0" diff --git a/README.md b/README.md index 6f7771b..aa4ee99 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ In application controls | button| result| |--|--| | ```( tab )``` or ```( shift+tab )``` | change panel, clicking on a panel also changes the selected panel| -| ```( ↑ ↓ )``` or ```( j k )``` or ```(PgUp PgDown)``` or ```(Home End)```| change selected line in selected panel, mouse scroll also changes selected line | +| ```( ↑ ↓ )``` or ```( j k )``` or ```( PgUp PgDown )``` or ```( Home End )```| change selected line in selected panel, mouse scroll also changes selected line | | ```( enter )```| execute selected docker command| | ```( 1-9 )``` | sort containers by heading, clicking on headings also sorts the selected column | | ```( 0 )``` | stop sorting | diff --git a/containerised/Dockerfile_dev b/containerised/Dockerfile_dev index 88accee..6ff45ba 100644 --- a/containerised/Dockerfile_dev +++ b/containerised/Dockerfile_dev @@ -14,7 +14,7 @@ COPY ./target/x86_64-unknown-linux-musl/release/oxker /app/ ENTRYPOINT [ "/app/oxker"] # Dev build for testing -# docker build -t oxker_dev -f Dockerfile . && docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev +# docker build -t oxker_dev -f containerised/Dockerfile_dev . && docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev # Dev build one liner, x86 host # docker image prune -a; cargo build --release --target x86_64-unknown-linux-musl && docker build -t oxker_dev -f containerised/Dockerfile_dev . && docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev diff --git a/src/app_data/container_state.rs b/src/app_data/container_state.rs index aedc613..b1e1ba6 100644 --- a/src/app_data/container_state.rs +++ b/src/app_data/container_state.rs @@ -142,7 +142,7 @@ impl State { _ => Color::Red, } } - // Dirty way to create order for the state, rather than impl Ord + /// Dirty way to create order for the state, rather than impl Ord pub const fn order(self) -> u8 { match self { Self::Running => 0, diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index 441c23a..3169b1e 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -203,7 +203,7 @@ impl AppData { self.containers.start(); } - // select the last container + /// select the last container pub fn containers_end(&mut self) { self.containers.end(); } @@ -213,7 +213,7 @@ impl AppData { self.containers.next(); } - // select the previous container + /// select the previous container pub fn containers_previous(&mut self) { self.containers.previous(); } @@ -428,12 +428,6 @@ impl AppData { .to_string(), ); - let rx_count = count(&container.rx.to_string()); - let tx_count = count(&container.tx.to_string()); - let image_count = count(&container.image); - let name_count = count(&container.name); - let state_count = count(&container.state.to_string()); - let status_count = count(&container.status); let mem_current_count = count( &container .mem_stats @@ -441,35 +435,16 @@ impl AppData { .unwrap_or(&ByteStats::default()) .to_string(), ); - let mem_limit_count = count(&container.mem_limit.to_string()); - if cpu_count > columns.cpu.1 { - columns.cpu.1 = cpu_count; - }; - if image_count > columns.image.1 { - columns.image.1 = image_count; - }; - if mem_current_count > columns.mem.1 { - columns.mem.1 = mem_current_count; - }; - if mem_limit_count > columns.mem.2 { - columns.mem.2 = mem_limit_count; - }; - if name_count > columns.name.1 { - columns.name.1 = name_count; - }; - if state_count > columns.state.1 { - columns.state.1 = state_count; - }; - if status_count > columns.status.1 { - columns.status.1 = status_count; - }; - if rx_count > columns.net_rx.1 { - columns.net_rx.1 = rx_count; - }; - if tx_count > columns.net_tx.1 { - columns.net_tx.1 = tx_count; - }; + columns.cpu.1 = columns.cpu.1.max(cpu_count); + columns.image.1 = columns.image.1.max(count(&container.image)); + columns.mem.1 = columns.mem.1.max(mem_current_count); + columns.mem.2 = columns.mem.2.max(count(&container.mem_limit.to_string())); + columns.name.1 = columns.name.1.max(count(&container.name)); + columns.net_rx.1 = columns.net_rx.1.max(count(&container.rx.to_string())); + columns.net_tx.1 = columns.net_tx.1.max(count(&container.tx.to_string())); + columns.state.1 = columns.state.1.max(count(&container.state.to_string())); + columns.status.1 = columns.status.1.max(count(&container.status)); } columns } @@ -519,9 +494,7 @@ impl AppData { container.mem_limit.update(mem_limit); } // need to benchmark this? - // if self.get_sorted().is_some() { self.sort_containers(); - // } } /// Update, or insert, containers diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs index 5d998e6..51b955e 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -7,10 +7,7 @@ use futures_util::StreamExt; use parking_lot::Mutex; use std::{ collections::HashMap, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, + sync::{atomic::AtomicBool, Arc}, }; use tokio::{sync::mpsc::Receiver, task::JoinHandle}; use uuid::Uuid; @@ -407,7 +404,8 @@ impl DockerData { .values() .into_iter() .for_each(tokio::task::JoinHandle::abort); - self.is_running.store(false, Ordering::SeqCst); + self.is_running + .store(false, std::sync::atomic::Ordering::SeqCst); } } } @@ -436,7 +434,6 @@ impl DockerData { spawns: Arc::new(Mutex::new(HashMap::new())), }; inner.initialise_container_data().await; - inner.message_handler().await; } } diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index 7e1f649..aa9a866 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -4,9 +4,7 @@ use std::sync::{ }; use crossterm::{ - event::{ - DisableMouseCapture, EnableMouseCapture, KeyCode, MouseButton, MouseEvent, MouseEventKind, - }, + event::{DisableMouseCapture, KeyCode, MouseButton, MouseEvent, MouseEventKind}, execute, }; use parking_lot::Mutex; @@ -21,7 +19,7 @@ use crate::{ app_data::{AppData, DockerControls, Header}, app_error::AppError, docker_data::DockerMessage, - ui::{GuiState, SelectablePanel, Status}, + ui::{GuiState, SelectablePanel, Status, Ui}, }; pub use message::InputMessages; @@ -94,15 +92,10 @@ impl InputHandler { } } } else { - match execute!(std::io::stdout(), EnableMouseCapture) { - Ok(_) => self - .gui_state - .lock() - .set_info_box("✓ mouse capture enabled".to_owned()), - Err(_) => { - self.app_data.lock().set_error(AppError::MouseCapture(true)); - } - } + Ui::enable_mouse_capture(); + self.gui_state + .lock() + .set_info_box("✓ mouse capture enabled".to_owned()); }; // If the info box sleep handle is currently being executed, as in 'm' is pressed twice within a 4000ms window @@ -134,7 +127,8 @@ impl InputHandler { .lock() .status_contains(&[Status::Error, Status::Init]); if error_init || self.docker_sender.send(DockerMessage::Quit).await.is_err() { - self.is_running.store(false, Ordering::SeqCst); + self.is_running + .store(false, std::sync::atomic::Ordering::SeqCst); } } diff --git a/src/main.rs b/src/main.rs index 660ae64..ec9eef4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ #![forbid(unsafe_code)] #![warn( - clippy::nursery, - clippy::pedantic, + clippy::nursery, + clippy::pedantic, clippy::expect_used, clippy::todo, clippy::unused_async, @@ -23,7 +23,7 @@ use docker_data::DockerData; use input_handler::InputMessages; use parking_lot::Mutex; use parse_args::CliArgs; -use std::{sync::{atomic::AtomicBool, Arc}, io::Write}; +use std::sync::{atomic::AtomicBool, Arc}; use tokio::sync::mpsc::{Receiver, Sender}; use tracing::{info, Level}; @@ -34,7 +34,7 @@ mod input_handler; mod parse_args; mod ui; -use ui::{create_ui, GuiState, Status}; +use ui::{GuiState, Status, Ui}; use crate::docker_data::DockerMessage; @@ -132,9 +132,7 @@ async fn main() { handler_init(&app_data, &docker_sx, &gui_state, input_rx, &is_running); if args.gui { - create_ui(app_data, docker_sx, gui_state, is_running, input_sx) - .await - .unwrap_or(()); + Ui::create(app_data, docker_sx, gui_state, is_running, input_sx).await; } else { // Debug mode for testing, mostly pointless, doesn't take terminal info!("in debug mode"); @@ -146,6 +144,4 @@ async fn main() { .await; } } - // Clear screen - std::io::stdout().flush().unwrap_or(()); } diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 309b2ab..f03a68d 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -481,93 +481,253 @@ pub fn heading_bar( /// From a given &str, return the maximum number of chars on a single line fn max_line_width(text: &str) -> usize { - let mut max_line_width = 0; - text.lines().into_iter().for_each(|line| { - let width = line.chars().count(); - if width > max_line_width { - max_line_width = width; + text.lines() + .into_iter() + .map(|i| i.chars().count()) + .max() + .unwrap_or_default() +} + +/// Help popup box needs these three pieces of information +struct HelpInfo { + spans: Vec>, + width: usize, + height: usize, +} + +impl HelpInfo { + /// Find the max width of a Span in &[Spans], although it isn't calculating it correctly + fn calc_width(spans: &[Spans]) -> usize { + spans + .iter() + .flat_map(|x| x.0.iter()) + .map(tui::text::Span::width) + .max() + .unwrap_or(1) + } + + /// Just an empty span, i.e. a new line + fn empty_span<'a>() -> Spans<'a> { + Spans::from(String::new()) + } + + /// generate a span, of given &str and given color + fn span<'a>(input: &str, color: Color) -> Span<'a> { + Span::styled(input.to_owned(), Style::default().fg(color)) + } + + /// Span to black text span + fn black_span<'a>(input: &str) -> Span<'a> { + Self::span(input, Color::Black) + } + + /// Span to white text span + fn white_span<'a>(input: &str) -> Span<'a> { + Self::span(input, Color::White) + } + + /// Generate the `oxker` name span + metadata + fn gen_name() -> Self { + let mut spans = NAME_TEXT + .lines() + .into_iter() + .map(|i| Spans::from(Self::white_span(i))) + .collect::>(); + spans.insert(0, Self::empty_span()); + let width = Self::calc_width(&spans); + let height = spans.len(); + Self { + spans, + width, + height, } - }); - max_line_width + } + + /// Generate the description span + metadata + fn gen_description() -> Self { + let spans = [ + Self::empty_span(), + Spans::from(Self::white_span(DESCRIPTION)), + Self::empty_span(), + ]; + let width = Self::calc_width(&spans); + let height = spans.len(); + Self { + spans: spans.to_vec(), + width, + height, + } + } + + /// Generate the button information span + metadata + fn gen_button() -> Self { + let button_item = |x: &str| Self::white_span(&format!(" {x} ")); + let button_desc = |x: &str| Self::black_span(x); + let or = || button_desc("or"); + let space = || button_desc(" "); + + let spans = [ + Spans::from(vec![ + space(), + button_item("( tab )"), + or(), + button_item("( shift+tab )"), + button_desc("to change panels"), + ]), + Spans::from(vec![ + space(), + button_item("( ↑ ↓ )"), + or(), + button_item("( j k )"), + or(), + button_item("( PgUp PgDown )"), + or(), + button_item("( Home End )"), + button_desc("to change selected line"), + ]), + Spans::from(vec![ + space(), + button_item("( enter )"), + button_desc("to send docker container command"), + ]), + Spans::from(vec![ + space(), + button_item("( h )"), + button_desc("to toggle this help information"), + ]), + Spans::from(vec![ + space(), + button_item("( 0 )"), + button_desc("to stop sort"), + ]), + Spans::from(vec![ + space(), + button_item("( 1 - 9 )"), + button_desc("sort by header - or click header"), + ]), + Spans::from(vec![ + space(), + button_item("( m )"), + button_desc( + "to toggle mouse capture - if disabled, text on screen can be selected & copied", + ), + ]), + Spans::from(vec![ + space(), + button_item("( q )"), + button_desc("to quit at any time"), + ]), + ]; + + let height = spans.len(); + let width = Self::calc_width(&spans); + Self { + spans: spans.to_vec(), + width, + height, + } + } + + /// Generate the final lines, GitHub link etc, + metadata + fn gen_final() -> Self { + let spans = [ + Self::empty_span(), + Spans::from(vec![Self::black_span( + "currently an early work in progress, all and any input appreciated", + )]), + Spans::from(vec![Span::styled( + REPO.to_owned(), + Style::default() + .bg(Color::Magenta) + .fg(Color::Black) + .add_modifier(Modifier::UNDERLINED), + )]), + ]; + let height = spans.len(); + let width = Self::calc_width(&spans); + Self { + spans: spans.to_vec(), + width, + height, + } + } } /// Draw the help box in the centre of the screen -/// TODO should make every line it's own renderable span pub fn help_box(f: &mut Frame<'_, B>) { let title = format!(" {VERSION} "); - let description_text = format!("\n{DESCRIPTION}"); + let name_info = HelpInfo::gen_name(); + let description_info = HelpInfo::gen_description(); + let button_info = HelpInfo::gen_button(); + let final_info = HelpInfo::gen_final(); - let mut help_text = String::from("\n ( tab ) or ( shift+tab ) to change panels"); - help_text - .push_str("\n ( ↑ ↓ ) or ( j k ) or (PgUp PgDown) or (Home End) to change selected line"); - help_text.push_str("\n ( enter ) to send docker container commands"); - help_text.push_str("\n ( h ) to toggle this help information"); - help_text.push_str("\n ( 0 ) stop sort"); - help_text.push_str("\n ( 1 - 9 ) sort by header - or click header"); - help_text.push_str( - "\n ( m ) to toggle mouse capture - if disabled, text on screen can be selected & copied", + // have to add 10, but shouldn't need to, is an error somewhere + let max_line_width = [ + name_info.width, + description_info.width, + button_info.width, + final_info.width, + ] + .into_iter() + .max() + .unwrap_or_default() + + 10; + let max_height = + name_info.height + description_info.height + button_info.height + final_info.height + 2; + + let area = popup( + max_height, + max_line_width, + f.size(), + BoxLocation::MiddleCentre, ); - help_text.push_str("\n ( q ) to quit at any time"); - help_text.push_str("\n mouse scrolling & clicking also available"); - help_text.push_str("\n\n currently an early work in progress, all and any input appreciated"); - help_text.push_str(format!("\n {}", REPO.trim()).as_str()); - // Find the maximum line widths & height - let all_text = format!("{NAME_TEXT}{description_text}{help_text}"); - let mut max_line_width = max_line_width(&all_text); - let mut lines = all_text.lines().count(); + let split_popup = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Max(name_info.height.try_into().unwrap_or_default()), + Constraint::Max(description_info.height.try_into().unwrap_or_default()), + Constraint::Max(button_info.height.try_into().unwrap_or_default()), + Constraint::Max(final_info.height.try_into().unwrap_or_default()), + ] + .as_ref(), + ) + .split(area); - // Add some vertical and horizontal padding to the info box - lines += 3; - max_line_width += 4; - - let name_paragraph = Paragraph::new(NAME_TEXT) + let name_paragraph = Paragraph::new(name_info.spans) .style(Style::default().bg(Color::Magenta).fg(Color::White)) .block(Block::default()) .alignment(Alignment::Center); - let description_paragraph = Paragraph::new(description_text.as_str()) + let description_paragraph = Paragraph::new(description_info.spans) .style(Style::default().bg(Color::Magenta).fg(Color::Black)) .block(Block::default()) .alignment(Alignment::Center); - let help_paragraph = Paragraph::new(help_text.as_str()) + let help_paragraph = Paragraph::new(button_info.spans) .style(Style::default().bg(Color::Magenta).fg(Color::Black)) .block(Block::default()) .alignment(Alignment::Left); + let final_paragraph = Paragraph::new(final_info.spans) + .style(Style::default().bg(Color::Magenta).fg(Color::Black)) + .block(Block::default()) + .alignment(Alignment::Center); + let block = Block::default() .title(title) .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(Color::Black)); - let area = popup(lines, max_line_width, f.size(), BoxLocation::MiddleCentre); - - let split_popup = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Max(NAME_TEXT.lines().count().try_into().unwrap_or_default()), - Constraint::Max( - description_text - .lines() - .count() - .try_into() - .unwrap_or_default(), - ), - Constraint::Max(help_text.lines().count().try_into().unwrap_or_default()), - ] - .as_ref(), - ) - .split(area); - // Order is important here f.render_widget(Clear, area); f.render_widget(name_paragraph, split_popup[0]); f.render_widget(description_paragraph, split_popup[1]); f.render_widget(help_paragraph, split_popup[2]); + f.render_widget(final_paragraph, split_popup[3]); f.render_widget(block, area); } @@ -668,3 +828,15 @@ fn popup(text_lines: usize, text_width: usize, r: Rect, box_location: BoxLocatio .constraints(h_constraints) .split(popup_layout[indexes.0])[indexes.1] } + +// Draw nothing, as in a blank screen +// pub fn nothing(f: &mut Frame<'_, B>) { +// let whole_layout = Layout::default() +// .direction(Direction::Vertical) +// .constraints([Constraint::Min(100)].as_ref()) +// .split(f.size()); + +// let block = Block::default() +// .borders(Borders::NONE); +// f.render_widget(block, whole_layout[0]); +// } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index d4720ed..2ee8788 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,16 +1,18 @@ use anyhow::Result; use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event}, + event::{self, DisableMouseCapture, Event}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use parking_lot::Mutex; use std::{ - io, + io::{self, Stdout, Write}, sync::{atomic::Ordering, Arc}, + time::Duration, }; use std::{sync::atomic::AtomicBool, time::Instant}; use tokio::sync::mpsc::Sender; +use tracing::error; use tui::{ backend::{Backend, CrosstermBackend}, layout::{Constraint, Direction, Layout}, @@ -28,109 +30,180 @@ use crate::{ input_handler::InputMessages, }; -/// Take control of the terminal in order to draw gui -pub async fn create_ui( +pub struct Ui { app_data: Arc>, docker_sx: Sender, gui_state: Arc>, + input_poll_rate: Duration, is_running: Arc, + now: Instant, sender: Sender, -) -> Result<()> { - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - let res = run_app( - app_data, - docker_sx, - gui_state, - is_running, - sender, - &mut terminal, - ) - .await; - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - terminal.show_cursor()?; - - if let Err(err) = res { - println!("error: {err}"); - } - Ok(()) + terminal: Terminal>, } -/// Run a loop to draw the gui -async fn run_app( - app_data: Arc>, - docker_sx: Sender, - gui_state: Arc>, - is_running: Arc, - sender: Sender, - terminal: &mut Terminal, -) -> Result<(), AppError> { - let update_duration = - std::time::Duration::from_millis(u64::from(app_data.lock().args.docker_interval)); - let input_poll_rate = std::time::Duration::from_millis(75); - let status_dockerconnect = gui_state.lock().status_contains(&[Status::DockerConnect]); - let mut now = Instant::now(); - if status_dockerconnect { +impl Ui { + /// Enable mouse capture, but don't enable capture of all the mouse movements, doing so will improve performance, and is part of the fix for the weird mouse event output bug + pub fn enable_mouse_capture() { + io::stdout() + .write_all( + concat!( + crossterm::csi!("?1000h"), + crossterm::csi!("?1015h"), + crossterm::csi!("?1006h"), + ) + .as_bytes(), + ) + .unwrap_or(()); + } + + /// Create a new Ui struct, and execute the drawing loop + pub async fn create( + app_data: Arc>, + docker_sx: Sender, + gui_state: Arc>, + is_running: Arc, + sender: Sender, + ) { + if let Ok(terminal) = Self::setup_terminal() { + let mut ui = Self { + app_data, + docker_sx, + gui_state, + input_poll_rate: std::time::Duration::from_millis(100), + is_running, + now: Instant::now(), + sender, + terminal, + }; + if let Err(e) = ui.draw_ui().await { + error!("{e}"); + } + if let Err(e) = ui.reset_terminal() { + error!("{e}"); + }; + } else { + error!("Terminal Error"); + } + } + + /// Setup the terminal for full-screen drawing mode, with mouse capture + fn setup_terminal() -> io::Result>> { + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen)?; + Self::enable_mouse_capture(); + let backend = CrosstermBackend::new(stdout); + Terminal::new(backend) + } + + /// This is a fix for mouse-events being printed to screen, read an event and do nothing with it + fn nullify_event_read(&self) { + if crossterm::event::poll(self.input_poll_rate).unwrap_or(true) { + event::read().ok(); + } + } + + /// reset the terminal back to default settings + pub fn reset_terminal(&mut self) -> Result<()> { + self.terminal.clear()?; + + execute!( + self.terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + disable_raw_mode()?; + self.terminal.show_cursor()?; + Ok(()) + } + + /// Draw the the error message ui, for 5 seconds, with a countdown + fn err_loop(&mut self) -> Result<(), AppError> { let mut seconds = 5; loop { - if seconds < 1 { - break; - } - if now.elapsed() >= std::time::Duration::from_secs(1) { + if self.now.elapsed() >= std::time::Duration::from_secs(1) { seconds -= 1; - now = Instant::now(); + self.now = Instant::now(); + if seconds < 1 { + break; + } } - if terminal + + // This is a fix for mouse-events being printed to screen + self.nullify_event_read(); + + if self + .terminal .draw(|f| draw_blocks::error(f, AppError::DockerConnect, Some(seconds))) .is_err() { return Err(AppError::Terminal); } } - } else { - while is_running.load(Ordering::SeqCst) { - if crossterm::event::poll(input_poll_rate).unwrap_or(false) { + Ok(()) + } + + /// The loop for drawing the main UI to the terminal + async fn gui_loop(&mut self) -> Result<(), AppError> { + let update_duration = + std::time::Duration::from_millis(u64::from(self.app_data.lock().args.docker_interval)); + + while self.is_running.load(Ordering::SeqCst) { + if self + .terminal + .draw(|frame| draw_frame(frame, &self.app_data, &self.gui_state)) + .is_err() + { + return Err(AppError::Terminal); + } + if crossterm::event::poll(self.input_poll_rate).unwrap_or(false) { if let Ok(event) = event::read() { if let Event::Key(key) = event { - sender + self.sender .send(InputMessages::ButtonPress(key.code)) .await .unwrap_or(()); } else if let Event::Mouse(m) = event { - sender + self.sender .send(InputMessages::MouseEvent(m)) .await .unwrap_or(()); } else if let Event::Resize(_, _) = event { - gui_state.lock().clear_area_map(); - terminal.autoresize().unwrap_or(()); + self.gui_state.lock().clear_area_map(); + self.terminal.autoresize().unwrap_or(()); } } } - if now.elapsed() >= update_duration { - docker_sx.send(DockerMessage::Update).await.unwrap_or(()); - now = Instant::now(); - } - if terminal.draw(|f| ui(f, &app_data, &gui_state)).is_err() { - return Err(AppError::Terminal); + if self.now.elapsed() >= update_duration { + self.docker_sx + .send(DockerMessage::Update) + .await + .unwrap_or(()); + self.now = Instant::now(); } } + Ok(()) + } + + /// Draw either the Error, or main oxker ui, to the terminal + async fn draw_ui(&mut self) -> Result<(), AppError> { + let status_dockerconnect = self + .gui_state + .lock() + .status_contains(&[Status::DockerConnect]); + if status_dockerconnect { + self.err_loop()?; + } else { + self.gui_loop().await?; + } + self.nullify_event_read(); + Ok(()) } - terminal.clear().unwrap_or(()); - Ok(()) } -fn ui( +/// Draw the main ui to a frame of the terminal +fn draw_frame( f: &mut Frame<'_, B>, app_data: &Arc>, gui_state: &Arc>, @@ -182,7 +255,7 @@ fn ui( vec![Constraint::Percentage(100)] }; - // Split into 3, containers+controls, logs, then graphs + // Split into 2, logs, and optional charts let lower_main = Layout::default() .direction(Direction::Vertical) .constraints(lower_split.as_ref())