zula_core/
lib.rs

1#![doc = r#"`zula-core` contains the core functionality of the zula shell, and is required for writing
2plugins. This api is experimental, and may introduce breaking changes.
3
4# Plugin Guide
5To create a plugin, first initialize a library crate.
6```sh
7cargo new my_plugin --lib
8```
9Set the crate type to `cdylib`, and add `zula-core` as a dependency.
10```toml
11[lib]
12crate-type = ["cdylib"]
13
14[dependencies]
15zula-core = "4.0.0"
16```
17Import the [`Plugin`] trait and implement it on your plugin type.
18```
19use zula_core::{Plugin, ShellState};
20use std::error::Error;
21
22pub struct MyPlugin;
23
24impl Plugin for MyPlugin {
25    //since this function is called across abi boundaries, its important to include no_mangle so
26    //that rustc leaves the symbol as-is and can be called properly.
27    #[no_mangle]
28    fn init(&self) -> Box<dyn Plugin> {
29        Box::new(Self)
30    }
31    fn name(&self) -> &str {
32        "my_plugin"
33    }
34    fn call(&self, state: *mut ShellState) -> Result<(), Box<dyn Error>> {
35        println!("Hello, plugin!");
36        Ok(())
37    }
38}
39```
40Run `cargo build --release` to build your plugin. The library file should be in `target/release/lib<name>.so`. This is the file that you'll put in your plugins folder.
41
42Thats it! Run `zula cfg` inside zula to check that its loaded, and run `plugin.<name>` to use it. Due to weird ownership relationships, `call` has to take a raw pointer, so use it responsibly.
43"#]
44
45use std::{
46    collections::HashMap,
47    env,
48    error::Error,
49    ffi::OsStr,
50    fmt::Display,
51    io::{self, stdin, stdout, ErrorKind, Stdin, Stdout},
52    ops::Deref,
53    process::Command,
54};
55
56use termion::raw::{IntoRawMode, RawTerminal};
57
58mod plug;
59pub use plug::{Plugin, PluginHook};
60
61#[repr(C)]
62///The core shell state object. This api is WIP, and may become more locked down in the future.
63pub struct ShellState {
64    cwd: String,
65    pub header: fn(state: &ShellState) -> String,
66    pub history: Vec<String>,
67    pub config: Config,
68
69    pub stdin: Stdin,
70    pub stdout: RawTerminal<Stdout>,
71}
72///Holds configuration info.
73pub struct Config {
74    pub aliases: HashMap<String, String>,
75    pub hotkeys: HashMap<char, String>,
76    plugins: HashMap<String, PluginHook>,
77    pub safety: bool, 
78}
79
80
81impl Config {
82    pub fn new() -> Self {
83        Self {
84            aliases: HashMap::new(),
85            hotkeys: HashMap::new(),
86            plugins: HashMap::new(),
87            safety: false
88        }
89    }
90}
91
92impl ShellState {
93    ///Initializes a new shell. Do not use this if making plugins.
94    pub fn new() -> Result<Self, ZulaError> {
95        let cwd = env::current_dir()?.to_string_lossy().to_string();
96
97        Ok(Self {
98            cwd,
99            header: {
100                |state| {
101                    format!(
102                        "\x1b[38;5;93mzula\x1b[38;5;5m @ \x1b[38;5;93m{} \x1b[0m-> ",
103                        state.get_cwd()
104                    )
105                }
106            },
107            config: Config::new(),
108            history: vec![],
109
110            stdin: stdin(),
111            stdout: stdout().into_raw_mode()?,
112        })
113    }
114
115    ///Get the current working directory of the shell.
116    pub fn get_cwd(&self) -> &str {
117        &self.cwd
118    }
119    ///Set the current working directory of the shell. Will error if the path is not found.
120    pub fn set_cwd(&mut self, path: &str) -> Result<(), ZulaError> {
121        env::set_current_dir(path).map_err(|_| ZulaError::InvalidDir)?;
122        self.cwd = env::current_dir().map(|s| s.to_string_lossy().to_string())?;
123        Ok(())
124    }
125
126    ///Returns the header or "status bar."
127    pub fn get_header(&self) -> String {
128        let mut head = (self.header)(self);
129        head.push_str("\x1b[0m");
130        head
131    }
132
133    ///Execute a command. Does no proccessing such as aliases, chaining, and quoting.
134    pub fn exec(
135        &mut self,
136        cmd: impl AsRef<str>,
137        args: &[impl AsRef<str>],
138    ) -> Result<(), ZulaError> {
139        if cmd.as_ref() == "cd" {
140            match args.get(0) {
141                Some(targ) => return self.set_cwd(targ.as_ref()),
142                None => return Err(ZulaError::CommandEmpty),
143            }
144        }
145
146        let mut exec = Command::new(cmd.as_ref());
147
148        for e in args {
149            exec.arg(e.as_ref());
150        }
151
152        let init = exec.spawn();
153
154        let mut proc = match init {
155            Ok(c) => c,
156            Err(e) if e.kind() == ErrorKind::NotFound => {
157                { Err(ZulaError::InvalidCmd(cmd.as_ref().to_owned())) }?
158            }
159            Err(e) => { Err(Into::<ZulaError>::into(e)) }?,
160        };
161        proc.wait()?;
162        Ok(())
163    }
164    ///Attempt to load a plugin from a path.
165    pub fn load_plugin(&mut self, path: impl AsRef<OsStr>) -> Result<(), libloading::Error> {
166        let plug = unsafe { PluginHook::new(path) }?;
167        self.config.plugins.insert(plug.name().to_owned(), plug);
168        Ok(())
169    }
170    ///Returns a hook to the given plugin if it exists.
171    pub fn plugin_lookup(&self, name: &str) -> Result<&PluginHook, ZulaError> {
172        self.config
173            .plugins
174            .get(name)
175            .ok_or(ZulaError::InvalidPlugin)
176    }
177    ///Returns an iterator over the currently loaded plugin names.
178    pub fn plugin_names(&self) -> std::collections::hash_map::Keys<'_, String, PluginHook> {
179        self.config.plugins.keys()
180    }
181}
182
183#[derive(Debug)]
184///The zula shell error type. All errors can be converted to the `Opaque` variant.
185pub enum ZulaError {
186    Io(io::Error),
187    InvalidCmd(String),
188    CommandEmpty,
189    InvalidDir,
190    RecursiveAlias,
191    InvalidPlugin,
192    LibErr(libloading::Error),
193    Opaque(Box<dyn Error + Send + Sync>),
194}
195
196impl From<io::Error> for ZulaError {
197    fn from(value: io::Error) -> Self {
198        Self::Io(value)
199    }
200}
201impl From<libloading::Error> for ZulaError {
202    fn from(value: libloading::Error) -> Self {
203        Self::LibErr(value)
204    }
205}
206impl From<Box<dyn Error + Send + Sync>> for ZulaError {
207    fn from(value: Box<(dyn std::error::Error + Send + Sync + 'static)>) -> Self {
208        Self::Opaque(value)
209    }
210}
211
212impl Display for ZulaError {
213    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214        match self {
215            Self::Io(e) => {
216                write!(f, "io error: {e}\r\n")
217            }
218            Self::InvalidCmd(cmd) => write!(f, "unknown command: {cmd}\r\n"),
219            Self::CommandEmpty => write!(f, "command not given\r\n"),
220            Self::InvalidDir => write!(f, "directory does not exist\r\n"),
221            Self::RecursiveAlias => write!(f, "recursive alias called\r\n"),
222            Self::InvalidPlugin => write!(f, "plugin not found\r\n"),
223            Self::LibErr(e) => write!(f, "lib error: {e}\r\n"),
224            Self::Opaque(e) => write!(f, "external error: {e}\r\n"),
225        }
226    }
227}
228
229impl Error for ZulaError {
230    fn source(&self) -> Option<&(dyn Error + 'static)> {
231        #[allow(unreachable_patterns)]
232        match self {
233            Self::Io(e) => Some(e),
234            Self::LibErr(e) => Some(e),
235            Self::Opaque(e) => Some(e.deref()),
236            _ => None,
237        }
238    }
239}