commit 461bca8338f3d38690a2437ece0e934f7154c089 Author: Aaron Gorodetzky Date: Wed May 13 22:45:28 2026 -0400 Paths for config diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..de14b69 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "cacoon" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f0f8ad5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "cacoon" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..af1c5c4 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,164 @@ +use std::env; +use std::fs; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn main() { + let mut shell = ShellState::new(); + + if let Err(err) = run_startup(&mut shell) { + eprintln!("cacoon: startup: {err}"); + } + + repl(&mut shell); +} + +struct ShellState { + // Later: aliases, variables, functions, options, etc. +} + +impl ShellState { + fn new() -> Self { + Self {} + } +} + +struct SilkInterpreter; + +impl SilkInterpreter { + fn new() -> Self { + Self + } + + fn eval_file( + &mut self, + _path: &Path, + _source: &str, + _shell: &mut ShellState, + ) -> Result<(), String> { + // Stub for now. + // + // Later this should parse/evaluate Silk and mutate ShellState: + // - aliases + // - exported vars + // - prompt config + // - shell options + // - functions + Ok(()) + } +} + +fn run_startup(shell: &mut ShellState) -> Result<(), String> { + let Some(config_path) = find_startup_config() else { + return Ok(()); + }; + + let source = fs::read_to_string(&config_path) + .map_err(|err| format!("failed to read {}: {err}", config_path.display()))?; + + let mut silk = SilkInterpreter::new(); + + silk.eval_file(&config_path, &source, shell) + .map_err(|err| format!("failed to evaluate {}: {err}", config_path.display()))?; + + Ok(()) +} + +fn find_startup_config() -> Option { + startup_config_candidates() + .into_iter() + .find(|path| path.is_file()) +} + +fn startup_config_candidates() -> Vec { + let mut paths = Vec::new(); + + if let Some(home) = env::var_os("HOME") { + let home = PathBuf::from(home); + + paths.push(home.join(".config/cacoonrc")); + paths.push(home.join(".cacoon/cacoonrc")); + paths.push(home.join(".cacoonrc")); + } + + paths.push(PathBuf::from("/etc/cacoon/cacoonrc")); + + paths +} + +fn repl(_shell: &mut ShellState) { + loop { + print_prompt(); + + let mut line = String::new(); + + if io::stdin().read_line(&mut line).is_err() { + eprintln!("failed to read input"); + continue; + } + + let line = line.trim(); + + if line.is_empty() { + continue; + } + + let mut parts = line.split_whitespace(); + let cmd = parts.next().unwrap(); + let args: Vec<&str> = parts.collect(); + + match cmd { + "cd" => { + let target = args + .first() + .map(PathBuf::from) + .or_else(|| env::var_os("HOME").map(PathBuf::from)) + .unwrap_or_else(|| PathBuf::from("/")); + + if let Err(err) = env::set_current_dir(&target) { + eprintln!("cd: {err}"); + } + } + + "pwd" => match env::current_dir() { + Ok(path) => println!("{}", path.display()), + Err(err) => eprintln!("pwd: {err}"), + }, + + "exit" => { + break; + } + + "help" => { + println!("builtins: cd pwd exit help"); + } + + _ => { + match Command::new(cmd).args(args).status() { + Ok(status) => { + if !status.success() { + if let Some(code) = status.code() { + eprintln!("process exited with status {code}"); + } else { + eprintln!("process terminated by signal"); + } + } + } + Err(err) => { + eprintln!("{cmd}: {err}"); + } + } + } + } + } +} + +fn print_prompt() { + let cwd = env::current_dir() + .map(|p| p.display().to_string()) + .unwrap_or_else(|_| "?".to_string()); + + print!("{cwd} $ "); + io::stdout().flush().unwrap(); +}