diff --git a/Cargo.lock b/Cargo.lock index 1e6a907..9167761 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,8 +38,8 @@ dependencies = [ "bytes", "clap", "crossterm", - "lazy_static", "num-traits", + "once_cell", "openssl", "pin-project", "regex", @@ -48,9 +48,11 @@ dependencies = [ "serde_json", "serde_yaml", "smartstring", + "tempfile", "thiserror", "tokio", "tokio-stream", + "tokio-util", ] [[package]] @@ -353,12 +355,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.9.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" [[package]] name = "flate2" @@ -614,15 +613,6 @@ dependencies = [ "hashbrown 0.14.0", ] -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - [[package]] name = "io-lifetimes" version = "1.0.10" @@ -648,7 +638,7 @@ checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" dependencies = [ "hermit-abi 0.3.1", "io-lifetimes", - "rustix", + "rustix 0.37.11", "windows-sys 0.48.0", ] @@ -685,6 +675,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" +[[package]] +name = "linux-raw-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" + [[package]] name = "lock_api" version = "0.4.9" @@ -776,9 +772,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "openssl" @@ -1004,7 +1000,20 @@ dependencies = [ "errno", "io-lifetimes", "libc", - "linux-raw-sys", + "linux-raw-sys 0.3.1", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys 0.4.7", "windows-sys 0.48.0", ] @@ -1220,15 +1229,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.5.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if", "fastrand", "redox_syscall 0.3.5", - "rustix", - "windows-sys 0.45.0", + "rustix 0.38.13", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5b45144..83041b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,8 +23,8 @@ async-trait = "0.1" bytes = "1.1.0" clap = { version = "4.2.2", features = ["derive"] } crossterm = "0.27" -lazy_static = "1.4.0" num-traits = "0.2" +once_cell = "1.18" pin-project = "1.1" regex = "1.7.3" reqwest = { version = "0.11", features = ["gzip", "brotli", "deflate", "json", "stream", "default-tls"] } @@ -32,9 +32,11 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.95" serde_yaml = "0.9" smartstring = { version = "1.0", features = ["serde"] } +tempfile = "3.8" thiserror = "1.0" tokio = { version = "1", features = ["full"] } tokio-stream = "0.1.12" +tokio-util = {version = "0.7", features = ["io"]} aio-cargo-info = { path = "./crates/aio-cargo-info", version = "0.1" } diff --git a/src/arguments.rs b/src/arguments.rs index 4a12983..0fb1b17 100644 --- a/src/arguments.rs +++ b/src/arguments.rs @@ -1,33 +1,5 @@ -use std::fmt::Display; - use clap::{Parser, ValueEnum}; -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] -pub enum FormatterChoice { - Markdown, - Raw, -} - -impl Default for FormatterChoice { - fn default() -> Self { - use std::io::IsTerminal; - if std::io::stdout().is_terminal() { - FormatterChoice::Markdown - } else { - FormatterChoice::Raw - } - } -} - -impl Display for FormatterChoice { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - FormatterChoice::Markdown => write!(f, "markdown"), - FormatterChoice::Raw => write!(f, "raw"), - } - } -} - /// Program to communicate with large language models and AI API #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] @@ -47,17 +19,44 @@ pub struct Args { /// Formatter /// /// Possible values: markdown, raw - #[arg(long, short, default_value_t = Default::default())] + #[arg(long, short, value_enum, default_value_t = Default::default())] pub formatter: FormatterChoice, + /// Run code block if the language is supported + #[arg(long, short, value_enum, default_value_t = Default::default())] + pub run: RunChoice, + /// Force to run code /// User text prompt pub input: Option, } +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +#[value(rename_all = "lowercase")] +pub enum FormatterChoice { + /// Markdown display + #[default] + Markdown, + /// Raw display + Raw, +} + +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +#[value(rename_all = "lowercase")] +pub enum RunChoice { + /// Doesn't run anything + #[default] + No, + /// Ask to run code + Ask, + /// Run code without asking + Force +} + pub struct ProcessedArgs { pub config_path: String, pub creds_path: String, pub engine: String, pub formatter: FormatterChoice, + pub run: RunChoice, pub input: String, } @@ -68,6 +67,7 @@ impl From for ProcessedArgs { creds_path: args.creds_path, engine: args.engine, formatter: args.formatter, + run: args.run, input: args.input.unwrap_or_default(), } } diff --git a/src/config.rs b/src/config.rs index 88b8508..5a9d827 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use once_cell::sync::Lazy; use regex::Regex; use serde::{Deserialize, Serialize}; @@ -24,9 +25,7 @@ impl Default for Config { } pub fn format_content<'a>(content: &'a str, args: &args::ProcessedArgs) -> Cow<'a, str> { - lazy_static::lazy_static!{ - static ref RE: Regex = Regex::new(r"(?P\$\$?)(?P\w+)").expect("Failed to compile regex"); - } + static RE: Lazy = Lazy::new(|| Regex::new(r"(?P\$\$?)(?P\w+)").expect("Failed to compile regex")); RE.replace_all(content, |caps: ®ex::Captures| { let prefix = &caps["prefix"]; if prefix == "$$" { diff --git a/src/formatters/markdown/renderer/terminal/utils.rs b/src/formatters/markdown/renderer/terminal/utils.rs index cf5e5bf..e11e9dc 100644 --- a/src/formatters/markdown/renderer/terminal/utils.rs +++ b/src/formatters/markdown/renderer/terminal/utils.rs @@ -1,5 +1,4 @@ -use lazy_static::lazy_static; - +use once_cell::sync::Lazy; use super::{token, queue}; use std::io::Error; @@ -77,9 +76,9 @@ pub fn repeat_char(c: char, n: usize) -> String { #[inline] pub fn draw_line() -> Result<(), Error> { - lazy_static! { - static ref LINE_STRING: String = repeat_char(CODE_BLOCK_LINE_CHAR[0], CODE_BLOCK_MARGIN.max(crossterm::terminal::size().unwrap_or_default().0 as usize)); - } + static LINE_STRING: Lazy = Lazy::new(|| { + repeat_char(CODE_BLOCK_LINE_CHAR[0], CODE_BLOCK_MARGIN.max(crossterm::terminal::size().unwrap_or_default().0 as usize)) + }); queue!(std::io::stdout(), crossterm::style::Print(&*LINE_STRING) ) diff --git a/src/generators/debug.rs b/src/generators/debug.rs new file mode 100644 index 0000000..005ca48 --- /dev/null +++ b/src/generators/debug.rs @@ -0,0 +1,14 @@ +use crate::args; +use super::{ResultRun, ResultStream, Error}; + +pub async fn run(_: crate::config::Config, args: args::ProcessedArgs) -> ResultRun { + use tokio_stream::StreamExt; + let input = args.input; + let file = tokio::fs::File::open(&input).await.map_err(|e| Error::Custom(std::borrow::Cow::Owned(e.to_string())))?; + + let stream = tokio_util::io::ReaderStream::new(file).map(|r| -> ResultStream { + let bytes = r.map_err(|e| Error::Custom(std::borrow::Cow::Owned(e.to_string())))?; + Ok(String::from_utf8(bytes.as_ref().to_vec()).map_err(|e| Error::Custom(std::borrow::Cow::Owned(e.to_string())))?) + }); + Ok(Box::pin(stream)) +} \ No newline at end of file diff --git a/src/generators/mod.rs b/src/generators/mod.rs index 9e81e8d..cddc92b 100644 --- a/src/generators/mod.rs +++ b/src/generators/mod.rs @@ -1,4 +1,5 @@ pub mod openai; +pub mod debug; use tokio_stream::Stream; use thiserror::Error; diff --git a/src/main.rs b/src/main.rs index 0dd92ec..de4c893 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ pub mod arguments; +mod runner; mod generators; mod formatters; mod config; @@ -21,13 +22,24 @@ macro_rules! raise_str { }; } -fn resolve_path(path: &str) -> Cow { - if path.starts_with("~/") { +fn home_dir() -> &'static str { + static HOME: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { #[cfg(unix)] - let home = std::env::var("HOME").expect("Failed to resolve home path"); + let path = std::env::var("HOME") + .expect("Failed to resolve home path"); + #[cfg(windows)] - let home = std::env::var("USERPROFILE").expect("Failed to resolve user profile path"); - Cow::Owned(format!("{}{}{}", home, std::path::MAIN_SEPARATOR, &path[2..])) + let path = std::env::var("USERPROFILE") + .expect("Failed to resolve user profile path"); + path + }); + + &*HOME +} + +fn resolve_path(path: &str) -> Cow { + if path.starts_with("~/") { + Cow::Owned(format!("{}{}{}", home_dir(), std::path::MAIN_SEPARATOR, &path[2..])) } else { Cow::Borrowed(path) } @@ -67,23 +79,30 @@ async fn main() -> Result<(), String> { args::FormatterChoice::Markdown => Box::new(formatters::new_markdown_formatter()), args::FormatterChoice::Raw => Box::new(formatters::new_raw_formatter()), }; + let mut runner = runner::Runner::new(args.run); - let engine = args.engine + let (engine, _prompt) = args.engine .find(':') - .map(|i| &args.engine[..i]) - .unwrap_or(args.engine.as_str()); + .map(|i| (&args.engine[..i], Some(&args.engine[i+1..]))) + .unwrap_or((args.engine.as_str(), None)); + let mut stream = match engine { "openai" => generators::openai::run(creds.openai, config, args).await, + "from-file" => generators::debug::run(config, args).await, _ => panic!("Unknown engine: {}", engine), }.map_err(|e| format!("Failed to request OpenAI API: {}", e))?; loop { match stream.next().await { - Some(Ok(token)) => raise_str!(formatter.push(&token), "Failed to parse markdown: {}"), + Some(Ok(token)) => { + raise_str!(formatter.push(&token), "Failed to parse markdown: {}"); + raise_str!(runner.push(&token), "Failed push text in the runner system: {}"); + }, Some(Err(e)) => Err(e.to_string())?, None => break, } } raise_str!(formatter.end_of_document(), "Failed to end markdown: {}"); + raise_str!(runner.end_of_document(), "Failed to run code: {}"); Ok(()) } \ No newline at end of file diff --git a/src/runner/mod.rs b/src/runner/mod.rs new file mode 100644 index 0000000..fc95e8a --- /dev/null +++ b/src/runner/mod.rs @@ -0,0 +1,132 @@ +mod program; +use crate::args; +use anyhow::Result; +use super::Formatter; + +#[derive(Default, Debug)] +pub struct CodeBlock { + code: String, + language: String, +} + +impl CodeBlock { + fn new(language: String) -> Self { + Self { code: String::new(), language } + } +} +#[derive(Default, Debug)] +pub struct Runner{ + interactive_mode: args::RunChoice, + is_code: bool, + is_newline: bool, + current_token: String, + codes: Vec +} + +impl Formatter for Runner { + fn push(&mut self, text: &str) -> Result<()> { + for c in text.chars() { + match c { + '`' => { + if self.is_newline { + self.current_token.push(c); + } + }, + '\n' => { + if self.current_token.starts_with("```") { + self.switch_code_block(); + } else if self.is_code { + self.codes.last_mut().unwrap().code.push(c); + } + self.current_token.clear(); + self.is_newline = true; + }, + _ => { + if self.is_code { + self.codes.last_mut().unwrap().code.push(c); + } else if self.is_newline && self.current_token.starts_with("```") { + self.current_token.push(c); + } else { + self.is_newline = false; + } + }, + } + } + Ok(()) + } + fn end_of_document(&mut self) -> Result<()> { + use std::io::IsTerminal; + if !std::io::stdout().is_terminal() { + // No code execution allowed if not in a terminal + return Ok(()) + } + match self.interactive_mode { + args::RunChoice::No => return Ok(()), + args::RunChoice::Ask => self.interactive_interface()?, + args::RunChoice::Force => { + for code_block in self.codes.iter() { + program::run(code_block)?; + } + }, + } + + Ok(()) + } +} + +impl Runner { + pub fn new(run_choice: args::RunChoice) -> Self { + Self { + is_newline: true, + interactive_mode: run_choice, + .. Default::default() + } + } + fn switch_code_block(&mut self) { + self.is_code = !self.is_code; + if self.is_code { + let language = self.current_token[3..].trim(); + self.codes.push(CodeBlock::new(language.into())); + } else { + // remove last newline + self.codes.last_mut().unwrap().code.pop(); + } + } + fn interactive_interface(&mut self) -> Result<()> { + use std::io::Write; + if self.codes.is_empty() { + return Ok(()); + } + loop { + println!("Execute code ?"); + if self.codes.len() == 1 { + println!("1: index of the code block"); + } else { + println!("1-{}: index of the code block", self.codes.len()); + } + println!("q: quit"); + print!("> "); + std::io::stdout().flush()?; + let mut stdin_buf = String::new(); + std::io::stdin().read_line(&mut stdin_buf)?; + let stdin_buf = stdin_buf.trim(); + if stdin_buf == "q" { + return Ok(()); + } + let index = match stdin_buf.parse::() { + Ok(i) => i, + Err(_) => { + println!("Not a number"); + continue; + } + }; + if !(1..=self.codes.len() as isize).contains(&index) { + println!("Index out of range"); + continue; + } + print!("\n"); + program::run(&self.codes[index as usize-1])?; + print!("\n"); + } + } +} \ No newline at end of file diff --git a/src/runner/program/cache.rs b/src/runner/program/cache.rs new file mode 100644 index 0000000..367fad9 --- /dev/null +++ b/src/runner/program/cache.rs @@ -0,0 +1,80 @@ +use std::ops::{Deref, DerefMut}; + +use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +use thiserror::Error; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct Cache { + programs: std::collections::HashMap, +} + +#[derive(Error, Debug)] +pub enum CacheError { + #[error("error while accessing to the cache file: {0}")] + Io(#[from] std::io::Error), + #[error("error while parsing or encoding the cache file: {0}")] + Parse(#[from] serde_yaml::Error), + #[error("Unable to access to cache directory")] + NoParent, +} + +static CACHE: once_cell::sync::Lazy, CacheError>> = once_cell::sync::Lazy::new(|| { + Cache::load().map(|cache| RwLock::new(cache)) +}); + +impl Cache { + fn load() -> Result { + let file_path = Cache::cache_path(); + if !file_path.exists() { + return Ok(Default::default()); + } + let cache_file = match std::fs::File::open(file_path) { + Ok(file) => file, + Err(e) => return Err(e.into()), + }; + match serde_yaml::from_reader(cache_file) { + Ok(cache) => return Ok(cache), + Err(e) => return Err(e.into()), + } + } + fn save(&self) -> Result<(), CacheError> { + let file_path = Self::cache_path(); + let Some(parent) = file_path.parent() else { return Err(CacheError::NoParent); }; + if !parent.exists() { + std::fs::create_dir_all(parent)?; + } + let mut cache_file = std::fs::File::create(file_path)?; + serde_yaml::to_writer(&mut cache_file, self)?; + Ok(()) + } + fn get() -> RwLockReadGuard<'static, Self> { + match *CACHE { + Ok(ref cache) => cache.read().expect("Error while accessing to the cache memory"), + Err(_) => panic!("Unable to access to the cache file"), + } + } + fn get_mut() -> RwLockWriteGuard<'static, Self> { + match *CACHE { + Ok(ref cache) => cache.write().expect("Error while accessing to the cache memory"), + Err(_) => panic!("Unable to access to the cache file"), + } + } + fn cache_path() -> std::path::PathBuf { + std::path::Path::new(crate::home_dir()).join(".cache").join("aio.yaml") + } +} +pub fn get_program(program: &str) -> Option { + let cache = Cache::get(); + let path = cache.programs.get(program)?; + if !std::path::Path::new(path).exists() { + return None; + } + Some(path.clone()) +} +pub fn set_program(program: String, path: String) -> Result<(), CacheError> { + let mut cache = Cache::get_mut(); + cache.programs.insert(program.to_string(), path.to_string()); + cache.save()?; + Ok(()) +} \ No newline at end of file diff --git a/src/runner/program/mod.rs b/src/runner/program/mod.rs new file mode 100644 index 0000000..1fa3d11 --- /dev/null +++ b/src/runner/program/mod.rs @@ -0,0 +1,108 @@ +mod cache; + +mod shell; +use shell::*; +mod rust; +use rust::*; +mod python; +use python::*; + +use std::borrow::Cow; +use thiserror::Error; +use super::CodeBlock; + +trait Program { + fn run(&self, code_block: &CodeBlock) -> Result; +} + +#[derive(Error, Debug)] +pub enum RunError { + #[error("error while searching a program: {0}")] + Search(#[from] SearchError), + #[error("program not found for `{0}`")] + ProgramNotFound(String), + #[error("io error: {0}")] + Io(#[from] std::io::Error), +} +#[derive(Error, Debug)] +pub enum SearchError { + #[error("env var {0} not found: {1}")] + EnvVarNotFound(Cow<'static, str>, std::env::VarError), + #[error("bad utf-8 encoding in path")] + BadUTF8, + #[error("no corresponding program found for `{0}`")] + NoCorrespondingProgram(String), + #[error("cache error: {0}")] + Cache(#[from] cache::CacheError) +} + +enum SearchStatus { + Found(Box), + NotFound, + Error(SearchError) +} + +fn search_program(program: &str) -> Result, SearchError> { + if let Some(found) = cache::get_program(program) { + return Ok(Some(found)); + } + #[cfg(target_family = "unix")] + const SEPARATOR: char = ':'; + #[cfg(target_family = "windows")] + const SEPARATOR: char = ';'; + + let path = std::env::var("PATH").map_err(|e| SearchError::EnvVarNotFound("PATH".into(), e))?; + let found = path.split(SEPARATOR).find_map(|p| { + let mut directory = std::fs::read_dir(p).ok()?; + let found_program = directory.find(|res_item| { + let Ok(item) = res_item else { return false }; + let Ok(file_type) = item.file_type() else { return false }; + if !(file_type.is_file() || file_type.is_symlink()) { + return false; + } + #[cfg(target_family = "unix")] + return item.file_name() == program; + #[cfg(target_family = "windows")] + { + use std::ffi::{OsString, OsStr}; + let os_program = OsString::from(program.to_lowercase()); + let os_extension = OsString::from("exe"); + let path = item.path(); + let Some(filestem) = path.file_stem().map(OsStr::to_ascii_lowercase) else { return false }; + let Some(extension) = path.file_stem().map(OsStr::to_ascii_lowercase) else { return false }; + return filestem == os_program && extension == os_extension; + } + }); + found_program + .and_then(Result::ok) + .map(|v| v.path()) + }); + let Some(found) = found else { return Ok(None); }; + let found = found.to_str().ok_or(SearchError::BadUTF8)?.to_string(); + cache::set_program(program.into(), found.clone())?; + Ok(Some(found)) +} + +fn get_program(language: &str) -> SearchStatus { + match language { + "sh" | "shell" => ShellProgram::search(&["zsh", "bash", "sh"]), + "bash" => ShellProgram::search(&["zsh", "bash"]), + "zsh" => ShellProgram::search(&["zsh"]), + "nu" => ShellProgram::search(&["nu"]), + "pwsh" | "powershell" => ShellProgram::search(&["pwsh", "powershell"]), + "rust" | "rs" => RustProgram::search(), + "py" | "python" => PythonProgram::search(), + _ => SearchStatus::Error(SearchError::NoCorrespondingProgram(language.to_string())), + } +} + + +pub fn run(code_block: &CodeBlock) -> Result { + let program = match get_program(code_block.language.as_str()) { + SearchStatus::Found(found) => found, + SearchStatus::NotFound => return Err(RunError::ProgramNotFound(code_block.language.clone())), + SearchStatus::Error(e) => return Err(RunError::Search(e)), + }; + Ok(program.run(code_block)?) +} + diff --git a/src/runner/program/python.rs b/src/runner/program/python.rs new file mode 100644 index 0000000..188b048 --- /dev/null +++ b/src/runner/program/python.rs @@ -0,0 +1,41 @@ +use super::*; +pub struct PythonProgram(String); + +impl Program for PythonProgram { + fn run(&self, code_block: &CodeBlock) -> Result { + use std::io::Write; + let mut process = std::process::Command::new(&self.0); + process + .arg("-") + .stdin(std::process::Stdio::piped()); + let mut child = process.spawn()?; + child.stdin.take().expect("Failed to get stdin of python").write_all(code_block.code.as_bytes())?; + Ok(child.wait_with_output()?) + } +} +impl PythonProgram { + pub(super) fn search() -> SearchStatus { + if let Ok(Some(found)) = search_program("python3") { + return SearchStatus::Found(Box::new(Self(found))); + } + if let Ok(Some(found)) = search_program("python") { + if let Ok(true) = Self::check_python_version(&found) { + return SearchStatus::Found(Box::new(Self(found))); + } + } + SearchStatus::NotFound + } + fn check_python_version(path: &str) -> Result { + let mut command = std::process::Command::new(path); + command.arg("-V"); + let output = command.output()?; + if !output.status.success() { + return Ok(false); + } + let str_output = String::from_utf8_lossy(&output.stdout); + + let Some(capture_version) = regex::Regex::new(r"Python (\d+)\.\d+\.\d+").unwrap().captures(&str_output) else { return Ok(false); }; + let Ok(major) = capture_version[1].parse::() else { return Ok(false); }; + Ok(major >= 3) + } +} \ No newline at end of file diff --git a/src/runner/program/rust.rs b/src/runner/program/rust.rs new file mode 100644 index 0000000..ccbc759 --- /dev/null +++ b/src/runner/program/rust.rs @@ -0,0 +1,37 @@ +use super::*; +pub struct RustProgram(String); + +impl Program for RustProgram { + fn run(&self, code_block: &CodeBlock) -> Result { + use std::io::Write; + let tmp = tempfile::NamedTempFile::new()?.into_temp_path(); + let tmp_path = tmp.to_str().expect("Failed to convert temp path to string"); + + let mut rustc_process = std::process::Command::new(&self.0); + rustc_process + .args(&[ + "-o", + tmp_path, + "-", + ]) + .stdin(std::process::Stdio::piped()); + let mut child = rustc_process.spawn()?; + child.stdin.take().expect("Failed to get stdin of rustc").write_all(code_block.code.as_bytes())?; + child.wait_with_output()?; + + let output = std::process::Command::new(tmp_path) + .spawn()? + .wait_with_output()?; + + Ok(output) + } +} +impl RustProgram { + pub(super) fn search() -> SearchStatus { + match search_program("rustc") { + Ok(Some(found)) => return SearchStatus::Found(Box::new(Self(found))), + Err(e) => SearchStatus::Error(e), + _ => SearchStatus::NotFound + } + } +} \ No newline at end of file diff --git a/src/runner/program/shell.rs b/src/runner/program/shell.rs new file mode 100644 index 0000000..b141dc4 --- /dev/null +++ b/src/runner/program/shell.rs @@ -0,0 +1,25 @@ +use super::*; +pub struct ShellProgram(String); + +impl Program for ShellProgram { + fn run(&self, code_block: &CodeBlock) -> Result { + let mut process = std::process::Command::new(&self.0); + process + .arg("-c") + .arg(&code_block.code); + let child = process.spawn()?; + Ok(child.wait_with_output()?) + } +} +impl ShellProgram { + pub(super) fn search(list_shells: &[&'static str]) -> SearchStatus { + for shell in list_shells { + match search_program(shell) { + Ok(Some(found)) => return SearchStatus::Found(Box::new(Self(found))), + // Err(e) => println!("Warning during search for {}: {}", shell, e), + _ => continue + } + } + SearchStatus::NotFound + } +} \ No newline at end of file