use std::io::Write as _;
use miette::IntoDiagnostic as _;
use r3bl_tui::{fg_guards_red,
fg_lizard_green,
fg_slate_gray,
inline_string,
ok,
readline_async::{ReadlineAsyncContext,
ReadlineEvent,
ReadlineEvent::{Eof, Interrupted, Line, Resized}},
set_jemalloc_in_main,
SharedWriter};
use tokio::{io::{AsyncBufReadExt as _, AsyncWriteExt as _},
sync::broadcast};
#[tokio::main]
#[allow(clippy::needless_return)]
async fn main() -> miette::Result<()> {
set_jemalloc_in_main!();
let (shutdown_sender, _) = broadcast::channel::<()>(1);
let child_process_constructor::ChildProcessHandle {
pid,
child,
stdin,
stdout,
stderr,
} = child_process_constructor::new("bash")?;
let terminal_async_constructor::TerminalAsyncHandle {
readline_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,
readline_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 == "request_shutdown" {
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 rl_async: ReadlineAsyncContext,
mut child: tokio::process::Child,
shutdown_sender: broadcast::Sender<()>,
) {
let mut shutdown_receiver = shutdown_sender.subscribe();
loop {
tokio::select! {
_ = shutdown_receiver.recv() => {
break;
}
result_readline_event = rl_async.read_line() => {
match ControlFlow::from(result_readline_event) {
ControlFlow::ShutdownKillChild => {
_ = child.kill().await;
_ = rl_async.request_shutdown(Some("โชโโฟโโซ Goodbye")).await;
_= shutdown_sender.send(());
break;
}
ControlFlow::ProcessLine(input) => {
let input = format!("{input}\n");
_ = 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: 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)) => {
_ = writeln!(shared_writer, "{}", fg_lizard_green(&line));
},
_ => {
_ = shutdown_sender.send(());
break;
}
}
}
result_line = stderr_lines.next_line() => {
match result_line {
Ok(Some(line)) => {
_ = writeln!(shared_writer, "{}", fg_guards_red(&line));
}
_ => {
_= shutdown_sender.send(());
break;
}
}
},
}
}
})
}
}
pub mod terminal_async_constructor {
use super::*;
pub struct TerminalAsyncHandle {
pub readline_async: ReadlineAsyncContext,
pub shared_writer: SharedWriter,
}
pub async fn new(pid: u32) -> miette::Result<TerminalAsyncHandle> {
let prompt = {
let prompt_str = inline_string!("โค{pid}โ");
let prompt_seg_1 = fg_slate_gray(&prompt_str).bg_moonlight_blue();
let prompt_seg_2 = " ";
format!("{prompt_seg_1}{prompt_seg_2}")
};
let Ok(Some(readline_async)) = ReadlineAsyncContext::try_new(Some(prompt)).await
else {
miette::bail!("Failed to create ReadlineAsync instance");
};
let shared_writer = readline_async.clone_shared_writer();
ok!(TerminalAsyncHandle {
readline_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,
})
}
}