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)]
62pub 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}
72pub 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 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 pub fn get_cwd(&self) -> &str {
117 &self.cwd
118 }
119 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 pub fn get_header(&self) -> String {
128 let mut head = (self.header)(self);
129 head.push_str("\x1b[0m");
130 head
131 }
132
133 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 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 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 pub fn plugin_names(&self) -> std::collections::hash_map::Keys<'_, String, PluginHook> {
179 self.config.plugins.keys()
180 }
181}
182
183#[derive(Debug)]
184pub 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}