use std::io::Write as _;
use crossterm::style::Stylize as _;
use miette::IntoDiagnostic as _;
use r3bl_core::{ok, SharedWriter};
use r3bl_terminal_async::{ReadlineEvent,
ReadlineEvent::{Eof, Interrupted, Line, Resized},
TerminalAsync};
use tokio::io::{AsyncBufReadExt as _, AsyncWriteExt as _};
#[tokio::main]
#[allow(clippy::needless_return)]
async fn main() -> miette::Result<()> {
let (shutdown_sender, _) = tokio::sync::broadcast::channel::<()>(1);
let child_process_constructor::ChildProcessHandle {
pid,
child,
stdin,
stdout,
stderr,
} = child_process_constructor::new("bash")?;
let terminal_async_constructor::TerminalAsyncHandle {
terminal_async,
shared_writer,
} = terminal_async_constructor::new(pid).await?;
_ = tokio::join!(
monitor_child_output::spawn(
stdout,
stderr,
shared_writer.clone(),
shutdown_sender.clone()
),
monitor_user_input_and_send_to_child::start_event_loop(
stdin,
terminal_async,
child,
shutdown_sender.clone()
)
);
ok!()
}
pub mod monitor_user_input_and_send_to_child {
use super::*;
enum ControlFlow {
ShutdownKillChild,
ProcessLine(String),
Resized,
}
impl From<miette::Result<ReadlineEvent>> for ControlFlow {
fn from(result_readline_event: miette::Result<ReadlineEvent>) -> Self {
match result_readline_event {
Ok(readline_event) => match readline_event {
Line(input) => {
let input = input.trim().to_string();
if input == "exit" {
ControlFlow::ShutdownKillChild
} else {
ControlFlow::ProcessLine(input)
}
}
Eof | Interrupted => ControlFlow::ShutdownKillChild,
Resized => ControlFlow::Resized,
},
_ => ControlFlow::ShutdownKillChild,
}
}
}
pub async fn start_event_loop(
mut stdin: tokio::process::ChildStdin,
mut terminal_async: TerminalAsync,
mut child: tokio::process::Child,
shutdown_sender: tokio::sync::broadcast::Sender<()>,
) {
let mut shutdown_receiver = shutdown_sender.subscribe();
loop {
tokio::select! {
_ = shutdown_receiver.recv() => {
break;
}
result_readline_event = terminal_async.get_readline_event() => {
match ControlFlow::from(result_readline_event) {
ControlFlow::ShutdownKillChild => {
_ = child.kill().await;
_= shutdown_sender.send(());
break;
}
ControlFlow::ProcessLine(input) => {
let input = format!("{}\n", input);
_ = stdin.write_all(input.as_bytes()).await;
_ = stdin.flush().await;
}
ControlFlow::Resized => {}
}
}
}
}
}
}
pub mod monitor_child_output {
use super::*;
pub async fn spawn(
stdout: tokio::process::ChildStdout,
stderr: tokio::process::ChildStderr,
mut shared_writer: SharedWriter,
shutdown_sender: tokio::sync::broadcast::Sender<()>,
) -> tokio::task::JoinHandle<()> {
let mut stdout_lines = tokio::io::BufReader::new(stdout).lines();
let mut stderr_lines = tokio::io::BufReader::new(stderr).lines();
let mut shutdown_receiver = shutdown_sender.subscribe();
tokio::spawn(async move {
loop {
tokio::select! {
_ = shutdown_receiver.recv() => {
break;
}
result_line = stdout_lines.next_line() => {
match result_line {
Ok(Some(line)) => {
let line = line.to_string().green();
_ = writeln!(shared_writer, "{}", line);
},
_ => {
_ = shutdown_sender.send(());
break;
}
}
}
result_line = stderr_lines.next_line() => {
match result_line {
Ok(Some(line)) => {
let line = line.to_string().red();
_ = writeln!(shared_writer, "{}", line);
}
_ => {
_= shutdown_sender.send(());
break;
}
}
},
}
}
})
}
}
pub mod terminal_async_constructor {
use super::*;
pub struct TerminalAsyncHandle {
pub terminal_async: TerminalAsync,
pub shared_writer: SharedWriter,
}
pub async fn new(pid: u32) -> miette::Result<TerminalAsyncHandle> {
let prompt = {
let prompt_seg_1 = "โญ".magenta().on_dark_grey().to_string();
let prompt_seg_2 = format!("โค{pid}โ").magenta().on_dark_grey().to_string();
let prompt_seg_3 = "โฎ".magenta().on_dark_grey().to_string();
format!("{}{}{} ", prompt_seg_1, prompt_seg_2, prompt_seg_3)
};
let Ok(Some(terminal_async)) = TerminalAsync::try_new(prompt.as_str()).await
else {
miette::bail!("Failed to create TerminalAsync instance");
};
let shared_writer = terminal_async.clone_shared_writer();
ok!(TerminalAsyncHandle {
terminal_async,
shared_writer
})
}
}
pub mod child_process_constructor {
use super::*;
pub struct ChildProcessHandle {
pub stdin: tokio::process::ChildStdin,
pub stdout: tokio::process::ChildStdout,
pub stderr: tokio::process::ChildStderr,
pub pid: u32,
pub child: tokio::process::Child,
}
pub fn new(program: &str) -> miette::Result<ChildProcessHandle> {
let mut child: tokio::process::Child = tokio::process::Command::new(program)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.into_diagnostic()?;
let stdout: tokio::process::ChildStdout = child
.stdout
.take()
.ok_or_else(|| miette::miette!("Failed to open stdout of child process"))?;
let stdin: tokio::process::ChildStdin = child
.stdin
.take()
.ok_or_else(|| miette::miette!("Failed to open stdin of child process"))?;
let stderr: tokio::process::ChildStderr = child
.stderr
.take()
.ok_or_else(|| miette::miette!("Failed to open stderr of child process"))?;
let pid = child
.id()
.ok_or_else(|| miette::miette!("Failed to get PID of child process"))?;
ok!(ChildProcessHandle {
pid,
child,
stdin,
stdout,
stderr,
})
}
}