Skip to content

Commit

Permalink
✨ notification popup on errors (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
pyaillet authored Dec 22, 2023
1 parent b9b2488 commit e95f1af
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 91 deletions.
55 changes: 53 additions & 2 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,35 @@ use crossterm::event::{self, KeyCode, KeyEventKind, KeyModifiers};
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Style, Stylize};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph};
use ratatui::widgets::{Block, Borders, Clear, Padding, Paragraph, Wrap};
use tokio::sync::mpsc::{self, UnboundedSender};

use crate::action::Action;
use crate::components::containers::Containers;
use crate::components::{Component, ComponentInit};
use crate::tui;
use crate::utils::centered_rect;

enum InputMode {
None,
Change,
//TODO Filter
}

const DEFAULT_TOAST_DELAY: usize = 5;

const CONTAINERS: &str = "containers";
const IMAGES: &str = "images";
const NETWORKS: &str = "networks";
const VOLUMES: &str = "volumes";

const SUGGESTIONS: [&str; 4] = [CONTAINERS, IMAGES, NETWORKS, VOLUMES];

pub enum Popup {
None,
Error(String, usize),
}

pub struct App {
should_quit: bool,
should_suspend: bool,
Expand All @@ -34,6 +42,7 @@ pub struct App {
version: &'static str,
frame_rate: f64,
tick_rate: f64,
show_popup: Popup,
}

impl App {
Expand All @@ -48,6 +57,7 @@ impl App {
version,
frame_rate,
tick_rate,
show_popup: Popup::None,
}
}

Expand Down Expand Up @@ -77,7 +87,7 @@ impl App {
self.handle_key(kevent, action_tx.clone())?;
}
},
_ => action_tx.send(Action::Error("Unhandled event".to_string()))?,
_ => {}
}
}

Expand All @@ -103,6 +113,7 @@ impl App {
tui.draw(|f| {
self.draw_header(f, main_layout[0]);
main.draw(f, main_layout[1]);
self.draw_popup(f);
self.draw_status(f, main_layout[2]);
})?;
}
Expand All @@ -121,9 +132,19 @@ impl App {
tui.draw(|f| {
self.draw_header(f, main_layout[0]);
main.draw(f, main_layout[1]);
self.draw_popup(f);
self.draw_status(f, main_layout[2]);
})?;
}
Action::Tick => {
if let Popup::Error(_msg, timeout) = &mut self.show_popup {
if *timeout > 0 {
*timeout = timeout.saturating_sub(1);
} else {
self.show_popup = Popup::None;
}
}
}
Action::Screen(ref screen) => {
let mut new_main = screen.clone().get_component();
new_main.register_action_handler(action_tx.clone());
Expand All @@ -138,6 +159,12 @@ impl App {
if let InputMode::Change = self.input_mode {
self.reset_input();
}
if let Popup::Error(_, _) = self.show_popup {
self.show_popup = Popup::None;
}
}
Action::Error(ref msg) => {
self.show_popup = Popup::Error(msg.to_string(), DEFAULT_TOAST_DELAY);
}
_ => {}
};
Expand Down Expand Up @@ -347,4 +374,28 @@ impl App {
}
Ok(())
}

fn draw_popup(&mut self, f: &mut ratatui::prelude::Frame<'_>) {
if let Popup::Error(msg, timeout) = &mut self.show_popup {
let text = vec![
Line::from(vec![Span::styled(msg.to_string(), Style::new().gray())]),
Line::from(""),
Line::from(format!("This popup will disappear in {}s", timeout)),
Line::from(vec![
Span::from("Press "),
Span::styled("ESC", Style::new().bold()),
Span::from(" to cancel"),
]),
];
let paragraph = Paragraph::new(text).wrap(Wrap { trim: false });

let block = Block::default()
.title("Error".bold().red())
.padding(Padding::new(1, 1, 1, 1))
.borders(Borders::ALL);
let area = centered_rect(60, 15, f.size());
f.render_widget(Clear, area); //this clears out the background
f.render_widget(paragraph.block(block), area);
}
}
}
6 changes: 4 additions & 2 deletions src/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub mod volumes;
#[derive(Clone, Debug)]
pub(crate) enum ComponentInit {
Containers,
ContainerExec(String, Option<String>),
ContainerExec(String, String, Option<String>),
ContainerInspect(String, String),
Images,
Networks,
Expand All @@ -36,7 +36,9 @@ impl ComponentInit {
match self {
ComponentInit::Containers => Box::new(Containers::new()),
ComponentInit::ContainerInspect(id, name) => Box::new(ContainerDetails::new(id, name)),
ComponentInit::ContainerExec(id, cmd) => Box::new(ContainerExec::new(id, cmd)),
ComponentInit::ContainerExec(id, cname, cmd) => {
Box::new(ContainerExec::new(id, cname, cmd))
}
ComponentInit::Images => Box::new(Images::new()),
ComponentInit::Networks => Box::new(Networks::new()),
ComponentInit::Volumes => Box::new(Volumes::new()),
Expand Down
162 changes: 82 additions & 80 deletions src/components/container_exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,91 @@ const DEFAULT_CMD: &str = "/bin/bash";

pub struct ContainerExec {
cid: String,
cname: String,
command: String,
action_tx: Option<UnboundedSender<Action>>,
should_stop: bool,
}

impl ContainerExec {
pub fn new(cid: String, command: Option<String>) -> Self {
pub fn new(cid: String, cname: String, command: Option<String>) -> Self {
log::debug!("{}>{:?}", cid, command);
ContainerExec {
cid,
cname,
command: command.unwrap_or(DEFAULT_CMD.to_string()),
action_tx: None,
should_stop: false,
}
}

async fn exec(&mut self) -> Result<()> {
let docker = Docker::connect_with_socket_defaults()?;

let tty_size = crossterm::terminal::size()?;
let mut stdout = std::io::stdout();

let exec = docker
.create_exec(
&self.cid,
CreateExecOptions {
attach_stdout: Some(true),
attach_stderr: Some(true),
attach_stdin: Some(true),
tty: Some(true),
cmd: Some(vec![self.command.to_string()]),
..Default::default()
},
)
.await?
.id;

if let StartExecResults::Attached {
mut output,
mut input,
} = docker.start_exec(&exec, None).await?
{
let (tx, mut rx) = channel();

// pipe stdin into the docker exec stream input
let handle = spawn(async move {
let mut buf: [u8; 1] = [0];
let mut should_stop = false;
let mut stdin = stdin();
while !should_stop {
select!(
_ = &mut rx => { should_stop = true; },
_ = stdin.read(&mut buf) => { input.write(&buf).await.ok(); }
);
}
});

stdout.execute(MoveTo(0, 0))?;
stdout.execute(Clear(ClearType::All))?;
stdout.execute(cursor::Show)?;

docker
.resize_exec(
&exec,
ResizeExecOptions {
height: tty_size.1,
width: tty_size.0,
},
)
.await?;

// pipe docker exec output into stdout
while let Some(Ok(output)) = output.next().await {
stdout.write_all(output.into_bytes().as_ref())?;
stdout.flush()?;
}

log::debug!("Closing terminal");
tx.send(0).expect("Unable to cancel stdin task");
handle.await?;
}
Ok(())
}
}

impl Component for ContainerExec {
Expand All @@ -62,87 +132,19 @@ impl Component for ContainerExec {
}

fn update(&mut self, _action: Action) -> Result<()> {
let tx = self.action_tx.clone().expect("Unable to get event sender");

if !self.should_stop {
block_on(async {
let docker = Docker::connect_with_socket_defaults().unwrap();

let tty_size = crossterm::terminal::size().expect("Unable to get tty size");
let mut stdout = std::io::stdout();

let exec = docker
.create_exec(
&self.cid,
CreateExecOptions {
attach_stdout: Some(true),
attach_stderr: Some(true),
attach_stdin: Some(true),
tty: Some(true),
cmd: Some(vec![self.command.to_string()]),
..Default::default()
},
)
.await
.expect("Unable to create exec")
.id;

if let StartExecResults::Attached {
mut output,
mut input,
} = docker
.start_exec(&exec, None)
.await
.expect("Unable to start container exec")
{
let (tx, mut rx) = channel();

// pipe stdin into the docker exec stream input
let handle = spawn(async move {
let mut buf: [u8; 1] = [0];
let mut should_stop = false;
let mut stdin = stdin();
while !should_stop {
select!(
_ = &mut rx => { should_stop = true; },
_ = stdin.read(&mut buf) => { input.write(&buf).await.ok(); }
);
}
});

stdout.execute(MoveTo(0, 0)).expect("Unable to move cursor");
stdout
.execute(Clear(ClearType::All))
.expect("Unable to clear screen");
stdout.execute(cursor::Show).expect("Unable to show cursor");

docker
.resize_exec(
&exec,
ResizeExecOptions {
height: tty_size.1,
width: tty_size.0,
},
)
.await
.expect("Unable to resize exec");

// pipe docker exec output into stdout
while let Some(Ok(output)) = output.next().await {
stdout
.write_all(output.into_bytes().as_ref())
.expect("Unable to write_all");
stdout.flush().expect("Unable to flush");
}

log::debug!("Closing terminal");
tx.send(0).expect("Unable to send close command");
handle.await.expect("Error waiting for thread termination");
}
});
if let Some(tx) = self.action_tx.clone() {
self.should_stop = true;
tx.send(Action::Resume)?;
tx.send(Action::Screen(super::ComponentInit::Containers))?;
if let Err(e) = block_on(self.exec()) {
tx.send(Action::Error(format!(
"Unable to execute command \"{}\" in container \"{}\"\n{}",
self.command, self.cname, e
)))?;
}

self.should_stop = true;
tx.send(Action::Resume)?;
tx.send(Action::Screen(super::ComponentInit::Containers))?;
}
Ok(())
}
Expand Down
16 changes: 9 additions & 7 deletions src/components/containers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,16 @@ enum Popup {
#[derive(Clone, Debug, Default)]
struct ShellPopup {
cid: String,
cname: String,
input: String,
cursor_position: usize,
}

impl ShellPopup {
fn new(cid: String) -> Self {
fn new(cid: String, cname: String) -> Self {
ShellPopup {
cid,
cname,
..Default::default()
}
}
Expand Down Expand Up @@ -279,17 +281,16 @@ impl Component for Containers {
}
}
(Action::Shell, Popup::None) => {
if let Some(action) = self
.get_selected_container_info()
.map(|cinfo| Action::Screen(super::ComponentInit::ContainerExec(cinfo.0, None)))
{
if let Some(action) = self.get_selected_container_info().map(|cinfo| {
Action::Screen(super::ComponentInit::ContainerExec(cinfo.0, cinfo.1, None))
}) {
tx.send(Action::Suspend)?;
tx.send(action)?;
}
}
(Action::CustomShell, Popup::None) => {
if let Some((cid, _cname)) = self.get_selected_container_info() {
self.show_popup = Popup::Shell(ShellPopup::new(cid));
if let Some((cid, cname)) = self.get_selected_container_info() {
self.show_popup = Popup::Shell(ShellPopup::new(cid, cname));
}
}
(Action::Delete, Popup::None) => {
Expand All @@ -313,6 +314,7 @@ impl Component for Containers {
(Action::Ok, Popup::Shell(shell)) => {
let action = Action::Screen(super::ComponentInit::ContainerExec(
shell.cid,
shell.cname,
Some(shell.input),
));
tx.send(Action::Suspend)?;
Expand Down

0 comments on commit e95f1af

Please sign in to comment.