Merge branch 'fix/mouse_capture' into dev

This commit is contained in:
Jack Wills
2023-03-02 04:22:57 +00:00
12 changed files with 433 additions and 223 deletions
+1 -1
View File
@@ -17,7 +17,7 @@
"seccomp=unconfined" "seccomp=unconfined"
], ],
"postCreateCommand": "cargo install cross typos-cli", "postCreateCommand": "rustup target add x86_64-unknown-linux-musl && cargo install cross typos-cli",
"mounts": [ "mounts": [
"source=/etc/timezone,target=/etc/timezone,type=bind,readonly" "source=/etc/timezone,target=/etc/timezone,type=bind,readonly"
+20
View File
@@ -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]
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.2.3'>v0.2.3</a> # <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.2.3'>v0.2.3</a>
### 2023-02-04 ### 2023-02-04
Generated
+14 -29
View File
@@ -131,9 +131,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.1.6" version = "4.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0b0588d44d4d63a87dbd75c136c166bbfd9a86a31cb89e09906521c7d3f5e3" checksum = "c3d7ae14b20b94cb02149ed21a86c423859cbe18dc7ed69845cace50e52b40a5"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"clap_derive", "clap_derive",
@@ -148,9 +148,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.1.0" version = "4.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "684a277d672e91966334af371f1a7b5833f9aa00b07c84e92fbce95e00208ce8" checksum = "44bec8e5c9d09e439c4335b1af0abaab56dcf3b94999a936e1bb47b9134288f0"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro-error", "proc-macro-error",
@@ -370,9 +370,9 @@ dependencies = [
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.3.15" version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
@@ -543,7 +543,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.45.0", "windows-sys",
] ]
[[package]] [[package]]
@@ -555,7 +555,7 @@ dependencies = [
"hermit-abi 0.3.1", "hermit-abi 0.3.1",
"io-lifetimes", "io-lifetimes",
"rustix", "rustix",
"windows-sys 0.45.0", "windows-sys",
] ]
[[package]] [[package]]
@@ -634,7 +634,7 @@ dependencies = [
"libc", "libc",
"log", "log",
"wasi", "wasi",
"windows-sys 0.45.0", "windows-sys",
] ]
[[package]] [[package]]
@@ -732,7 +732,7 @@ dependencies = [
"libc", "libc",
"redox_syscall", "redox_syscall",
"smallvec", "smallvec",
"windows-sys 0.45.0", "windows-sys",
] ]
[[package]] [[package]]
@@ -871,7 +871,7 @@ dependencies = [
"io-lifetimes", "io-lifetimes",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.45.0", "windows-sys",
] ]
[[package]] [[package]]
@@ -1125,9 +1125,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.25.0" version = "1.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"bytes", "bytes",
@@ -1140,7 +1140,7 @@ dependencies = [
"signal-hook-registry", "signal-hook-registry",
"socket2", "socket2",
"tokio-macros", "tokio-macros",
"windows-sys 0.42.0", "windows-sys",
] ]
[[package]] [[package]]
@@ -1427,21 +1427,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.45.0" version = "0.45.0"
+1 -1
View File
@@ -84,7 +84,7 @@ In application controls
| button| result| | button| result|
|--|--| |--|--|
| ```( tab )``` or ```( shift+tab )``` | change panel, clicking on a panel also changes the selected panel| | ```( 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| | ```( enter )```| execute selected docker command|
| ```( 1-9 )``` | sort containers by heading, clicking on headings also sorts the selected column | | ```( 1-9 )``` | sort containers by heading, clicking on headings also sorts the selected column |
| ```( 0 )``` | stop sorting | | ```( 0 )``` | stop sorting |
+1 -1
View File
@@ -14,7 +14,7 @@ COPY ./target/x86_64-unknown-linux-musl/release/oxker /app/
ENTRYPOINT [ "/app/oxker"] ENTRYPOINT [ "/app/oxker"]
# Dev build for testing # 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 # 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 # 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
+1 -1
View File
@@ -142,7 +142,7 @@ impl State {
_ => Color::Red, _ => 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 { pub const fn order(self) -> u8 {
match self { match self {
Self::Running => 0, Self::Running => 0,
+11 -38
View File
@@ -203,7 +203,7 @@ impl AppData {
self.containers.start(); self.containers.start();
} }
// select the last container /// select the last container
pub fn containers_end(&mut self) { pub fn containers_end(&mut self) {
self.containers.end(); self.containers.end();
} }
@@ -213,7 +213,7 @@ impl AppData {
self.containers.next(); self.containers.next();
} }
// select the previous container /// select the previous container
pub fn containers_previous(&mut self) { pub fn containers_previous(&mut self) {
self.containers.previous(); self.containers.previous();
} }
@@ -428,12 +428,6 @@ impl AppData {
.to_string(), .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( let mem_current_count = count(
&container &container
.mem_stats .mem_stats
@@ -441,35 +435,16 @@ impl AppData {
.unwrap_or(&ByteStats::default()) .unwrap_or(&ByteStats::default())
.to_string(), .to_string(),
); );
let mem_limit_count = count(&container.mem_limit.to_string());
if cpu_count > columns.cpu.1 { columns.cpu.1 = columns.cpu.1.max(cpu_count);
columns.cpu.1 = cpu_count; columns.image.1 = columns.image.1.max(count(&container.image));
}; columns.mem.1 = columns.mem.1.max(mem_current_count);
if image_count > columns.image.1 { columns.mem.2 = columns.mem.2.max(count(&container.mem_limit.to_string()));
columns.image.1 = image_count; columns.name.1 = columns.name.1.max(count(&container.name));
}; columns.net_rx.1 = columns.net_rx.1.max(count(&container.rx.to_string()));
if mem_current_count > columns.mem.1 { columns.net_tx.1 = columns.net_tx.1.max(count(&container.tx.to_string()));
columns.mem.1 = mem_current_count; columns.state.1 = columns.state.1.max(count(&container.state.to_string()));
}; columns.status.1 = columns.status.1.max(count(&container.status));
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 columns
} }
@@ -519,9 +494,7 @@ impl AppData {
container.mem_limit.update(mem_limit); container.mem_limit.update(mem_limit);
} }
// need to benchmark this? // need to benchmark this?
// if self.get_sorted().is_some() {
self.sort_containers(); self.sort_containers();
// }
} }
/// Update, or insert, containers /// Update, or insert, containers
+3 -6
View File
@@ -7,10 +7,7 @@ use futures_util::StreamExt;
use parking_lot::Mutex; use parking_lot::Mutex;
use std::{ use std::{
collections::HashMap, collections::HashMap,
sync::{ sync::{atomic::AtomicBool, Arc},
atomic::{AtomicBool, Ordering},
Arc,
},
}; };
use tokio::{sync::mpsc::Receiver, task::JoinHandle}; use tokio::{sync::mpsc::Receiver, task::JoinHandle};
use uuid::Uuid; use uuid::Uuid;
@@ -407,7 +404,8 @@ impl DockerData {
.values() .values()
.into_iter() .into_iter()
.for_each(tokio::task::JoinHandle::abort); .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())), spawns: Arc::new(Mutex::new(HashMap::new())),
}; };
inner.initialise_container_data().await; inner.initialise_container_data().await;
inner.message_handler().await; inner.message_handler().await;
} }
} }
+8 -14
View File
@@ -4,9 +4,7 @@ use std::sync::{
}; };
use crossterm::{ use crossterm::{
event::{ event::{DisableMouseCapture, KeyCode, MouseButton, MouseEvent, MouseEventKind},
DisableMouseCapture, EnableMouseCapture, KeyCode, MouseButton, MouseEvent, MouseEventKind,
},
execute, execute,
}; };
use parking_lot::Mutex; use parking_lot::Mutex;
@@ -21,7 +19,7 @@ use crate::{
app_data::{AppData, DockerControls, Header}, app_data::{AppData, DockerControls, Header},
app_error::AppError, app_error::AppError,
docker_data::DockerMessage, docker_data::DockerMessage,
ui::{GuiState, SelectablePanel, Status}, ui::{GuiState, SelectablePanel, Status, Ui},
}; };
pub use message::InputMessages; pub use message::InputMessages;
@@ -94,15 +92,10 @@ impl InputHandler {
} }
} }
} else { } else {
match execute!(std::io::stdout(), EnableMouseCapture) { Ui::enable_mouse_capture();
Ok(_) => self self.gui_state
.gui_state .lock()
.lock() .set_info_box("✓ mouse capture enabled".to_owned());
.set_info_box("✓ mouse capture enabled".to_owned()),
Err(_) => {
self.app_data.lock().set_error(AppError::MouseCapture(true));
}
}
}; };
// If the info box sleep handle is currently being executed, as in 'm' is pressed twice within a 4000ms window // 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() .lock()
.status_contains(&[Status::Error, Status::Init]); .status_contains(&[Status::Error, Status::Init]);
if error_init || self.docker_sender.send(DockerMessage::Quit).await.is_err() { 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);
} }
} }
+5 -9
View File
@@ -1,7 +1,7 @@
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![warn( #![warn(
clippy::nursery, clippy::nursery,
clippy::pedantic, clippy::pedantic,
clippy::expect_used, clippy::expect_used,
clippy::todo, clippy::todo,
clippy::unused_async, clippy::unused_async,
@@ -23,7 +23,7 @@ use docker_data::DockerData;
use input_handler::InputMessages; use input_handler::InputMessages;
use parking_lot::Mutex; use parking_lot::Mutex;
use parse_args::CliArgs; 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 tokio::sync::mpsc::{Receiver, Sender};
use tracing::{info, Level}; use tracing::{info, Level};
@@ -34,7 +34,7 @@ mod input_handler;
mod parse_args; mod parse_args;
mod ui; mod ui;
use ui::{create_ui, GuiState, Status}; use ui::{GuiState, Status, Ui};
use crate::docker_data::DockerMessage; use crate::docker_data::DockerMessage;
@@ -132,9 +132,7 @@ async fn main() {
handler_init(&app_data, &docker_sx, &gui_state, input_rx, &is_running); handler_init(&app_data, &docker_sx, &gui_state, input_rx, &is_running);
if args.gui { if args.gui {
create_ui(app_data, docker_sx, gui_state, is_running, input_sx) Ui::create(app_data, docker_sx, gui_state, is_running, input_sx).await;
.await
.unwrap_or(());
} else { } else {
// Debug mode for testing, mostly pointless, doesn't take terminal // Debug mode for testing, mostly pointless, doesn't take terminal
info!("in debug mode"); info!("in debug mode");
@@ -146,6 +144,4 @@ async fn main() {
.await; .await;
} }
} }
// Clear screen
std::io::stdout().flush().unwrap_or(());
} }
+225 -53
View File
@@ -481,93 +481,253 @@ pub fn heading_bar<B: Backend>(
/// From a given &str, return the maximum number of chars on a single line /// From a given &str, return the maximum number of chars on a single line
fn max_line_width(text: &str) -> usize { fn max_line_width(text: &str) -> usize {
let mut max_line_width = 0; text.lines()
text.lines().into_iter().for_each(|line| { .into_iter()
let width = line.chars().count(); .map(|i| i.chars().count())
if width > max_line_width { .max()
max_line_width = width; .unwrap_or_default()
}
/// Help popup box needs these three pieces of information
struct HelpInfo {
spans: Vec<Spans<'static>>,
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::<Vec<_>>();
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 /// Draw the help box in the centre of the screen
/// TODO should make every line it's own renderable span
pub fn help_box<B: Backend>(f: &mut Frame<'_, B>) { pub fn help_box<B: Backend>(f: &mut Frame<'_, B>) {
let title = format!(" {VERSION} "); 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"); // have to add 10, but shouldn't need to, is an error somewhere
help_text let max_line_width = [
.push_str("\n ( ↑ ↓ ) or ( j k ) or (PgUp PgDown) or (Home End) to change selected line"); name_info.width,
help_text.push_str("\n ( enter ) to send docker container commands"); description_info.width,
help_text.push_str("\n ( h ) to toggle this help information"); button_info.width,
help_text.push_str("\n ( 0 ) stop sort"); final_info.width,
help_text.push_str("\n ( 1 - 9 ) sort by header - or click header"); ]
help_text.push_str( .into_iter()
"\n ( m ) to toggle mouse capture - if disabled, text on screen can be selected & copied", .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 split_popup = Layout::default()
let all_text = format!("{NAME_TEXT}{description_text}{help_text}"); .direction(Direction::Vertical)
let mut max_line_width = max_line_width(&all_text); .constraints(
let mut lines = all_text.lines().count(); [
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 let name_paragraph = Paragraph::new(name_info.spans)
lines += 3;
max_line_width += 4;
let name_paragraph = Paragraph::new(NAME_TEXT)
.style(Style::default().bg(Color::Magenta).fg(Color::White)) .style(Style::default().bg(Color::Magenta).fg(Color::White))
.block(Block::default()) .block(Block::default())
.alignment(Alignment::Center); .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)) .style(Style::default().bg(Color::Magenta).fg(Color::Black))
.block(Block::default()) .block(Block::default())
.alignment(Alignment::Center); .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)) .style(Style::default().bg(Color::Magenta).fg(Color::Black))
.block(Block::default()) .block(Block::default())
.alignment(Alignment::Left); .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() let block = Block::default()
.title(title) .title(title)
.borders(Borders::ALL) .borders(Borders::ALL)
.border_type(BorderType::Rounded) .border_type(BorderType::Rounded)
.border_style(Style::default().fg(Color::Black)); .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 // Order is important here
f.render_widget(Clear, area); f.render_widget(Clear, area);
f.render_widget(name_paragraph, split_popup[0]); f.render_widget(name_paragraph, split_popup[0]);
f.render_widget(description_paragraph, split_popup[1]); f.render_widget(description_paragraph, split_popup[1]);
f.render_widget(help_paragraph, split_popup[2]); f.render_widget(help_paragraph, split_popup[2]);
f.render_widget(final_paragraph, split_popup[3]);
f.render_widget(block, area); 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) .constraints(h_constraints)
.split(popup_layout[indexes.0])[indexes.1] .split(popup_layout[indexes.0])[indexes.1]
} }
// Draw nothing, as in a blank screen
// pub fn nothing<B: Backend>(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]);
// }
+143 -70
View File
@@ -1,16 +1,18 @@
use anyhow::Result; use anyhow::Result;
use crossterm::{ use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event}, event::{self, DisableMouseCapture, Event},
execute, execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
}; };
use parking_lot::Mutex; use parking_lot::Mutex;
use std::{ use std::{
io, io::{self, Stdout, Write},
sync::{atomic::Ordering, Arc}, sync::{atomic::Ordering, Arc},
time::Duration,
}; };
use std::{sync::atomic::AtomicBool, time::Instant}; use std::{sync::atomic::AtomicBool, time::Instant};
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tracing::error;
use tui::{ use tui::{
backend::{Backend, CrosstermBackend}, backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout}, layout::{Constraint, Direction, Layout},
@@ -28,109 +30,180 @@ use crate::{
input_handler::InputMessages, input_handler::InputMessages,
}; };
/// Take control of the terminal in order to draw gui pub struct Ui {
pub async fn create_ui(
app_data: Arc<Mutex<AppData>>, app_data: Arc<Mutex<AppData>>,
docker_sx: Sender<DockerMessage>, docker_sx: Sender<DockerMessage>,
gui_state: Arc<Mutex<GuiState>>, gui_state: Arc<Mutex<GuiState>>,
input_poll_rate: Duration,
is_running: Arc<AtomicBool>, is_running: Arc<AtomicBool>,
now: Instant,
sender: Sender<InputMessages>, sender: Sender<InputMessages>,
) -> Result<()> { terminal: Terminal<CrosstermBackend<Stdout>>,
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(())
} }
/// Run a loop to draw the gui impl Ui {
async fn run_app<B: Backend + Send>( /// 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
app_data: Arc<Mutex<AppData>>, pub fn enable_mouse_capture() {
docker_sx: Sender<DockerMessage>, io::stdout()
gui_state: Arc<Mutex<GuiState>>, .write_all(
is_running: Arc<AtomicBool>, concat!(
sender: Sender<InputMessages>, crossterm::csi!("?1000h"),
terminal: &mut Terminal<B>, crossterm::csi!("?1015h"),
) -> Result<(), AppError> { crossterm::csi!("?1006h"),
let update_duration = )
std::time::Duration::from_millis(u64::from(app_data.lock().args.docker_interval)); .as_bytes(),
let input_poll_rate = std::time::Duration::from_millis(75); )
let status_dockerconnect = gui_state.lock().status_contains(&[Status::DockerConnect]); .unwrap_or(());
let mut now = Instant::now(); }
if status_dockerconnect {
/// Create a new Ui struct, and execute the drawing loop
pub async fn create(
app_data: Arc<Mutex<AppData>>,
docker_sx: Sender<DockerMessage>,
gui_state: Arc<Mutex<GuiState>>,
is_running: Arc<AtomicBool>,
sender: Sender<InputMessages>,
) {
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<Terminal<CrosstermBackend<Stdout>>> {
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; let mut seconds = 5;
loop { loop {
if seconds < 1 { if self.now.elapsed() >= std::time::Duration::from_secs(1) {
break;
}
if now.elapsed() >= std::time::Duration::from_secs(1) {
seconds -= 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))) .draw(|f| draw_blocks::error(f, AppError::DockerConnect, Some(seconds)))
.is_err() .is_err()
{ {
return Err(AppError::Terminal); return Err(AppError::Terminal);
} }
} }
} else { Ok(())
while is_running.load(Ordering::SeqCst) { }
if crossterm::event::poll(input_poll_rate).unwrap_or(false) {
/// 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 Ok(event) = event::read() {
if let Event::Key(key) = event { if let Event::Key(key) = event {
sender self.sender
.send(InputMessages::ButtonPress(key.code)) .send(InputMessages::ButtonPress(key.code))
.await .await
.unwrap_or(()); .unwrap_or(());
} else if let Event::Mouse(m) = event { } else if let Event::Mouse(m) = event {
sender self.sender
.send(InputMessages::MouseEvent(m)) .send(InputMessages::MouseEvent(m))
.await .await
.unwrap_or(()); .unwrap_or(());
} else if let Event::Resize(_, _) = event { } else if let Event::Resize(_, _) = event {
gui_state.lock().clear_area_map(); self.gui_state.lock().clear_area_map();
terminal.autoresize().unwrap_or(()); self.terminal.autoresize().unwrap_or(());
} }
} }
} }
if now.elapsed() >= update_duration { if self.now.elapsed() >= update_duration {
docker_sx.send(DockerMessage::Update).await.unwrap_or(()); self.docker_sx
now = Instant::now(); .send(DockerMessage::Update)
} .await
if terminal.draw(|f| ui(f, &app_data, &gui_state)).is_err() { .unwrap_or(());
return Err(AppError::Terminal); 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<B: Backend>( /// Draw the main ui to a frame of the terminal
fn draw_frame<B: Backend>(
f: &mut Frame<'_, B>, f: &mut Frame<'_, B>,
app_data: &Arc<Mutex<AppData>>, app_data: &Arc<Mutex<AppData>>,
gui_state: &Arc<Mutex<GuiState>>, gui_state: &Arc<Mutex<GuiState>>,
@@ -182,7 +255,7 @@ fn ui<B: Backend>(
vec![Constraint::Percentage(100)] vec![Constraint::Percentage(100)]
}; };
// Split into 3, containers+controls, logs, then graphs // Split into 2, logs, and optional charts
let lower_main = Layout::default() let lower_main = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints(lower_split.as_ref()) .constraints(lower_split.as_ref())