use std::io::Write;
use miette::IntoDiagnostic;
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_mimalloc_in_main, SharedWriter};
use tokio::{io::{AsyncBufReadExt, AsyncWriteExt},
join,
sync::broadcast,
task::JoinHandle};
#[tokio::main]
#[allow(clippy::needless_return)]
async fn main() -> miette::Result<()> {
set_mimalloc_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 {
rl_ctx: readline_async,
shared_writer,
} = terminal_async_constructor::new(pid).await?;
let _unused: (JoinHandle<_>, _) = 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::{broadcast, AsyncWriteExt, Eof, Interrupted, Line, ReadlineAsyncContext,
ReadlineEvent, Resized};
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.ok();
rl_async.request_shutdown(Some("โชโโฟโโซ Goodbye")).await.ok();
shutdown_sender.send(()).ok();
break;
}
ControlFlow::ProcessLine(input) => {
let input = format!("{input}\n");
stdin.write_all(input.as_bytes()).await.ok();
stdin.flush().await.ok();
}
ControlFlow::Resized => {}
}
}
}
}
}
}
pub mod monitor_child_output {
use super::{broadcast, fg_guards_red, fg_lizard_green, AsyncBufReadExt,
SharedWriter, Write};
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)).ok();
},
_ => {
shutdown_sender.send(()).ok();
break;
}
}
}
result_line = stderr_lines.next_line() => {
match result_line {
Ok(Some(line)) => {
writeln!(shared_writer, "{}", fg_guards_red(&line)).ok();
}
_ => {
shutdown_sender.send(()).ok();
break;
}
}
},
}
}
})
}
}
pub mod terminal_async_constructor {
use super::{fg_slate_gray, inline_string, ok, ReadlineAsyncContext, SharedWriter};
#[allow(missing_debug_implementations)]
pub struct TerminalAsyncHandle {
pub rl_ctx: 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(rl_ctx)) = ReadlineAsyncContext::try_new(Some(prompt)).await else {
miette::bail!("Failed to create ReadlineAsyncContext instance");
};
let shared_writer = rl_ctx.clone_shared_writer();
ok!(TerminalAsyncHandle {
rl_ctx,
shared_writer
})
}
}
pub mod child_process_constructor {
use super::{ok, IntoDiagnostic};
#[derive(Debug)]
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,
})
}
}